蓝桥杯第十届国赛代码错误总结

蓝桥杯第十届国赛代码错误总结

代码错误列表


1. :cross_mark: 串口波特率不匹配导致收发乱码

错误现象:

  • 发送: ST\r\n

  • 接收: 乱码!

错误原因:

单片机代码设置的波特率:

// uart.c
void Uart1_Init(void)   //9600bps@12.000MHz
{
    // ... T2L和T2H配置对应9600波特率 ...
}

代码设置的是 9600 bps

但上位机(STC-ISP)设置的波特率是 4800 bps (或其他值)

波特率不匹配的后果:

单片机发送: 9600 bps (每位时间约104μs)
上位机接收: 4800 bps (每位时间约208μs)
          ↓
      时序完全错乱,解析出乱码!

举例说明:

单片机以9600bps发送字符'$' (0x24 = 00100100b):
  起始位 0 0 1 0 0 1 0 0 停止位
  ↓ (每位 104μs)
​
上位机以4800bps接收 (每位 208μs):
  误以为每位时间是208μs
  采样点错位,解析成完全不同的字符
  → 显示乱码 "鲑"

解决方案 (选一个):

方案1: 改上位机波特率 (推荐,最简单)

  1. 在STC-ISP中找到 波特率 下拉框

  2. 改成 9600 (与代码一致)

  3. 点击"设置"

  4. 重新发送 ST\r\n

优点: 不用改代码,不用重新编译烧录


方案2: 改单片机波特率 (如果题目要求必须用4800)

使用STC-ISP的"串口波特率计算器":

  1. 打开STC-ISP → 工具(T) → 串口波特率计算器

  2. 选择: STC15系列

  3. 频率: 12.000 MHz

  4. 波特率: 4800 (或题目要求的波特率)

  5. 定时器: T2 (1T mode)

  6. 点击"生成初始化代码"

  7. 复制T2L和T2H的值到uart.c

4800bps的配置 (@12MHz):

void Uart1_Init(void)   //4800bps@12.000MHz
{
    SCON = 0x50;
    AUXR |= 0x01;
    AUXR &= 0xFB;
    T2L = 0xCC;     // ✓ 改成4800对应的值
    T2H = 0xFF;     // ✓ 改成4800对应的值
    AUXR |= 0x10;
    ES = 1;
    EA = 1;
}

关键点:

  • 单片机和上位机的波特率必须完全一致

  • 波特率不匹配是串口通信最常见的错误

  • 症状: 收到乱码、无响应、或部分字符错误

  • 除了波特率,还要检查:

    • 数据位: 8位

    • 停止位: 1位

    • 校验位: 无

  • 调试技巧: 先用常见波特率(9600, 115200)测试


2. :cross_mark: S13从参数界面切回数据界面时,没有同步 Real_Tem / Real_Dis

错误代码:

if(Seg_Mode==0)  // 从参数界面切回数据界面
{
    Data_Mode=0;
    if((Param_Tem!=Real_Tem)||(Param_Dis!=Real_Dis))
    {
        Change++;
        // EEPROM保存代码...
        EEPROM_Write((unsigned char*)&Param_Tem,0,2);
        EEPROM_Write((unsigned char*)&Param_Dis,3,2);
        EEPROM_Write((unsigned char*)&Change,6,2);
        // ❌ 这里缺少同步!
    }
}

错误原因:

  • 保存参数到EEPROM后,没有更新 Real_TemReal_Dis

  • 导致下次再切换界面时, Param_Tem != Real_Tem 的判断仍然成立

  • 结果: Change 会被重复累加!

示例:

第1次切换:
  Param_Tem=32, Real_Tem=30 → 不相等 → Change++ (变成1) → 保存
  但Real_Tem还是30! ← ❌
​
第2次切换(参数没改):
  Param_Tem=32, Real_Tem=30 → 还是不相等! → Change++ (变成2) ← ❌ 错误!
  应该Change不变,因为参数实际没有改动

正确代码:

