串口通信在近年蓝桥杯中的地位越来越重要。第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;  // 恢复

这个"保存-尝试-回滚"的模式在处理外部输入时很实用,防止脏数据污染当前状态。


调试技巧

如果串口通信不正常,按这个顺序排查:

  1. 收到乱码 → 波特率不匹配。检查晶振频率和T2初值
  2. printf不输出 → 检查TI标志是否清零了
  3. 接收数据不完整 → 超时时间太短,调大到20试试
  4. sscanf解析失败 → 用串口助手确认发送端格式,注意括号和逗号不能少

最简单的串口测试方法:

// 收到'A'回复'OK'
if(Uart_Buf[0] == 'A')
    printf("OK");

这个测试能同时验证收发两条路径是否都通。调通了再写复杂协议。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