蓝桥杯第十届国赛代码错误总结
代码错误列表
1.
串口波特率不匹配导致收发乱码
错误现象:
-
发送:
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: 改上位机波特率 (推荐,最简单)
-
在STC-ISP中找到 波特率 下拉框
-
改成 9600 (与代码一致)
-
点击"设置"
-
重新发送
ST\r\n
优点: 不用改代码,不用重新编译烧录
方案2: 改单片机波特率 (如果题目要求必须用4800)
使用STC-ISP的"串口波特率计算器":
-
打开STC-ISP → 工具(T) → 串口波特率计算器
-
选择: STC15系列
-
频率: 12.000 MHz
-
波特率: 4800 (或题目要求的波特率)
-
定时器: T2 (1T mode)
-
点击"生成初始化代码"
-
复制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.
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_Tem和Real_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.
温度显示前导零消除起始位置错误
错误代码:
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=2时Seg_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);
↑
匹配类型
-
Distance是unsigned 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_100x是unsigned 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 |
05 ✓ |
| 2500 | 0 | 0 |
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个空格) |
为什么不推荐在单片机上用 %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 | 慢 (软件模拟) | 需配置 |
| 整数拼接 | +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?
-
超时方案: 兼容性好,无论上位机是否发回车换行都能识别 -
检测 \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
蓝桥杯第十届国赛代码错误总结与串口通信笔记