UART串口通信——蓝桥杯国赛必考的模块,从初始化到完整协议实现
·
串口通信在近年蓝桥杯中的地位越来越重要。第15届国赛直接把串口作为核心交互方式——上位机发坐标给单片机,单片机回传状态和位置。这篇文章把串口相关的全部知识点讲透。
硬件基础
STC15F2K60S2有两路串口,蓝桥杯用的是UART1:
- RXD:P3.0
- TXD:P3.1
- 通信参数:9600bps, 8N1(8数据位、无校验、1停止位)
串口初始化
使用Timer2作为波特率发生器:
void Uart_Init(void) // 9600bps@12.000MHz
{
SCON = 0x50; // SM0=0, SM1=1 (模式1), REN=1 (允许接收)
AUXR |= 0x01; // 串口1选择Timer2作为波特率发生器
AUXR &= 0xFB; // Timer2时钟12T模式
T2L = 0xE6; // Timer2初值低字节
T2H = 0xFF; // Timer2初值高字节
AUXR |= 0x10; // 启动Timer2
ES = 1; // 开串口中断
EA = 1; // 开总中断
}
SCON寄存器拆解
SCON = 0x50 = 0101_0000
bit7 SM0 = 0 \
bit6 SM1 = 1 > 模式1:8位UART,可变波特率
bit5 SM2 = 0 / 多机通信(不用)
bit4 REN = 1 允许接收
bit3 TB8 = 0 发送第9位(不用)
bit2 RB8 = 0 接收第9位(不用)
bit1 TI = 0 发送中断标志(软件清零)
bit0 RI = 0 接收中断标志(软件清零)
printf重定向
这是一个非常实用的技巧——通过重写 putchar 函数,让标准库的 printf 直接输出到串口:
#include <stdio.h>
extern char putchar(char ch) {
SBUF = ch; // 把字符放入发送缓冲区
while(TI == 0); // 等待发送完成
TI = 0; // 清除发送标志
return ch;
}
这样你在代码里就能直接写:
printf("Got it"); // 发送字符串
printf("(%u,%u)", Pos_Now_X, Pos_Now_Y); // 格式化输出坐标
printf("Idle"); // 发送状态
重要提醒:while(TI == 0) 是阻塞式的。这意味着在等待发送完成期间,CPU什么都不做。在主循环里用没问题,但绝对不能在中断服务函数里调用printf。中断里要用的话,直接写 SBUF = ch,然后在TI中断里处理后续字符。
中断接收 + 超时帧检测
串口接收的难点在于:一帧数据有多个字节,你需要判断什么时候"一帧结束了"。方案是用超时检测——如果超过一定时间没有收到新字符,就认为一帧结束。
串口接收中断
/* 接收缓冲区和状态变量 */
pdata uc Uart_Buf[16] = {0}; // 接收缓冲区
idata uc Uart_Recv_Index; // 写入指针
idata uc Uart_Recv_Time; // 超时计时
idata uc Uart_Recv_Flag; // 接收中标志
void Uart_Server() interrupt 4 {
if(RI) {
Uart_Recv_Time = 0; // 每收到一个字节就重置超时计时
Uart_Recv_Flag = 1; // 标记正在接收
Uart_Buf[Uart_Recv_Index++] = SBUF; // 存入缓冲区
RI = 0; // 清除接收标志
if(Uart_Recv_Index > 16) { // 防止缓冲区溢出
Uart_Recv_Index = 0;
memset(Uart_Buf, 0, 16);
}
}
}
Timer1中断中递增超时计数
void Time1_Server() interrupt 3 {
uwTick++;
if(Uart_Recv_Flag == 1)
Uart_Recv_Time++; // 每毫秒+1
// ...
}
主循环中检测超时并解析
void Uart_Proc() {
if(Uart_Recv_Index == 0)
return; // 没有数据直接返回
if(Uart_Recv_Time >= 10) { // 10ms无新数据 = 一帧结束
Uart_Recv_Time = 0;
Uart_Recv_Flag = 0;
// 解析坐标数据
Pos_Goal_x_Old = Pos_Goal_X; // 保存旧值(解析失败时恢复)
Pos_Goal_y_Old = Pos_Goal_Y;
if(sscanf(Uart_Buf, "(%u,%u)", &Pos_Goal_X, &Pos_Goal_Y) == 2) {
if(Running_Mode == 0) {
printf("Got it"); // 空闲时接收成功
Uart_Get_Flag = 1;
} else {
printf("Busy"); // 忙碌时拒绝
Pos_Goal_X = Pos_Goal_x_Old; // 恢复旧值
Pos_Goal_Y = Pos_Goal_y_Old;
}
}
else if(strcmp(Uart_Buf, "#") == 0) {
printf("(%u,%u)", Pos_Now_X, Pos_Now_Y); // 查询当前位置
}
else if(strcmp(Uart_Buf, "?") == 0) {
switch(Running_Mode) {
case 0: printf("Idle"); break;
case 1: printf("Wait"); break;
case 2: printf("Busy"); break;
}
}
else {
printf("Error"); // 无法识别的命令
}
// 清空缓冲区
memset(Uart_Buf, 0, Uart_Recv_Index);
Uart_Recv_Index = 0;
}
}
超时检测的时间线
串口数据流: ( 1 2 3 , 4 5 6 ) \n
↑ ↑
收到第一个字节 收到最后一个字节
时间线:
t=0ms: 收到'(',Uart_Recv_Time = 0
t=1ms: 定时器中断,Uart_Recv_Time = 1
t=2ms: 收到'1',Uart_Recv_Time = 0(重置)
t=3ms: 定时器中断,Uart_Recv_Time = 1
...
t=8ms: 收到')',Uart_Recv_Time = 0
t=18ms: 定时器中断,Uart_Recv_Time = 10 → 触发解析!
10ms的超时阈值是经验值。9600bps下每个字符约1ms传输时间,帧间间隔通常不会超过几毫秒,10ms足够区分帧边界了。
第15届国赛的串口协议设计
这是我认为国赛代码里设计得最好的部分。完整的命令/响应协议:
发送端命令:
(x,y)— 设置目标坐标(两位无符号整数)#— 查询当前位置?— 查询运行状态
单片机响应:
Got it— 成功接收坐标Busy— 当前忙碌,拒绝接收Idle— 空闲状态Wait— 暂停(等待障碍物清除)Error— 命令格式错误
状态机:
收到坐标时:
空闲 → Got it + 进入运行
暂停 → Busy(拒绝)
运行 → Busy(拒绝)
收到#时:
任何状态 → 返回当前位置
收到?时:
任何状态 → 返回当前状态
有一个细节:当运行中收到坐标命令被拒绝时,代码会恢复旧的目标坐标:
Pos_Goal_x_Old = Pos_Goal_X; // 解析前保存
// ... 尝试解析 ...
// 解析失败或被拒绝
Pos_Goal_X = Pos_Goal_x_Old; // 恢复
这个"保存-尝试-回滚"的模式在处理外部输入时很实用,防止脏数据污染当前状态。
调试技巧
如果串口通信不正常,按这个顺序排查:
- 收到乱码 → 波特率不匹配。检查晶振频率和T2初值
- printf不输出 → 检查TI标志是否清零了
- 接收数据不完整 → 超时时间太短,调大到20试试
- sscanf解析失败 → 用串口助手确认发送端格式,注意括号和逗号不能少
最简单的串口测试方法:
// 收到'A'回复'OK'
if(Uart_Buf[0] == 'A')
printf("OK");
这个测试能同时验证收发两条路径是否都通。调通了再写复杂协议。
更多推荐



所有评论(0)