if(Seg_Mode==0)
{
    Data_Mode=0;
    if((Param_Tem!=Real_Tem)||(Param_Dis!=Real_Dis))
    {
        Change++;
        EEPROM_Read(&EEPROM_Temp, 22, 1);
        if(EEPROM_Temp == EEPROM_Lock)
        {
            // 写入参数
            EEPROM_Write((unsigned char*)&Param_Tem,0,2);
            EEPROM_Write((unsigned char*)&Param_Dis,3,2);
            EEPROM_Write((unsigned char*)&Change,6,2);
​
            // ✓ 方法1: 从EEPROM回读同步
            EEPROM_Read((unsigned char*)&Real_Tem,0,2);
            EEPROM_Read((unsigned char*)&Real_Dis,3,2);
​
            // ✓ 方法2: 直接赋值同步(更高效,推荐)
            // Real_Tem = Param_Tem;
            // Real_Dis = Param_Dis;
        }
    }
}

优化建议:

  • 方法1 (代码中采用): 从EEPROM回读 → 确保数据一致,但需要I2C通信开销

  • 方法2 (推荐): 直接赋值 → 更高效,省去两次I2C读取

关键点:

  • 保存参数后必须同步 Real 值

  • 否则会导致修改次数重复累加

  • LED控制依赖Real值,不同步会导致LED指示错误


3. :cross_mark: 温度显示前导零消除起始位置错误

错误代码:

case 0://温度界面
    j=2;  // ❌ 应该是 j=4
    Seg_Buf[0]=11;  // C
    Seg_Buf[1]=10;
    Seg_Buf[2]=10;
    Seg_Buf[3]=10;
    Seg_Buf[4]=Temperature_100x/1000%10;      // 温度千位
    Seg_Buf[5]=Temperature_100x/100%10+',';   // 温度百位+小数点
    Seg_Buf[6]=Temperature_100x/10%10;        // 温度十位
    Seg_Buf[7]=Temperature_100x%10;           // 温度个位
​
    while(Seg_Buf[j]==0)  // j=2时, Seg_Buf[2]=10≠0, 循环根本不会进入!
    {
        Seg_Buf[j]=10;
        j++;
        if(j==7) break;
    }
break;

错误原因:

  • j=2Seg_Buf[2] 已经是10(熄灭),不等于0

  • 循环直接跳过,前导零消除失效

  • 如果温度 < 10°C (比如8.50°C), Seg_Buf[4]=0 不会被消除

  • 显示 C 0850 而不是 C 850

正确代码:

case 0://温度界面
    j=4;  // ✓ 从数据位开始消零
    Seg_Buf[0]=11;
    Seg_Buf[1]=10;
    Seg_Buf[2]=10;
    Seg_Buf[3]=10;
    Seg_Buf[4]=Temperature_100x/1000%10;
    Seg_Buf[5]=Temperature_100x/100%10+',';
    Seg_Buf[6]=Temperature_100x/10%10;
    Seg_Buf[7]=Temperature_100x%10;

    while(Seg_Buf[j]==0)  // ✓ 从Seg_Buf[4]开始检查
    {
        Seg_Buf[j]=10;
        j++;
        if(j==7) break;
    }
break;

或者扩展数据位 (你采用的方案):

case 0://温度界面
    j=2;  // ✓ 从更前面开始
    Seg_Buf[0]=11;
    Seg_Buf[1]=10;
    Seg_Buf[2]=Temperature_100x/100000%10;  // 十万位
    Seg_Buf[3]=Temperature_100x/10000%10;   // 万位
    Seg_Buf[4]=Temperature_100x/1000%10;    // 千位
    Seg_Buf[5]=Temperature_100x/100%10+','; // 百位+小数点
    Seg_Buf[6]=Temperature_100x/10%10;
    Seg_Buf[7]=Temperature_100x%10;

    while(Seg_Buf[j]==0)  // ✓ 从Seg_Buf[2]开始消零
    {
        Seg_Buf[j]=10;
        j++;
        if(j==7) break;
    }
break;

关键点:

  • 前导零消除的起始位置 j 必须指向第一个可能为0的数据位

  • 不能从熄灭位(10)开始检查

  • 确保个位和带小数点的位不被消零 (if(j==7) break)


串口通信知识总结

一、串口处理函数逐行解释

完整代码:

