0. 时钟基础
时钟是串口通信的基石。51 单片机(如 STC89C51)内部所有时序逻辑,包括 CPU 取指、定时器计数、波特率生成等,都依赖统一的时钟信号驱动。理解时钟的来龙去脉,是正确配置串口的前提。
0.1 晶振与时钟周期
单片机通常外接石英晶振产生稳定的振荡信号,作为系统时钟源。以 STC89C51 为例,常见晶振频率为 11.0592 MHz。之所以常选用 11.0592 MHz,是因为该频率可以精确分频出 2400、4800、9600、115200 等常用串口波特率,从而减少通信误码。
由晶振频率可以推导出几个关键时间参数:
- 时钟周期:晶振频率的倒数,即 1 / fosc。11.0592 MHz 晶振对应的时钟周期约为 0.09 微秒。
- 机器周期:51 单片机中 12 个时钟周期构成 1 个机器周期,即 12 / fosc。11.0592 MHz 下约为 1.085 微秒。
- 指令周期:执行一条指令所需的时间,通常为 1 到 4 个机器周期。
0.2 定时器与波特率的关系
串口波特率由定时器 1 作为波特率发生器产生。定时器 1 以机器周期为基准进行计数,当计数溢出时产生一次中断或触发重装载,从而形成稳定的波特率时钟。因此,晶振频率直接决定了定时器初值的计算。
在方式 1(8 位数据位,无奇偶校验位)且波特率不加倍(SMOD = 0)时,波特率计算公式为:
c
波特率 = (2^SMOD / 32) × (定时器1溢出率)
定时器1溢出率 = fosc / (12 × (256 - TH1))
将 fosc = 11.0592 MHz、SMOD = 0、目标波特率 2400 代入,可解得 TH1 = 0xF4,这正是示例代码中初值的由来。
1. 串口通信概述
串口通信(UART,Universal Asynchronous Receiver/Transmitter,串行异步收发器)是单片机与单片机之间、或单片机与其余主控之间进行通信的常用方式。它通过一根数据线逐位传输数据,实现设备之间的信息交换。
2. 引脚接线
UART 通信至少需要连接 3 根线:
- RXD:接收数据线。
- TXD:发送数据线。
- GND:通信双方一定要共地。因为传输的是电压信号,电压信号是差值,所以基准电压 GND 一定要保持一致。
常见接线方式包括点对点通信,以及单片机与 PC 机之间的通信。
3. 传输特性
3.1 波特率
波特率是串口传输数据的速率,单位为 bps(bit per second),即每秒发送的比特数。常见波特率有 2400、4800、9600、115200 等。
3.2 串行
串口将数据按照字节逐位发送,先发 LSB(最低位)再发 MSB(最高位)。
3.3 异步
通信双方使用不同的时钟源来统一设置传输的波特率。虽然波特率一致,但时钟源会有不同的偏差,容易造成串口通信误码。
3.4 全双工
串口有 TX 和 RX 两根线实现数据收发,因此收发能够同时进行。
4. 串口通信时序
串口发送一个字节的时序为:1 位起始位 + 8 位数据位 + 1 位奇偶校验位(可省略)+ 1 位停止位。
- 起始位:1 位,低电平,标志数据传输开始。
- 数据位:通常为 8 位。
- 校验位:可选,用于简单的错误检测。
- 停止位:1 位或 2 位,高电平,标志一帧数据结束。
收发双方必须约定相同的帧格式,否则无法正确解析数据。
5. 串口使用流程
5.1 串口发送流程
- 设置串口工作模式为模式 1(8 位数据位,无奇偶校验位)。
- 设置波特率是否加倍(SMOD 为 0 不加倍,SMOD 为 1 加倍)。
- 根据公式设置波特率。
- 设置定时器 1 相关功能:将定时器 1 设置为 8 位自动重装载;将定时器 TH1 设置初值(根据公式换算);开启定时器 1。
- 发送数据。
- 关注 TI(数据发送完 1 个字节),需要手动清除标志位。
5.2 串口接收流程
- 设置串口工作模式为模式 1(8 位数据位,无奇偶校验位)。
- 设置串口使能接收开关。
- 设置波特率是否加倍(SMOD 为 0 不加倍,SMOD 为 1 加倍)。
- 根据公式设置波特率。
- 设置定时器 1 相关功能:将定时器 1 设置为 8 位自动重装载;将定时器 TH1 设置初值(根据公式换算);开启定时器 1。
- 编写中断处理函数。
- 开启中断开关。
- 等待中断到来。
6. 实践示例:51 单片机串口发送
下面以 STC89C51 芯片(11.0592 MHz 晶振)为例,演示如何初始化串口并循环发送字符串。该实验使用定时器 1 作为波特率发生器,波特率设置为 2400。
c
#include <reg52.h>
#include <intrins.h>
void Delay1000ms(void) //@11.0592MHz
{
unsigned char data i, j, k;
_nop_();
i = 8;
j = 1;
k = 243;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void UartInit(void)
{
//设置串口为方式1
SCON &= ~(0x3 << 6);
SCON |= (0x1 << 6);
//允许接收
REN = 0;
//波特率不加倍
PCON &= ~(0x1 << 7);
//定时器1设置为8位自动重装载模式
TMOD &= ~(0x3 << 4);
TMOD |= 0x1 << 5;
//根据波特率2400配置TH1初值0xF4
TH1 = 0xF4;
//开启定时器
TR1 = 1;
return;
}
void UartSendByte(unsigned char TmpByte)
{
SBUF = TmpByte;
while (0 == TI);
TI = 0;
return;
}
void UartSendString(char *pstr)
{
while (*pstr != '\0')
{
UartSendByte(*pstr);
pstr++;
}
return;
}
int main(void)
{
//初始化串口波特率为2400
UartInit();
while (1)
{
UartSendString("hello world");
Delay1000ms();
}
return 0;
}
该实验的关键在于:波特率由定时器 1 的初值决定。在 11.0592 MHz 晶振下,将 TH1 配置为 0xF4,即可得到 2400 波特率。主循环中每隔 1 秒通过串口发送一次 "hello world"。
6.1 实践示例:51 单片机串口接收与命令控制
下面以 STC89C51 芯片(11.0592 MHz 晶振)为例,演示如何通过串口接收上位机发送的命令,并控制 LED、蜂鸣器和继电器。该实验使用定时器 1 作为波特率发生器,波特率设置为 2400,同时开启串口接收中断。
c
#include <reg52.h>
#include <intrins.h>
#include <string.h>
sbit Buzzer = P2^1;
sbit Relay = P2^7;
unsigned char Duty = 0;
unsigned char ReloadTH0 = 0;
unsigned char ReloadTL0 = 0;
volatile unsigned char RecvBuff[20] = {0};
volatile unsigned char RecvLen = 0;
volatile unsigned char IsCommandLine = 0;
//串口接收中断处理函数
void UartHandler(void) interrupt 4
{
if (RI)
{
RecvBuff[RecvLen] = SBUF;
RI = 0;
RecvLen++;
if ('\n' == RecvBuff[RecvLen-1])
{
IsCommandLine = 1;
}
}
return;
}
void Delay1000ms(void) //@11.0592MHz
{
unsigned char data i, j, k;
_nop_();
i = 8;
j = 1;
k = 243;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void UartInit(void)
{
//设置串口为方式1
SCON &= ~(0x3 << 6);
SCON |= (0x1 << 6);
//允许接收
REN = 1;
//波特率不加倍
PCON &= ~(0x1 << 7);
//定时器1设置为8位自动重装载模式
TMOD &= ~(0x3 << 4);
TMOD |= 0x1 << 5;
//根据波特率2400配置TH1初值0xF4
TH1 = 0xF4;
//开启定时器
TR1 = 1;
//开启中断
ES = 1;
EA = 1;
return;
}
void UartSendByte(unsigned char TmpByte)
{
SBUF = TmpByte;
while (0 == TI);
TI = 0;
return;
}
void UartSendString(char *pstr)
{
while (*pstr != '\0')
{
UartSendByte(*pstr);
pstr++;
}
return;
}
void Timer0Handler(void) interrupt 1
{
static unsigned char curcnt = 0;
TH0 = ReloadTH0;
TL0 = ReloadTL0;
curcnt += 10;
if (curcnt >= 100)
{
curcnt = 0;
}
if (curcnt > Duty)
{
Buzzer = 0;
}
else
{
Buzzer = 1;
}
return;
}
void SetPwmFreq(unsigned long freq, unsigned char TmpDuty)
{
unsigned long value = 0;
value = 65536UL - ((1000000UL / freq) / 10UL / 1UL);
ReloadTH0 = value / 256;
ReloadTL0 = value % 256;
TH0 = ReloadTH0;
TL0 = ReloadTL0;
Buzzer = 1;
Duty = TmpDuty;
return;
}
void Timer0Init(void)
{
TMOD &= ~0x3;
TMOD |= 0x1;
SetPwmFreq(1000, 50);
ET0 = 1;
EA = 1;
return;
}
int main(void)
{
//初始化串口波特率为2400
UartInit();
Timer0Init();
while (1)
{
if (!IsCommandLine)
{
continue;
}
if (0 == strcmp(RecvBuff, "LEDON\r\n"))
{
P2 = 0x00;
}
else if (0 == strcmp(RecvBuff, "LEDOFF\r\n"))
{
P2 = 0xFF;
}
else if (0 == strcmp(RecvBuff, "BEEPON\r\n"))
{
TR0 = 1;
}
else if (0 == strcmp(RecvBuff, "BEEPOFF\r\n"))
{
TR0 = 0;
}
else if (0 == strcmp(RecvBuff, "RELAYON\r\n"))
{
Relay = 0;
}
else if (0 == strcmp(RecvBuff, "RELAYOFF\r\n"))
{
Relay = 1;
}
UartSendString("ok\r\n");
memset(RecvBuff, 0, sizeof(RecvBuff));
RecvLen = 0;
IsCommandLine = 0;
}
return 0;
}
该实验的关键在于:串口接收采用中断方式 。当上位机发送一帧以 \r\n 结尾的命令时,中断服务函数逐字节存入缓冲区,并在收到换行符后置位 IsCommandLine 标志。主循环检测到该标志后,通过 strcmp 匹配命令并执行对应操作,最后回发 ok 确认。
常用命令如下:
- LEDON:点亮 LED(P2 口输出低电平)。
- LEDOFF:熄灭 LED(P2 口输出高电平)。
- BEEPON:开启蜂鸣器(启动定时器 0 产生 PWM)。
- BEEPOFF:关闭蜂鸣器(停止定时器 0)。
- RELAYON:吸合继电器(Relay 输出低电平)。
- RELAYOFF:断开继电器(Relay 输出高电平)。
7. 常见问题与排查
- 乱码:通常是波特率不匹配或时钟配置错误,检查双方波特率是否一致。
- 无数据:检查时钟是否使能、引脚复用是否正确、电平是否匹配。
- 偶发误码:可能是时钟误差偏大或信号干扰,尝试降低波特率或使用屏蔽线。
- 高速通信不稳定:检查过采样配置和时钟精度,必要时使用外部高精度晶振。
8. 总结
串口通信看似简单,但背后离不开时钟系统的支撑。从波特率的生成、数据位的采样,到收发双方的同步,时钟贯穿始终。理解时钟与串口的关系,不仅能帮助我们正确配置硬件,还能在遇到通信异常时快速定位问题。