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

蓝桥杯第十五届国赛(智能物流机器人)代码错误总结

题目信息

  • 题目名称: 智能物流机器人

  • 考察模块: 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(无效)
:white_check_mark: 满分版(当前) bak7 完全按满分代码重写:浮点线性插值 + 200ms调度架构 85 / 85

注意:bak4 与 bak2 内容完全相同(回滚时的备份副本)。

得分修正:bak2 之前记录为 79.9 分,后来重新测评为 80.7 分(可能因超声波驱动 Ut_Wave_Data()+3 校准偏移量的改动带来微小提升)。


错误列表

1. :cross_mark: 参数没有"确认"机制 - 运行时参数实时被修改

严重等级: :red_circle: 致命(运行参数不稳定)

错误代码(第一版):

// 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. :cross_mark: 超声波障碍检测缺失 - 障碍直接穿越

严重等级: :red_circle: 致命(核心安全功能缺失)

错误代码(第一版):

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. :cross_mark: 运行中位置不更新 - 坐标始终是起点

严重等级: :red_circle: 致命(坐标显示功能失效)

错误代码(第一版):

// ISR中没有位置更新逻辑
// Key_Proc S4按下时只是 Set_Up=1,没有计算距离参数

错误原因:

第一版 S4 启动后只是设置 Set_Up=1,没有:

  1. 记录 Start_X/Start_Y(出发坐标)

  2. 计算 Total_Dist_10x(总距离)

  3. 在 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. :cross_mark: 启动时未重置频率计数器 - 位置偏差 2 倍(79.9版残余Bug)

严重等级: :red_circle: 高(坐标误差2倍)

错误代码(bak2 = 79.9版):

// Key_Proc S4启动时:
Traveled_10x = 0;
Set_Up = 1;
// ← 没有 Time_1s=0; TH0=TL0=0;

错误原因:

4T测试系统的测试流程:

  1. 启动时刻 T=0:按 S4 开始运行

  2. 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. :cross_mark: 等待状态恢复运行时旧距离未重算 - 立即变空闲(79.9版残余Bug)

严重等级: :red_circle: 高(等待恢复功能失效)

