ARTICLE DETAIL

资讯详情

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

从状态机到PID:STM32循迹小车控制算法实战解析

从状态机到PID:STM32循迹小车控制算法实战解析 1. 为什么我一开始坚持用状态机做循迹而不是直接上PID先交代一下背景。我做的这台循迹小车用的是STM32F103ZET6传感器是三路灰度传感器电机是两路带编码器的直流减速电机驱动方式是最常见的差速转向。项目开始的时候我的目标很简单让小车完整跑完一圈由黑白胶带组成的赛道速度不要求太快稳定不冲出赛道就行。那时候我翻了不少网上分享的代码发现绝大多数教程都在教“三段式状态机”或者“四段式状态机”三路传感器分别检测黑线位置CPU按组合关系判断小车当前处于“左偏、右偏、居中”中的哪一个状态再据此给左右电机输出固定的PWM差速值。代码逻辑非常直观一个switch-case或者if-else链就能写完我当时也毫不犹豫选了这条路。状态机方案的核心思想是把连续空间离散化。赛道上的位置误差被划分成几个有限状态每个状态对应一套固定的执行动作。这种思路在控制理论里叫“离散事件动态系统”它天然适合处理传感器的开关量信号灰度传感器输出要么是0要么是1三路组合最多8种状态映射到偏差判断上绰绰有余。但问题恰恰出在这里。状态机只在“状态边界”上做切换它完全不关心小车偏离赛道中心的程度。三路传感器能提供的信息太粗了。拿三路排布来说传感器间距一般做1.52厘米小车压线时左、中、右三路的通断组合只能告诉你“大概偏了”却告诉不了你“偏了1厘米还是偏了3厘米”。理论上可以通过增加传感器路数来细化状态比如7路、13路甚至更多很多竞赛队伍确实这么干。传感器路数越多位置分辨率越高状态机就越接近连续控制。但随之而来的问题是状态表爆炸式增长7路传感器的有效组合是2的7次方减1人工枚举所有组合并逐一设定执行参数工作量非常可观而且每一路传感器的阈值标定都必须严格一致否则某个边界状态会误触发。我实际跑下来还遇到另一个隐蔽问题状态切换产生的控制量跳变。当小车从“居中”状态切换到“左偏”状态时左右电机的PWM是瞬间从一组固定值跳到另一组固定值的。这个跳变在直道上表现为车身横向抖动在高速过弯时表现为甩尾甚至直接甩出赛道。你如果把状态机的输出波形用逻辑分析仪抓出来看会发现控制量曲线是阶梯状的而电机的机械响应不可能瞬间跟上这种阶跃于是整个系统始终在“过冲-修正-再过冲”之间震荡。这些问题的根源不在于代码写不好而在于状态机这个模型本身不适合作为“精细运动控制”的核心。状态机的强项是处理离散的逻辑判断比如开机自检、寻线模式切换、避障决策、停车判断但把它用在“连续调节转向力度”这个场景上就属于拿游标卡尺去量头发丝的粗细——工具选错了。所以后面我把架构改成了“状态机连续控制器”的分层设计状态机负责流程切换和大逻辑判断连续控制部分交给PID算法。这个改动看起来只是加了一段控制代码实际上是把整台车从“离散逻辑主导”改造成了“连续反馈主导”效果完全是两个层次。2. 从离散状态到连续偏差量改变控制对象才是关键很多教程在讲PID循迹时上来就贴一段PID计算函数然后告诉你“把误差喂进去把输出给到电机”。这个说法没错但忽略了最核心的前置问题误差到底从哪来用什么数据结构来表示采样频率多少这些不搞清楚PID代码写得再漂亮也是空中楼阁。2.1 用加权位置计算法提取连续偏差量三路灰度传感器如果只做if判断那得到的就是离散状态。要做PID必须把三路过通道断信号换算成一个连续的数值一般在[-100, 100]或者[-50, 50]区间内表示小车中心相对赛道中心的偏移量。最常用的方法是加权位置计算float get_position_error(void) { uint8_t s1 GPIO_ReadPin(SENSOR1_PORT, SENSOR1_PIN); // 左 uint8_t s2 GPIO_ReadPin(SENSOR2_PORT, SENSOR2_PIN); // 中 uint8_t s3 GPIO_ReadPin(SENSOR3_PORT, SENSOR3_PIN); // 右 float pos 0.0f; uint8_t count 0; if (s1) { pos -50.0f; count; } if (s2) { pos 0.0f; count; } if (s3) { pos 50.0f; count; } if (count 0) { return last_error; // 丢线时保持上次偏差交由上层逻辑处理 } last_error pos / count; return last_error; }这段代码的逻辑是每一路被压到的传感器赋予一个位置权重最后取平均。左边传感器亮贡献-50中间贡献0右边贡献50。如果同时压到左边和中间偏差就是-25表示“偏左但不是特别左”。如果你用5路传感器权重可以设为-100、-50、0、50、100分辨率更高。这个连续偏差量就是PID控制器的输入。它和状态机的本质区别在于状态机只能告诉你“偏左”而偏差量告诉你“偏左了25%的量程”。有了这个量控制器才能输出“向左修正25%力度”的平滑指令而不是“向左修正满力度”。2.2 差分转向和比例转向的执行层差异电机执行层也需要配合调整。在状态机时代我的代码是直接给左右轮各一个固定PWMLEFT_MOTOR_PWM 45; // 居中时 RIGHT_MOTOR_PWM 45;状态切换到左偏时可能变成LEFT_MOTOR_PWM 30; RIGHT_MOTOR_PWM 55;这种固定值执行的缺点你已经知道了。换成PID之后执行逻辑应该变成“基础速度补偿量”的结构int base_speed 60; int turn_correction (int)pid_calculate(position_error); LEFT_MOTOR_PWM base_speed turn_correction; RIGHT_MOTOR_PWM base_speed - turn_correction;注意这里是差速转向左轮和右轮的速度差决定了转向快慢。PID输出的turn_correction是一个连续的转向补偿值偏差越大补偿越大转向越猛。偏差小的时候补偿小车就顺滑地微调方向。这里有一个新手特别容易踩的坑PID输出值必须匹配PWM的物理量纲。如果你的PWM范围是0100而PID输出范围是-10001000直接相加会导致PWM溢出或者长期占空比饱和车会一头扎向一边。解决办法要么对PID输出做限幅要么做线性映射把PID输出映射到合适的区间int turn_correction (int)(pid_output * 0.1f); // 映射到电机PWM尺度 turn_correction clamp(turn_correction, -40, 40);限幅值要根据实测的赛道最大弯道曲率来定。弯道越急需要的最大修正量越大限幅太死过急弯时修正不够车一样会冲出赛道。2.3 增量式PID和位置式PID怎么选循迹小车这个场景编码器是装在电机上的如果你用的是带编码器的直流减速电机那么你自然可以测量实际轮速做闭环速度控制。但如果像我一开始那样只做了位置环PID不加速度环那位置式PID和增量式PID都可以用区别主要在输出形式和抗积分饱和能力。位置式PID的输出是“绝对控制量”直接是PWM的基准值公式是每一项分别算出再求和output Kp * error Ki * integral Kd * derivative;增量式PID输出的是“控制量的增量”每次在上次输出上叠加delta_output Kp * (error - last_error) Ki * error Kd * (error - 2 * last_error last_last_error); output delta_output;在循迹小车上我更推荐增量式理由有两个第一增量式天然带“记忆”比例项处理的是误差变化率即使目标值突变输出也不会瞬间跳变这对于防止电机PWM突变导致的机械抖动非常有效。第二增量式比较容易做积分限幅。位置式PID的积分项如果处理不好会出现积分饱和——误差一直存在积分项越来越大等误差消失时积分项还在推动输出导致明显的超调。增量式因为每次只累加一个小增量即使误差持续存在输出也是缓慢爬升而不是瞬间爆表容错性更好。当然增量式也有代价它对PID系数的敏感度更高参数稍微调大一点就容易振荡。所以实际应用时我采用的策略是位置环用增量式PID速度环用位置式PID各取所长。你要是只有单PID资源全用增量式也没问题。3. PID参数整定实战P、I、D每一项都是试出来的说句实在话PID参数整定没有捷径它就是用实验换经验的过程。网上流传的那些理论方法比如Ziegler-Nichols临界比例度法、Cohen-Coon法在工业过程控制场合很管用但在循迹小车这种“执行机构响应快、控制周期短、机械限位明显”的场合实用度有限。我最终是靠“试凑法系统观察法”完成的调参效率反而更高。3.1 先来搞清楚P项跨度只调P跑到临界振荡调参第一步把I和D全部置0只保留Kp。从小到大慢慢增加Kp每次在直道上测试车的响应。注意观察两个指标车是否能在直道上稳定居中行驶如果车始终偏向一侧回不来说明Kp太小比例修正力度不够。车是否出现明显蛇形运动如果车在直道上左右扭动说明Kp过大修正力度过猛产生了持续振荡。从0开始以0.5为步进增加Kp是我实测下来比较稳妥的做法。对于三路灰度传感器差速转向的配置Kp通常在520之间。当你加到一个值车在直道上出现等幅振荡也就是左右扭动的幅度稳定且不衰减记录这个Kp为临界值Kr。这一步的关键是每一轮测试必须只改一个参数。不要同时调Kp和Kd否则你根本分不清振荡是哪个参数引起的。老手可以凭经验跳过新手最好老老实实按流程走。3.2 I项消除稳态误差但它的副作用比你想的大循迹小车在跑长直道时一个很典型的现象是Kp已经调好了车不蛇形了但小车整体偏离赛道中心车身一直往一侧偏着走。这就是稳态误差。造成稳态误差的原因很多两边电机性能不完全一致、轮胎打滑率不同、重心偏置、灰度传感器灵敏度不一致。I项的职责就是消除这个稳态误差。I项会对历史误差做累积只要偏差不消失积分项就一直增长直到输出足够修正偏差。Kp校准后把Ki从0开始逐渐增加常见的有效范围在0.11之间。注意KI千万不要大积分项大误差累积会非常迅速车会变成一个“越来越偏激”的修正器表现为低频振荡周期性地往一边猛打方向再往另一边猛打方向。实操中我强烈建议两个保护措施积分限幅积分项的最大值限制在PID输出限幅的30%以内。比如输出限幅是±40积分项限幅就设为±12。这样即使误差长时间存在积分项也不会喧宾夺主。积分分离当偏差绝对值大于某个阈值时比如超过30%认为进入“追赶状态”此时把积分项清零全力用比例项追赶。误差掉到阈值以内再恢复积分作用。我用的是分段策略if (fabs(error) 30.0f) { integral 0.0f; } else { integral error * dt; }这样既保证了直道上的稳态精度又避免了大偏差时的积分饱和。3.3 D项的“刹车”作用对振荡和过弯超调的压制D项预测误差的变化趋势它的本质是对误差变化率的阻尼。误差在快速减小说明车正在回正D项会输出反向力量来“踩刹车”防止车因为惯性冲到另一侧。误差在快速增大说明车正在往外偏D项会提前施加修正力不让偏差继续扩大。加了I项之后车如果在直道上有轻微的高频抖动那就是D项出场的时候了。Kp和Ki维持不动从0开始增加Kd每次0.1步进直到车身抖动变得柔顺为止。我这里实测下来的Kd范围一般在0.53之间。D项有它的麻烦之处对噪声非常敏感。灰度传感器输出的模拟量经过阈值判定后变成数字量如果阈值设置在临界点附近传感器的读数会在0和1之间频繁跳变误差值也会跟着抖动D项会把这一抖放大成剧烈的转向修正。所以我在采集环节做了一定程度的滤波具体就是取多次读数的平均值作为当前值并且每次控制周期只更新一次误差。补充一点D项的“微分”在实际代码里通常替换为“误差差分”而不是严格的微积分。所以它的频率特性受控制周期影响极大。控制周期越短D项的作用越强换一个定时器分频参数同样的Kd实际效果可能差一倍。所以改控制周期后Kd必须重新调这个坑我印象太深了。3.4 我使用的PID调参记录表格我自己给调参过程做了个记录表每次改动参数后跑一圈赛道记录如下信息这比凭感觉调高效得多参数组合KpKiKd直道表现弯道表现最高稳定速度第1轮500居中有偏移大弯勉强过30% PWM第2轮1000开始蛇形回正过快40% PWM第3轮800稳定略有偏移可过中弯50% PWM第4轮80.30稳态误差消除略有过冲55% PWM第5轮80.31.5平滑过弯流畅65% PWM最后一轮我是在这个基础上继续优化最终参数定格在Kp8.5、Ki0.3、Kd1.8基础PWM在60%左右小车能以这个状态连续跑完整圈赛道。当然这个参数只适用我这台车的机械结构和传感器布局换一台车必须重新调。这里再提醒一下控制周期、PWM频率、传感器读数周期这三个时间参数要保持数量级一致。我使用的是定时器中断驱动控制算法每10ms执行一次位置采样、PID计算、PWM更新。PWM输出频率设置在20kHz左右避开了人耳可听范围也减少了电机因频率过低产生的振动噪声。传感器的采样在同一个中断里完成保证数据的同步性。4. 提速以后的问题为什么直道上稳了弯道还是冲出去把PID在直道上调好之后最自然的需求就是提速。我把基础PWM从40%提到60%再提到70%问题就开始集中爆发了直道上没问题进弯道时车头来不及转过来直接冲出赛道或者弯道内轮过慢导致车身明显倾斜甚至翻车。这一阶段的问题已经不是参数能解决的而是控制架构需要升级。4.1 位置环速度环级联从“转向管转向”到“速度也参与转向”我之前的基础PWM是固定的等于说车的目标速度永远不变PID只负责转向补偿。这在低速时没有大问题但速度一高进弯的依据就变了外轮需要更快内轮需要更慢过弯速度本身应该根据弯道曲率动态调整。所以我把控制架构改成了级联PID内环是速度环每个轮子一个PID读取编码器测得的实际转速控制PWM输出让实际转速跟上目标转速。外环是位置环灰度传感器读到的位置偏差作为输入PID输出的是“目标速度差”左右轮的期望速度差分配给两个轮子的速度环作为目标值。这个架构下过弯逻辑就变成了位置偏差大PID输出大的目标速度差外轮加速、内轮减速车以更高的转向力矩完成弯道。你不需要额外去判定“这是直道还是弯道”位置偏差自己会告诉你。速度环的PID参数基本不需要调太激进Kp适中就行因为电机的机械惯性本身就是一种阻尼。关键是编码器的测速要准。我用的是定时器编码器模式直接读TIMx-CNT寄存器两个通道正交解码得到的脉冲数除以单位时间就是转速。常见的问题是编码器数据噪声大速度环输出抖动。解决方法是滤波最简单有效的是一阶低通滤波filtered_speed 0.7f * filtered_speed 0.3f * instant_speed;这个公式的意思很容易理解新值保留70%的旧成分和30%的新成分把突变磨平。滤波系数要看控制周期和编码器线数来配我的控制周期10ms、编码器线数500线0.3的系数用起来比较合适。4.2 弯道预瞄视线放远一点转向就能早一步级联PID解决了高速进弯能力的问题但还有一个更高的需求就是预见性。人开车看的是前方好几米的路况而灰度传感器只看车轮附近的地面信息天然滞后。要提高弯道处理能力一个务实的办法是调整传感器的安装位置把灰度传感器一排安装在小车前部离前轮有一段前伸距离相当于让“视线”提前探入弯道。另一个办法是传感器排布密度。三路传感器在高速时确实不够用我后来扩成了5路位置分辨率和可感知范围都明显提升。5路灰度传感器的加权权重设置为-100、-50、0、50、100这套配置下小车能分辨出更细的偏差梯度位置环的输入更平滑PID的自然输出也更流畅。如果你有条件可以用线性CCD或者摄像头做视觉循迹那偏差分辨率就更高了但算法复杂度也上一个台阶。我在这台车上保持灰度传感器方案因为它的实时性和稳定性更容易保证。4.3 弯道中PID增益切换一套参数打天下不现实我在实测中发现一个很尴尬的问题直道上跑得很稳的一组参数到弯道上表现得拖泥带水转向响应慢半拍而弯道调好的参数直道上就会出现高频抖动。原因很简单直道和弯道对控制器的要求不同。直道需要的是抗扰动要求增益不能太高弯道需要的是快速响应要求增益要足够高。解决这个问题有两个思路第一个思路是速度自适应PID增益车速越高PID系数等比放大。因为高速状态下相同位置偏差需要更大的转向修正力放大系数大致和速度成正比。第二个思路是基于偏差范围的参数切换偏差大于某个阈值时切换成一组“弯道参数”偏差小于阈值时使用“直道参数”。这个方案对参数边界敏感可能出现在切换点附近反复跳动的问题我用滞回比较来解决——进入弯道时偏差超过40%才切换回到直道时偏差低于25才切回。我最终使用的是速度自适应方案因为它参数少、逻辑简单、没有切换跳变。实测下来速度从40%提到75%直道依然稳定弯道也能流畅通过。这套方案的核心代码大致是这样的float speed_factor base_speed / 60.0f; // 60是基准速度 float kp_scaled Kp * speed_factor; float ki_scaled Ki * speed_factor; float kd_scaled Kd * speed_factor * 0.8f; // D项不要放大太快要注意的是D项的放大系数要保守一些否则高速下微小误差变化率很容易被放大成猛烈的转向冲击。4.4 丢线处理小车冲出虚线拼接处怎么办真正跑过比赛的人都知道赛道不一定有连续的黑线。有时候是虚线有时候是断开的线段灰度传感器全部扫不到线此时PID的误差输入是无效的必须做丢线处理。我的处理策略是分层级如果丢线时间小于200ms认为是传感器“短暂盲区”保持最近一次有效误差让小车继续沿原来的方向前进。如果丢线时间超过200ms判定为“出了赛道”状态机切换到急停或者倒退搜线模式由上层决策逻辑接管。这个策略有实际教训最初我让小车丢线时直接急停结果在虚线区小车走走停停根本跑不快。后来改成短时保持长时急停虚线区就能顺利通过了。5. 状态机和PID的协同框架车子跑得好靠的是各司其职讲到这里有必要重新梳理一下状态机到底还留在系统里吗答案是非常重要但它不再负责直接的运动控制而是退居二线负责整个系统的运行阶段管理。我最后敲定的软件框架是这样的状态机负责四个运行阶段启动自检、手动调试、自动循迹、异常停车/搜索。自动循迹状态下位置环PID和速度环PID正常运行状态机不干预控制量。如果连续丢线超过阈值状态机介入切换到异常停车/搜索。手动调试状态下允许通过遥控或者按键在线调整PID参数方便现场调校。这个架构的好处是控制层保持连续不会被离散逻辑打断决策层保持有序不会因为控制层的数据异常而陷入混乱。两层的关注点分离排查问题的时候也容易定位——车跑不直问题大概率在控制层车乱跑乱停问题大概率在状态机层。在线调参这个功能我要多说一句。做PID调优的人都知道每改一次参数就重新烧录一次程序效率极低而且无法对比。我在项目里加了一个串口指令解析模块通过蓝牙或者USB串口发送p10.5这样的指令就能实时修改Kp整个过程只需要在车上跑不需要停机。这个功能让我调参效率提升了不止一倍强烈建议做PID项目的朋友都加上。6. 实测对比与复盘状态机方案和PID方案差了多远最后用数据说话。同一台车、同一条赛道我分别用完整状态机方案和“状态机级联PID”方案做了对比测试。赛道是周长约12米的环形赛道包含两段直线、两个中曲率弯道、一个急弯以及一段约30cm的虚线拼接区。评价指标纯状态机状态机级联PID最高稳定速度约40% PWM约75% PWM直道横向偏移范围±3cm±1cm弯道甩尾频率高频低频虚线区通过性走走停停稳定通过跑圈成功率约60%约95%这个结果超出我的预期尤其是弯道表现和虚线区通过性的提升完全不是同一个驾驶风格。总结这次项目的经验我认为最重要的不是PID公式本身——那个公式任何教材都能找到——而是你对“控制对象”的理解深度误差从哪来输出送到哪执行机构怎么响应时间参数怎么匹配。状态机给了我一个很好的起点它让我先把整车的逻辑跑通然后用PID把跑不通的“精细运动控制”补上。两者分工明确这台小车才算真正站到了“智能控制”的门槛上。如果你正准备做类似的项目我的建议是先从状态机起步不要直接跳PID。状态机能帮你把传感器、电机、供电、机械结构这些底层问题全部验证一遍等你对整台车的“脾气”摸清楚了再把PID加上去你会发现调参的过程顺得多。反过来如果你一上来就抱着PID调遇到问题你根本不知道是传感器没标定好、还是电机响应慢、还是参数本身不对排查链路会非常长。这就是我从状态机走到PID控制这一段路最值钱的体会。
返回列表