简介:这份开源代码面向参加全国大学生电子设计竞赛及学习嵌入式控制的学生与开发者,提供基于MSPM0G3507微控制器的三轮智能循迹小车完整方案,涵盖PID速度控制、八路灰度传感器寻迹与电机编码器反馈等核心模块,可用于竞赛备赛、课程设计或嵌入式控制算法入门实践。压缩包共38个文件,约74KB,以c与cpp源文件、hpp与h头文件为主,辅以syscfg配置、ccxml调试目标、project工程文件及说明文档,覆盖驱动、控制、状态机、菜单与传感器等模块,结构清晰便于按功能阅读。目前已有307人学习下载。读者可从中获取完整的赛题实现思路,理解PID参数调节、灰度寻迹与编码器闭环的软硬件协同方式,并借助说明文件与附赠资料快速搭建、调试与二次开发,是嵌入式系统综合实践的实用参考。
1. 从电赛 H 题说起:这套 MSPM0G3507 三轮循迹车代码到底能跑成什么样
如果你正在准备 2024 年全国大学生电子设计竞赛 H 题,或者手头刚好有一块 MSPM0G3507 LaunchPad,想找一套能直接编译、能上车、能调参的三轮智能循迹小车工程,那这份开源代码值得你花一个下午拆一遍。它不是那种只丢几个 .c 文件的“半成品”,而是把八路灰度传感器寻迹、电机编码器反馈、PID 速度闭环、三轮差速转向这几块完整串起来的工程。我拿到手第一反应是看目录结构——外设驱动、控制算法、传感器采样、电机 PWM 输出分层清晰,不是一坨 main.c 里塞两千行的那种。
三轮车和四轮车在循迹逻辑上差别很大:三轮通常前轮万向、后两轮独立驱动,转向靠左右轮差速,没有舵机。这意味着你的控制量只有两个电机的 PWM,但反馈量有八路灰度加两路编码器。这套代码的核心思路是:灰度传感器做位置偏差计算,编码器做速度闭环,两个环串起来——外环位置、内环速度。适合谁?适合已经调过电机驱动、能看懂 PID 基本形式、但卡在“传感器值怎么变成转向量”这一步的人。如果你连 GPIO 都没配过,建议先把 MSPM0G3507 的 GPIO 和定时器例程跑通再来看。
2. 工程结构与 MSPM0G3507 外设初始化:从 SysConfig 到能转的轮子
2.1 为什么这套代码用 SysConfig 而不是手写寄存器
MSPM0G3507 是 TI 的 Arm Cortex-M0+ 内核,主频 80MHz,外设不算特别复杂但寄存器数量不少。这套工程用了 TI 的 SysConfig 工具生成外设初始化代码,而不是手写寄存器。我一开始觉得 SysConfig 生成的代码“黑匣子”太多,但实际调车时发现一个好处:改引脚、改定时器分频、改 PWM 频率,只需要在图形界面点几下,不用翻几百页数据手册去查某个寄存器的位定义。
工程里 SysConfig 主要配了这几块:
| 外设 | 用途 | 关键配置 |
|---|---|---|
| TIMG0 | 电机 PWM 输出 | 20kHz 载波,双通道 |
| TIMG6 | 编码器正交解码 | 四倍频计数 |
| ADC0 | 八路灰度采样 | 序列扫描,DMA 搬运 |
| UART0 | 调试串口 | 115200 波特率 |
| GPIO | 电机方向、LED | 推挽输出 |
PWM 频率选 20kHz 是有讲究的:低于 15kHz 电机会有可闻啸叫,高于 25kHz 部分 MOS 管驱动发热明显。20kHz 是这套代码里实测比较稳的点。编码器用 TIMG6 的正交解码模式,硬件自动四倍频,省了软件中断里手动判断方向的开销。
2.2 电机驱动与编码器反馈的初始化代码
电机驱动部分,代码里用的是双 H 桥方案,每路电机两个方向引脚加一个 PWM 引脚。初始化逻辑我摘了关键部分:
// 电机初始化:方向引脚 + PWM 通道 void Motor_Init(void) { // 方向引脚配置为输出 DL_GPIO_initDigitalOutput(MOTOR_A_DIR_PIN); DL_GPIO_initDigitalOutput(MOTOR_B_DIR_PIN); DL_GPIO_setPins(MOTOR_A_DIR_PORT, MOTOR_A_DIR_PIN); DL_GPIO_setPins(MOTOR_B_DIR_PORT, MOTOR_B_DIR_PIN); // PWM 定时器启动,20kHz DL_TimerG_setLoadValue(PWM_TIMER, 4000 - 1); // 80MHz / 4000 = 20kHz DL_TimerG_setCaptureCompareValue(PWM_TIMER, 0, DL_TIMER_CC_0_INDEX); DL_TimerG_setCaptureCompareValue(PWM_TIMER, 0, DL_TIMER_CC_1_INDEX); DL_TimerG_startCounter(PWM_TIMER); // 编码器定时器:正交解码模式 DL_TimerG_setLoadValue(ENC_TIMER, 0xFFFF); DL_TimerG_startCounter(ENC_TIMER); }逻辑说明:PWM 定时器装载值 4000,对应 80MHz 时钟下 20kHz 频率。占空比通过DL_TimerG_setCaptureCompareValue动态改,范围 0 到 4000。编码器定时器不设分频,直接对正交信号四倍频计数,读值时用DL_TimerG_getCurrentCounterValue拿当前计数值,两次采样做差就是增量。
参数怎么改:如果你的电机驱动板 PWM 极性相反,把DL_TimerG_setCaptureCompareValue的值反过来写——比如原来写duty,改成4000 - duty。编码器方向反了,交换 A/B 相引脚定义即可,不用改代码逻辑。
2.3 八路灰度传感器的 ADC 采样与 DMA 搬运
八路灰度传感器输出的是模拟量,代码里用 ADC0 的序列模式扫描八个通道,DMA 自动把结果搬到数组里。这样 CPU 不用轮询等 ADC 完成,采样周期稳定在 1ms 左右。
// ADC + DMA 初始化:八路灰度自动扫描 void Gray_Init(void) { // 配置 ADC 序列通道 DL_ADC12_setSampleTime0(ADC12_0_INST, 100); // 采样时间 100 个时钟 DL_ADC12_setSequencerMode(ADC12_0_INST, DL_ADC12_SEQ_MODE_SEQUENCE); DL_ADC12_addSequenceChannel(ADC12_0_INST, GRAY_CH0); DL_ADC12_addSequenceChannel(ADC12_0_INST, GRAY_CH1); // ... 依次添加八路 // DMA 搬运:ADC 结果寄存器 -> gray_raw[8] DL_DMA_setSrcAddr(DMA, DMA_CH0, DL_ADC12_getMemResultAddress(ADC12_0_INST, 0)); DL_DMA_setDestAddr(DMA, DMA_CH0, (uint32_t)&gray_raw[0]); DL_DMA_setTransferSize(DMA, DMA_CH0, 8); DL_DMA_enableChannel(DMA, DMA_CH0); DL_ADC12_startConversion(ADC12_0_INST); }逻辑说明:ADC 序列模式按顺序采八个通道,每采完一个触发一次 DMA 搬运,八个搬完 DMA 产生中断。gray_raw数组里就是八路原始值,范围 0 到 4095。采样时间设 100 个时钟是折中——太短了灰度传感器输出阻抗高会采不准,太长了整体采样周期拉长影响实时性。
常见做法是:先读gray_raw,做归一化处理,再算位置偏差。归一化时每路减去白底值、除以黑白差值,这样不同场地光照变化时不用重新调阈值。
3. PID 速度闭环与八路灰度寻迹:从传感器值到电机 PWM 的完整链路
3.1 位置偏差怎么算:八路灰度的加权法
八路灰度排成一行,每个传感器有固定位置权重。代码里用的是加权平均法算偏差:
// 八路灰度位置偏差计算 float Gray_GetError(void) { float sum = 0, weight_sum = 0; // 权重从 -7 到 +7,中间为 0 const float weight[8] = {-7, -5, -3, -1, 1, 3, 5, 7}; for (int i = 0; i < 8; i++) { float val = Gray_Normalize(gray_raw[i]); // 归一化到 0~1 sum += val * weight[i]; weight_sum += val; } if (weight_sum < 0.1f) { return last_error; // 全白或全黑,保持上次偏差 } return sum / weight_sum; // 范围约 -7 到 +7 }逻辑说明:每路灰度归一化后作为权重,乘以对应位置权重再求和,除以权重和得到偏差。全白(丢线)时返回上次偏差,避免偏差突变导致电机猛打。偏差范围约 -7 到 +7,负值偏左、正值偏右。
参数怎么改:如果你的传感器间距不均匀,把weight数组改成实际物理位置比例。比如中间密两边疏,权重可以设成{-8, -5, -2, -0.5, 0.5, 2, 5, 8}。这个数组直接决定循迹的“手感”——权重差距越大,偏差对位置变化越敏感,车走得越“神经质”。
3.2 串级 PID:外环位置、内环速度
这套代码用的是串级 PID:外环输入是灰度偏差,输出是目标速度差;内环输入是编码器实测速度,输出是 PWM 占空比。为什么用串级而不是单环?因为三轮车差速转向时,如果只做位置环,直道会画龙——车头左右摆。加了速度内环后,左右轮速度被强制跟踪目标值,直道稳定性明显提升。
// 串级 PID 主循环(1ms 周期) void Control_Loop(void) { // 外环:位置 PID float error = Gray_GetError(); float turn = PID_Position_Calc(&pos_pid, error); // 内环:速度 PID,左右轮分别算 float target_left = base_speed - turn; float target_right = base_speed + turn; float actual_left = Encoder_GetSpeed(LEFT); float actual_right = Encoder_GetSpeed(RIGHT); float pwm_left = PID_Speed_Calc(&spd_pid_l, target_left, actual_left); float pwm_right = PID_Speed_Calc(&spd_pid_r, target_right, actual_right); Motor_SetPWM(LEFT, pwm_left); Motor_SetPWM(RIGHT, pwm_right); }逻辑说明:base_speed是基础速度,turn是位置环输出的转向量。左轮目标 = 基础速度 - 转向量,右轮 = 基础速度 + 转向量。速度环用增量式 PID,输出直接叠加到 PWM 上。整个循环 1ms 一次,用定时器中断触发。
参数怎么调:先调内环。把车架空,给左轮固定目标速度,调spd_pid_l的 Kp 直到实际速度能跟上目标且不振荡,一般 Kp 在 0.5 到 2.0 之间。然后调外环,Kp 从 0.1 开始加,直到车能沿直线走但稍微有点摆,再加 Kd 抑制摆动。Ki 一般给很小或者不给,因为循迹对稳态误差要求不高。
3.3 编码器速度计算与滤波
编码器读的是计数值,要转成速度需要除以采样周期。代码里用 1ms 定时器读一次编码器,做差得到增量:
// 编码器速度计算(1ms 周期) int16_t Encoder_GetSpeed(uint8_t motor) { static int16_t last_cnt[2] = {0, 0}; int16_t now_cnt; if (motor == LEFT) { now_cnt = (int16_t)DL_TimerG_getCurrentCounterValue(ENC_TIMER_L); } else { now_cnt = (int16_t)DL_TimerG_getCurrentCounterValue(ENC_TIMER_R); } int16_t diff = now_cnt - last_cnt[motor]; last_cnt[motor] = now_cnt; // 一阶低通滤波,系数 0.7 static float filtered[2] = {0, 0}; filtered[motor] = 0.7f * filtered[motor] + 0.3f * diff; return (int16_t)filtered[motor]; }逻辑说明:编码器计数值是 16 位有符号,做差时直接强转 int16_t 能自动处理溢出。一阶低通滤波系数 0.7 是经验值——系数越大速度越平滑但延迟越大,0.7 在 1ms 周期下延迟约 3ms,对循迹够用。
注意:如果编码器线数低(比如 13 线),1ms 内增量可能只有几个计数,量化误差大。常见做法是降低采样频率到 5ms 或 10ms,或者用 M/T 法测速。这套代码用的是 1ms 定时器,适合 100 线以上的编码器。
4. 避坑与排查:调车时最容易翻车的五个地方
4.1 灰度传感器全白或全黑时车直接冲出去
现象:车走到赛道边缘或十字路口,八路灰度全白,偏差计算返回 0,车保持直行冲出赛道。
原因:Gray_GetError里全白时返回last_error,但如果上次偏差刚好是 0,车就直行了。十字路口全黑同理。
解决:加丢线保护。全白时强制给一个上次非零偏差的方向,或者直接停车。代码里可以这样改:
if (weight_sum < 0.1f) { if (fabs(last_error) < 0.5f) { return (last_nonzero_error > 0) ? 3.0f : -3.0f; } return last_error; }4.2 电机 PWM 频率选错导致驱动板发热
现象:车跑几分钟后驱动板烫手,甚至 MOS 管烧掉。
原因:PWM 频率太低(比如 1kHz),电机电流纹波大,MOS 管开关损耗和导通损耗都高。或者频率太高(比如 50kHz),驱动板死区时间不够导致上下桥直通。
解决:20kHz 是大多数有刷电机驱动板的甜点频率。如果驱动板手册给了推荐频率,优先按手册来。没有手册就 15kHz 到 25kHz 之间试,摸驱动板温度。
4.3 PID 积分项导致起步猛冲
现象:车从静止启动时猛冲一下,然后才稳定到目标速度。
原因:速度环积分项在起步时累积了大量误差,一旦电机开始转,积分项输出一个很大的 PWM 值。
解决:积分分离或者积分限幅。代码里可以在PID_Speed_Calc里加:
if (fabs(error) > 50) { integral = 0; // 误差大时清零积分 } else { integral += error; if (integral > 1000) integral = 1000; if (integral < -1000) integral = -1000; }4.4 编码器计数方向反了导致速度环正反馈
现象:车一启动就全速冲出去,速度环完全失控。
原因:编码器 A/B 相接反,速度反馈符号和实际转向相反,PID 变成正反馈。
解决:交换编码器 A/B 相引脚定义,或者在速度计算里取反。判断方法:手动转轮子,看串口打印的速度值符号和转向是否一致。
4.5 ADC 采样值跳变导致循迹抖动
现象:车在直道上左右小幅高频摆动,像“发抖”。
原因:灰度传感器模拟输出有噪声,ADC 采样值跳变导致偏差计算不稳定。
解决:硬件上在灰度输出加 0.1uF 电容滤波,软件上对gray_raw做滑动平均或一阶低通。代码里可以在Gray_Normalize之前加:
for (int i = 0; i < 8; i++) { gray_filtered[i] = 0.8f * gray_filtered[i] + 0.2f * gray_raw[i]; }5. 进阶调参与验证:用串口波形把 PID 调明白
5.1 串口打印关键变量:偏差、目标速度、实际速度
调 PID 最怕“盲调”——改一个参数跑一圈,凭感觉判断好坏。这套代码留了 UART0 调试口,我习惯把关键变量按固定格式打印出来,用串口示波器看波形。
// 1ms 控制循环里打印,每 10ms 打一次 static uint8_t print_cnt = 0; if (++print_cnt >= 10) { print_cnt = 0; printf("%.2f,%.2f,%.2f,%.2f,%.2f\r\n", error, // 位置偏差 target_left, // 左轮目标速度 actual_left, // 左轮实际速度 target_right, // 右轮目标速度 actual_right); // 右轮实际速度 }逻辑说明:每 10ms 打印一行 CSV,串口示波器直接能解析。error看位置环响应,target和actual的差值看速度环跟踪效果。如果actual总是滞后target很多,说明速度环 Kp 太小或者基础速度给太高。
参数怎么改:打印周期别太快,115200 波特率下每行约 50 字节,10ms 一行刚好不丢数据。如果发现打印影响控制周期,把打印放到空闲中断里或者降低频率到 20ms。
5.2 用阶跃响应判断 PID 参数是否合理
调速度环时,我给目标速度一个阶跃(比如从 0 跳到 100),看实际速度的响应曲线。理想情况是:上升时间短、超调小、稳态无静差。
| 现象 | 可能原因 | 调整方向 |
|---|---|---|
| 上升慢,很久才到目标 | Kp 太小 | 增大 Kp |
| 超调大,冲过头再回来 | Kp 太大或 Kd 太小 | 减小 Kp,增大 Kd |
| 稳态有静差,到不了目标 | Ki 太小或没加 Ki | 适当增大 Ki |
| 高频振荡 | Kp 太大或滤波不够 | 减小 Kp,加强滤波 |
我一般先把 Kd 和 Ki 置零,只调 Kp 到临界振荡,然后取临界值的 60% 作为最终 Kp。再加 Kd 抑制超调,最后加一点点 Ki 消静差。这套流程走下来,速度环基本能调到跟踪误差小于 5%。
5.3 赛道实测:从直道到 S 弯的验证顺序
调完 PID 别直接上完整赛道,按这个顺序验证:
第一步,直道。车能沿直线走不画龙,偏差在 ±1 以内。如果画龙,减小位置环 Kp 或增大 Kd。
第二步,大弯。车能沿弯道走不冲出去,转向量够用。如果冲出去,增大位置环 Kp 或提高基础速度。
第三步,S 弯。连续左右弯,车能快速切换转向方向。如果切换慢,检查速度环响应是否够快。
第四步,十字和丢线。车能识别十字直行,丢线能保护。如果十字处乱转,检查灰度全黑时的偏差处理逻辑。
我自己的习惯是:每次改完参数,先架空跑 10 秒看波形,再落地跑直道,最后跑完整赛道。从那以后我每次调车都强制走一遍“架空→直道→弯道→完整赛道”的流程,省得反复拆车。希望帮到你。
本文还有配套的精品资源,点击获取