错误代码(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. :cross_mark: Count_3000ms 初值为0 - 开机L3误亮(79.9版残余Bug)

严重等级: :yellow_circle: 中等(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. :collision: 架构大改导致灾难性得分暴跌 - fix_main3 事故复盘

严重等级: :skull: 灾难性(2.9分,已回滚)

事故经过:

为了进一步提高分数,参考了另一份"学员zero"满分代码进行大规模架构重写:

  • 在 ISR 中移除所有 float 运算(理由:“C51 float库不可重入”)

  • 引入 Ctrl_Proc()(位置插值,200ms调度)和 Speed_Calc_Proc()(速度计算,200ms调度)

  • speed_x/speed_y 向量分量替代距离比例插值

  • 引入 Speed_Update_FlagMovement_Time 等新变量

结果: 编译通过但得分从79.9直接跌到2.9

原因分析:

  1. 这份"学员zero"代码与原始满分代码(十五届国赛官方)架构完全不同,不是同一套逻辑

  2. 原始满分代码的位置更新逻辑与"学员zero"不兼容,混用后逻辑冲突

  3. C51 float 实际上在主循环任务中是可以用的,"ISR不能用float"是错误判断

  4. 引入 pdata float 变量在 C51 内存模型下可能产生对齐/访问问题

  5. 调度频率从1ms改为200ms,定时器行为根本改变

教训:

❌ 错误做法: 参考未验证的第三方代码,大规模架构重写
✓ 正确做法: 基于分析出的具体扣分点,进行最小化精准修复

回滚方式:

# 立刻回滚到已验证版本
shutil.copy2('main.c.bak2', 'main.c')

8. :cross_mark: sscanf %n 格式符在 Keil C51 中不可用 — 导致所有坐标命令失效(bak5 = 2.9分的真凶)

严重等级: :skull: 灾难性(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. :cross_mark: bsp_* 驱动接口重写失败 — 驱动层不兼容导致2.9分

严重等级: :skull: 灾难性(2.9分,已废弃)

事故经过:

尝试基于满分代码逻辑(write_main.py),用 十五届国赛 项目的 bsp_* 驱动接口(bsp_init.hbsp_key.hbsp_led.hbsp_seg.h 等)完全重写 main.c

失败原因:

  1. 十五届国赛 项目和 我的考号 项目的驱动接口完全不同

    • 我的考号System_Init()Key_Read()Led_Disp()Seg_Disp()

    • 十五届国赛cls_per_init()key_read()led_disp()seg_disp()

  2. 数码管驱动机制不同(段码表、小数点处理、消隐逻辑)

  3. 重写代码虽然编译通过,但与底层驱动的交互方式不匹配

教训:

❌ 错误: 假设不同项目的驱动层可以通过改函数名互换
✓ 正确: 驱动层差异远不止函数名,还包括参数含义、调用时序、硬件配置

10. :warning: Count_3000ms=3001 和 Rec_Flag=0 修复对分数无影响

严重等级: :information_source: 信息(实测无效果)

修改内容:

  1. Count_3000ms = 0Count_3000ms = 3001(防止开机L3误亮)

  2. 到达后 Set_Up=0; 之后添加 Rec_Flag=0;(防止到达后误触发重启)

测试结果: 81.6 → 81.6(分数无变化)

可能原因:

  • 4T 测评系统可能不检测 LED3 的初始状态(开机瞬间)

  • Rec_Flag 到达后被清零的场景在 4T 测试流程中可能不会触发

  • 这两个修复理论上正确,但实际扣分点不在这里

结论: 这两个修改可以保留(无害),但不能指望它们带来分数提升。


11. :warning: 运行状态中 Vel 由 Seg_Proc 更新存在延迟

严重等级: :warning: 低风险(有轻微精度影响)

现象:

在测试 #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 :white_check_mark:
Count_3000ms 初值 = 0 = 3001 :white_circle:
到达后清 Rec_Flag Rec_Flag=0; :white_circle:

81.6版 → bak6版(4处针对性修复):无效,仍为81.6

修改 旧代码(81.6) 新代码(bak6) 分析
[Fix1] ISR立即更新Vel 位置更新用旧Vel(T=1s时可能=0) ISR读Freq后立即算Vel再更新位置 :red_circle: T=1s坐标(0,0)问题的根因
[Fix2] 等待→运行重置 Set_Up=1,Time_1s/TH0不重置 重算起点距离,Time_1s=0;TH0=TL0=0; :red_circle: 恢复后第一秒Freq短测量误差
[Fix3] Distance=0处理 0被判为<30→误进等待状态 if(Distance==0) Distance=255; :yellow_circle: 超声波超时假障碍
[Fix4] UART Wait回复 Set_Up==2时也回"Busy" 分离:Set_Up==2回"Wait",==1回"Busy" :yellow_circle: 等待状态响应不符规范

:warning: 结论:bak6 的4处修复全部属于"打补丁",没有改变1s整数积分的根本架构缺陷。
4T测评在亚秒时刻查询位置,1s粒度的方案必然给出错误坐标,打补丁无法解决。

:white_check_mark: 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按键 事件 立即用当前freqSpeed_10x,重置Movement_Time=0,计算speed_x/speed_y

其他保留的正确修复(这些修复在新架构中同样体现):

  • Barrier_Distance=0 → 255:超声波超时不误判为障碍

  • UART等待态回"Wait"、运行态回"Busy":状态响应符合规范

  • Barrier_Distance >= 30 才允许S4恢复运行

  • Gotit_Flag(替代Rec_Flag):必须先收到坐标才能S4启动

:cross_mark: 曾尝试但失败的修改(bak5 = 2.9分)

修改 说明 结果
sscanf 加 %n 验证 C51不支持%n,坐标解析全部失效 :skull: 致命
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 修复):

  1. 参数无确认机制 — Par_R_Sure/Par_B_Sure 概念缺失

  2. 障碍检测缺失 — 超声波 <30cm 无响应

  3. 位置不更新 — 无行驶进度计算与坐标插值

已修复Bug(bak2→当前版本):

  1. 启动不重置频率计数Time_1s=0; TH0=TL0=0;80.7→81.6 (+0.9)

未修复Bug(修复无效或未验证):

  1. 等待→运行旧距离不重算 — 理论正确,但单独修复未测试(被 bak5 的 %n 掩盖)

  2. Count_3000ms初值为0 — 修复后实测无分数提升

  3. 到达后Rec_Flag未清零 — 修复后实测无分数提升

工程事故(三次翻车):

  1. fix_main3 灾难(bak3) — 盲目参考"学员zero"代码,架构大改 → 2.9分

  2. sscanf %n 灾难(bak5) — C51不支持 %n,坐标解析全部失效 → 2.9分

  3. 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);
        }
    }
}

:warning: 注意: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 :white_check_mark: 满分(浮点线性插值+200ms调度架构,bak7)