第十五届国赛:复盘

:trophy: 第十五届单片机国赛:复盘

Writer: 你还真是满奈子想着你的篮球杯呢

:open_book: 导语
本文档完整记录了本项目从"基础功能实现"到"底层架构重构"的全过程。按照实际开发中遇到的问题顺序,从基础逻辑、串口通信、坐标运动算法,一路打磨到系统底层与状态机设计。


:bullseye: 阶段一:基础功能与逻辑控制陷阱

:warning: 问题 1:LED 反复亮灭(电平触发引发的无限循环)

:bullseye: 目标功能
设备到达目标坐标后,LED 亮 3 秒,然后熄灭(且只触发一次)。

:cross_mark: 初始错误代码:

if(Time_3s < 3000) {
    Reach_Flag = (X_Equipment == X_Destination && Y_Equipment == Y_Destination);
}
// 中断中:
if(++Time_3s == 3000) { Reach_Flag = 0; }

:magnifying_glass_tilted_left: 现象与分析:
这是一个电平触发(Level Trigger)。只要坐标相等的条件成立,系统就会不断把 Reach_Flag 置 1。导致现象为:置1 → 计时3秒 → 清0 → 下一轮又被置1 → 无限循环反复亮灭。

:white_check_mark: 改进方案(边沿触发):
引入当前状态与上一次状态的对比,只在"未到达 → 到达"的瞬间触发一次。


:warning: 问题 2:边沿触发失效(作用域错误)

:cross_mark: 初始错误代码:

idata bit Last_Reach; // 写在函数内部

:magnifying_glass_tilted_left: 现象与分析:
每次函数调用都会重新定义变量,值不被保存(默认为 0)。导致 Current = 1, Last = 0 永远成立,还是重复触发。

:white_check_mark: 改进方案:
状态变量必须是全局变量或静态变量。

idata bit Last_Reach = 0; // 必须全局且赋初值

:warning: 问题 3:计时逻辑高耦合(标志位职责混乱)

:magnifying_glass_tilted_left: 问题分析:
Reach_Flag 同时承担了"控制 LED"和"控制计时"两个角色,耦合过高,逻辑混乱。

:white_check_mark: 改进方案(信号解耦):
将信号拆分为 Reach_Trig(单次触发)和 Reach_Show(持续显示)。

idata bit Reach_Trig;  // 触发信号(一次)
idata bit Reach_Show;  // 显示信号(3秒)
​
// 1. 到达检测 (上升沿)
Current_Reach = (X_Equipment==X_Destination && Y_Equipment==Y_Destination);
if(Current_Reach && !Last_Reach) {
    Reach_Trig = 1;
    Time_3s = 0;
    Status = 3; // 推荐加入完整状态机设计:3-到达
}
Last_Reach = Current_Reach;
​
// 2. 定时器中断执行
if(Reach_Trig) {
    Reach_Show = 1;
    if(++Time_3s >= 3000) {
        Reach_Show = 0;
        Reach_Trig = 0;
        Status = 0; // 回到空闲
    }
}
​
// 3. LED控制
ucLed[2] = Reach_Show;

:light_bulb: 核心总结
这几步本质上解决了**“如何把一个持续条件,变成一次事件”**的核心痛点。


:satellite_antenna: 阶段二:串口通信与数据解析问题

:warning: 问题 4:命令解析不完整与负数下溢

:magnifying_glass_tilted_left: 问题分析:
原代码只能解析坐标 (x,y)。且解析负数 B 参数时,使用 %bu 会导致解析错误。

:white_check_mark: 改进方案:
增加多分支解析,并利用 %bd 处理有符号 8 位整数。

// 解析坐标:(100,233)
if(sscanf(Uart_Rx_Buf, "(%bu,%bu)", &x, &y) == 2) {
    X_Destination = x; Y_Destination = y;
}
// 解析状态:S0 S1 S2
else if(sscanf(Uart_Rx_Buf, "S%bu", &x) == 1 && x <= 2) {
    Status = x;
}
// 解析R参数:R15
else if(sscanf(Uart_Rx_Buf, "R%bu", &x) == 1 && x >= 10 && x <= 20) {
    R_Para = x;
}
// 解析B参数(支持负数):B-45 或 B30
else if(sscanf(Uart_Rx_Buf, "B%bd", &x) == 1) {
    B_Para = (char)x;
}

:pushpin: 附录:STC15 printf/sscanf 格式符对照表