void Uart_Proc()
{
    unsigned char len;//--------------------------------------自己定义的

    // 1. 检查是否有数据
    if (Uart_Rx_Index == 0)
        return;

    // 2. 等待超时,确认接收完毕
    if (Uart_Rx_Tick >= 10)
    {
        Uart_Rx_Flag = 0;
        Uart_Rx_Tick = 0;
//-------------------------------------------------------------------------------------这之间都是自己写的
        // 3. 去掉末尾\r\n
        len = Uart_Rx_Index;
        while(len > 0 && (Uart_Rx_Buf[len-1] == '\r' || Uart_Rx_Buf[len-1] == '\n'))
            len--;

        // 4. 匹配命令并响应
        if(len == 2 && Uart_Rx_Buf[0] == 'S' && Uart_Rx_Buf[1] == 'T')
        {
            printf("$%bu,%u.%02u\r\n", Distance, Temperature_100x/100, Temperature_100x%100);
        }
        else if(len == 4 && Uart_Rx_Buf[0] == 'P' && Uart_Rx_Buf[1] == 'A' && Uart_Rx_Buf[2] == 'R' && Uart_Rx_Buf[3] == 'A')
        {
            printf("#%u,%u\r\n", Param_Dis, Param_Tem);
        }
        else
        {
            printf("ERROR\r\n");
        }
//-------------------------------------------------------------------------------------这之间都是自己写的
        // 5. 清空缓冲区
        memset(Uart_Rx_Buf, 0, Uart_Rx_Index);
        Uart_Rx_Index = 0;
    }
}

逐行详解:

第1步: 检查是否有数据

unsigned char len;
if (Uart_Rx_Index == 0)
    return;
  • len → 用于存储去掉 \r\n 后的有效字符长度

  • Uart_Rx_Index == 0 → 缓冲区是空的,没有数据

  • 直接返回,不做任何处理


第2步: 等待超时

