尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

智能车差速转向与PID控制:从运动学基础到调参实战

智能车差速转向与PID控制:从运动学基础到调参实战 先跟你交代一个我常看到的现象很多新手拿到智能车套件第一件事就是调PID结果不是原地打转就是直道跑着跑着冲到赛道外。问题往往不在PID本身而在你根本没搞明白这辆车的运动方式——它靠什么转弯转弯时每个轮子的速度该怎么分配PID又在中间扮演什么角色。这篇文章就是冲着这两个核心问题来的差速转弯的底层逻辑以及PID算法怎么和差速配合让车又能走直线又敢过弯。内容会覆盖位置式PID、增量式PID、串级控制的基本思路附上可以直接改着用的C代码最后把我这几年调试智能车踩过的坑一条一条列给你。我假设你用的是常见的主控平台STM32、TC264这类带光电编码器或者磁编码器测速电机是直流减速电机或者无刷电机。如果你的车是麦克纳姆轮或者舵机转向本篇文章的差速部分不适用但PID部分的思路完全可以平移过去。1. 为什么你的车直道跑不直、弯道冲出去差速转向的运动学本质1.1 差速运动的几何关系先把这个搞清楚两轮差速小车本质上是一个平面运动模型。左右轮分别以速度 vL 和 vR 转动车轮间距两轮中心线的距离记为 d。当 vL vR 时车走直线当 vL 不等于 vR 时车画弧线当 vL -vR 时车原地旋转。这看起来是常识但实际控制时有一个容易被忽略的量——角速度 ω[ \omega \frac{v_R - v_L}{d} ]也就是说速度差越大车的旋转角速度越大轮距 d 越小同样的速度差会造成更明显的转向趋势。赛车一轮距做得窄一部分原因就是为了在同样速度差下获得更高横摆角速度但代价是稳定性下降跑起来更贼。转弯半径 R 和速度差的关系是[ R \frac{d \cdot (v_L v_R)}{2 \cdot (v_R - v_L)} ]看到没有分母是速度差分子是速度总和。这意味着同样一个速度差车速越高实际转弯半径越大。很多新手犯的错误是只用固定的差速比去转弯低速赛道管用高速区段就冲出去了。因为车速一快相同的速度差对应的转弯半径已经成倍放大必须动态增大差速才能维持同一个过弯曲率。1.2 直道跑不直的根源一半在机械一半在控制直道偏移是一个复合问题。机械层面的典型元凶包括左右电机减速箱减速比不一致同样的PWM占空比下两轮实际转速不同轮胎磨损差异、胎压差异导致滚动半径不同底盘两侧重心分布不对称一侧负载大、摩擦力大轮轴虚位、联轴器松动导致动力传递不均匀。控制层面的问题往往是你把左右轮PWM占空比写成了固定值比如左右都是50%然后默认车就会跑直线。这是把占空比当成了速度而实际上占空比只是输入电压的on/off比例电机输出转速还要受负载、电池电压、摩擦差异影响。正确的做法是把左右轮的实际转速采集回来用PID闭环分别锁住左右轮目标转速让系统自己补偿机械不对称性。1.3 转向失控当轮胎先于控制失稳弯道冲出去还有一个物理极限问题轮胎与地面的摩擦力是有限的。差速转弯本质上需要两个轮子产生不同的纵向速度这个速度差最终靠胎面摩擦转化为横摆力矩。如果过弯速度太快需要的横摆角速度超出了轮胎侧偏和纵向滑转能提供的极限车就会推头冲出去或者甩尾失控。这时候PID填不回来——PID能纠正的是系统有响应可纠正的偏差而轮胎已经突破了附着极限属于非线性饱和区。所以真正让车过弯更稳的重点有两个方向一是正确设计过弯速度包络入弯前减速弯中稳定速度出弯再加速二是让转向修正量不要超出轮速差的实际执行范围一旦转向PID输出被限幅就说明物理上已经到极限继续盲目加大P只会让车更不稳定。2. PID到底在解决什么问题从打方向盘到自己修正的闭环逻辑2.1 开环为什么救不了智能车如果你只是给电机一个固定占空比车是开环跑的控制量不反馈结果。路面坡度变化、电池电压波动、轮胎附着变化任何扰动都会造成实际速度和期望值的偏差而开环系统对此毫无感知。换到赛道场景更明显。摄像头采集到一条1米外的赛道参考线你想要车沿着参考线走。开环的做法是偏差大就往左打多一点偏差小就打少一点。这种查表式思路也不是不行但它本质上是一个极其粗糙的比例控制器表项不够细腻且完全没有考虑偏差的变化速率和累计趋势。于是车表现为轻微偏差时反应迟钝偏差一大又过冲然后左右震荡。PID解决的是这个问题它把偏差控制量拆成三项——当前偏差比例、历史累计积分、变化趋势微分三项加权叠加后作为修正输出。你可以粗浅地把PID理解成一个会不断自我修正的驾驶员P负责看现在偏了多少D负责看偏离的速度有多快I负责抹平长期存在的微小偏差。2.2 P、I、D每一项在智能车场景中的直观意义比例项 Kp·e(t)偏差直接映射成修正量。比如巡线时赛道横向偏差30像素Kp2修正输出60车就往弯心调整。Kp太小车反应迟钝弯道内侧偏出去Kp太大车像喝醉了酒一样左右甩头。微分项 Kd·de/dt抑制偏差的变化趋势。当偏差在快速增大车正在冲向外侧微分项输出一个反向阻力相当于提前刹车当偏差快速收敛车正在回正微分项防止它冲过头。这就是阻尼的角色。智能车转向环里D项往往比P项还重要因为赛道图像的刷新率有限你需要在两次采集之间靠微分预测趋势。积分项 Ki·∫e(t)dt消除稳态误差。当你发现车在长直道上总是偏向赛道一侧几厘米却不再继续扩大的时候说明P和D都作用了但存在一个残余偏差——可能是视觉中心标定偏了也可能是某个轮胎慢速打滑。积分项会慢慢累加这个偏差输出一个额外的修正量把它顶回去。但积分项在智能车里是双刃剑过弯时偏差持续单侧存在积分会迅速饱和出弯后给你一个很大的反向过冲反而把车搞乱。很多竞赛车队干脆巡线环不用I只用PD。2.3 位置式PID和增量式PID到底选哪个位置式PID输出的是绝对控制量直接作为执行器的目标值比如目标转速、目标PWM。它的缺点是积分项需要特殊处理否则一旦误差持续存在积分项可能把输出推到饱和区出现积分饱和问题。增量式PID输出的是控制量的增量 Δu每次只加到上一次的输出上。它天然带有积分效果但不显式存储一个巨大的积分累加项适合输出目标速度和增量修正的场景更重要的是它可以做平稳切换切换目标不会导致输出跳变。智能车常见用法是电机速度环用位置式PID输出目标PWM转向环把赛道偏差映射成差速修正量用增量式PID输出角度修正增量或者用位置式PD输出差速修正目标速度。各种方案都能跑关键是你要知道你用的公式对应的对消关系不要混着写还搞不清自己算的是什么。2.4 串级PID在智能车里的位置为什么需要两个环串级PID的核心思想是外层环路的输出作为内层环路的目标值。在速度控制里典型结构是外环位置环/速度环。输入是目标车速反馈是当前车速输出是期望加速度或期望pwm补量。内环电流环/转矩环。输入是期望转矩反馈是实际电流输出是PWM占空比。在更简单的智能车主板方案里如果你用带电流闭环的驱动芯片比如DRV8301配无刷电机内环常是电流环如果你的电机是带霍尔传感器的无刷电机或者有刷电机内环则常是速度环。但并不是说必须上串级才能跑很多做得好的车队用单级PID照样拿国一。串级的优势在于它把响应速度差异很大的两个对象拆开控制电流环快速度环慢。如果你拿一个大Kp去直接压PWM电流会瞬间过大电机剧烈抖动串级之后外环给了个期望加速度内环在几毫秒内微妙地调整PWM整体柔顺得多。3. 一份能直接上车的PID与差速转向代码实现3.1 速度环代码位置式PID输出占空比先来一个最常见的电机速度环这个代码我建议你直接吃掉之后所有调参都建立在它之上。注意我做了几个基础保护积分限幅防止积分饱和输出限幅防止PWM打满导致冲击电流过大微分项对测量值求导不对误差求导避免目标跳变时微分项冲向无穷。避免微分冲击这个细节是很多教程不写的。当你把目标速度直接从0调到1000的时候目标值跳变会让误差瞬间变大如果D直接作用于误差微分项会输出一个巨大的负向冲击车会猛地抖一下。改成对测量值微分目标跳变不再影响微分项过渡就平稳很多。typedef struct { float kp; // 比例系数 float ki; // 积分系数 float kd; // 微分系数 float integral; // 积分累计值 float last_meas; // 上一次的测量值(用于微分) float out_max; // 输出限幅 float integral_max; // 积分限幅 } SpeedPID_t; float SpeedPID_Update(SpeedPID_t *pid, float target, float measure, float dt) { float error target - measure; // 积分项带限幅和抗积分饱和(误差较大时停止积分) if (fabs(error) 50.0f) { pid-integral 0.0f; // 大偏差时清零积分防止过冲 } else { pid-integral error * dt; if (pid-integral pid-integral_max) pid-integral pid-integral_max; if (pid-integral -pid-integral_max) pid-integral -pid-integral_max; } // 微分对测量值求导避免目标跳变的微分冲击 float d_meas (measure - pid-last_meas) / dt; pid-last_meas measure; float out pid-kp * error pid-ki * pid-integral - pid-kd * d_meas; if (out pid-out_max) out pid-out_max; if (out -pid-out_max) out -pid-out_max; return out; }调用方式每个速度控制周期比如5ms或者10ms执行一次读取编码器计算实际轮速然后把输出当PWM目标值写入电机驱动。out_max不要直接设成PWM上限9999先给个70%左右的余量等参数稳了再放开。3.2 转向环代码增量式PID输出差速修正巡线场景里转向环的输入是横向偏差输出是差速修正量。增量式PID在这里非常合适因为它的输出是在上次修正量基础上再调整多少天然平滑不容易出现修正量跳变。typedef struct { float kp; float ki; float kd; float last_error; float pre_error; float out; } TurnPID_t; float TurnPID_Update(TurnPID_t *pid, float target, float measure, float dt) { float error target - measure; // 增量式PID: delta_out Kp(e_n - e_{n-1}) Ki*e_n Kd(e_n - 2e_{n-1} e_{n-2}) float delta_out pid-kp * (error - pid-last_error) pid-ki * error * dt pid-kd * (error - 2.0f*pid-last_error pid-pre_error) / dt; pid-pre_error pid-last_error; pid-last_error error; pid-out delta_out; float out_max 25.0f; // 差速修正量上限按速度环目标的比例设 if (pid-out out_max) pid-out out_max; if (pid-out -out_max) pid-out -out_max; return pid-out; }为什么out_max设成25这是一个经验值取决于你的基础速度。假设你直道目标速度是100编码器采样值差速修正量不能超过25%否则内侧轮可能反转或者完全失去抓地。上限要根据实际跑车情况放宽或者收紧原则是转向修正量永远大于0让内侧轮正向滚动而不是刹死。3.3 差速转弯的最终速度分配有了速度环和转向环差速转弯的操作就只剩下一个映射公式float base_speed 100.0f; // 当前目标车速 float delta_v TurnPID_Update(turn_pid, 0.0f, line_offset, dt); // 转向修正量 float left_target base_speed - delta_v; float right_target base_speed delta_v; // 限幅处理 if (left_target 10.0f) left_target 10.0f; // 防止内侧轮完全停止 if (right_target 10.0f) right_target 10.0f; float left_pwm SpeedPID_Update(left_speed_pid, left_target, left_measure, dt); float right_pwm SpeedPID_Update(right_speed_pid, right_target, right_measure, dt); // 写入驱动 motor_set_duty(LEFT_MOTOR, left_pwm); motor_set_duty(RIGHT_MOTOR, right_pwm);这里要注意的是两个速度环分别闭环而不是直接把PWM占空比做差速。如果只在PWM层面做差速等于回到了开环控制你根本不知道左右轮实际转速是多少。正确的架构是差速修正的目标速度然后每个轮子自己闭环跟上各自的目标速度。这样即使左轮摩擦力大一点、减速比差一点PID也会自动补偿。3.4 运动学前馈让差速转向更平滑的一个加分项如果你的车要高速过弯纯PID负反馈已经不够了。PID是被动的——它等偏差出现才修正。高速过弯你应该主动预判根据当前车速和期望曲率提前计算一个理论差速量作为前馈叠加在PID输出上。float expected_v base_speed; // 当前车速 float steer_angle line_offset; // 你从图像/电磁线里解算出的横向偏差 float feed_forward k_ff * expected_v * steer_angle; // 车速越快, 同样的偏差需要更大的差速量 float delta_v TurnPID_Update(turn_pid, 0.0f, line_offset, dt) feed_forward;之所以把前馈加在PID输出外面是因为它不需要等偏差累积根据车速和路径曲率直接就能算出来。这相当于你的车预习了一下弯道。很多竞赛车队说我的车入弯不推头秘诀就在这个前馈项里。4. 调参没有捷径但有章法从曲线到参数整定的实操流程4.1 调参前必须做的三件事调PID之前先确认以下三项否则你拿着再好的PID也白搭测速可靠。编码器的读数必须稳定。转速环调试前把车架起来给一个开环的固定PWM观察左右轮的编码器读数是否平滑。如果读数剧烈跳动先处理编码器接线、滤波、电气噪声再谈PID。方向一致。确认电机正转时编码器计数方向、PWM方向、速度符号三者一致。方向弄反PID会表现为正反馈——偏差越大修正越大车直接疯掉。极限标定。测出在设定电池电压下PWM和转速的近似关系给输出限幅定一个合理范围。4.2 vofa上位机的接线与配置智能车圈现在调PID几乎人手一个vofa或者VOFA、Vofa JustFloat。这个工具读float数据的原理其实很简单主控按IEEE 754的4字节浮点格式把数据打包成一帧帧尾加特定校验字节JustFloat协议是0x00 0x00 0x80 0x7F通过串口发到上位机上位机把这些字节流解析成float并画成曲线。代码实现使用串口打印几个数据到上位机可视化核心就一行发送逻辑如下#include string.h // JustFloat 协议帧尾 const uint8_t tail[4] {0x00, 0x00, 0x80, 0x7F}; void SendFloat(float a, float b, float c, float d) { uint8_t buf[16]; memcpy(buf, a, 4); memcpy(buf 4, b, 4); memcpy(buf 8, c, 4); memcpy(buf 12, d, 4); // 发送 buf, 直接字节流发送, 上位机选 JustFloat 协议 uart_send_bytes(buf, 16); uart_send_bytes(tail, 4); }使用方式在速度环更新函数里把左右轮的目标速度和实际速度传进来SendFloat(left_target, left_measure, right_target, right_measure);打开vofa端口选你主控对应的串口波特率和你代码里配置的一致协议选JustFloat画面上就应该能看到四条曲线。你会直观看到左右轮是否跟得上目标、超调有多少、稳定时间多长。调速度环时不需要每次烧录直接在串口助手/上位机上观察即可。这也是为什么我建议你用串口方式调参而不是每次改个Kp就重新下载固件——耗时又容易让你失去手感。4.3 速度环调参的顺序和判断标准速度环的调参我一般走这个流程先只调P。Ki和Kd设为0Kp从非常小比如0.5开始每隔几次更新加一点直到转速出现小幅震荡。判断标准是编码器曲线出现高频抖动或者嗡嗡声。记下这个震荡临界Kp然后回调30~40%。再加D。速度环的D主要是抑制超调和震荡把Kd从0.05开始慢慢加观察阶跃响应突然给一个目标速度的超调量是否下降。加太多会出现高频抖动甚至电机啸叫。最后加I。一般只有在稳态转速和目标始终存在几转误差的时候才需要加I。Ki从0.01开始慢慢加注意观察过冲积分限幅要设小一点比如输出范围的10%~20%。记住一个原则Kp是最硬的响应决定系统反应快慢Kd是最柔的阻尼决定系统是否安静地稳定下来Ki是最慢的修为用来抹平小误差。调参顺序永远是先P后D再I不要上来就改三个参数否则调死都理不清是哪个造成的问题。4.4 转向环调参和速度环的关系转向环的调参必须在速度环稳定之后进行。速度环都没调好左右轮目标转速根本追不上转向环看到的偏差里混着大量速度环的性能损失所有调参结论都是错的。转向环本身遵循类似流程固定车速由速度环锁住目标把转向PID的I设为0D为0P从小开始加观察赛道曲线横向偏差曲线在弯道处应该平滑爬升然后回落而不是陡冲出去当弯道偏差曲线出现高频振荡时P到极限了回调20%加D你会看到偏差曲线变得圆润车的过弯姿态也稳定很多。调试时我强烈建议你在代码里用按键或者蓝牙模块实现在线调参不用每次重烧固件。哪怕只是把Kp、Ki、Kd存储到Flash里开机通过串口助手修改都能大幅缩短调参周期。4.5 常见调参怪现象对应的根因现象大概率原因处理方向直道上车左右摆头转向环P过大D不足降低Kp增加Kd弯道入弯晚兜大圈转向环响应过慢P太小或速度环跟不上加大Kp检查速度环稳定性稳定后始终偏一侧视觉中心偏置/轮胎差异先做机械标定再加小Ki高速过弯推头冲出物理极限PID无法救入弯减速策略或加前馈差速电机嗡嗡响温度升高速度环震荡或PWM频率不合适回调Kp/Kd确认PWM频率原地打转左右电机方向反了检查接线和符号方向5. 路试踩坑清单新手最容易翻车的六个隐藏细节5.1 PWM死区比你想象的更大直流减速电机的启动电压并不为零。在PWM占空比很低时电机可能根本不转占空比达到某个阈值后才开始缓慢转动。这就是死区。如果速度环PID输出落在死区范围你会看到目标转速80实际转速0然后PID疯狂输出直到输出跨过死区电机猛地一冲。解决方法是死区补偿在PID输出上叠加一个基础占空比。这个补偿值要开机自检时测定不同电压下不一样。5.2 编码器安装位置决定你的控制对象编码器装在电机轴上测的是电机转速装在轮轴上测的是轮子转速。电机到轮子之间如果有减速箱电机轴转速和轮端转速的比例是固定的但如果你只有一个编码器你测的其实是电机有没有转而不是轮子真正转不转。轮胎打滑时电机转得飞快但车没动测速读数确实很高——这时速度环以为速度够了实际车身速度很低。如果你要求过弯稳定性最好能加一个IMU感知真实车速或者在运算里融入加速度信息。否则你至少要清楚编码器的测量位置带来的局限性。5.3 积分限幅设多大直接决定出弯质量转向环的积分限幅尤其要小心。前面我给的代码里转向环输出上限是25对应的是差速修正量。如果你把积分限幅设成和输出限幅一样大过弯时积分项很快跑到25出弯后你要靠P和D花很长时间把它拉回来——车的偏移感会持续一整秒。正确做法是把积分限幅设在输出限幅的10%~20%让积分充当微调者而不是主力。5.4 PWM频率和电机驱动芯片的配合PWM频率选低了电机会明显发出可听见的噪声扭矩纹波大选太高了驱动芯片和电机感抗的搭配可能导致电流跟不上实际驱动力下降。我常用15kHz到20kHz的PWM频率做直流有刷电机基本听不到电机噪声扭矩输出也稳定。如果你用的是双路H桥还要注意PWM和方向引脚的模式一些驱动器是PWM-IN1, IN2模式另一些是IN1, IN2-PWM模式方向搞反会导致一路正转一路反转。5.5 转向限幅不能只是封顶转向环的out_max不只是防过冲的手段它还直接决定了车的极限过弯能力。限幅太小车过不了的弯就过不了限幅太大内侧轮反转、动力损耗、车子横向打滑。推荐的调整思路是先给一个保守值比如基础速度的20%然后通过实测弯道最小半径算出理论所需最大速度差按这个值扩到1.2倍即可。实测过程中如果转向环输出频繁撞到限幅值说明你的目标速度已经超出了这辆车的物理弯道能力不是调参数能解决的。5.6 电池电压下降为什么下午跑的参数早上不能用了锂电池在整个放电过程中电压从高到低持续变化同样的PWM占空比在满电时能跑1000转速低电量时可能只有800。这意味着你早上调的PID参数下午电量低的时候拿过来用输出就不够了。很多车队最终的解决方案是在代码里实时检测电池电压用它做前馈补偿比如float batt_comp batt_nominal / batt_voltage; // 电池电压越低, 补偿系数越大 float pwm_out SpeedPID_Update(...) * batt_comp;严格来说这是一个电压前馈不是PID的一部分。但它对智能车是刚需——否则你八成会遇到上午调好的弯下午死活过不去的郁闷场景。6. 再分享几个能让车跑得更顺的操作细节最后一节不讲算法了讲几个我实际比赛中反复用到的软技巧。第一转向环的执行频率不要和速度环一样快。速度环是底层执行器动作越快越好典型1~5ms一个周期转向环吃的是图像或传感器的偏差数据本身更新没那么快如果也一样跑5ms同一帧偏差会被重复计算很多次微分项会被虚假放大。我通常把转向环放在图像数据到达后的下一次控制周期里执行频率和图像帧率匹配20ms左右效果反而比高频执行更稳。第二过弯前预减速度弯中稳速出弯再加速。这个听起来像废话但真正实现的代码逻辑需要你做一个简单状态机检测到赛道曲率超过阈值就进入减速状态把目标车速拉低一个区间曲率回落再恢复目标车速。不要让转向环在高速状态下独自扛下所有它扛不住物理极限。第三PID参数不是一次性调完就固定了。我自己的习惯是每个速度区间跑一圈记录每组参数下车的表现最后用枚举表格在运行中查表切换。低速弯一个参数高速弯换一套成本参数这样既能保证低速灵敏又能保证高速稳定。车跑到赛道不同区域时根据当前速度和曲率查表调用不同PID参数比一套参数打天下效果好得多。第四调参时间要留足给手感。我见过很多新手花几个晚上调参数越调越烦躁最后把车调成一碰就炸。我的建议是一次调参session控制在2小时以内每次只动一个参数改完必须记录下来写在笔记本或者Excel上下一次改之前先看上一次是什么状态。PID调参本质上是在噪声和响应之间找平衡手感也是重要输入。这些细节看着小最后都反映在赛道上那零点几秒的差距里。先把差速和PID配合的逻辑理顺把每一条代码跑通把参数一个个调稳定你的车自然就稳了。
返回列表