格式符 对应类型 说明
%bd char 有符号8位整数(可解析负数)
%bu unsigned char 无符号8位整数
%d / %u int / unsigned int 16位整数(有符号/无符号)
%f float 浮点数(sscanf支持不完整,慎用)
%s char * 字符串
%bx unsigned char 十六进制无符号8位

:memo: 注:b 前缀是 Keil C51 特有,表示 byte


:automobile: 阶段三:坐标模拟运动开发的阵痛与重构

:warning: 问题 5:坐标计算的类型下溢与方向写反

:cross_mark: 初始错误代码:

idata unsigned char DetaX; // 错误类型
X_Equipment -= step_x;     // 方向写反

:magnifying_glass_tilted_left: 现象与分析:
当目标小于当前坐标时,unsigned char 下溢变成大正数(-5 变 251)。方向用减法导致向正方向时坐标乱跳(0→255→254)。

:white_check_mark: 改进方案:
改用有符号 int,更新坐标使用加法。


:warning: 问题 6:步进叠加导致越走越快

:cross_mark: 初始错误代码:

unsigned char step_x = (unsigned char)((reduce * DetaX) / Init_Distance);
X_Equipment += step_x; // 错误:每次把累积量又加了一遍

:magnifying_glass_tilted_left: 现象与分析:
由于 step_x 是从起点算起的"总位移",每次用 += 叠加会导致误差成倍放大。

:white_check_mark: 阶段性改进方案(记录起点法):

X_Equipment = X_Equipment_Origin + (long)Walked_Distance * DetaX / Init_Distance;

:collision: 问题 7(致命):1 秒跳跃跳出轨迹(核心物理模型重构)

:magnifying_glass_tilted_left: 问题深度分析:

上述方案采用"每 1 秒在定时器里跳跃一次" + “按累计距离判断到达”,存在三大致命缺陷:

  1. :stopwatch: 不平滑:1 秒更新一次,小车走走停停。

  2. :bullseye: 易偏离:累计距离法只要方向稍微算错,累计距离够了但坐标对不上。

  3. :prohibited: 无修正:全程沿用初始计算的 DetaX/Y,无实时修正能力。

:white_check_mark: 终极重构方案(1ms 连续微小步进):

将运动逻辑移入 1ms 调度的 Ctrl_Proc,每次计算"当前到目标的剩余距离"和实时方向向量

idata unsigned long int Last_Tick = 0;
idata bit First_Flag = 0;
pdata float X_Float = 0, Y_Float = 0;       // 必须用浮点防精度截断
pdata float X_Dest_Float = 0, Y_Dest_Float = 0;
​
void Ctrl_Proc() {
    idata unsigned long int Dt_Ms;
    idata float Dt, Dx, Dy, Dist, Step;
​
    if(Status != 1) return;
​
    // 1. 首次启动记录浮点起点与时间基准
    if(First_Flag == 0) {
        X_Float = (float)X_Equipment; Y_Float = (float)Y_Equipment;
        X_Dest_Float = (float)X_Destination; Y_Dest_Float = (float)Y_Destination;
        Last_Tick = uwTick;
        First_Flag = 1;
        return;
    }
​
    // 2. 计算本次时间差 dt
    Dt_Ms = uwTick - Last_Tick;
    Last_Tick = uwTick;
    if(Dt_Ms == 0) return;
    Dt = (float)Dt_Ms / 1000.0f;
​
    // 3. 实时计算剩余距离与方向
    Dx = X_Dest_Float - X_Float;
    Dy = Y_Dest_Float - Y_Float;
    Dist = sqrt(Dx*Dx + Dy*Dy);
    if(Dist <= 0) return;
​
    // 4. 计算本次应走距离
    Step = ((float)Speed_10x / 10.0f) * Dt;
​
    // 5. 到达判断(剩余距离不足一步)
    if(Dist <= Step) {
        X_Float = X_Dest_Float; Y_Float = Y_Dest_Float;
        X_Equipment = X_Destination; Y_Equipment = Y_Destination;
        Status = 0; Speed_10x = 0; Start_Move_Flag = 0; First_Flag = 0;
        Reach_Flag = 1;
    } else {
        // 沿向量走一小步
        X_Float += (Dx / Dist) * Step;
        Y_Float += (Dy / Dist) * Step;
        X_Equipment = (int)X_Float;
        Y_Equipment = (int)Y_Float;
    }
}

:wrench: 阶段四:系统底层与状态机填坑(解决卡死玄学)

:warning: 问题 8:Timer0 初始化瘫痪

:cross_mark: 错误代码:
AUXR &= 0x80;(清空了其他所有位,且若初始为0,bit7 也没置位)。

