简介:一份面向2025年全国大学生电子设计竞赛G题《电路模型探究装置》的代码方案包,适合信号类赛题备赛选手参考。方案按基础与发挥两部分展开,基础部分覆盖传递函数计算、RMS有效值检测、VGA增益控制等关键实现;发挥部分引入LMS算法学习未知电路传递函数,结合FIR数字滤波器与查表法完成滤波器类型判断和信号重建,体现了从算法选型到代码落地的完整思路。同时保留了2020—2023年经典电赛题目复盘与实战评审突发问题记录,对理解出题趋势和临场应对很有帮助。压缩包共3个文件,包含inscode项目文件、index.html说明页面及.gitignore配置,整体仅13KB,轻量便携;HTML页面可作为方案索引入口,inscode文件承载核心代码示例。目前已有379人学习下载。借助这份代码包,可快速理清电路模型探究的难点突破顺序,节省环境配置与算法验证时间,尤其适合需要在短时间内搭建可运行框架的参赛者。
1. 2025电赛G题方案:先别急着写算法,先把代码骨架立住
每年电赛题目公布之后,G题这个位置几乎都是一道综合控制类大题,载体可能是车模、云台或者某种运动平台,但内核逃不出「感知—决策—执行」这条链路。很多队伍拿到题目之后,前三天耗在机械结构和硬件选型上,最后一天赶出一份顺序执行的代码,一上电就翻车。真正的问题往往不在PID公式记没记住、卡尔曼滤波会不会推,而在代码从第一秒起就没有骨架:传感器数据在一个循环里挤成一团,模式切换靠if嵌套,现场改一个参数要重新编译烧录,改三次就不知道哪版能用。这套2025电赛G题方案要讲的,就是怎么把这类题目拆成分层结构,用一套模块化骨架把代码组织起来,再给出滤波、PID、状态机这些核心模块的直接可抄代码和参数设定。适合正在备赛的队伍和带队指导,新手能照着把工程搭起来,熟手能直接替换自己的算法模块而不动整体结构。
2. 先搭骨架:G题代码的模块化组织与主循环设计
2.1 文件怎么分:把「驱动」「算法」「调度」拆成三个独立模块
G题代码最常见的一种死法,就是所有东西都写在main.c里。传感器初始化、中断服务、PID计算、显示刷新、模式切换全在一个文件里挤着,到了比赛现场想改一个滤波参数,得先翻几百行代码找到那个藏在角落的宏定义,改完还不确定有没有别的模块引用。这种「单文件大循环」的结构在跑通demo阶段没问题,但G题要求的是一个能在现场快速迭代调试的系统,代码组织的混乱会在最后三天集中爆发。
我一般会把工程按三个目录拆开:driver目录放芯片外设和传感器驱动,algorithm目录放滤波、PID、卡尔曼这类纯算法,task目录放状态机调度和主循环。驱动层有硬件依赖,算法层完全独立不依赖任何外设,调度层负责把「什么时候调用谁」管起来。这样拆之后,工程结构大致是这样:
| 目录 | 典型文件 | 职责 | 依赖 |
|---|---|---|---|
| driver/ | encoder.c、imu.c、motor.c、display.c | 读传感器、输出PWM、刷新屏幕 | 芯片寄存器、外设库 |
| algorithm/ | filter.c、pid.c、kalman.c | 数据处理与控制计算 | 无,纯C语言 |
| task/ | fsm.c、main.c | 状态切换、任务调度、初始化 | 依赖上面两层 |
这样拆有两个实际好处。第一个是可替换性:比赛现场发现陀螺仪数据异常,只需要换driver层对应驱动,算法层完全不用动;想从位置式PID换成增量式PID,也只需要改algorithm层,不用去中断里翻代码。第二个是可测试性:algorithm层的代码是纯C,不依赖任何硬件,可以拿到电脑上编一个测试程序,用模拟数据验证滤波效果和PID响应,不需要等硬件就绪。这两个好处在比赛现场的价值非常大,因为G题的调试时间永远不够用,任何一次「改一处坏三处」都是致命的。
另外建议文件命名直接按功能走,别用gpio.c、utils.c这种名字。motor_ctrl.c、line_vision.c这种命名在三天之后你还认得出是干嘛的,test1.c就没有任何线索了。
2.2 状态机做主循环:从顺序执行到模式切换
G题装置一般都要经历明显的阶段变化——上电自检、待机等待、正常运行、急停保护。用一个大while循环加if判断也能跑,但一旦有十几个标志位互相置位,调试的时候就会变成「改一处、坏三处」。状态机写法最大的优势是:当前状态只有一个活跃分支,转移条件集中写在一起,查看状态、修改转移条件都方便。
用一个典型的控制装置举例,它的状态至少要分四个:上电自检、待机、运行、急停。代码如下:
typedef enum { ST_BOOT, // 上电自检 ST_STANDBY, // 待机等待启动信号 ST_RUNNING, // 正常运行 ST_EMERGENCY, // 急停保护 } sys_state_t; static sys_state_t g_state = ST_BOOT; void fsm_run(void) { switch (g_state) { case ST_BOOT: if (boot_self_check() == 0) { // 自检不通过,停在自检态 break; } g_state = ST_STANDBY; // 自检通过进入待机 break; case ST_STANDBY: if (start_signal_received()) { // 收到启动指令 g_state = ST_RUNNING; } break; case ST_RUNNING: if (any_fault_detected()) { // 任意故障直接切急停 g_state = ST_EMERGENCY; break; } control_loop_running(); // 正常运行时的控制计算 break; case ST_EMERGENCY: set_motor_duty(0); // 输出强制清零 break; default: g_state = ST_BOOT; // 未知状态兜底回到自检 break; } }这个结构里有几个关键点。ST_RUNNING状态下,先检查故障再执行控制任务——故障检测优先级必须高于控制计算,因为任何一拍的失控都可能让装置冲出赛道或者损坏机械结构。ST_EMERGENCY状态下只做一件事,就是输出清零,不要在这里再调用任何控制算法或者显示刷新,急停操作必须在最快路径里完成。g_state是静态全局变量,整个task模块内可见,但外部只能通过fsm_run()推进状态,不要让中断服务函数直接改写状态机——否则主循环还没处理完急停,中断又把它切回运行态,急停就失效了。
这里不推荐上实时操作系统,G题的代码规模通常在几千行量级,裸机状态机完全够用。RTOS的优先级调度反而会引入调试复杂度,现场出问题时排查面更大。裸机加状态机是G题里控制类题目最可靠的做法。
2.3 中断里只做采集,主循环里只做决策
这是G题代码骨架里最重要的一条分工原则。很多队伍把编码器读数、陀螺仪融合、PID计算全放在定时器中断里,理由是控制周期要准。但中断服务函数越长,主循环越容易被饿死,一旦某个中断里跑了一个耗时不确定的运算,整个系统的实时性就崩了。而且中断里调用浮点运算还有一个隐藏问题:浮点单元在处理高精度运算时会被长时间占用,其他中断进来只能干等,延迟抖动加大。
常见做法是中断里只做三件事:读寄存器、存变量、置标志位。滤波、PID、状态机全部放在主循环里执行。代码结构如下:
// 定时器中断:只负责标记数据就绪 volatile uint32_t g_sensor_ready = 0; void TIM_IRQHandler(void) { enc_read_raw(); // 读编码器计数值,存入全局变量 imu_read_raw(); // 读IMU原始值,存入全局结构体 g_sensor_ready = 1; // 置标志位,不在中断里做滤波 } // 主循环:等标志位,再统一处理 while (1) { if (g_sensor_ready) { g_sensor_ready = 0; filter_update(); // 低通/卡尔曼在这里执行 pid_update(); // PID在这里执行 motor_output_update();// 更新PWM输出 } fsm_run(); // 状态机放在最后推进 }这个结构的关键在于中断里只做「读寄存器→存变量→置标志」,耗时是微秒级的,符合中断处理函数要短的原则。滤波和PID放在主循环里,计算耗时可以浮动,只要控制周期基本稳定就没问题。g_sensor_ready必须用volatile修饰,否则编译器很可能把这个变量优化到寄存器里,主循环读到的永远是旧值,系统就会变成「时灵时不灵」的玄学bug。
控制周期的选择也有讲究。常见做法是:速度环和电流环这类内环跑1kHz左右,位置环和视觉处理这类外环跑50到200Hz。如果主循环里还有其他耗时任务(比如屏幕刷新或者串口打印),可以单独用一个低速定时器置标志位,控制循环里只响应控制周期标志,不然显示刷新会拖垮控制频率。实测下来,这种改动往往比调任何PID参数都有效——因为系统的实时性不稳,控制算法算得再准也白搭。
3. 数据是控制的地基:传感器预处理与融合参数
3.1 三种数据的典型病:编码器毛刺、陀螺仪零漂、视觉跳变
G题控制对象的传感器组合,无外乎编码器测速度和位移、IMU测姿态、摄像头做目标识别与定位。直接把这些原始数据喂给控制器,系统一加反馈就会抖起来。三种数据各有各的毛病,处理方式完全不同,先认清症状再选方案。
编码器的典型毛病是低速毛刺。电机堵转或者正反转切换瞬间,AB相在临界位置会有反复跳变,计数结果出现±1的抖动。这个抖动经过速度微分计算之后会被放大,一帧速度值可能从0跳到几倍于正常值,速度环碰到这种数据直接振荡。陀螺仪的典型毛病是零漂。静止时角速度输出并不严格为0,而且这个零偏会随温度缓慢变化,直接积分之后姿态角就开始往一个方向飘,几分钟之内就能飘出好几度。视觉的典型毛病是跳变。目标被遮挡几帧后重新出现,或者背景里有颜色相近的干扰物闪过,检测输出的目标坐标会瞬间跳到一个不合理的位置,这时候执行机构会猛地抽动一下。
这三类问题的处理思路不一样。编码器毛刺用低通滤波或者滑动中值滤波消掉,代价是速度环的相位滞后;陀螺仪零漂需要启动时静态校准,运行中用互补滤波让加速度计来纠正长期漂移;视觉跳变则要靠限幅和预测——上一帧和这一帧的目标位置差超过一个阈值,就认为当前帧检测异常,暂时用上一拍的值替代。很多队伍把这三类问题混在一起,想用一个「高级滤波」全解决,结果每个方向都没处理好。
3.2 一阶低通滤波:截止频率到底设多少
一阶低通滤波是消毛刺性价比最高的手段,代码就几行,参数就一个alpha。但alpha拍脑袋设,要么滤不干净要么滞后太大。一阶低通的核心公式是:当前输出等于上一次输出乘以alpha,加上当前输入乘以一减alpha。alpha越大,平滑效果越强,跟踪越慢。
typedef struct { float y_prev; // 上一次滤波输出 float alpha; // 滤波系数 0~1,越大越平滑 } lp_filter_t; float lowpass_update(lp_filter_t *f, float x) { // 一阶低通:新输出 = alpha×旧输出 + (1-alpha)×新输入 f->y_prev = f->alpha * f->y_prev + (1.0f - f->alpha) * x; return f->y_prev; }alpha和截止频率之间有明确对应关系,不要靠蒙。alpha = exp(-2π·fc/fs),其中fc是目标截止频率,fs是采样频率。举个例子:编码器采样周期是1ms,也就是fs=1000Hz,想让速度环只保留20Hz以下的成分,alpha = exp(-2π×20/1000),算出来约等于0.88。从这个值起步,再观察实际响应做微调,比随机试alpha高效得多。
实际调试时,针对不同数据源的起步参考值如下:
| 应用对象 | 采样率 | 截止频率建议 | 说明 |
|---|---|---|---|
| 编码器速度 | 1kHz | 15~30Hz | 滤毛刺;频率太高滤不干净,太低速度环滞后 |
| IMU角速度 | 200Hz | 20~50Hz | 保留动态响应,滤电路高频噪声 |
| 视觉坐标 | 30Hz | 2~5Hz | 视觉本身帧率低,以压制跳动为主 |
这里有一个血泪经验值得强调:低通滤波带来的滞后对控制系统是坏事。如果系统出现等幅振荡,先把滤波截止频率往高调一个量级,看振荡是不是来自滤波滞后。很多人一看到振荡就动PID,实际上数据链路没洗干净,PID怎么调都压不住。我在帮A同学调一台云台装置时遇到过类似情况,速度环加低通之后系统出现2Hz左右的等幅振荡,换了三组PID参数都没解决,最后把编码器滤波截止频率从10Hz调到50Hz,振荡直接消失——数据环节的滞后被错怪成了控制器的问题。
3.3 一维卡尔曼裁剪版:在单片机上跑起来
低通滤波只能处理单一数据源的高频噪声,但G题里经常要做多传感器融合——比如用加速度计和陀螺仪融合出姿态角,或者用编码器和视觉融合出位置。卡尔曼滤波是标准答案,但完整版矩阵运算在主流Cortex-M系列单片机上有点重,而且状态维数一高,调试难度成倍增加。这里我给一个一维裁剪版,适用于对单个测量量的最优平滑,也可以作为多源融合的基础组件。
typedef struct { float x; // 状态估计值 float p; // 估计误差协方差 float q; // 过程噪声协方差:越大越信测量 float r; // 测量噪声协方差:越大越信预测 } kalman_1d_t; void kalman_1d_init(kalman_1d_t *k, float init_x) { k->x = init_x; k->p = 1.0f; k->q = 0.01f; // 起步值,按实际响应调 k->r = 0.1f; // 起步值,按实际响应调 } float kalman_1d_update(kalman_1d_t *k, float z) { // 预测:系统模型为静态,协方差加上过程噪声 k->p = k->p + k->q; // 卡尔曼增益:权衡预测和测量的置信度 float kgain = k->p / (k->p + k->r); // 更新状态:用新测量修正预测 k->x = k->x + kgain * (z - k->x); // 更新协方差 k->p = (1.0f - kgain) * k->p; return k->x; }这段代码去掉了矩阵运算和状态转移矩阵,只剩下加减乘除,在常见的Cortex-M系列单片机上跑完全没问题。参数里q和r的比值决定一切:q/r越大,滤波器越相信测量值,响应快但噪声大;q/r越小,越相信预测值,平滑但滞后明显。建议先固定r不动,只调q;等曲线形态满意了,再整体缩放q和r来调整收敛速度。
一个实用的现场调试方法:给滤波器输入一段有明显阶跃的原始数据,观察融合输出。如果曲线平滑但阶跃处明显跟不上,说明r偏大,把r缩小一半再试;如果曲线还有明显毛刺说明r偏小,把r加大一倍。注意q和r同时变动是调参大忌,你永远不会知道是哪边起的作用。另外这个一维卡尔曼的定位是「对单一测量源做最优平滑」,如果要做编码器和视觉的位置融合,正确做法是分别对两个源做一维卡尔曼,然后按各自r值的反比做加权合成——两个r值分别代表两个传感器各自的信任度,比值越小越值得被相信。
4. PID不玄学:调参顺序与代码落法
4.1 位置式还是增量式:G题场景里怎么选
PID的代码网上到处都是,真正拉开差距的是「知道选哪种形式」和「知道先调哪个参数」。位置式PID输出的是执行机构的绝对值,适合需要明确给定位置的场合,比如舵机打角、云台角度控制——输出直接对应到一个绝对角度。增量式PID输出的是本次应该增加的量,适合本身带积分性质的执行机构,比如直流电机的PWM调速——上一拍的输出加上增量就是这一拍的输出,电机这个对象天然就是一个积分器。
两种形式的对比,一张表能说清楚:
| 比较项 | 位置式 | 增量式 |
|---|---|---|
| 输出含义 | 当前执行量绝对值 | 本次增量 |
| 是否需要保存历史输出 | 不需要,每拍独立算 | 需要,输出累加 |
| 对积分饱和的敏感度 | 高,必须加抗饱和 | 低,但输出累积误差需关注 |
| 典型场景 | 舵机、云台角度、倒立摆角度 | 电机速度环、循迹差速 |
G题里最常见的运动控制场景,速度环用增量式比较稳,因为执行机构是电机,PWM的累加效应天然存在,位置式反而容易因为一拍的异常输出造成电机猛冲。云台这类角度控制则用位置式,增量式在目标角度附近会因为累积误差来回震荡。一个快速判断方法:看执行器有没有记忆效应。舵机和云台的角度是保持性的,用位置式;电机转速不给定PWM就掉下来,用增量式更顺。
无论选哪种,输出限幅都是必须的。G题现场任何一个坏数据进来,如果没有输出限幅,执行机构会直接打到极限位置——轻则装置冲出赛道,重则损坏机械结构。这行保护代码是整个PID里性价比最高的一行。
4.2 先P再I最后D:一个不翻车的调参顺序
PID调参翻车九成是因为「三个参数一起动,出了问题时不知道是谁的锅」。我带的队伍我要求严格按P到I再到D的顺序走,每一步只动一个参数,并且记录当时的响应特征。
第一步,把I和D设成0,只保留P项。从一个很小的值开始慢慢加大,观察对象的响应:P太小,响应慢、到不了目标;P适中,系统有轻微超调但能稳定;P过大,系统开始持续等幅振荡。记下开始出现持续振荡的那个P值,这是后续所有调参的基准。第二步,在P的基础上加I。I的作用是消除稳态误差,观察目标只有两个:静态误差有没有被吃掉、系统有没有出现低频慢振荡。如果出现低频振荡,说明I偏大,积分项在目标附近反复积累过冲——这时候要么降低I,要么加积分限幅。G题里强烈建议加积分限幅,因为起步阶段目标差值大,积分项一秒就能累积到很大的值,目标到达之后积分回退慢,超调一大截。第三步,加D。D的作用是抑制超调,但微分项对测量噪声极其敏感,速度环里经常是D一加系统就高频抖动。所以D项必须在前面章节讲到的传感器滤波完成之后再调——否则你压的其实不是超调,而是噪声经过微分放大之后的高频分量。
这里有一个关键认知:G题里如果P能调到系统不振荡,优先不加D或者给很小的D。微分项是双刃剑,它带来的高频噪声放大问题在比赛现场的电磁环境下会比实验室严重得多。现场环境的电机干扰、电源纹波、传感器线缆串扰都会让微分项变成噪声放大器。
4.3 输出限幅、积分限幅、微分滤波:三个必调的保护参数
一个裸的PID公式在实验室的干净数据下跑没问题,但比赛现场一个意外的大误差就能让积分饱和或者输出打满。一个带全套保护的PID实现,才是G题现场能用的版本。
typedef struct { float kp, ki, kd; // 三个增益 float out_max; // 输出限幅 float integral_max; // 积分限幅 float d_filter_alpha; // 微分低通系数 0.2~0.5 float integral; // 积分累计值 float prev_error; // 上一拍误差 float d_filted; // 滤波后的微分项 } pid_t; float pid_update(pid_t *p, float target, float current) { float error = target - current; // 比例项:直接输出 float p_out = p->kp * error; // 积分项:累加后限幅,防止积分饱和 p->integral += error; if (p->integral > p->integral_max) p->integral = p->integral_max; if (p->integral < -p->integral_max) p->integral = -p->integral_max; float i_out = p->ki * p->integral; // 微分项:先差分再低通,防止噪声放大 float d_raw = error - p->prev_error; p->d_filted = p->d_filter_alpha * p->d_filted + (1.0f - p->d_filter_alpha) * d_raw; float d_out = p->kd * p->d_filted; p->prev_error = error; // 总输出限幅 float out = p_out + i_out + d_out; if (out > p->out_max) out = p->out_max; if (out < -p->out_max) out = -p->out_max; return out; }这段代码把所有容易翻车的位置全部保护住了。积分限幅:误差持续偏大时,积分项被限幅挡住,不会积累到几百几千,目标到达之后超调幅度可控。微分低通:直接用error差做微分,误差一跳微分项就巨大,加一个低通系数让微分项平滑下来,这个系数一般取0.2到0.5,越大越平滑。输出限幅:总输出一旦超过out_max直接截断,这行代码在正常调试时好像无所谓,但现场一个坏数据进来时就是靠它兜底。
调参锚点可以给一组我常用的起步值:外环位置环kp取0.5到2.0,内环速度环kp取5到15,ki取kp的十分之一到五分之一,kd最后加且从小值起步。但这些只是起步锚点,不是万能参数,真正的调参方法还得回到上一节说的P到I到D顺序。另一个建议是把每次调参的结果记录下来,包括目标值、增益、现象——不带记录的调参就是原地打转,调了一晚上第二天全忘光。
5. 常见问题排查:现场最容易翻车的五个坑
5.1 上电瞬间执行机构猛冲一下
现象:代码烧录后一上电,电机或者舵机猛动一下,有的装置直接冲出起始区域。
原因:上电瞬间PID内部状态没有初始化,prev_error默认值不确定,如果当前误差刚好很大,第一拍比例项就会输出巨大值;另外电机驱动芯片在供电建立过程中,PWM引脚电平不确定,也可能输出一个短暂的异常占空比。
解决:分两步处理。第一步,在所有外设初始化和传感器校准完成之后再启动控制任务,main函数开头只做初始化,不要一进来就调用control_loop_running;第二步,在电机驱动接口加一个使能门,初始化期间PWM输出强制为0,所有初始化完成之后再打开使能。这个使能门的实现很简单,就是初始化结束前让设备处于输出封锁状态,初始化结束后切到正常状态,对可靠性提升非常明显。
5.2 陀螺仪零漂让装置慢慢自己转起来
现象:静止状态下装置状态正常,运行一两分钟后缓慢偏向一侧,越来越明显。
原因:陀螺仪的零漂不是固定值,温度升高之后零偏会缓慢变化,纯积分姿态估计必然漂移;也可能是启动时静态校准采集帧数太少,平均值偏差没校干净。
解决:启动时增加至少一秒钟的静态数据采集,取平均值写进校准参数;运行中加一个轻量高通滤波把低频漂移压掉,或者直接用互补滤波把加速度计作为长期校正源。加速度计在静止时的长期精度远高于陀螺仪积分,陀螺仪短期精度高,互补滤波结合起来,就是G题里性价比最高的姿态方案。注意互补滤波的融合系数也是需要现场调的——加速度计权重太大会引入高频抖动,太小又校正不住漂移,一般从0.02起步调整。
5.3 中断里跑浮点运算,控制周期一抖一抖
现象:系统时而灵敏时而迟钝,用示波器看PWM占空比波形,偶尔出现一个异常跳动,控制效果不稳定。
原因:中断服务函数里直接调用了卡尔曼或浮点PID运算。这些函数执行时间会因数据内容不同而浮动,如果碰上浮点单元被其他运算占用,中断时长远超设定周期,主循环里的标志位被反复置位,控制节奏全乱。更要命的是这种问题很难复现,排查起来非常耗时。
解决:把滤波和PID计算全部移到主循环,中断里只留读寄存器、存变量、置标志位。这个改动是这个章节里价值最高的一条——它不增加任何算法复杂度,只是重新划分了任务的执行位置,但系统实时性会立刻变稳。判断方法很简单:把中断服务函数里的代码全部注释掉换成标志位,如果系统变好了,说明问题就在中断里。
5.4 摄像头在实验室好好的,赛场识别率掉一半
现象:实验室里用一套固定的曝光和阈值参数,对目标识别率很高;到了赛场换了一个环境光照和背景颜色,识别率断崖式下降,目标偶尔丢失。
原因:视觉参数是依据实验室光照条件调的,赛场的光线色温、窗户反光、地面颜色都会改变像素分布;固定曝光参数在亮度差异大的环境下会过曝或欠曝,目标颜色特征直接失真。
解决:在视觉代码里至少加两个保护:第一,曝光参数改自动模式,或者按场景预置两套参数,通过串口命令切换;第二,目标识别不要用单一硬阈值做二值化,改用相对统计的方法——在目标颜色附近做一个概率加权的质心计算,降低对绝对阈值的依赖。这一步对于G题里大量依赖摄像头找目标的题目是保命级的,很多队伍在赛场上挂掉不是机械或控制问题,而是视觉数据本身不可靠。
5.5 现场改了一个参数,程序「变慢」了
现象:现场把PID增益调大之后,整个控制循环明显变慢,液晶刷新开始卡顿,反应迟钝。
原因:增益变大导致系统进入振荡,控制输出频繁到达限幅值,陷入深度饱和;同时振荡让误差信号变化剧烈,微分项持续输出较大值,运算路径变长;另外现场的调试器连接和屏幕刷新本身就占用了CPU周期,被误判为程序变慢。
解决:第一步拔掉调试器和显示屏,确认「慢」是否来自外部因素;第二步检查输出是否频繁等于out_max——如果输出几乎每一拍都顶着限幅跑,说明系统已经深度饱和,不是程序变慢了,是控制失效了,要从限幅值和积分限幅重新调。这里有一个容易忽略的细节:G题现场很多人插着屏幕跑到最后,显示屏的刷新对控制周期的影响在慢速场景下不明显,但在高速运动控制中可能直接吃掉三分之一的主循环时间,紧急时刻果断拔屏。
6. 从「跑通」到「跑稳」:最后一公里只做三件事
方案分享到这里,骨架、滤波、PID和坑都讲完了,最后说一个大多数队伍都没做的事——鲁棒性验证。跑通和跑稳之间差距很大,一个能走完完整流程的装置,和一个能在现场环境下稳定重复的装置,是两种完全不同的调试深度。比赛前的最后一周,值得把精力全放在三件事上。
第一件,连续运行测试。每天至少让装置完整运行30分钟以上,记录每次运行中出现的异常——不管是一个小小的抖动还是偶发的温度漂移,全部记下来。很多问题需要长时间运行才会暴露:陀螺仪温度零漂、电机发热导致摩擦系数变化、电池电压下降之后PWM响应差异。只跑三分钟验证不出任何一个。我印象很深的一次,某支队伍在现场的第三轮测试中突然出现大幅度漂移,排查到最后是电池从满电状态掉到某个电压区间后,电机响应变得迟钝——这在三分钟短测里完全发现不了。
第二件,真值回放。把传感器原始数据录下来,回放时用同一份数据反复测试算法。这样改一次滤波参数、调整一次PID,都能用相同输入对比输出差异。没有这样一套离线调参工具,现场每次改参数都靠肉眼和经验判断,来回折腾半天也还原不了「昨天那个参数」。回放工具不用做得多复杂,串口把原始数据发给电脑存成文件,电脑读取文件逐帧喂给算法就行。
第三件,参数热切换。G题现场的时间是按分钟算的,每改一个参数重新编译烧录一次,一天就没了。在代码里加一个串口命令解析,通过特定格式的报文在线修改PID参数和滤波系数,现场反馈调试效率能提升一个量级。我习惯在每次训练结束后把当天调过的所有参数和现象记录写进代码文件头注释里,比赛现场翻一下就知道哪组参数在什么条件下出现过什么表现——不然第二天醒来只剩一句「昨天调得还行」。这个习惯帮我省了很多重复劳动,也推荐给你。
这套方案的每一块,从模块化代码骨架到传感器数据预处理,再到PID的完整保护和鲁棒性验证,都是G题备赛里最常规也最可靠的做法。希望帮到你。
本文还有配套的精品资源,点击获取