
个人主页:
wengqidaifeng
✨ 永远在路上,永远向前走
个人专栏:
数据结构
文章目录
-
- 一、先把底盘的任务说透
- 二、工程入口其实很干净
- 三、底盘状态不是散的,而是集中管理的
-
- [1. `chassis_config_t` 是"可调参数表"](#1.
chassis_config_t是“可调参数表”) - [2. `chassis_state_t` 是"运行时实况"](#2.
chassis_state_t是“运行时实况”)
- [1. `chassis_config_t` 是"可调参数表"](#1.
- 四、底盘状态机到底怎么跑
- [五、1ms 调度是这套底盘的中枢](#五、1ms 调度是这套底盘的中枢)
-
- [为什么是 5ms 和 20ms](#为什么是 5ms 和 20ms)
- [六、8 路灰度循迹是底盘的眼睛](#六、8 路灰度循迹是底盘的眼睛)
-
- [1. 不要只看单个传感器](#1. 不要只看单个传感器)
- [2. `track_black_level` 很重要](#2.
track_black_level很重要)
- 七、电机和编码器:底盘的手脚和反馈
-
- [1. TB6612 负责输出](#1. TB6612 负责输出)
- [2. 编码器负责反馈](#2. 编码器负责反馈)
- 八、速度环和循迹环是两层关系
-
- [1. 速度环](#1. 速度环)
- [2. 循迹环](#2. 循迹环)
- [3. 两层怎么配合](#3. 两层怎么配合)
- 九、转弯和缺口不是一个问题
-
- [1. 直角转弯](#1. 直角转弯)
- [2. 长缺口](#2. 长缺口)
- [十、K1 / K2 和 OLED 是最实用的调车界面](#十、K1 / K2 和 OLED 是最实用的调车界面)
- [十一、UART 上报其实很有用](#十一、UART 上报其实很有用)
- 十二、底盘调试顺序,别一上来就猛加速度
- 十三、这一篇讲完之后,底盘逻辑就通了
本篇只讲底盘,不讲云台。核心目标是把 MSPM0G3507 底盘跑稳、跑准、跑得可调。
如果说前提篇是在画地图,那这一篇就是正式下场修路。底盘看起来只是"循迹小车",但真正做过的人都知道,它远不止这么简单:你要让车知道自己在哪条线、什么时候该转弯、什么时候只是暂时丢线、什么时候该继续直行穿过缺口、什么时候该停,以及停下以后还能把状态说清楚。
这就是为什么我把底盘单独拿出来讲。它是整套系统的地基,地基不稳,后面的云台和联动都白搭。
如果你是第一次接触这类项目,可以先把底盘上的名词认全。后面所有调试,基本都绕不开这些部件:
- MSPM0G3507:底盘主控,大脑。
- 8 路灰度传感器:看黑线用的眼睛。
- TB6612:电机驱动,负责把控制信号变成电机能跑的电流。
- 直流减速电机:真正带动车轮的东西。
- 编码器:测速度和位移的反馈器。
- OLED:调试时最友好的显示屏。
- K1 / K2:底盘按键,负责启动、停止、切圈数。
- UART:和云台交换状态的串口。
你可以先把它们想成一辆小车的"感官系统"和"运动系统":
- 感官系统负责告诉它"我在哪、线在哪、快不快、对不对";
- 运动系统负责把这些信息变成前进、转弯和停车。
该篇所涉及代码均在GitHub仓库

一、先把底盘的任务说透
这套 2025 年电赛 E 题的底盘部分,核心不是"能动",而是"能按题目要求稳定地走完一整套流程"。
从工程角度看,MSPM0G3507 底盘承担的是这些职责:
- 读取 8 路灰度传感器,判断车是否压在线上;
- 控制 TB6612 驱动的左右电机;
- 读取编码器,得到左右轮实际速度;
- 做速度闭环和循迹闭环;
- 识别直角转弯和路口标记;
- 穿过长缺口后重新找线;
- 统计圈数和总进度;
- 通过 OLED 把关键状态显示出来;
- 通过 UART 向 STM32 云台上报底盘状态。
如果你第一次看这些功能,可能会觉得很碎。其实它们可以按"输入、处理、输出"三层来理解:
| 层级 | 具体内容 | 通俗理解 |
|---|---|---|
| 输入 | 灰度传感器、编码器、按键 | 告诉车现在发生了什么 |
| 处理 | 状态机、PID、计圈逻辑 | 决定下一步该怎么做 |
| 输出 | 电机、OLED、UART、蜂鸣器/LED | 把决定变成动作和提示 |
只要先按这个三层框架去看代码,底盘就不会显得那么乱。
也就是说,底盘不是一个"电机驱动器",而是一个完整的实时控制系统。
在代码里,这些逻辑都被收敛到一个很清楚的入口:
- empty.c
- app_chassis.c
- chassis_state.h
empty.c 只负责初始化和主循环,真正的调度和控制都在 app_chassis.c。
二、工程入口其实很干净
先看入口:
c
int main(void)
{
SYSCFG_DL_init();
app_chassis_init();
while (1) {
app_chassis_loop();
}
}
这段代码很朴素,但思路是对的。
底盘系统并不把所有事塞进 while(1),而是把周期任务拆给 1ms 中断:
c
void TIMER_1MS_INST_IRQHandler(void)
{
switch (DL_TimerG_getPendingInterrupt(TIMER_1MS_INST)) {
case DL_TIMER_IIDX_ZERO:
app_chassis_on_1ms_tick();
break;
default:
break;
}
}
这样分的好处很明显:
- 快速且固定周期的事情放在中断里;
- OLED 刷新、UART 发送这类稍重的事情放主循环;
- 逻辑更清晰,也更容易调参。
你如果以后复盘代码,一定要先记住这个结构:
1ms tick 是节拍,主循环是后台。
三、底盘状态不是散的,而是集中管理的
底盘最重要的不是某个函数,而是两个结构体:
chassis_config_tchassis_state_t
定义都在 chassis_state.h。
1. chassis_config_t 是"可调参数表"
它里面放的是会跟着实车调试变化的参数,比如:
target_lapscruise_speed_tickslost_line_speed_ticksspeed_kp / ki / kdtrack_kp / ki / kdcorner_forward_mscorner_turn_mscorner_turn_speedwheel_diameter_mmencoder_ppr
你可以把它理解成"写在代码里的调试面板"。
2. chassis_state_t 是"运行时实况"
它保存的是实时状态,比如:
- 当前模式
mode - 故障位
faults - 当前圈数
current_lap - 当前循迹误差
track_error - 左右轮速度
left_speed_ticks / right_speed_ticks - 左右轮目标
left_target_ticks / right_target_ticks - PWM 输出
left_pwm / right_pwm - 通信在线状态
comm_online - 云台状态
gimbal_state
这就很重要了。因为后面的 OLED 显示、通信上报、故障判断,都是围绕这个状态结构展开的,而不是各写各的。
四、底盘状态机到底怎么跑
底盘的模式定义在 chassis_state.h 中:
c
CHASSIS_MODE_IDLE
CHASSIS_MODE_RUN
CHASSIS_MODE_CORNER
CHASSIS_MODE_FINISHED
CHASSIS_MODE_FAULT
这套状态机的意义非常实际。
IDLE:上电待机,不跑;RUN:正常循迹;CORNER:进入直角转弯;FINISHED:完成目标圈数;FAULT:出现故障并锁定。
这里最容易忽略的一点是:
"停车"不等于"故障"。
在 app_chassis_request_stop() 里,普通停车和故障停车是分开的。正常按 K1 停止时,通常只是回到 IDLE;但如果出现急停、通信超时或编码器异常,就会进入 FAULT。
这对比赛很重要。因为比赛里你需要的是"能安全停住",而不是"每次停住都像出事了"。
五、1ms 调度是这套底盘的中枢
app_chassis_on_1ms_tick() 是整个底盘的心跳。
它每 1ms 做这些事:
- 系统毫秒计数加一;
- 处理短时提示;
- 扫按键;
- 采灰度;
- 每 5ms 跑一次循迹 PID;
- 每 5ms 跑一次速度 PID;
- 每 100ms 刷一次 OLED;
- 每 20ms 触发一次 UART 状态上报;
- 检查云台通信是否超时。
这段逻辑在 app_chassis.c 里非常核心。
为什么是 5ms 和 20ms
这不是拍脑袋定的,而是比较典型的实时控制节奏:
- 1ms:系统节拍;
- 5ms:速度环和循迹环,既不太慢,也不会太抖;
- 20ms:通信状态上报,足够给云台看,不会刷太频繁;
- 100ms:OLED 刷新,显示够及时,还不会拖慢控制。
这也是你写任何嵌入式项目都该有的意识:
不是越快越好,而是每个任务都要配对合适的周期。
六、8 路灰度循迹是底盘的眼睛
底盘最先接触赛道的,是 8 路灰度传感器。驱动在 bsp_track8.c。
它输出的不是"黑 / 白"两个简单状态,而是:
line_mask:哪些传感器压到了黑线;raw_mask:原始 GPIO 电平;active_count:当前有多少路有效;error:加权后的循迹偏差;line_valid:当前是否真的看到线。
这几个值很关键,因为后面的转弯、找线、计圈都要依赖它们。
1. 不要只看单个传感器
循迹不是"最中间那个黑了就往左 / 往右",而是看整组传感器的分布。
例如:
- 中间几路黑了,说明车大致在中心附近;
- 左边黑得多,说明车往左偏;
- 右边黑得多,说明车往右偏;
- 大面积全黑,往往意味着路口或转角。
2. track_black_level 很重要
灰度模块有些是黑线低电平,有些是黑线高电平。
所以代码里不是写死"黑线一定是 0 或 1",而是通过:
c
g_chassis_cfg.track_black_level
来配置。
如果这项设错了,最典型的表现就是:
- OLED 上的 mask 反了;
- 车一上赛道就像没看到线;
- 你以为是 PID 没调好,其实是极性错了。
这就是为什么调车第一步通常不是调 PID,而是先看 bitmask。
七、电机和编码器:底盘的手脚和反馈
底盘驱动层在 bsp_motor_tb6612.c 和 bsp_encoder.c。
1. TB6612 负责输出
底盘工程里配置了双 TB6612,代码以四驱模式运行:
c
bsp_motor_set_mode(BSP_MOTOR_MODE_4CH);
四驱模式的意义不是"看起来更豪华",而是:
- 让前后轮都参与出力;
- 提高起步和过缺口的稳定性;
- 转弯时更容易维持姿态;
- 低速时不容易只靠单桥带不动。
代码中还有 PWM 限幅:
c
bsp_motor_set_pwm_limit(g_chassis_cfg.pwm_limit);
这个限幅非常重要,尤其是你第一次上车的时候。先把车开得稳,再开得快。
2. 编码器负责反馈
编码器不是"装饰件",它决定你能不能真的知道轮子跑了多少。
底盘通过编码器得到:
- 左右轮 tick;
- 左右轮速度;
- 车体平均速度;
- 后续换算成 mm/s 的实际运动速度。
protocol_stm32.c 里有一个很实用的地方:把 tick 速度换算成毫米每秒,上报给 STM32。
这能帮助云台判断底盘当前到底是快跑还是慢跑,也方便后面同步画圆、画矩形。
八、速度环和循迹环是两层关系
底盘不是一个 PID,而是两层。
1. 速度环
左轮和右轮各有一套速度 PID。
目标是:
- 让电机输出尽量跟上目标速度;
- 减少左右轮不一致;
- 在负载变化时仍然保持速度稳定。
2. 循迹环
循迹 PID 根据灰度误差,算出一个差速修正量:
- 车偏左,就让左轮慢一点、右轮快一点;
- 车偏右,就反过来修正。
这层环路决定车是不是"沿线走",而不是"只会转但不知道往哪转"。
3. 两层怎么配合
在代码里,底盘先根据循迹误差算出左右轮的目标 tick,再由速度环去逼近实际输出。
这就是嵌入式控制里很经典的"双环"结构:
text
循迹误差 -> 差速目标
差速目标 -> 速度闭环
PWM 输出 -> 电机
编码器反馈 -> 再回到速度环
你在实车上调的时候,通常也是:
- 先让速度环稳;
- 再加循迹环;
- 再去做转弯、缺口和圈数。
九、转弯和缺口不是一个问题
这部分是底盘里最像"比赛经验"的地方。
1. 直角转弯
赛道不是只会平滑转弯,很多时候是 L 形或路口,这时候你不能继续按普通循迹处理。
工程里有一整套和转弯相关的判定逻辑:
is_corner_candidate()corner_turn_right_from_sample()corner_forward_mscorner_turn_mscorner_min_sensorscorner_cooldown_ms
简单说就是:
- 先识别当前是不是像路口;
- 决定往左还是往右转;
- 进入转弯状态;
- 经过一段时间和传感器确认后,再回到循迹;
- 加冷却,防止同一个角被重复识别。
这比"看到黑块就猛打方向"稳定得多。
2. 长缺口
赛道里还有长缺口。车一旦进入缺口,灰度传感器会短暂看不到线。
这时不能立刻停车,而是要进入"丢线直行 / 找线"逻辑:
- 降速;
- 保持一个较稳的前进姿态;
- 等重新看到线再恢复正常循迹;
- 如果丢线太久,就进入故障或停机。
这也是为什么 lost_line_speed_ticks 很重要。
速度太高,缺口中间就容易跑飞;速度太低,又可能过不去。
十、K1 / K2 和 OLED 是最实用的调车界面
底盘按键很简单,但非常好用。
K1
在停止状态下启动;
运行时再按则停止。
K2
切换目标圈数:
text
1 -> 2 -> 3 -> 4 -> 5 -> 1
这意味着你不用改代码就能快速测试不同的跑圈任务。
OLED 显示
底盘 OLED 上一般会显示这些内容:
- 模式
- 目标圈数
- 当前圈数
- 左右轮速度
- 循迹误差
- 传感器 bitmask
- 进度
- 故障位
- 云台在线状态
调底盘的时候,我建议优先看这几个:
M:灰度是否压线;E:误差方向对不对;VL/VR:两轮速度是否一致;G+/G-:云台通信状态;F:有没有故障锁住。
十一、UART 上报其实很有用
底盘不是只会自己跑,它还会向 STM32 云台上报状态。
这部分在 protocol_stm32.c 里。
上报什么
chassis_status_payload_t 里包括:
tick_msstatetarget_lapsdone_lapslap_indexsegmentlap_progresssegment_progressv_left_mm_sv_right_mm_sv_body_mm_sline_errorturn_cmdmotion_flagsfault_bits
这已经不是"随便发个状态"了,而是把底盘的运行语义完整交给云台。
为什么云台需要这些
因为后面综合联动时,云台要知道:
- 车现在是不是在跑;
- 车跑到第几圈了;
- 当前是不是在转弯;
- 是不是能开激光;
- 圈进度是否足够稳定,能不能拿来同步画圆或画矩形。
所以底盘不是单独存在的,它会成为整个系统的"节拍器"。
十二、底盘调试顺序,别一上来就猛加速度
这部分是经验,但非常关键。
第一步:先不装轮子,检查电机方向
确认:
- 左右轮方向对不对;
- PWM 输出有没有;
- 电机能不能被单独驱动;
- 四驱是否都接上了。
第二步:只看编码器
手转轮子,看速度符号和计数是否正确。
如果正转显示负值,先别怪 PID,先看方向是否反了。
第三步:只调速度环
离地或者低速空跑时,先把左右轮速度调平。
目标是:
- 左右轮能跟随目标速度;
- 不明显抖动;
- 不容易超调。
第四步:再调循迹环
把车放到黑线上,低速让它走起来。
如果车总是左偏:
- 先看误差符号;
- 再看传感器极性;
- 然后再调
track_kp和track_kd。
第五步:最后再做转弯和缺口
这一步最容易翻车,但也最有成就感。
你需要不断调整:
corner_forward_mscorner_turn_mscorner_turn_speedlost_line_speed_ticksMARKER_IGNORE_START_MSMARKER_RELEASE_MS
这几个参数组合起来,决定了底盘到底像不像一辆会比赛的小车。
十三、这一篇讲完之后,底盘逻辑就通了
如果把底盘压缩成一句话,那就是:
用 8 路灰度看路,用编码器看速度,用 PID 管输出,用状态机管行为,用 UART 把底盘状态交给云台。
它不是最花哨的部分,但它是最不能出错的部分。
下一篇我会讲 STM32 云台篇,也就是如何用 STM32F103C8T6 处理视觉、步进电机、激光、按键和 CAN 通信。那一篇会更像"瞄准系统的搭建笔记"。