:white_check_mark: 改进方案:
永远使用或运算置位 AUXR |= 0x80;


:warning: 问题 9:上电卡死或自动跳界面(未初始化 idata)

:magnifying_glass_tilted_left: 现象与分析:
STC 系列单片机 idata 上电不清零。StatusSeg_Mode 初值随机,导致程序走入异常 switch 分支。

:white_check_mark: 改进方案:
所有关键状态变量必须显式赋初值。

idata unsigned char Status = 0;
idata unsigned char Seg_Mode = 0;

:warning: 问题 10:栈溢出危机(未使用的 idata 变量)

:magnifying_glass_tilted_left: 现象与分析:
定义了 temp_SpeedKey_Down_data 等多个没用到的 idata 变量。8051 的 idata 仅有 256 字节,挤压栈空间后,一旦调用 sqrtsscanf 极易引发栈溢出,导致各种玄学 Bug(如切换不了界面)。

:white_check_mark: 改进方案:
果断删除所有未使用的变量,释放内存。


:warning: 问题 11:到达判断"早产"与卡死在运行模式不动

:magnifying_glass_tilted_left: 现象与错误代码:
刚启动就立刻触发到达,或者速度为 0 时永远卡死在运行。

if(Dist <= 0.5f) return; // 错误:目标=当前时,直接return不触发到达
if(Speed_10x == 0) return; // 错误:速度为0时直接跳过,丧失了更新机制

:white_check_mark: 改进方案:
去掉提前 return,统一用 if(Dist > 0) 包裹移动逻辑。速度为 0 时 Step 算出来自然是 0,不需要额外打断流转。


:warning: 问题 12:负 B 值导致速度溢出崩溃

:magnifying_glass_tilted_left: 现象与分析:
B_Ctrl 为负,直接强转 unsigned int 会变成极大的正数。

:white_check_mark: 改进方案:
分正负处理。

if(B_Ctrl < 0)
    Speed_10x = (unsigned int)((float)(3.14*R_Ctrl*Freq)/100.0f - (float)(-B_Ctrl)*10);
else
    Speed_10x = (unsigned int)((float)(3.14*R_Ctrl*Freq)/100.0f + (float)B_Ctrl*10);

:video_game: 阶段五:按键业务流转(彻底告别死锁)

:warning: 问题 13:彻底卡死在运行模式(缺失退出机制)

:magnifying_glass_tilted_left: 现象与分析:
一旦进入运行模式,如果速度一直为 0(无频率且 B=0),小车永远走不到终点。此时按 S4 没反应,因为 Key_Proc 漏写了运行中切回等待的处理。

:white_check_mark: 改进方案:
补全状态流转。

case 4:
    if(Status == 0 && Get_Coordinate_Flag) {
        Status = 1; First_Flag = 0;
        Reach_Flag = 0;             // ✅ 修复幽灵数据:进入时清除旧标志
        Get_Coordinate_Flag = 0;    // ✅ 清除坐标标志
    }
    else if(Status == 1) {          // ✅ 修复卡死根因:补充运行->等待
        Status = 2;
    }
    else if(Status == 2 && Clear_Rubbish_Flag) {
        Status = 1; First_Flag = 0;
    }
    break;

:warning: 问题 14:参数界面外被"悄悄篡改"

:magnifying_glass_tilted_left: 现象与分析:
在坐标/速度界面按 S12/S13,不知不觉把 R/B 参数改掉了。

:white_check_mark: 改进方案:
控制类按键必须与 UI 界面绑定。

case 12:
    if(Seg_Mode == 2) { // 必须限制在参数界面
        if(Para_Select==0) R_Para = (R_Para>=20)?20:R_Para+1; // 修复边界保护逻辑
        else B_Para = (B_Para>=90)?90:B_Para+5;
    }
    break;

:graduation_cap: 总结

  1. 题目不能只看文字描述 由于只看S4的功能描述导致漏掉了S4切换运行切换成等待模式的漏洞

  2. idata 定义的变量不用 即时删除 不然内存爆满连数码管都显示不了

  3. 距离判断 用每一次前进是否已经抵达终点判断 比已前进距离和应前进距离的判断好

  4. 因为Speed的时序跟Ctrl_Proc()时序不一致 加入if(Dist>0) 和 if(Speed==0)的阻拦可以保障启动

  5. 每次启动时收到坐标标志位和初始化位置标志位 在按键层要置

  6. 关于速度的浮点计算 最后如果要显示要强转回uint 需要参加后续计算则不用