ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于MSPM0G3507的循迹小车实战:PID调参与电赛H题避坑指南

基于MSPM0G3507的循迹小车实战:PID调参与电赛H题避坑指南 每年电赛H题都会刷掉一批“看起来简单”的队伍。今年我们组做2024电赛H题——基于MSPM0G3507的循迹小车前期最痛苦的其实不是PID公式而是从STM32切到TI这颗Cortex-M0芯片之后所有外设都要重新学。GPIO怎么翻、定时器怎么出PWM、串口怎么重定向每一步都在查手册。等把这些基础设施磨通了PID只是最后那一层窗户纸。这篇博文不是我复制一份能跑的工程而是把从硬件选型、传感器误差计算、源码模块拆解到现场调参和各类翻车场景的记录完整捋一遍。里面有能直接用的代码框架也有只有跑到赛场才会踩到的细节。如果你今年也打算用MSPM0G3507做循迹小车或者正在为H题的头疼这篇文章应该能帮你少走很多弯路。1. 先复盘2024电赛H题这道题真正难在哪1.1 赛题硬指标与隐藏得分点2024年电赛H题的核心是让一辆自动行驶小车沿着地面黑线行走在指定位置停车、停留再启动继续走到终点全程不压线、不出赛道。表面看就是“循迹定点停车二次启动”很多队伍觉得这比C题、D题简单多了结果到了现场才发现坑全埋在细节里。这道题真正考的是三件事行驶稳定性黑线可能有不连续、有反光、有直角弯小车能不能稳定咬住线直接决定后续所有得分项。停车精度到达B点时不能冲过头也不能在线外停蜂鸣器提示的时间点要和物理停车位置对上。全流程可靠性从A出发到B停车再到C点终点整个流程是状态机切换任何一个环节卡死成绩直接归零。我在第一轮测试时以为“只要PID能跟上误差就能沿黑线跑”结果小车在直线段疯狂画龙一到弯道直接冲出去。后来才意识到循迹车是典型的“低速移动机器人”它的问题不是算法不够聪明而是传感器噪声、机械公差、电压波动这些工程因素全被塞进了PID的误差信号里。所以真正拉开差距的是对这些干扰项的处理而不是PID公式本身。1.2 为什么MSPM0G3507会成为很多队伍的主控2024年电赛赛场上MSPM0G3507出镜率很高原因也不复杂TI作为赞助商提供了这块板子很多赛区甚至直接推荐使用。但不少队伍是临时从STM32转过来的上手才发现这是两个世界。MSPM0G3507的核心参数其实不差Cortex-M0内核主频最高80MHz128KB Flash、32KB RAM内置12位ADC、模拟比较器、运放、多个高级定时器串口、SPI、I2C都不缺。单做循迹小车绰绰有余甚至有点性能富余。但它的外设库和STM32标准库完全是两套逻辑函数前缀从HAL_变成了DL_外设初始化大量依赖TI的SysConfig图形化工具定时器类型分成TIMG和TIMA配置思路完全不同串口重定向printf需要自己实现fputc中断优先级和NVIC分组规则也和Cortex-M3/M4不一样。有队友第一周用SysConfig生成代码生成了一堆“看不懂但能用”的初始化结果改一个引脚配置就把整个工程搞崩了。所以我建议后面想用这颗芯片的同学先把SysConfig生成的代码读懂再往里面加自己的逻辑不要当黑盒用。2. 小车硬件方案与传感器选型PID调得再好也救不了错误底盘2.1 底盘结构差速后驱是H题最稳的方案循迹小车的底盘方案我在实验室里试过三种前轮转向后轮驱动、四轮直驱、后轮差速前万向轮。前轮转向方案看着像真车但机械结构复杂转向舵机响应慢PID输出到前轮转角之间还隔着一层舵机控制调起来很不直接。四轮直驱在平整地面还行但四个电机的一致性很难保证直线都跑不正。最后我们选了最土的方案两个带霍尔编码器的直流减速电机做后轮差速驱动前面加一个万向轮。这个方案的好处是转向就是左右轮速差PID输出可以直接映射到PWM差量链路短、实时性高霍尔编码器带回来的速度反馈后续要加速度环也很方便万向轮天生不会和地面较劲机械公差对循迹的影响被降到最低。底盘选型时还要注意轮距和轴距。我们第一版车模轴距太长直角弯时后轮扫线严重被迫加宽传感器阵列的覆盖范围才解决。如果你们还在画车模阶段建议轴距控制在20cm以内传感器支架前伸让传感器比前轮更早“看到”黑线这样转向有提前量。2.2 8路灰度阵列误差怎么算才合理传感器方面主流方案就是灰度传感器阵列。我们用的是8路数字量输出模块单路就是一对红外发射管接收管检测到黑线时输出电平翻转。为什么不推荐用模拟量输出因为8路模拟量要占8个ADC通道MSPM0G3507虽然有多个ADC但采样排队和切换通道也需要时间控制周期很容易被拉长。数字量输出直接接GPIO读电平速度快、逻辑简单10ms控制周期内完成8路读取绰绰有余。关键在安装间距。我们用的模块传感器中心间距1.5cm8路覆盖约12cm宽度。赛道黑线一般2cm宽这样黑线在传感器下面移动时通常会有1到3路同时检测到黑线。误差计算用经典的加权平均法int8_t position_error 0; uint8_t active_count 0; for (uint8_t i 0; i 8; i) { if (sensor_black[i]) { position_error (int8_t)(i - 3) * 100; // 每个传感器权重中心为0 active_count; } } if (active_count 0) { position_error / active_count; } else { position_error 0; // 丢线处理后面细说 }这里i - 3的权重设计有一个坑如果传感器下标从0到7那中心位置在3和4之间如果想更精确可以把权重设为i - 3.5。但要避免浮点除法直接乘10再除把误差放大到“百分之一”量级方便后面调PID时Kp的数值不至于太小。这个误差值的含义要统一误差为负代表黑线偏左也就是车偏右误差为正代表黑线偏右。后面PID输出映射到自己写的左右轮PWM时正负方向必须和这个定义一致否则一上电就朝线外跑。3. PID源码级拆解从定时器PWM到编码器反馈的完整链路3.1 基于MSPM0G3507的PWM输出与电机驱动配置电机驱动用的是TB6612这芯片便宜、好用两路电机正反转都能控制。MSPM0G3507的定时器主要用来产生PWM信号。我们在SysConfig里选了TIMG0的通道0和通道1分别作为左轮和右轮的PWM输出。PWM频率我建议设10kHz。频率太高TB6612的开关损耗变大电机电流波形会有毛刺频率太低比如几百Hz电机会发出明显啸叫而且低速时扭矩波动很大。10kHz在这两者之间比较平衡。SysConfig里配置好引脚后初始化代码大致是这个逻辑void pwm_init(void) { // 假设 SysConfig 已经生成了 TIMG0 初始化 DL_TimerG_setCaptureCompareValue(PWM_0_INST, 0, DL_TIMER_CC_0_NUM); DL_TimerG_setCaptureCompareValue(PWM_0_INST, 0, DL_TIMER_CC_1_NUM); DL_TimerG_startCounter(PWM_0_INST); } void set_motor_pwm(uint8_t left_pwm, int8_t left_dir, uint8_t right_pwm, int8_t right_dir) { // TB6612 的 AIN1/AIN2/BIN1/BIN2 控制方向PWM 引脚控制速度 DL_GPIO_writePins(GPIOA, AIN1_PIN, left_dir 0 ? PIN_LOW : PIN_HIGH); DL_GPIO_writePins(GPIOA, AIN2_PIN, left_dir 0 ? PIN_HIGH : PIN_LOW); DL_GPIO_writePins(GPIOA, BIN1_PIN, right_dir 0 ? PIN_LOW : PIN_HIGH); DL_GPIO_writePins(GPIOA, BIN2_PIN, right_dir 0 ? PIN_HIGH : PIN_LOW); DL_TimerG_setCaptureCompareValue(PWM_0_INST, left_pwm, DL_TIMER_CC_0_NUM); DL_TimerG_setCaptureCompareValue(PWM_0_INST, right_pwm, DL_TIMER_CC_1_NUM); }注意MSPM0的定时器PWM输出是“捕获比较值”来控制占空比SysConfig里通常用8位模式还是16位模式要提前想清楚。我们用的是8位PWM模式占空比范围0~255代码里直接传left_pwm就行。如果你发现占空比和预期差得很远先检查SysConfig里的PWM分辨率是不是8位。另外TB6612有一个STBY引脚这个引脚不拉高电机驱动芯片完全罢工。我们调试时出现过“代码跑得好好的但轮子就是不转”的灵异现象最后发现是STBY没接被悬空了。这种低级错误特别容易在比赛当天紧张时出现建议所有使能引脚都在初始化里固定写死不要依赖默认电平。3.2 编码器测速没有专用QEI接口时怎么办MSPM0G3507和STM32不一样不带硬件正交编码器接口QEI。很多从STM32转过来的同学在这里卡了很久。我们的做法是用定时器计数 GPIO方向判断把编码器A相接定时器的输入捕获引脚将其配置为外部计数模式每个上升沿计数器加一编码器B相接普通GPIO在读取计数器时顺便读一下B相电平用来判断正反转。核心代码是一个10ms周期的采样函数volatile int16_t encoder_left_count; uint16_t last_left_count; void encoder_sample_10ms(void) { uint16_t now DL_TimerG_getTimerCount(ENCODER_TIMER_INST); int16_t diff (int16_t)(now - last_left_count); last_left_count now; // B相电平决定方向假设高电平为反转 uint8_t b_level DL_GPIO_readPins(GPIOB, B_PIN); if (b_level 1) { diff -diff; } encoder_left_count diff; // 10ms内的脉冲数增量 }这个方案在低中速下非常可靠但有几个坑定时器计数模式和PWM模式要分开用我们用了TIMG1做编码器计数TIMG0做PWM不要把两个功能挤在同一个定时器上编码器AB相有没有接反直接决定速度反馈的正负。调速度环之前先用串口把encoder_left_count打出来手动转轮子验证方向读计数器和读B相电平之间如果电机正高速转理论上可能读到“旧计数新方向”的组合。实际测试中10ms周期下误差很小不必过度设计但如果追求严谨可以在读B相前后各读一次计数器做校验。速度换算方面我们用的电机是1:30减速比霍尔编码器是11线轮子直径65mm。那么每转一圈的脉冲数大约是 11 × 30 330个轮子周长约0.204m10ms内的脉冲增量乘以 0.204 / 330就能得到小车速度m/s。这个换算在做速度闭环时要反复用到。3.3 转向环PID的实现与输出映射PID部分我们只做了转向环没做速度环也能满足H题要求。转向环的输入是8路灰度传感器算出来的position_error输出是一个校正量correction叠加到左右轮基础PWM上。PID结构体和更新函数如下typedef struct { float kp; float ki; float kd; float target; float error; float last_error; float integral; float output; float integral_limit; float output_limit; } pid_t; float pid_update(pid_t *pid, float feedback, float dt) { pid-error pid-target - feedback; pid-integral pid-error * dt; if (pid-integral pid-integral_limit) pid-integral pid-integral_limit; if (pid-integral -pid-integral_limit) pid-integral -pid-integral_limit; float derivative (pid-error - pid-last_error) / dt; pid-output pid-kp * pid-error pid-ki * pid-integral pid-kd * derivative; pid-last_error pid-error; if (pid-output pid-output_limit) pid-output pid-output_limit; if (pid-output -pid-output_limit) pid-output -pid-output_limit; return pid-output; }在循迹小车里target永远是0因为目标就是让误差归零。要注意的是pid-integral的限幅循迹场景积分项很容易饱和因为只要车一直偏在线的一侧积分就会一直累积最后输出一个巨大的转向角车直接甩出去。所以积分限幅我们设得很小比如在±30左右或者干脆Ki0完全不用积分项。输出映射是另一个高频翻车点int base_speed 60; // 基础PWM范围0~255 int speed_limit 220; // 最大PWM限制 int left_pwm base_speed (int)correction; int right_pwm base_speed - (int)correction; // 限幅 if (left_pwm speed_limit) left_pwm speed_limit; if (left_pwm 0) left_pwm 0; if (right_pwm speed_limit) right_pwm speed_limit; if (right_pwm 0) right_pwm 0; set_motor_pwm(left_pwm, 1, right_pwm, 1);这里最关键的是correction的极性。以我们的安装为例position_error为负表示黑线在左、车偏右此时应该左转。左转意味着左轮减速、右轮加速。那么在刚才的映射里correction必须是负的也就是left_pwm base negative变小right_pwm base - negative变大。这个逻辑和PID输出的output默认是反向还是正向有关系我们最后在代码里加了一个POLARITY_REVERSE宏#define POLARITY_REVERSE 1 #if POLARITY_REVERSE correction -correction; #endif调参时如果发现车往线的反方向猛打改这个宏就够了不用重新接线。3.4 停车与再启动状态机才是控制逻辑的主心骨H题要求到指定点停车再二次启动。如果不做状态机只在主循环里“死磕PID”停车和启动逻辑会变成一团乱麻。我们用了一个五状态状态机typedef enum { STATE_START 0, STATE_TRACK, STATE_STOPPING, STATE_STOPPED, STATE_FINISH } run_state_t; run_state_t current_state STATE_START;STATE_START启动瞬间车辆从头开始电机从零加速到基础PWM避免起步过猛STATE_TRACK正常巡线状态PID持续运行STATE_STOPPING检测到停车标志后减速阶段PID停止工作PWM线性下降到0STATE_STOPPED完全停止蜂鸣器响车等待设定的暂停时间STATE_FINISH到达终点电机停止蜂鸣器长响。停车标志的检测我们是利用8路传感器“全黑”来判断——如果赛道上在停车点画了一条横贯黑线那小车经过时8路传感器会全部检测到黑线和正常巡线时“只有1到3路黑”有明显区别。但“全黑”不能直接当停车标志因为直角弯的时候也可能短暂全黑。我们做了一级滤波连续5个控制周期50ms都检测到全黑才进入STATE_STOPPING。这个滤波时间要根据车速调整车速越快滤波周期要越短否则车都已经冲过停车点了还没触发。停车后二次启动我们直接延时2秒然后切回STATE_TRACK。如果赛题对暂停时间有窗口要求这个延时可以用定时器精确计时不要用delay()死等否则电机停止时整个系统的传感器采样也会被卡住恢复后会出现一段时间的控制真空。4. 调参实战临界比例度法在循迹车上的具体操作4.1 调参顺序先把转向环调稳再谈速度环我们组的调参顺序是先调转向环转向环稳了再回头考虑基础速度。很多同学一上来就追求“跑得快”把base_speed设到200多结果车在赛道上像喝了酒一样。原因很简单转向环还没调好速度越快误差被PID放得越剧烈。所以调参第一步先把base_speed压到30左右让车龟速爬上赛道。这个时候如果转向环的Kp、Kd合理车应该能慢悠悠地咬住线。等低速下能稳定巡线了再把速度往上加。这里有个反直觉的经验低速下能巡线不代表高速下也能巡线。因为车速高了之后同样的控制周期意味着车辆行进距离变长传感器看到误差到执行转向之间多了一段“盲区”。所以我们每提高一次base_speed都要重新检查Kp、Kd。我整理了一张自己调参时常用的对照表供参考现象问题根源应对方法直线段画龙蛇形Kp太大或Kd太小降低Kp或增大Kd抑制振荡入弯反应慢压外线Kp太小或Kd太大增大Kp适当降低Kd弯道内甩尾输出限幅不够转向过猛降低output_limit或降低base_speed直行时左右轮偏航电机机械误差、电压波动加编码器速度环或修正基础PWM偏置4.2 Kp/Ki/Kd各自影响什么我的观测方法循迹小车里PID三个参数的作用其实很直观Kp比例车偏得越多转向就越猛。Kp太小弯道转不过来Kp太大直线上出现高频抖动。Kd微分起到“阻尼”作用抑制Kp带来的过冲。Kd太小车会蛇形Kd太大会引入噪声因为误差信号本身有抖动微分会把抖动放大成很大的转向量。Ki积分在循迹里基本可以设0。因为循迹的目标是让误差归零比例项已经能完成这个任务积分反而容易积累饱和。我们用的是“先P、再加D、最后考虑I”的顺序。把Ki和Kd先设0Kp从10开始往上加每次加5直到发现直线段开始蛇形。记录这个临界Kp然后取它的一半作为实际Kp再慢慢加Kd直到蛇形消失。临界Kp的判断要小心有时候直线蛇形不是Kp太大而是控制周期不确定。我们之前把PID更新放在主循环里而主循环里还有OLED刷新和串口打印控制周期在8ms到15ms之间跳跃导致蛇形怎么调都调不掉。后来把PID计算挪到10ms定时器中断里问题立刻消失。控制周期必须是稳定、固定的否则PID的所有经验值都失效。4.3 用串口和波形工具把调参变成看曲线而不是猜调PID最怕的就是“盲调”——不知道误差曲线长什么样全凭肉眼判断车轮状态。我们用MSPM0G3507的串口把关键数据发到上位机用波形工具直接看曲线。串口输出我们用了一个轻量级协议每10ms发送一次char tx_buf[64]; sprintf(tx_buf, err:%d out:%d L:%d R:%d\r\n, (int)position_error, (int)correction, left_pwm, right_pwm); uart_send_string(tx_buf);上位机用VOFA选择ASCII模式按\r\n换行分隔就能直接在波形窗口看到误差随时间的变化。看波形比看车跑要高效得多如果误差曲线是等幅振荡说明Kp进入了临界状态如果误差曲线低频大摆幅说明Kp不够或者Kd太大如果误差曲线高频细碎抖动说明传感器噪声被微分项放大了要降低Kd或在信号端先做滤波。printf重定向在MSPM0G3507上要自己实现fputc把输出字符逐个通过UART发送。这里有一个性能陷阱如果直接在10ms定时器中断里调sprintf和串口发送串口波特率115200时发送完一帧几十字节可能就要花掉几百微秒到1毫秒中断时间被拉长。我们最后是先把要发送的数据写进一个环形缓冲主循环里再真正把数据发出去这样PID计算和打印互不干扰。5. 避坑实录从实验室到电赛现场我们踩过的坑5.1 阈值漂移现场标定比调Kp更重要灰度传感器的核心问题不是“能不能检测到黑线”而是“阈值怎么设”。传感器模块输出的是数字电平但模块内部是通过比较器判断“黑”和“白”的阈值电位器调好后在实验室白底黑线环境没问题到了赛场地面颜色、灯光强度、传感器高度稍有变化阈值就可能失效。我们实验室调试时用的是灰蓝色地面黑色电工胶带到赛场发现地面是偏红色的传感器检测黑线的灵敏度和实验室完全不一样。第一圈测试车在线上面“瞎了”8路全白直接丢线冲出赛道。解决办法是写一个“一键标定”函数小车启动前长按按键2秒进入标定模式手动推动小车扫过一段黑白分明的区域程序记录每一路传感器输出的最大值和最小值自动算出中间值作为判定阈值。如果传感器模块用的是数字量输出这个标定其实是对模块的电位器做了一个“挡位校准”能保证黑白判断稳定。更简单粗暴的方案是换用模拟量灰度传感器用ADC读取原始反射值在主控里设定阈值。这样可以把阈值做成变量代码里直接调整不用每次拆螺丝去拧电位器。5.2 电池电压掉电同样的占空比跑出不同的车3S锂电池满电12.6V快没电时可能只有10V出头。同样的PWM占空比在不同的电压下电机实际功率差很多。这就导致一个现象上午满电测试车跑得很稳下午电池电量低了同一个程序在直线段越来越慢弯道还冲不出去。我们加了一个简单的电压补偿逻辑uint16_t battery_mv read_battery_voltage(); // 通过分压电阻采样 float voltage_ratio 12.6f / (battery_mv / 1000.0f); int base_speed (int)(50 * voltage_ratio); // 基础速度随电压升高这个补偿不需要很精确只要让base_speed在电池电压下降时自动升高一点保持整个下午的车辆速度基本一致。如果把补偿做得太激进低压下PWM会接近饱和反而失去调速空间所以补偿系数一定要现场实测不能拍脑袋。另外电机启动瞬时电流很大会拉低总线电压。如果MSPM0G3507和传感器直接和电机共用一个电源一启动就可能跑飞。我们的经验是主控和传感器用独立的5V稳压供电电机驱动直接吃电池电压两边只共地。地线也要用粗一点否则编码器信号在电机启动瞬间会出现大量毛刺。5.3 丢线处理与全黑误判这两个问题会在赛场上杀死你的车丢线是循迹车最经典的事故场景。正常巡线时8路传感器中至少有一路检测到黑线如果8路全白说明车已经偏离黑线很远了可能整个传感器阵列都滑出了赛道。我们最初的做法是丢线后保持上一次误差继续转向试图“转回来”。结果在直角弯里车一旦冲过弯心传感器全白它会一直朝一个方向猛转最后原地打转或者彻底偏离赛道。正确的丢线策略应该是记录丢线前最后一帧有效误差的方向丢线后按这个方向输出一个递减的校正量而不是保持原值连续丢线超过一定时间比如300ms直接降低基础速度防止越冲越远如果丢线超过1秒强制停车等待人工介入或自动回退。我把这个逻辑放到了PID更新函数外面uint8_t lost_count 0; if (active_count 0 current_state STATE_TRACK) { lost_count; if (lost_count 30) { base_speed 20; // 低速继续尝试寻线 } if (lost_count 100) { set_motor_pwm(0, 1, 0, 1); // 彻底停车 } } else { lost_count 0; }全黑误判的问题刚好相反。正常巡线时全黑是异常状态但停车线区域全黑是正常状态。如果代码里把“全黑”一刀切当成停车标志直角弯时就会误停车。前面的状态机已经解决了这个问题但还要注意一点停车线触发后车要完全静止再倒计时否则车停在停车线上时传感器还是全黑的状态机可能会反复触发停车逻辑。5.4 代码层级的小陷阱中断、浮点、优化等级这部分的坑属于不跑现场永远不知道的隐性bug。第一个坑中断里不要调printf。MSPM0G3507的UART发送是阻塞式的如果在中断里发一长串字符系统会连续卡在发送函数里所有其他中断都排队等待。我们曾把PID调试信息直接放在定时器中断里发送结果速度环一直调不稳因为整个控制周期因为串口发送变得极不稳定。后来改成环形缓冲 主循环发送问题立刻消失。第二个坑Cortex-M0没有FPU。MSPM0G3507是M0内核没有硬件浮点单元所有float运算都是软件模拟。PID公式里float乘法在几十微秒级别确实不慢但如果你的控制周期是1ms以内频繁的float运算会占满CPU。我们最终把PID参数全部改成整数运算误差乘一个放大系数速度提升明显。M0连硬件除法都没有整数除法也是软件实现的能避免就避免。第三个坑编译器优化等级。我们用的是Keil MDK默认优化等级是-O0烧进去车跑得很正常后来手贱把优化等级调到-O2小车开始随机抽风。排查了半天发现是一个GPIO读函数里没加volatile被优化后读到的值完全是缓存的旧值。如果遇到“同样的代码优化等级一变行为就变”优先查外设寄存器相关的变量有没有加volatile。第四个坑SysConfig生成代码的初始化顺序。SysConfig会把所有初始化代码集中生成但如果你在代码里手动调用了某个外设的初始化函数然后又执行了SysConfig的初始化外设配置可能被覆盖。我们Debug时发现串口初始化和定时器初始化之间GPIO电平被重置了导致电机方向控制瞬间翻转。后来统一在SysConfig里配置所有外设不再手动调用重复的初始化函数问题才解决。调试循迹小车的过程本质上就是把“理论上的PID”变成“现实中的PID”的过程。你可能花三天把公式写好但真正让车稳定跑起来、能在赛场上不出意外地跑完全程靠的是对传感器阈值、电压波动、丢线处理、状态机切换这些细节的打磨。比赛结束后我最大的感受是对于H题这种“任务不复杂但环境不确定”的场景稳定性的优先级永远高于速度。赛前把所有可能翻车的环节都写进代码比在赛场上寄希望于运气要靠谱得多。
返回列表