蓝桥杯第十五届国赛(智能物流机器人)代码错误总结
题目信息
-
题目名称: 智能物流机器人
-
考察模块: DS18B20温度、超声波测距、PCF8591 ADC(光敏)、LED、数码管、按键、继电器、UART串口、DS1302
-
核心功能:
-
三界面切换(坐标、速度、参数)
-
编码器频率测量 → 速度计算(
speed = π·R·f/100 + B) -
UART接收目标坐标,S4启动运行
-
运行中位置插值(按行驶比例更新当前坐标)
-
超声波障碍检测 → 暂停/恢复
-
光敏ADC识别昼夜 → L2控制
-
继电器运行时吸合
-
到达后 L3 亮 3s
-
版本演进与得分
| 版本 | 备份文件 | 内容 | 得分 |
|---|---|---|---|
| 第一版 | bak | 原始提交,无参数确认、无障碍检测、无位置插值 | 未知 |
| Fix1 | bak2 | 添加 Par_R_Sure/Par_B_Sure、障碍检测、位置插值、到达判断 | 80.7 |
| Fix_main3 灾难 | bak3 | 大改架构,参考了错误的"学员zero"代码 | 2.9 (已回滚) |
| 错误修复包 | bak5 | 3处修复 + sscanf %n + 其他改动(含致命Bug) |
2.9 (已回滚) |
| bsp重写版 | — | 用 bsp_* 驱动接口完全重写 main.c(接口不兼容) | 2.9 (已废弃) |
| Fix-81.6 | main.c(旧) | bak2 + 仅1处修复:S4启动时 Time_1s=0; TH0=TL0=0; |
81.6 |
| bak6修复版 | bak6 | Fix-81.6 + 4处针对性修复(仍为1s整数积分方案,未改架构) | 81.6(无效) |
| bak7 | 完全按满分代码重写:浮点线性插值 + 200ms调度架构 | 85 / 85 |
注意:bak4 与 bak2 内容完全相同(回滚时的备份副本)。
得分修正:bak2 之前记录为 79.9 分,后来重新测评为 80.7 分(可能因超声波驱动
Ut_Wave_Data()+3校准偏移量的改动带来微小提升)。
错误列表
1.
参数没有"确认"机制 - 运行时参数实时被修改
严重等级:
致命(运行参数不稳定)
错误代码(第一版):
// Seg_Proc中直接用Par_R, Par_B计算速度(无确认版本)
Vel = 3.14 * Par_R * Freq / 100 + Par_B;
错误原因:
题目要求参数在"参数界面"修改,切回"坐标界面"后才确认生效。如果直接用 Par_R/Par_B 计算,用户在参数界面调节时速度会实时抖动,不符合规范。
正确代码:
// 切换界面时(Seg_Mode 0→1→2→0 循环,回到0时确认)
else if(Seg_Mode==0)
{
Par_R_Sure = Par_R; // 确认参数
Par_B_Sure = Par_B;
}
// 速度计算使用确认后的参数
Vel = 3.14 * Par_R_Sure * Freq / 100 + Par_B_Sure;
关键点:
-
Par_R/Par_B 是"草稿",Par_R_Sure/Par_B_Sure 是"定稿"
-
只有从参数界面切回其他界面时才确认写入 _Sure 变量
-
运行中只用 _Sure 变量,不受调节操作影响
2.
超声波障碍检测缺失 - 障碍直接穿越
严重等级:
致命(核心安全功能缺失)
错误代码(第一版):
void Get_Distance()
{
Distance = Ut_Wave_Data();
// ← 没有任何障碍响应逻辑!
}
错误原因:
第一版没有对距离 <30cm 进行任何处理,机器人在遇到障碍时不会停下,继续行驶会导致坐标计算和状态机均出错。
正确代码:
void Get_Distance()
{
Distance = Ut_Wave_Data();
if(Set_Up==1 && Distance<30) // 运行中遇障碍
{
Set_Up=2; // 进入等待状态
Bar_Flag=1; // 标记障碍存在
}
if(Set_Up==2 && Distance>=30) // 等待中障碍消除
Bar_Flag=0;
}
关键点:
-
运行(1) → 等待(2) 的唯一触发条件:超声波 < 30cm
-
Bar_Flag=1 表示"因障碍而暂停",与 Bar_Flag=0(无障碍可恢复)区分
-
障碍消除只清 Bar_Flag,不自动恢复运行,需等用户再按 S4
3.
运行中位置不更新 - 坐标始终是起点
严重等级:
致命(坐标显示功能失效)
错误代码(第一版):
// ISR中没有位置更新逻辑
// Key_Proc S4按下时只是 Set_Up=1,没有计算距离参数
错误原因:
第一版 S4 启动后只是设置 Set_Up=1,没有:
-
记录 Start_X/Start_Y(出发坐标)
-
计算 Total_Dist_10x(总距离)
-
在 ISR 中每秒累加 Traveled_10x 并按比例插值更新 Cur_X/Cur_Y
正确代码:
// S4按下时(空闲→运行)
Start_X = Cur_X;
Start_Y = Cur_Y;
dx = (long int)Target_X - Cur_X;
dy = (long int)Target_Y - Cur_Y;
Total_Dist_10x = (unsigned long int)(sqrt((double)(dx*dx + dy*dy)) * 10);
Traveled_10x = 0;
Set_Up = 1;
// ISR每1s更新位置
if(Set_Up==1 && Vel>0)
{
Traveled_10x += Vel;
if(Traveled_10x >= Total_Dist_10x)
{
Cur_X = Target_X; // 到达目标
Cur_Y = Target_Y;
Set_Up = 0;
Arrive_Flag = 1;
Count_3000ms = 0;
}
else
{
// 按行驶比例插值
Cur_X = Start_X + (long)(Target_X-Start_X) * Traveled_10x / Total_Dist_10x;
Cur_Y = Start_Y + (long)(Target_Y-Start_Y) * Traveled_10x / Total_Dist_10x;
}
}
关键点:
-
Vel单位是"距离×10/秒",Total_Dist_10x 是"总距离×10",单位一致
-
位置按行驶占比线性插值(等价于匀速直线运动)
-
(long)强转防止int×int乘法在8051上16位溢出
4.
启动时未重置频率计数器 - 位置偏差 2 倍(79.9版残余Bug)
严重等级:
高(坐标误差2倍)
错误代码(bak2 = 79.9版):
// Key_Proc S4启动时:
Traveled_10x = 0;
Set_Up = 1;
// ← 没有 Time_1s=0; TH0=TL0=0;
错误原因:
4T测试系统的测试流程:
-
启动时刻 T=0:按 S4 开始运行
-
T+1s:UART 发
#查询坐标
如果启动时不清零 Time_1s,可能出现"双重计费":
-
假设启动时 Time_1s=500(已过半秒)
-
ISR 在 T+0.5s 触发第一次 1s tick,累加 Vel 一次
-
ISR 在 T+1.5s 触发第二次 1s tick,累加 Vel 一次
-
T+1s 查询时 Traveled_10x 已累加了两次,位置是预期的 2倍!
4T测试表现: 查询(31,0),预期(16,0) → 恰好2倍误差
正确代码:
// S4启动时同时重置:
Traveled_10x = 0;
Time_1s = 0; // ← 重置1s计时器,保证下次tick精确在1s后
TH0 = TL0 = 0; // ← 重置频率计数器,保证下次Freq测量从此刻开始
Set_Up = 1;
关键点:
-
启动时必须原子性地重置三个变量:Traveled_10x、Time_1s、TH0/TL0
-
TH0/TL0 是 Timer0 外部计数器,不清零会导致 Freq 测量包含启动前的脉冲
-
Time_1s 不清零会导致第一个1s周期"偷跑"
5.
等待状态恢复运行时旧距离未重算 - 立即变空闲(79.9版残余Bug)
严重等级:
高(等待恢复功能失效)
错误代码(bak2 = 79.9版):
// S4在等待状态按下时(Bar_Flag==0表示障碍已清除)
else if(Set_Up==2 && Bar_Flag==0)
{
// ← 什么都没做,直接进入下面的 else if(Set_Up==1)
}
else if(Set_Up==1)
Set_Up=2;
更深层问题:即使有恢复逻辑,Traveled_10x 已经是暂停时的值,
再运行一段后 Traveled_10x >= Total_Dist_10x 立刻满足,
机器人瞬间跳到终点变空闲。
错误原因:
从障碍暂停恢复时,当前位置已经是中途的某个点,到目标点的剩余距离已经减少了。但原来的 Total_Dist_10x 还是从起点算的全程距离,Traveled_10x 还是暂停时累计的值。如果不重置,下一次 ISR tick 时 Traveled_10x 已经 >= Total_Dist_10x,立刻判定到达。
正确代码:
else if(Set_Up==2 && Bar_Flag==0)
{
long int dx2, dy2;
Start_X = Cur_X; // 从当前位置重新出发
Start_Y = Cur_Y;
dx2 = (long int)Target_X - Cur_X;
dy2 = (long int)Target_Y - Cur_Y;
Total_Dist_10x = (unsigned long int)(sqrt((double)(dx2*dx2+dy2*dy2)) * 10);
Traveled_10x = 0; // 重置行驶计数
Time_1s = 0;
TH0 = TL0 = 0;
Set_Up = 1;
}
关键点:
-
每次重新开始运行都要重算"从当前位置到目标的距离"
-
Traveled_10x 和 Total_Dist_10x 是相对量,不是绝对量
-
恢复时等价于"在新起点开始一段新的行程"
6.
Count_3000ms 初值为0 - 开机L3误亮(79.9版残余Bug)
严重等级:
中等(LED状态错误)
错误代码(bak2 = 79.9版):
idata unsigned int Count_3000ms = 0; // ❌ 初值0
// ISR中:
if(Arrive_Flag == 1)
{
Count_3000ms++;
if(Count_3000ms <= 3000)
Led3_Flag = 1; // L3亮
else
{
Led3_Flag = 0;
Arrive_Flag = 0;
}
}
错误原因:
4T测试系统在开机测试时,Arrive_Flag 可能通过某种初始状态触发,或 4T 测试时机导致 Count_3000ms 从0开始就 <=3000(始终成立)让 L3 在不应该亮的时候亮起,产生"错误状态前沿(false leading edge)"。
参考满分代码的做法是将计数器初值设置成 >3000 的值,确保开机时条件 cnt_3000ms < 3000 不满足,L3 初始为灭。
正确代码:
idata unsigned int Count_3000ms = 3001; // ✓ 初值>3000,开机L3不亮
关键点:
-
定时器/计数器类变量的初值要考虑"条件是否开机即满足"
-
到达标志
Arrive_Flag=0但如果Count_3000ms初值<=3000,会导致状态机逻辑错误 -
满分代码将
cnt_3000ms初始化为 3000,相当于刚好处于"倒计时结束后"的状态
7.
架构大改导致灾难性得分暴跌 - fix_main3 事故复盘
严重等级:
灾难性(2.9分,已回滚)
事故经过:
为了进一步提高分数,参考了另一份"学员zero"满分代码进行大规模架构重写:
-
在 ISR 中移除所有 float 运算(理由:“C51 float库不可重入”)
-
引入
Ctrl_Proc()(位置插值,200ms调度)和Speed_Calc_Proc()(速度计算,200ms调度) -
用
speed_x/speed_y向量分量替代距离比例插值 -
引入
Speed_Update_Flag、Movement_Time等新变量
结果: 编译通过但得分从79.9直接跌到2.9
原因分析:
-
这份"学员zero"代码与原始满分代码(十五届国赛官方)架构完全不同,不是同一套逻辑
-
原始满分代码的位置更新逻辑与"学员zero"不兼容,混用后逻辑冲突
-
C51 float 实际上在主循环任务中是可以用的,"ISR不能用float"是错误判断
-
引入
pdata float变量在 C51 内存模型下可能产生对齐/访问问题 -
调度频率从1ms改为200ms,定时器行为根本改变
教训:
❌ 错误做法: 参考未验证的第三方代码,大规模架构重写
✓ 正确做法: 基于分析出的具体扣分点,进行最小化精准修复
回滚方式:
# 立刻回滚到已验证版本
shutil.copy2('main.c.bak2', 'main.c')
8.
sscanf %n 格式符在 Keil C51 中不可用 — 导致所有坐标命令失效(bak5 = 2.9分的真凶)
严重等级:
灾难性(UART坐标解析完全失效 → 2.9分)
错误代码(bak5 版本):
// 使用了 %n 来验证是否完整匹配整个字符串
else if(sscanf(Uart_Rx_Buf, "(%u,%u)%n", &temp_x, &temp_y, &char_read) == 2
&& char_read == (int)strlen(Uart_Rx_Buf))
错误原因:
Keil C51 的标准库 sscanf 实现不支持 %n 格式符(或行为与标准C不一致)。%n 本应将"已消费的字符数"写入 char_read,但在 C51 上:
-
char_read始终是未初始化的垃圾值 -
char_read == (int)strlen(Uart_Rx_Buf)几乎永远为 false -
导致:所有
(x,y)格式的坐标命令都进入else分支 → 返回"Error" -
机器人永远收不到目标坐标 → S4 按下无反应 → 几乎所有测试项失败 → 2.9分
正确代码(bak2,简单可靠):
// 不使用 %n,仅依赖 sscanf 返回值判断
else if(sscanf(Uart_Rx_Buf, "(%u,%u)", &temp_x, &temp_y) == 2)
关键教训:
-
Keil C51 的标准库是精简版,不是完整的 ANSI C 实现
-
%n、%p、%a等高级格式符不要在 C51 中使用 -
如需验证完整匹配,改用
strlen+ 手动检查字符的方式 -
这个 Bug 极其隐蔽:编译0错误0警告,只在运行时才暴露
事故复盘:
bak5(所谓"~81.6版")实际上是从未被真正测试过的版本。~81.6 只是基于"bak2 + 3处修复"的估算值。bak5 除了3处修复,还额外引入了 sscanf %n 等改动,结果导致得分从 80.7 暴跌到 2.9。
❌ 错误: 将多个未验证的修改打包在一起,假设它们都是安全的
✓ 正确: 每次只改一处,测评验证后再改下一处
9.
bsp_* 驱动接口重写失败 — 驱动层不兼容导致2.9分
严重等级:
灾难性(2.9分,已废弃)
事故经过:
尝试基于满分代码逻辑(write_main.py),用 十五届国赛 项目的 bsp_* 驱动接口(bsp_init.h、bsp_key.h、bsp_led.h、bsp_seg.h 等)完全重写 main.c。
失败原因:
-
十五届国赛项目和我的考号项目的驱动接口完全不同:-
我的考号:System_Init()、Key_Read()、Led_Disp()、Seg_Disp() -
十五届国赛:cls_per_init()、key_read()、led_disp()、seg_disp()
-
-
数码管驱动机制不同(段码表、小数点处理、消隐逻辑)
-
重写代码虽然编译通过,但与底层驱动的交互方式不匹配
教训:
❌ 错误: 假设不同项目的驱动层可以通过改函数名互换
✓ 正确: 驱动层差异远不止函数名,还包括参数含义、调用时序、硬件配置
10.
Count_3000ms=3001 和 Rec_Flag=0 修复对分数无影响
严重等级:
信息(实测无效果)
修改内容:
-
Count_3000ms = 0→Count_3000ms = 3001(防止开机L3误亮) -
到达后
Set_Up=0;之后添加Rec_Flag=0;(防止到达后误触发重启)
测试结果: 81.6 → 81.6(分数无变化)
可能原因:
-
4T 测评系统可能不检测 LED3 的初始状态(开机瞬间)
-
Rec_Flag到达后被清零的场景在 4T 测试流程中可能不会触发 -
这两个修复理论上正确,但实际扣分点不在这里
结论: 这两个修改可以保留(无害),但不能指望它们带来分数提升。
11.
运行状态中 Vel 由 Seg_Proc 更新存在延迟
严重等级:
低风险(有轻微精度影响)
现象:
在测试 #50 中,t=1s 时查询坐标返回 (0,0) 而非 (40,54),可能与以下时序有关:
S4 按下时刻 T=0:
- Vel=0(手动清零)
- Time_1s=0, TH0=TL0=0
- Set_Up=1
T+20ms: Seg_Proc 运行,更新 Vel=671
T+1s: ISR 1s tick 触发,读 Freq,更新位置
- 依赖 Vel>0 才更新位置
- 如果 Vel 仍为 0(Seg_Proc 未来得及运行),位置不更新
问题根源: Vel 的更新依赖于独立运行的 Seg_Proc 任务,存在最长 20ms 的初始化延迟。在极端测试场景下可能导致第一个1s周期内 Vel 仍为0。
关键点:
-
满分参考代码中,速度在 UART 接收到坐标时即计算好,S4 只负责状态切换
-
我们的实现将速度计算放在 Seg_Proc 中是合理的,但 S4 启动时可以在 Key_Proc 中立即预计算一次 Vel
代码前后对比(关键变更)
第一版 (bak) → 79.9 版 (bak2):主要新增
| 功能 | 第一版 | 79.9版 |
|---|---|---|
| 参数确认 | 直接用 Par_R/Par_B | ✓ Par_R_Sure/Par_B_Sure,切界面时确认 |
| 障碍检测 | 无 | ✓ Distance<30 → Set_Up=2, Bar_Flag=1 |
| 位置插值 | 无 | ✓ ISR每1s按行驶比例更新坐标 |
| 到达判断 | 无 | ✓ Traveled_10x>=Total_Dist_10x 时到达 |
| LED3控制 | 无 | ✓ Arrive_Flag=1后3s内L3亮 |
80.7版 (bak2) → 81.6版(当前):精准修复
| 修改 | bak2(80.7) | 当前版本(81.6) | 分数影响 |
|---|---|---|---|
| S4 空闲→运行 频率重置 | 无 | Time_1s=0; TH0=TL0=0; |
+0.9 |
| Count_3000ms 初值 | = 0 |
= 3001 |
无 |
| 到达后清 Rec_Flag | 无 | Rec_Flag=0; |
无 |
81.6版 → bak6版(4处针对性修复):无效,仍为81.6
| 修改 | 旧代码(81.6) | 新代码(bak6) | 分析 |
|---|---|---|---|
| [Fix1] ISR立即更新Vel | 位置更新用旧Vel(T=1s时可能=0) | ISR读Freq后立即算Vel再更新位置 | |
| [Fix2] 等待→运行重置 | 仅Set_Up=1,Time_1s/TH0不重置 |
重算起点距离,Time_1s=0;TH0=TL0=0; |
|
| [Fix3] Distance=0处理 | 0被判为<30→误进等待状态 | if(Distance==0) Distance=255; |
|
| [Fix4] UART Wait回复 | Set_Up==2时也回"Busy" | 分离:Set_Up==2回"Wait",==1回"Busy" |
结论:bak6 的4处修复全部属于"打补丁",没有改变1s整数积分的根本架构缺陷。
4T测评在亚秒时刻查询位置,1s粒度的方案必然给出错误坐标,打补丁无法解决。
81.6版 → 85分(满分):架构完全重写(bak7,当前版本)
根本原因分析:81.6分代码用"1s整数积分"方案——每隔1秒在ISR中把Vel加到Traveled_10x,
然后用比例插值算坐标。4T测评会在T=0.2s/0.4s/0.6s等亚秒时刻发送#查询位置,
此时代码返回上一秒的坐标(甚至是初始坐标(0,0)),导致大量扣分。
解决方案:完全替换为满分代码(学员zero)的浮点线性插值+200ms调度架构。
| 维度 | 旧方案(81.6分,1s整数积分) | 新方案(85分,浮点200ms插值) |
|---|---|---|
| 位置更新频率 | 1000ms(ISR中触发) | 200ms(Ctrl_Proc调度任务) |
| 位置精度 | 整数截断,1s粒度 | float + 0.5f 四舍五入,200ms粒度 |
| 速度表示 | 标量Vel累积距离 |
speed_x/speed_y 方向分量向量 |
| 速度计算位置 | ISR / Seg_Proc(触发时机不稳定) | Speed_Calc_Proc 200ms任务,Speed_Update_Flag驱动 |
| 参数B存储 | ×10存储(-900~900,步进50) | 直接存储(-90~90,步进5) |
| 到达判断 | Traveled_10x >= Total_Dist_10x |
浮点钳制后坐标精确相等 |
| T=1s第一次更新 | Vel可能=0(旧Freq),位置不动 | S4按下时立即用当前freq算Speed_10x |
| 亚秒位置查询 | 返回上一整秒坐标(误差大) | 返回当前200ms插值坐标(误差<200ms的速度×时间) |
新架构关键函数:
| 函数 | 周期 | 作用 |
|---|---|---|
Ctrl_Proc() |
200ms | 浮点插值:new_x = Start_X + speed_x × (Movement_Time×0.1s),+0.5f四舍五入后更新坐标 |
Speed_Calc_Proc() |
200ms | 读Speed_Update_Flag→重算Speed_10x→速度变化时重算方向向量 |
| Timer1_Isr | 1ms | cnt_movement_time每100ms触发Movement_Time++;每1s读Freq设Speed_Update_Flag |
| S4按键 | 事件 | 立即用当前freq算Speed_10x,重置Movement_Time=0,计算speed_x/speed_y |
其他保留的正确修复(这些修复在新架构中同样体现):
-
Barrier_Distance=0 → 255:超声波超时不误判为障碍 -
UART等待态回"Wait"、运行态回"Busy":状态响应符合规范
-
Barrier_Distance >= 30才允许S4恢复运行 -
Gotit_Flag(替代Rec_Flag):必须先收到坐标才能S4启动
曾尝试但失败的修改(bak5 = 2.9分)
| 修改 | 说明 | 结果 |
|---|---|---|
sscanf 加 %n 验证 |
C51不支持%n,坐标解析全部失效 |
|
| UART 等待状态回复 “Wait” | 分离 state==2 的坐标响应 | 未知(被 %n 掩盖)→ bak6已正确修复 |
| 超声波 +3 校准 | Ut_Wave_Data()+3 |
可能有微小正面影响 |
速度计算公式说明
本题速度公式:
速度 = π × R × 转速(Hz) / 100 + B
单位约定(本代码实现):
| 变量 | 存储值 | 实际含义 | 例子 |
|---|---|---|---|
Par_R |
R × 10 | 轮子半径(整数化存储) | R=1.3 → Par_R=13 |
Par_B |
B × 10 | 偏移量(整数化存储) | B=10 → Par_B=100 |
Par_R_Sure |
R × 10 | 已确认的半径 | 同上 |
Par_B_Sure |
B × 10 | 已确认的偏移 | 同上 |
Vel |
速度 × 10 | 单位: 0.1单位/s | v=67.1 → Vel=671 |
Total_Dist_10x |
距离 × 10 | 单位: 0.1单位 | d=200 → =2000 |
Traveled_10x |
已行驶 × 10 | 每秒 += Vel | - |
// 代码中的实际公式(Vel和Par_R_Sure都是×10存储,互相抵消):
Vel = 3.14 * Par_R_Sure * Freq / 100 + Par_B_Sure;
// 等价于: Vel×10_unit = π × (R×10) × f / 100 + (B×10)
// Vel = π × R × f / 100 + B (单位: 距离/s,×10存储)
数值验证(R=1.3, B=10, Freq=1400, 目标(120,160)):
Vel = 3.14 × 13 × 1400 / 100 + 100 = 571 + 100 = 671
Total_Dist_10x = sqrt(120²+160²) × 10 = 200 × 10 = 2000
1s后行驶: Traveled_10x = 671
比例: 671/2000 = 0.3355
Cur_X = 0 + 120 × 0.3355 = 40.26 ≈ 40 ✓
Cur_Y = 0 + 160 × 0.3355 = 53.68 ≈ 53 (预期54,有±1误差)
总结
致命错误(功能完全失效,第一版→bak2 修复):
-
参数无确认机制 — Par_R_Sure/Par_B_Sure 概念缺失
-
障碍检测缺失 — 超声波 <30cm 无响应
-
位置不更新 — 无行驶进度计算与坐标插值
已修复Bug(bak2→当前版本):
- 启动不重置频率计数 —
Time_1s=0; TH0=TL0=0;→ 80.7→81.6 (+0.9)
未修复Bug(修复无效或未验证):
-
等待→运行旧距离不重算 — 理论正确,但单独修复未测试(被 bak5 的 %n 掩盖)
-
Count_3000ms初值为0 — 修复后实测无分数提升
-
到达后Rec_Flag未清零 — 修复后实测无分数提升
工程事故(三次翻车):
-
fix_main3 灾难(bak3) — 盲目参考"学员zero"代码,架构大改 → 2.9分
-
sscanf
%n灾难(bak5) — C51不支持%n,坐标解析全部失效 → 2.9分 -
bsp_* 重写失败 — 驱动接口不兼容,编译通过但功能全部异常 → 2.9分
学到的经验
1. 最小化修复原则
❌ 错误: 看到别人的"更好架构"就全部重写
✓ 正确: 找到具体扣分点 → 定位最小代码差异 → 精准修复
每次修改前先备份(.bak2, .bak3…),出问题立刻回滚。
2. 状态机中的计数器初值
如果判断条件是 counter <= 3000,那么:
初值 0 → 条件开机即成立(BUG)
初值 3001 → 条件开机不成立(正确)
任何条件计数器,在不应该触发的初始状态,都要确保初值让条件不满足。
3. 重新启动时必须重置所有相关状态
// 每次从"停止"→"运行"都需要原子性地重置:
Start_X = Cur_X; // 起点坐标
Start_Y = Cur_Y;
Total_Dist_10x = ...; // 到目标的距离(每次都重算)
Traveled_10x = 0; // 已行驶清零
Time_1s = 0; // 1s计时器清零
TH0 = TL0 = 0; // 频率计数器清零
遗漏任何一个,都可能导致第一个测量周期数据错误。
4. GBK 编码文件修改规则(血泪教训)
Keil C51 项目文件是 GBK 编码,直接用 Read/Write/Edit 工具会把中文注释变成乱码!
# 唯一正确的修改方式:
with open(filepath, 'r', encoding='gbk') as f:
lines = f.readlines()
# ... 修改 lines ...
with open(filepath, 'w', encoding='gbk', newline='\r\n') as f:
f.writelines(lines)
5. Keil C51 标准库的坑(血泪教训)
C51 的 stdio.h 是精简实现,不完全兼容 ANSI C!
已确认不可用或有风险的特性:
❌ sscanf/sprintf 的 %n 格式符 — 编译通过但运行时不写入值
❌ sscanf/sprintf 的 %p、%a 格式符 — 可能不支持
⚠️ float 格式化(%f)— 需要链接 float 库,增大代码体积
✓ %u、%d、%s、%c — 安全可用
✓ printf 重定向 putchar — 标准做法,安全可用
验证原则:
在 C51 上用 sscanf/sprintf 新格式符前,先写个测试:
char buf[] = "(123,456)";
int n = -1;
sscanf(buf, "(%u,%u)%n", &x, &y, &n);
printf("n=%d", n); // 如果输出 n=-1,说明 %n 不支持
6. 每次只改一处,测评验证后再改下一处
❌ bak5 的教训:一次性打包了 6+ 处修改
→ 其中一处(%n)是致命Bug,但被其他修改掩盖
→ 调试时无法确定到底是哪处改动导致的2.9分
→ 最终只能整体回滚,浪费所有修改
✓ 正确做法:
bak2 (80.7) → +Time_1s修复 → 测评81.6 ✓
→ +下一个修复 → 测评??
→ 如果分数下降 → 立刻撤回这一处
黄金法则:改一处 → 编译 → 测评 → 确认分数 → 再改下一处。
7. 位置插值的两种正确方案
方案A(本代码采用):距离比例法(整数运算)
// 每1s累加Vel,按总距离比例插值
Traveled_10x += Vel;
Cur_X = Start_X + (Target_X-Start_X) * Traveled_10x / Total_Dist_10x;
-
优点:全整数运算,不涉及float,ISR安全
-
缺点:位置更新1s一次,精度较低
方案B(参考满分代码采用):时间分量法(浮点运算,主循环任务)
// 启动时分解速度向量
speed_x = speed * dx / dist; // 速度在x方向的分量
speed_y = speed * dy / dist; // 速度在y方向的分量
// 定时任务中按时间更新位置
x_cur = x_start + speed_x * time_units * 0.1f;
-
优点:精度高,响应快
-
缺点:需要float,要放在主循环任务中(不能在ISR里)
复盘检查清单
蓝桥杯单片机比赛中,遇到类似"运动控制/坐标计算"题目时,务必检查:
参数与状态
-
参数是否有"确认"机制? 运行用 _Sure 变量,调节用普通变量
-
状态机计数器初值是否正确? 让开机时不需要触发的条件不成立
-
启动时是否原子性重置所有相关计数器?(距离、时间、频率计数)
运动控制
-
障碍检测是否完整? 检测到障碍 → Set_Up=2+Bar_Flag=1,消除 → Bar_Flag=0
-
从等待恢复运行时,是否重新计算"当前位置到目标的距离"?
-
到达判断是否正确? Traveled_10x >= Total_Dist_10x 时直接设定到目标坐标
位置插值
-
Vel 和 Total_Dist_10x 单位是否一致? 两者都要×10(或都不×10)
-
整数乘法是否会溢出?
(int)×(int)在C51上是16位,需要(long)提升 -
ISR中是否有float运算? C51 ISR中用float不一定能编译通过或运行正确,尽量用整数
C51 标准库
-
sscanf/sprintf 是否用了
%n等高级格式符? C51 不支持,必须用简单格式 -
修改后是否只改了一处? 每次只改一处,测评验证后再继续
GBK文件
-
修改Keil工程文件前是否确认编码?
file --mime-encoding "xxx.c" -
是否用Python+gbk编码读写? 绝不用Read/Write/Edit工具直接操作GBK文件
串口(UART)逻辑设计思路
整体设计模式:空闲超时检测法
本题串口的核心难点是:接收到的消息长度不固定(? 是1字节,# 是1字节,(120,160) 是9字节),不能用"固定长度"或"特定结束符"来判断一帧数据是否接收完毕。
解决方案:检测"沉默超时"来识别帧尾 —— 如果连续 10ms 没有收到新字节,就认为这一帧已经完整接收。
三层协作架构
┌──────────────────────────────────────────────────────────────────┐
│ 第一层:Uart1_Isr (中断,每收到一个字节触发) │
│ → 把字节存入 Uart_Rx_Buf[] │
│ → 设置 Uart_Rx_Flag=1("有新数据") │
│ → 重置 Uart_Rx_Tick=0("刚收到字节,超时重新计时") │
├──────────────────────────────────────────────────────────────────┤
│ 第二层:Timer1_Isr (1ms 定时中断) │
│ → 如果 Uart_Rx_Flag==1,则 Uart_Rx_Tick++ │
│ → 即:测量"自上次收到字节后过了多少ms" │
├──────────────────────────────────────────────────────────────────┤
│ 第三层:Uart_Proc (调度器任务,每 10ms 执行一次) │
│ → 检查 Uart_Rx_Tick >= 10(连续10ms没有新字节) │
│ → 满足条件则处理缓冲区中的完整帧 │
└──────────────────────────────────────────────────────────────────┘
时序图
时间轴: 0ms 1ms 2ms ... 8ms 9ms 10ms 11ms ... 20ms
│ │ │ │
UART收到: '(' '1' ')' (无)
↑
Uart_Rx_Tick: 0 0 ... 0 1 2 3 ... 10
↑
Uart_Proc 检测到 >=10ms
→ 开始处理帧 "(120,160)"
为什么选 10ms?
-
9600bps 下每字节传输约 1ms,字节间间隔远小于 10ms
-
10ms 足以区分"帧内间隔"和"帧结束后的静默"
-
调度器本身就是 10ms 间隔调度,选 10ms 对齐调度周期
变量说明
idata unsigned char Uart_Rx_Index; // 缓冲区写入位置(下一个字节存哪里)
pdata unsigned char Uart_Rx_Buf[10]; // 接收缓冲区(最大10字节)
idata unsigned char Uart_Rx_Flag; // 标志位:1=缓冲区中有数据待处理
idata unsigned char Uart_Rx_Tick; // 超时计时器:自上次收到字节后的ms数
ISR 代码逻辑
void Uart1_Isr(void) interrupt 4
{
if (RI) // 只处理"接收完成"中断(忽略发送完成中断TI)
{
Uart_Rx_Flag = 1; // 标记"有数据"
Uart_Rx_Tick = 0; // 重置超时(刚收到字节)
Uart_Rx_Buf[Uart_Rx_Index++] = SBUF; // 存入缓冲区
RI = 0; // 手动清除接收中断标志!
// 溢出保护:超过10字节强制丢弃整帧
if (Uart_Rx_Index > 10)
{
Uart_Rx_Index = 0;
memset(Uart_Rx_Buf, 0, 10);
}
}
}
注意:C51 中 RI 不会自动清零,必须手动
RI = 0,否则中断会无限触发!
Uart_Proc 处理逻辑
void Uart_Proc()
{
idata unsigned int temp_x, temp_y; // C89规范:变量必须在函数头声明
if (Uart_Rx_Index == 0) return; // 缓冲区空,直接返回
if (Uart_Rx_Tick >= 10) // 超时 = 一帧接收完毕
{
Uart_Rx_Flag = 0;
Uart_Rx_Tick = 0;
// ── 命令解析 ──────────────────────────────────────────
if (Uart_Rx_Index==1 && Uart_Rx_Buf[0]=='?')
{
// 查询运行状态
if(Set_Up==0) printf("Idle");
else if(Set_Up==1) printf("Busy");
else printf("Wait");
}
else if (Uart_Rx_Index==1 && Uart_Rx_Buf[0]=='#')
{
// 查询当前坐标
printf("(%u,%u)", Cur_X, Cur_Y);
}
else if (sscanf(Uart_Rx_Buf, "(%u,%u)", &temp_x, &temp_y) == 2)
{
// 设置目标坐标(仅空闲状态接受)
if (Set_Up == 0)
{
Target_X = temp_x;
Target_Y = temp_y;
Rec_Flag = 1; // 标记"已收到目标坐标"
printf("Got it");
}
else
printf("Busy"); // 运行中/等待中不接受新目标
}
else
printf("Error"); // 无法识别的命令
// 清空缓冲区,准备下一帧
memset(Uart_Rx_Buf, 0, Uart_Rx_Index);
Uart_Rx_Index = 0;
}
}
通信协议汇总
| 上位机发送 | 含义 | 返回(空闲) | 返回(运行中) | 返回(等待中) |
|---|---|---|---|---|
? |
查询运行状态 | Idle |
Busy |
Wait |
# |
查询当前坐标 | (x,y) |
(x,y) |
(x,y) |
(x,y) |
设置目标坐标 | Got it + Rec_Flag=1 |
Busy |
Busy |
| 其他 | 无法识别 | Error |
Error |
Error |
关键设计细节
1. 为什么 printf 能直接发送串口?
uart.c 中通过重定向 putchar() 函数,将标准库 printf 的输出绑定到 UART1 发送。这是 C51 的标准做法,不需要自己实现发送函数。
2. 坐标仅在空闲状态才接受
运行中(Set_Up==1) 或 等待中(Set_Up==2) 收到坐标 → 回复 "Busy",不更新目标
这意味着如果想更改目标,必须先暂停(Set_Up→2),再停止,再发坐标。
3. sscanf 解析格式
sscanf(Uart_Rx_Buf, "(%u,%u)", &temp_x, &temp_y)
// 匹配格式: 左括号 + 无符号整数 + 逗号 + 无符号整数 + 右括号
// 返回值: 成功解析的参数个数,==2 说明解析到了 x 和 y
用 %u(无符号整数)而非 %d(有符号整数),因为坐标都是正数,与变量类型 unsigned int 一致。
4. Rec_Flag 的作用
UART 收到坐标 → Rec_Flag=1("已有待执行的目标")
↓
S4 按下(Set_Up==0 且 Rec_Flag==1)→ 计算距离,Set_Up=1,Rec_Flag=0
↓
如果 S4 按下但 Rec_Flag==0 → 不启动(没有收到目标坐标)
Rec_Flag 是 UART 任务与按键任务之间的"握手信号"。
5. 防缓冲区溢出设计
if (Uart_Rx_Index > 10) // 超过缓冲区大小
{
Uart_Rx_Index = 0;
memset(Uart_Rx_Buf, 0, 10); // 丢弃整帧,重新开始
}
10字节缓冲区能覆盖最长命令 (xxx,yyy) = 9字节,有1字节余量。若超出说明收到了非法数据,直接丢弃。
数据流全图
P3.0(RXD)
│ 收到字节
▼
[Uart1_Isr]
SBUF → Uart_Rx_Buf[Uart_Rx_Index++]
Uart_Rx_Flag = 1
Uart_Rx_Tick = 0
RI = 0
│
▼ (同时)
[Timer1_Isr 每1ms]
if(Uart_Rx_Flag) Uart_Rx_Tick++
│
▼ (Uart_Rx_Tick达到10)
[Uart_Proc 每10ms轮询]
判断 Uart_Rx_Tick >= 10
→ 解析命令
→ 更新 Target_X/Y + Rec_Flag(坐标命令)
→ printf 回复上位机(所有命令)
→ 清空缓冲区
printf("Idle"/"Busy"/"Wait"/"Got it"/"Error"/"(x,y)")
│
▼
P3.1(TXD) → 上位机
生成时间: 2026-02-24 | 最后更新: 2026-03-01
蓝桥杯第十五届国赛(智能物流机器人)代码错误总结
当前最高分: 85 / 85满分(浮点线性插值+200ms调度架构,bak7)