一、先上代码
usart.c
cpp
/*
============================================================
89C51 串口通信功能函数库
适合初学者阅读 ------ 每一句都有注释
作者:示例教学代码
日期:2024
============================================================
【用法示例 - 请先看这里!】
#include <reg51.h>
void main(void)
{
// 第一步:初始化串口,设置波特率为 9600
UART_Init();
// 第二步:发送一个字节(例如发送字符 'A',ASCII = 0x41)
UART_SendByte('A');
// 第三步:发送一个字符串
UART_SendString("Hello, 89C51!\r\n");
// 第四步:开启总中断,开始接收数据(初始化时已开启,这里提示一下)
// 当串口收到数据时,会自动调用中断服务函数 UART_ISR()
// 接收到的数据存放在 rx_buffer[] 缓冲区中
// 可以用 rx_count 知道当前缓冲区里有几个字节
// 第五步:主循环中检查缓冲区是否有数据
while (1)
{
if (rx_count > 0) // 如果缓冲区里有数据
{
unsigned char ch;
EA = 0; // 关总中断,防止读取时被打断
ch = rx_buffer[0]; // 取出第一个字节
// 把缓冲区往前移一位(简单处理方式)
unsigned char i;
for (i = 0; i < rx_count - 1; i++)
rx_buffer[i] = rx_buffer[i + 1];
rx_count--; // 计数减一
EA = 1; // 重新开总中断
UART_SendByte(ch); // 把收到的字节原样发回去(回显)
}
}
}
============================================================
*/
#include <usart.h> /* 包含89C51寄存器定义头文件,里面定义了TMOD、SCON等寄存器 */
/* -------------------------------------------------------
宏定义:波特率相关常数
-------------------------------------------------------
晶振频率:89C51 最常见的是 11.0592 MHz
为什么选 11.0592?因为用它可以精确产生标准波特率,误差为 0%!
如果用 12 MHz,波特率会有误差,通信容易出错。
------------------------------------------------------- */
#define FOSC 11059200UL /* 晶振频率,单位 Hz,UL 表示无符号长整型 */
#define BAUD_RATE 9600 /* 目标波特率,常用值:9600、19200、38400 bps */
/*
波特率计算方法(重点!):
------------------------------------------------
89C51 串口工作在方式1时,使用定时器1产生波特率。
定时器1 工作在方式2(8位自动重装),溢出时产生时钟。
公式:
TH1 = 256 - (FOSC / 12 / 32 / BAUD_RATE)
拆解说明:
- FOSC / 12:定时器的时钟频率 = 晶振 ÷ 12(每个机器周期=12个振荡周期)
例:11059200 / 12 = 921600 Hz
- / 32: 串口方式1每位需要采样32次
例:921600 / 32 = 28800 Hz
- / BAUD_RATE:计算每个波特需要溢出几次
例:28800 / 9600 = 3
- 256 - 结果:得到定时器初值(方式2下,TH1存的是重装值)
例:256 - 3 = 253 = 0xFD
所以 TH1 = 0xFD 时,波特率恰好是 9600 bps(晶振11.0592MHz)
如果 SMOD = 1(电源控制寄存器PCON的最高位),波特率加倍:
TH1 = 256 - (FOSC / 12 / 16 / BAUD_RATE)
------------------------------------------------
*/
#define TH1_VALUE (256 - (FOSC / 12 / 32 / BAUD_RATE))
/* 上面这行:用宏自动计算 TH1 的值,这样修改 BAUD_RATE 就能自动更新 */
/* -------------------------------------------------------
接收缓冲区定义
------------------------------------------------------- */
#define RX_BUF_SIZE 10 /* 缓冲区大小:最多存32个字节,可根据需要修改 */
unsigned char rx_buffer[RX_BUF_SIZE]; /* 接收数据缓冲区数组 */
unsigned char rx_count = 0; /* 当前缓冲区中已存数据的字节数,初始为0 */
/* ============================================================
函数:UART_Init
功能:初始化89C51串口
参数:无
返回:无
============================================================ */
void UART_Init(void)
{
/* --- 第1步:配置定时器1,用于产生波特率 --- */
SCON = 0x50;
TMOD &= 0x0F; /* 先清除 TMOD 高4位(定时器1的配置位),保留低4位(定时器0)
0x0F = 0000 1111,与运算后 TMOD 高4位变0 */
TMOD |= 0x20; /* 设置定时器1 为 方式2(8位自动重装模式)
0x20 = 0010 0000,即 M1=1, M0=0,表示方式2
方式2优点:溢出后自动从TH1重装到TL1,不需要软件手动重装 */
TH1 = TH1_VALUE; /* 设置重装值(溢出后自动装入TL1的值)
根据波特率公式计算,9600bps时 = 0xFD = 253 */
TL1 = TH1_VALUE; /* 设置初始值,让定时器从这个值开始计数
第一次溢出时间和以后一样,保证第一个波特率周期正确 */
TR1 = 1; /* 启动定时器1,开始计数产生波特率
TR1 是 TCON 寄存器中的位,=1 表示运行 */
/* --- 第2步:配置串口控制寄存器 SCON --- */
// SCON = 0x50;
/* 0x50 = 0101 0000,逐位说明:
位7 SM0 = 0 \
位6 SM1 = 1 / SM0=0,SM1=1:串口工作方式1(10位异步)
方式1:1位起始位 + 8位数据 + 1位停止位
位5 SM2 = 0 多机通信位,单机通信置0
位4 REN = 1 允许接收,必须置1才能接收数据
位3 TB8 = 0 方式1不用,置0
位2 RB8 = 0 方式1不用,置0
位1 TI = 0 发送中断标志,初始清0
位0 RI = 0 接收中断标志,初始清0 */
/* --- 第3步:开启中断 --- */
ES = 1; /* 开串口中断(Enable Serial port interrupt)
ES 是 IE 寄存器中的位,=1 允许串口产生中断 */
EA = 1; /* 开总中断(Enable All interrupts)
EA 必须=1,各个中断才能真正起作用
如果 EA=0,即使 ES=1 也不会触发中断 */
}
/* ============================================================
函数:UART_SendByte
功能:通过串口发送一个字节的数据
参数:dat - 要发送的字节数据(0x00 ~ 0xFF)
返回:无
发送流程:把数据写入SBUF,硬件自动发送,等待TI=1表示发完
============================================================ */
void UART_SendByte(unsigned char dat)
{
unsigned char t = 0;
// TI = 0; /* 清除发送中断标志
// TI=1 表示"上一次发送完成了",这里先清0,准备新的发送 */
SBUF = dat; /* 把数据写入串口发送缓冲寄存器 SBUF
写入后,硬件自动将数据转换成串行信号发送出去
89C51 中 SBUF 发送和接收共用地址,但物理上是两个不同的寄存器 */
while (!TI)
{
t++;
if(t>200)
break;
} /* 等待发送完成
发送过程中 TI=0,发完一个字节后硬件自动将 TI 置1
while(!TI) 就是:只要 TI=0(还没发完),就一直等待
注意:这是"查询等待"方式,CPU会在这里空转,直到发完 */
TI = 0; /* 发送完成后,手动清除 TI 标志
因为这里没有用发送中断,所以要手动清TI
如果不清,下次调用此函数时可能出错 */
}
/* ============================================================
函数:UART_SendString
功能:通过串口发送一个字符串(以'\0'结尾的C字符串)
参数:str - 指向字符串的指针,字符串必须以'\0'结尾
返回:无
示例:UART_SendString("Hello!\r\n");
\r\n 是回车+换行,串口调试助手中会换行显示
============================================================ */
void UART_SendString(unsigned char *str)
{
while (*str != '\0') /* 循环条件:当前字符不是字符串结束符'\0'
*str 表示取指针str指向的字符
字符串"Hello"在内存中是 H,e,l,l,o,\0 */
{
UART_SendByte(*str); /* 发送当前字符(调用上面的发送字节函数) */
str++; /* 指针移向下一个字符
str++ 使 str 指向字符串的下一个位置 */
}
/* 退出循环时,*str == '\0',字符串发送完毕 */
}
/* ============================================================
中断服务函数:UART_ISR(串口中断服务程序)
功能:当串口收到数据时自动执行,将数据存入缓冲区
触发条件:RI=1(收到数据)或 TI=1(发送完成)都会触发串口中断
中断号 4 = 串口中断(89C51固定分配)
中断优先级可在 IP 寄存器中设置,这里使用默认低优先级
注意:中断函数不能有参数,不能有返回值
============================================================ */
void UART_ISR(void) interrupt 4
{
/* --- 处理接收中断 --- */
if (RI == 1) /* 判断是否是"接收完成"中断
RI=1 表示串口接收到了一个完整字节
(如果是TI=1则跳过,我们不处理发送中断) */
{
RI = 0; /* 立即清除接收中断标志
必须用软件手动清零,否则会不断触发中断!
这是初学者最容易忘记的一步 */
if (rx_count < RX_BUF_SIZE) /* 检查缓冲区是否已满
如果满了就丢弃新数据,防止数组越界 */
{
rx_buffer[rx_count] = SBUF; /* 从接收缓冲寄存器SBUF读出数据
存入接收缓冲区的下一个空位
rx_buffer[0]是第1个收到的字节
rx_buffer[1]是第2个,以此类推 */
rx_count++; /* 缓冲区数据计数加1 */
}
UART_SendByte(SBUF); //发送收到的数据
/* 如果 rx_count >= RX_BUF_SIZE,说明缓冲区满了,新数据被丢弃
实际项目中可以设置一个溢出标志位来通知主程序 */
}
/* --- 处理发送中断(这里不需要处理,忽略即可)--- */
if (TI == 1) /* 如果是发送完成中断 */
{
TI = 0; /* 清除发送中断标志,防止反复进入中断 */
/* 在本例中,发送使用查询方式(在SendByte中等待TI),
所以发送中断不需要额外处理,直接清标志退出即可 */
}
}
void rx_buffer_clear(void)
{
rx_count = 0;
rx_buffer[0] = '\0';
}
/* ============================================================
【补充说明 - 常见问题】
Q1: 为什么串口通信会乱码?
A: 1. 检查两端波特率是否一致
2. 检查晶振是否是 11.0592 MHz(其他晶振会有误差)
3. 检查 TH1 值是否正确
Q2: 发送函数卡住不动(死在while(!TI))?
A: 检查 TR1 是否启动,检查 TI 是否被意外清零
Q3: 接收不到数据?
A: 1. 检查 REN=1(允许接收位)
2. 检查 EA=1 且 ES=1(总中断和串口中断都开了)
3. 检查 RI 是否在中断里清零了
Q4: 缓冲区总是满?
A: 主程序处理数据的速度太慢,增大 RX_BUF_SIZE 或加快处理
【波特率与 TH1 对照表(晶振 11.0592 MHz,SMOD=0)】
波特率 TH1值(十进制) TH1值(十六进制)
1200 bps 230 0xE6
2400 bps 244 0xF4
4800 bps 250 0xFA
9600 bps 253 0xFD
19200 bps 253 (SMOD=1) 0xFD(需要设置PCON |= 0x80)
38400 bps 255 (SMOD=1) 0xFF(误差较大,不推荐)
============================================================ */
usart.h
cpp
#ifndef __USART_H__
#define __USART_H__
#include <reg52.h>
void UART_Init(void);
void UART_SendByte(unsigned char dat);
void UART_SendString(char *str);
void rx_buffer_clear(void);
#endif
二、硬件连接说明
TX RX
三、代码用法
cpp
UART_Init();
UART_SendByte('K');
UART_SendString("while\r\n");