if (Uart_Rx_Tick >= 10)
{
    Uart_Rx_Flag = 0;
    Uart_Rx_Tick = 0;
  • Uart_Rx_Tick >= 10 → 距离上次接收已过10ms (超时)

  • 说明数据接收完毕,可以开始处理

  • 清除接收标志和超时计数器

为什么用10ms超时?

  • 9600波特率下,1个字节传输时间约1ms

  • 10ms足够接收完整命令 (如 “PARA\r\n” 共6字节需6ms)

  • 避免在接收过程中就开始解析


第3步: 去掉末尾 \r\n

len = Uart_Rx_Index;
  • 把接收到的总字节数赋值给 len

  • 例如收到 "ST\r\n" 共4个字节,则 len = 4

while(len > 0 && (Uart_Rx_Buf[len-1] == '\r' || Uart_Rx_Buf[len-1] == '\n'))
  • len > 0 → 确保不越界

  • Uart_Rx_Buf[len-1] → 数组最后一个字符 (索引 = len-1)

  • == '\r' || == '\n' → 判断最后一个字符是回车还是换行

  • 整句意思: 只要数组末尾是 \r\n 就一直循环

    len--;
  • len 减1,相当于"丢弃"末尾的回车/换行符

执行过程示例:

初始: Uart_Rx_Buf = ['S','T','\r','\n'], len = 4
循环1: Buf[3]='\n' 是换行 → len变成3
循环2: Buf[2]='\r' 是回车 → len变成2
循环3: Buf[1]='T' 不是\r\n → 退出循环
最终: len = 2 (只计算"ST"两个有效字符)

为什么要去掉 \r\n?

  • 不同上位机软件发送习惯不同:

    • 有的发 "ST\r\n" (回车+换行)

    • 有的发 "ST\r" (只回车)

    • 有的发 "ST\n" (只换行)

  • 去掉后统一成 len=2,只比较有效字符,兼容性最好


第4步: 匹配命令 “ST”

if(len == 2 && Uart_Rx_Buf[0] == 'S' && Uart_Rx_Buf[1] == 'T')
  • len == 2 → 有效字符长度是2

  • Uart_Rx_Buf[0] == 'S' → 第1个字符是 ‘S’

  • Uart_Rx_Buf[1] == 'T' → 第2个字符是 ‘T’

  • 整句意思: 如果收到的是 “ST” 命令

{
    printf("$%bu,%u.%02u\r\n", Distance, Temperature_100x/100, Temperature_100x%100);
}

格式化输出详解:

  • printf → 格式化输出到串口

  • "$%bu,%u.%02u\r\n" → 输出格式模板:

    • $ → 固定字符 ‘$’

    • %bu → 输出 Distance (unsigned char类型)

    • , → 固定字符 ‘,’

    • %u → 输出 Temperature_100x/100 (整数部分)

    • . → 固定字符 ‘.’

    • %02u → 输出 Temperature_100x%100 (小数部分,不足2位补0)

    • \r\n → 回车换行

实际例子:

Distance = 20, Temperature_100x = 2550
输出: "$20,25.50\r\n"
       ↑  ↑  ↑  ↑
       $  20 25 50

小数点实现原理:

Temperature_100x = 2550 (代表25.50°C)
整数部分: 2550 / 100 = 25
小数部分: 2550 % 100 = 50
拼接: "25" + "." + "50" = "25.50"

第5步: 匹配命令 “PARA”

else if(len == 4 && Uart_Rx_Buf[0] == 'P' && Uart_Rx_Buf[1] == 'A' && Uart_Rx_Buf[2] == 'R' && Uart_Rx_Buf[3] == 'A')
  • len == 4 → 有效字符长度是4

  • 逐个字符匹配 ‘P’, ‘A’, ‘R’, ‘A’

  • 整句意思: 如果收到的是 “PARA” 命令

{
    printf("#%u,%u\r\n", Param_Dis, Param_Tem);
}
  • # → 固定字符 ‘#’

  • %u → 输出 Param_Dis (设定距离,整数)

  • , → 固定字符 ‘,’

  • %u → 输出 Param_Tem (设定温度,整数)

  • \r\n → 回车换行

实际例子:

Param_Dis = 35, Param_Tem = 30
输出: "#35,30\r\n"

第6步: 其他命令返回ERROR

else
{
    printf("ERROR\r\n");
}
  • 如果既不是 “ST” 也不是 “PARA”,就输出 "ERROR\r\n"

触发场景:

收到 len 匹配结果 返回
"ST\r\n" 2 匹配ST ✓ "$20,25.50\r\n"
"PARA\r\n" 4 匹配PARA ✓ "#35,30\r\n"
"ABC\r\n" 3 长度不对 ✗ "ERROR\r\n"
"S\r\n" 1 长度不对 ✗ "ERROR\r\n"
"PART\r\n" 4 第4个字符不对 ✗ "ERROR\r\n"

第7步: 清空缓冲区

memset(Uart_Rx_Buf, 0, Uart_Rx_Index);
Uart_Rx_Index = 0;
  • 处理完一条命令后,清空缓冲区

  • 重置索引,准备接收下一条命令


二、printf格式化符号使用规则

1. 核心原则: 看变量类型选格式符

变量类型 格式符 说明
unsigned char %bu Keil C51专用 (标准C用%hhu)
unsigned int %u 无符号整数 (0~65535)
int %d 有符号整数 (-32768~32767)
unsigned long %lu 无符号长整数

2. 带格式修饰符的用法

格式符 含义 示例 输出
%02u 至少2位,不足补0 printf("%02u", 5) 05
%03d 至少3位,不足补0 printf("%03d", 42) 042
%5u 至少5位,不足补空格(右对齐) printf("%5u", 12) 12

格式符结构:

%02u
 ↑↑
 ||
 |└→ 至少显示2位数字
 └→ 不足位数用 0 补齐 (如果没有0,用空格补齐)

3. 代码中三个格式符详解

%bu - 用于 Distance

idata unsigned char Distance;  // ← 类型是 unsigned char

printf("$%bu,...", Distance);
        ↑
      匹配类型
  • Distanceunsigned char 类型 (0~255)

  • Keil C51 用 %bu 表示 unsigned char

  • 标准C用 %hhu,但Keil C51不支持


%u - 用于整数部分

idata unsigned int Temperature_100x;  // ← 类型是 unsigned int

printf("...,%u....", Temperature_100x/100);
           ↑                ↑
        匹配类型        除法结果仍是unsigned int
  • Temperature_100xunsigned int 类型

  • 2550 / 100 = 25,结果也是 unsigned int

  • 所以用 %u


%02u - 用于小数部分(带前导0)

printf("...%02u...", Temperature_100x%100);
          ↑↑             ↑
          ||           取模结果也是unsigned int
          ||
          |└→ 至少显示2位
          └→ 不足补0

实际效果对比:

Temperature_100x %100结果 %u显示 %02u显示
2550 50 50 50
2505 5 5 :cross_mark: 05
2500 0 0 :cross_mark: 00

为什么小数部分要用 %02u?

  • 保证温度显示格式统一: "25.50", "25.05", "25.00"

  • 如果用 %u,会变成: "25.50", "25.5", "25.0" (不规范!)

  • %02u 确保小数部分始终是2位数字


4. 浮点数格式符 (不推荐)

格式符 对应类型 示例 输出
%f float/double printf("%f", 25.5) 25.500000 (默认6位小数)
%.2f 保留2位小数 printf("%.2f", 25.5) 25.50
%.1f 保留1位小数 printf("%.1f", 25.567) 25.6
%6.2f 总宽度6,小数2位 printf("%6.2f", 25.5) 25.50 (前面1个空格)

:warning: 为什么不推荐在单片机上用 %f?

原因1: Keil C51 默认不支持 %f

  • 直接用会编译错误或输出乱码

  • 需要勾选 Use MicroLIB 或手动添加浮点库

  • 代价: 代码空间增加 2~3KB!

原因2: 浮点运算效率低

// ❌ 浮点方案 (慢+占空间)
float temp = rd_temperature();  // 返回25.5
printf("%.2f", temp);           // 需要浮点库支持

// ✅ 整数方案 (快+省空间)
unsigned int temp_100x = rd_temperature() * 100;  // 2550
printf("%u.%02u", temp_100x/100, temp_100x%100);  // "25.50"
方案 代码空间 运行速度 Keil C51支持
浮点 %f +2~3KB 慢 (软件模拟) 需配置 :cross_mark:
整数拼接 +100字节 快 (硬件支持) 原生支持 ✓

原因3: 蓝桥杯比赛规范

  • 禁止使用浮点printf (会被判定为不规范)

  • 必须用整数拼接实现小数显示

  • 考察选手的数据处理能力


5. 选择格式符的步骤

口诀: 先看类型,再看格式

步骤1: 看变量类型

unsigned char  count;  → %bu
unsigned int   value;  → %u
int            temp;   → %d
unsigned long  big;    → %lu

步骤2: 看输出要求

  • 需要补0? → 加 0

  • 需要指定宽度? → 加数字

  • 需要小数点? → 用整数拼接

实战示例:

unsigned char  count = 8;
unsigned int   value = 1234;
int            temp = -5;

printf("count=%bu\n", count);      // count=8
printf("count=%03bu\n", count);    // count=008 (补0到3位)

printf("value=%u\n", value);       // value=1234
printf("value=%05u\n", value);     // value=01234 (补0到5位)

printf("temp=%d\n", temp);         // temp=-5

三、串口通信工作流程

硬件中断接收 (Uart1_Isr)

void Uart1_Isr(void) interrupt 4
{
    if (RI)  // 接收中断
    {
        Uart_Rx_Flag = 1;                          // 标记"正在接收"
        Uart_Rx_Tick = 0;                          // 重置超时计数
        Uart_Rx_Buf[Uart_Rx_Index++] = SBUF;       // 存入缓冲区
        RI = 0;                                    // 清除中断标志

        if (Uart_Rx_Index > 10)                    // 防止溢出
        {
            Uart_Rx_Index = 0;
            memset(Uart_Rx_Buf, 0, 10);
        }
    }
}

关键点:

  • 每收到1个字节就中断1次 (硬件自动触发)

  • SBUF 只能存1个字节,必须立即读走

  • RI标志位 必须手动清0,否则中断会一直触发

  • Uart_Rx_Tick重置为0 → 用于后续超时判断


定时器累加超时计数 (Timer1_Isr)

void Timer1_Isr(void) interrupt 3  // 每1ms中断一次
{
    uwTick++;

    // 如果串口正在接收,累加超时计数
    if (Uart_Rx_Flag)
        Uart_Rx_Tick++;

    // ... 其他代码 ...
}

超时原理:

收到"ST\r\n"的时间轴:
0ms: 收'S' → Tick=0(重置)
1ms: 收'T' → Tick=0(重置)
2ms: 收'\r' → Tick=0(重置)
3ms: 收'\n' → Tick=0(重置)
4ms: (无数据) → Tick=1
...
12ms: (无数据) → Tick=10 ← 超时,可以处理了!

为什么用超时而不是检测 \r\n?

  • :white_check_mark: 超时方案: 兼容性好,无论上位机是否发回车换行都能识别

  • :cross_mark: 检测 \r\n: 需要在中断里逐字符解析,增加中断负担


完整流程示例

场景: PC发送 "ST\r\n",单片机返回当前数据

时间轴    硬件中断              定时器              主循环
───────────────────────────────────────────────────────
0ms    收到'S' → Buf[0]='S'   Tick累加(被重置)    等待
1ms    收到'T' → Buf[1]='T'   Tick累加(被重置)    等待
2ms    收到'\r'→ Buf[2]='\r'  Tick累加(被重置)    等待
3ms    收到'\n'→ Buf[3]='\n'  Tick累加(被重置)    等待
4ms    (无数据)               Tick=1              检查Tick<10,继续等
...
12ms   (无数据)               Tick=10             Tick>=10 ✓
                                                  ↓
                                                  去\r\n: len=2
                                                  匹配"ST" ✓
                                                  printf("$20,25.50\r\n")
                                                  清空缓冲区
13ms   (printf逐字节发送)                         等待下一条命令

总结

代码错误严重程度

严重错误 (导致系统完全不工作):

波特率不匹配 - 串口通信完全乱码,无法正常收发数据

中等错误 (功能异常或数据不一致):

S13切换界面后没同步Real值 - 导致修改次数重复累加,LED指示错误

温度前导零消除起始位置错误 - 小温度显示不规范

轻微错误 (优化建议):

温度读取逻辑可优化 - 扩展显示位数后前导零逻辑更清晰


学到的经验

1. 串口通信波特率必须匹配

  • 单片机和上位机波特率必须完全一致

  • 波特率不匹配会导致乱码或无响应

  • 调试时先检查波特率配置

  • 除波特率外,还要确保数据位、停止位、校验位一致

  • 使用STC-ISP波特率计算器生成正确配置

2. 参数保存后必须同步

  • EEPROM保存参数后,必须更新对应的Real值

  • 否则会导致重复累加或LED指示错误

  • 推荐直接赋值而不是EEPROM回读 (更高效)

3. 串口超时机制的优势

  • 10ms超时判断接收完毕,兼容性好

  • 无论上位机是否发 \r\n 都能正确识别

  • 避免在中断里做复杂解析

4. 整数拼接实现小数显示

  • 单片机上禁止用浮点printf (占空间+慢+不规范)

  • 用整数除法和取模拼接: 25.50 = 2550/100 + . + 2550%100

  • %02u 确保小数部分始终2位数字

5. printf格式符选择规则

  • 第1步: 看变量类型 → 决定用 %bu, %u, %d, %lu

  • 第2步: 看输出要求 → 需要补0就加 0, 需要指定宽度就加数字

  • 记住: Keil C51 用 %bu 而不是 %hhu

6. 前导零消除的正确方式

  • 起始位置必须是第一个可能为0的数据位

  • 不能从熄灭位(10)开始检查

  • 要保护个位和带小数点的位不被消零


复盘检查清单

在蓝桥杯单片机比赛中,遇到类似题目时,务必检查:

串口初始化:

  • main函数中确保Uart1_Init()被调用

  • 检查波特率配置与上位机一致 (9600或题目要求)

  • 数据位8位、停止位1位、校验位无

  • 使用STC-ISP波特率计算器生成正确配置

串口通信逻辑:

  • 用超时机制(10ms)判断接收完毕

  • 去掉末尾 \r\n 后再匹配命令

  • 温度用整数拼接实现小数点: %u.%02u

  • 根据变量类型选格式符: unsigned char用%bu, unsigned int用%u

  • 小数部分用 %02u 确保始终2位数字

按键处理:

  • S13切换界面后同步 Real_Tem = Param_Tem; Real_Dis = Param_Dis;

数据显示:

  • 前导零消除起始位置是第一个数据位

  • 保护个位和带小数点的位不被消零

通用规范:

  • 单片机上禁止用浮点printf (%f)

  • 优先用整数拼接实现小数显示

  • 参数修改后立即同步对应变量

  • 调试时先检查系统是否正常启动 (LED、数码管有显示)


最后更新: 2026-02-21
蓝桥杯第十届国赛代码错误总结与串口通信笔记