MSPM0G3507三轮循迹车实战:八路灰度+PID速度闭环代码解析 简介这份开源代码面向参加全国大学生电子设计竞赛及学习嵌入式控制的学生与开发者提供基于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 秒看波形再落地跑直道最后跑完整赛道。从那以后我每次调车都强制走一遍“架空→直道→弯道→完整赛道”的流程省得反复拆车。希望帮到你。本文还有配套的精品资源点击获取