在嵌入式系统开发中,串行通信是最基础也最重要的功能之一。作为一位从事单片机开发多年的工程师,我经常需要处理各种串行通信问题。今天我想系统地分享一下51单片机串行通信的实现方法,特别是UART模块的详细使用技巧。
串行通信与并行通信最大的区别在于数据传输方式。串行通信使用单根数据线逐位传输数据,而并行通信则使用多根数据线同时传输多位数据。在实际工程中,串行通信因其布线简单、成本低廉、抗干扰能力强等优势,成为嵌入式系统中最常用的通信方式。
串行通信又可分为同步和异步两种类型。同步通信需要额外的时钟线来同步数据传输,如SPI和I2C协议;而异步通信则依靠双方预先约定的波特率来实现同步,UART就是典型的异步通信协议。
51单片机内部集成了全双工的UART模块,可以实现同时收发数据。这个模块包含以下关键部件:
UART通信需要配置几个关键参数:
51单片机的UART波特率由Timer1产生。当使用11.0592MHz晶振时,波特率计算公式为:
波特率 = (2^SMOD × 晶振频率) / (32 × 12 × (256 - TH1))
其中SMOD是PCON寄存器的一个控制位,用于波特率加倍。TH1是Timer1的重装值。常用的波特率对应TH1值如下:
| 波特率 | SMOD | TH1值 | 实际波特率 | 误差率 |
|---|---|---|---|---|
| 2400 | 1 | 0xF4 | 2403.85 | 0.16% |
| 4800 | 0 | 0xFA | 4807.69 | 0.16% |
| 9600 | 0 | 0xFD | 9615.38 | 0.16% |
| 19200 | 0 | 0xFE | 19230.77 | 0.16% |
| 115200 | 1 | 0xFF | 115200 | 0% |
提示:11.0592MHz晶振被广泛使用的原因就是它能够精确产生这些常用波特率,误差极小。
UART初始化涉及多个寄存器的配置:
SCON寄存器(98H)
PCON寄存器(87H)
TMOD寄存器(89H)
IE寄存器(A8H)
下面是一个完整的UART初始化函数示例,配置为2400bps,8位数据,无校验,1停止位:
c复制void UART_Init(void)
{
// 1. 配置SCON寄存器
SCON = 0x50; // 01010000
// SM0=0,SM1=1: 模式1(8位UART)
// REN=1: 允许接收
// 2. 配置PCON寄存器
PCON |= 0x80; // SMOD=1,波特率加倍
// 3. 配置Timer1为模式2(8位自动重装)
TMOD &= 0x0F; // 清零高4位
TMOD |= 0x20; // 设置Timer1为模式2
// 4. 设置波特率重装值
TH1 = 0xF4; // 2400bps@11.0592MHz
TL1 = 0xF4;
// 5. 启动Timer1
TR1 = 1;
// 6. 使能中断
EA = 1; // 总中断使能
ES = 1; // 串口中断使能
}
UART发送数据相对简单,只需将数据写入SBUF寄存器即可。但需要注意等待发送完成标志TI置位,并在发送完成后手动清零TI。
c复制void UART_SendByte(unsigned char dat)
{
SBUF = dat; // 写入发送缓冲器
while(!TI); // 等待发送完成
TI = 0; // 清零发送中断标志
}
void UART_SendString(char *str)
{
while(*str != '\0')
{
UART_SendByte(*str++);
}
}
UART接收通常采用中断方式,以提高系统效率。下面是一个典型的中断服务函数实现:
c复制unsigned char UART_RxBuf[32];
unsigned char UART_RxIndex = 0;
void UART_ISR(void) interrupt 4
{
if(RI) // 接收中断
{
RI = 0; // 必须先清零RI标志
// 将接收到的数据存入缓冲区
UART_RxBuf[UART_RxIndex++] = SBUF;
// 防止缓冲区溢出
if(UART_RxIndex >= sizeof(UART_RxBuf))
{
UART_RxIndex = 0;
}
}
// 发送中断处理(如果需要)
if(TI)
{
TI = 0; // 清零TI标志
}
}
在实际项目中,单纯的字节传输往往不能满足需求,我们需要设计更完善的通信协议。下面介绍一个类似Modbus的自定义协议实现。
我们设计一个7字节的固定长度帧格式:
| 字节位置 | 字段名 | 说明 |
|---|---|---|
| 0 | 起始位 | 固定为0xAA,标识帧开始 |
| 1 | 地址码 | 设备地址,0x01-0xFE |
| 2 | 功能码 | 指令类型,如0x01控制LED |
| 3 | 数据1 | 第一个数据参数 |
| 4 | 数据2 | 第二个数据参数 |
| 5 | 校验码 | 前5字节的累加和校验 |
| 6 | 结束位 | 固定为0xBB,标识帧结束 |
c复制#define FRAME_HEADER 0xAA
#define FRAME_FOOTER 0xBB
#define DEVICE_ADDR 0x01
unsigned char Parse_Frame(unsigned char *buf)
{
unsigned char i, checksum = 0;
// 检查帧头和帧尾
if(buf[0] != FRAME_HEADER || buf[6] != FRAME_FOOTER)
{
return 0; // 帧格式错误
}
// 检查设备地址
if(buf[1] != DEVICE_ADDR)
{
return 0; // 地址不匹配
}
// 计算校验和
for(i=0; i<5; i++)
{
checksum += buf[i];
}
// 校验和比对
if(checksum != buf[5])
{
return 0; // 校验失败
}
return buf[2]; // 返回功能码
}
c复制void Process_Command(unsigned char cmd)
{
unsigned char response[7];
unsigned char i, checksum = 0;
// 复制接收到的帧
for(i=0; i<7; i++)
{
response[i] = UART_RxBuf[i];
}
// 设置应答标志(功能码最高位置1)
response[2] |= 0x80;
// 根据功能码执行相应操作
switch(cmd)
{
case 0x01: // 控制LED
LED_Control(response[3]);
break;
case 0x02: // 控制数码管
DigitalTube_Display(response[3]);
break;
case 0x03: // 控制蜂鸣器
Buzzer_Control(response[3]);
break;
default:
break;
}
// 重新计算校验和
for(i=0; i<5; i++)
{
checksum += response[i];
}
response[5] = checksum;
// 发送应答帧
for(i=0; i<7; i++)
{
UART_SendByte(response[i]);
}
}
虽然11.0592MHz晶振能精确产生常用波特率,但在实际应用中仍需注意:
经验:在要求高的场合,可以使用示波器测量实际波特率,必要时调整TH1值进行微调。
串口通信容易受到干扰,特别是在工业环境中。以下方法可以提高通信可靠性:
在RTOS或多任务环境中使用串口时,需要注意:
c复制// RTOS环境下的串口中断处理示例
void UART_ISR(void) interrupt 4
{
if(RI)
{
RI = 0;
OS_ENTER_CRITICAL();
UART_RxBuf[UART_RxIndex++] = SBUF;
OS_EXIT_CRITICAL();
// 发送信号量通知任务
OS_SemaphorePost(UART_Rx_Sem);
}
}
串口调试是嵌入式开发中最常用的调试手段之一,以下是一些实用技巧:
c复制// printf重定向示例
char putchar(char c)
{
UART_SendByte(c);
return c;
}
// 十六进制打印函数
void Print_Hex(unsigned char dat)
{
unsigned char nibble;
nibble = (dat >> 4) & 0x0F;
UART_SendByte(nibble > 9 ? nibble - 10 + 'A' : nibble + '0');
nibble = dat & 0x0F;
UART_SendByte(nibble > 9 ? nibble - 10 + 'A' : nibble + '0');
}
可能原因及解决方案:
可能原因及解决方案:
可能原因及解决方案:
c复制// CRC8查表法实现
static const unsigned char CRC8_Table[256] = {
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15,
// ... 省略其余表格数据
};
unsigned char CRC8_Calculate(unsigned char *buf, unsigned char len)
{
unsigned char crc = 0;
while(len--)
{
crc = CRC8_Table[crc ^ *buf++];
}
return crc;
}
通过以上内容的详细介绍,相信大家对51单片机的串行通信有了更深入的理解。在实际项目中,UART通信看似简单,但要实现稳定可靠的通信仍需注意很多细节。我在多年的开发实践中总结出的这些经验,希望能帮助大家少走弯路。