STM32麦轮底盘全向移动:轮速闭环与位置控制实战
四个轮子往四个方向一装PID 一调就能全向移动了我第一次做麦轮底盘的时候就是这么想的。结果上电之后小车原地画了个圈——左右两侧的轮子对转本该横移的动作变成了自转。后来把代码翻了个底朝天才发现问题根本不在 PID 参数上而是最底层的运动学符号写反了。这篇内容就是把这台麦轮底盘从建工程到轮速闭环、再到位置控制的完整链路捋一遍。核心关键词是 STM32、麦轮底盘、轮速闭环、位置控制适合已经会点亮 LED、会写串口收发但没做过移动底盘的朋友如果你已经做过两轮差速小车想升级到全向平台这里关于符号标定、测速分辨率、串级 PID 的部分也会省你不少时间。我会把选型理由、参数计算过程、以及那些只有真正上过电才会遇到的坑都写清楚代码基于 HAL 库能直接抄。1. 先搞清楚麦轮凭什么能全向移动运动学是所有后续工作的地基在动烙铁之前得先把麦轮的运动学搞明白。这一步偷懒后面调试就是纯粹的撞运气。1.1 麦轮和普通轮的差别就在那排斜着的辊子普通橡胶轮只能沿轮子的滚动方向产生驱动力垂直于滚动方向是被锁死的靠摩擦硬扛。麦轮在轮毂外圈装了一圈与轮毂轴线成 45 度的辊子辊子自身可以自由转动所以垂直方向不再是死约束而是自由滑动。四个轮子按特定朝向装好之后每个轮子贡献的力方向都不同把它们合成起来就能同时得到前后、左右、自转三个自由度。这也是为什么麦轮底盘能斜着走、能原地转圈而差速底盘只能前后加转弯。代价是麦轮的效率低、越障能力差、对地面平整度敏感四个轮子必须同时着地否则某个轮子打滑整个解算就偏了。1.2 逆解公式把底盘速度拆成四个轮子的转速先定义车身坐标系这一步必须说死否则后面的符号讨论全是糊涂账。我用的定义是x 轴指向前方为正y 轴指向左方为正ω 绕 z 轴逆时针从上往下看为正单位分别是 m/s、m/s、rad/s。设 L 为半轴距前后轮中心距的一半W 为半轮距左右轮中心距的一半令 k (L W) × ω。那么四个轮子的线速度目标值是轮子位置线速度表达式说明左前 FLvx − vy − k参与左向平移与逆时针自转右前 FRvx vy k参与左向平移与逆时针自转左后 RLvx vy − k参与左向平移与逆时针自转右后 RRvx − vy k参与左向平移与逆时针自转对着这张表验证一下直觉想让小车只往前开vy ω 0四个轮子都是 vx全是正转没问题。想原地逆时针转vx vy 0左边两个轮子是 −k后退右边两个是 k前进正好符合左后右前的转向逻辑。想往左平移vy 0左前和右后后退、右前和左后前进这就是麦轮横移的典型姿态。要注意这套符号是绑定辊子朝向的。麦轮有两种装法业内俗称X 型和O 型区别在于辊子在地面上的接触线是朝内还是朝外。如果你按上面的公式写代码实测发现横移方向反了最省事的做法不是去推导而是把 vy 的符号整体取反然后重新测一遍。工程上没人会因为你没推导出符号而扣分但方向反了车就是不走。1.3 从线速度到 RPM 再到 PWM一条完整的换算链代码里最后写进定时器寄存器的是 CCR 值不是 m/s中间隔着好几层换算每一层都要算清楚。先算轮子周长。假设用的是直径 60 mm 的麦轮周长 C π × 0.06 ≈ 0.1885 m。轮子转速 n单位 r/s和线速度 v 的关系是 n v / C。如果底盘设计最高速度是 1.0 m/s那么轮子最高需要 1.0 / 0.1885 ≈ 5.31 r/s换成 RPM 就是 318 转每分。再看电机端的转速。常见的 12V 直流减速电机比如带 30:1 减速箱、空载输出 300 RPM 的那类额定转速和需求的 318 RPM 就比较接近但余量不多。我的建议是留出至少 30% 的转速余量因为电池电压从 12.6V 掉到 10V 的时候电机转速会跟着掉一大截没有余量的话满速就跑不到设计值。所以选型时我更倾向于 1:20 左右的减速比让空载转速到 400 RPM 以上。最后是 PWM 占空比到电压的映射。如果驱动是 TB6612 这类 H 桥PWM 占空比乘以电池电压就是电机端平均电压。但电机转速和电压不是严格线性的低压段有死区一般 5% 到 15% 的占空比电机才开始转高压段有饱和。所以开环控制注定不准这也就是为什么必须上轮速闭环。2. 从零建一个能跑的 STM32 工程时钟树、引脚分配与第一次验证环境这块我强烈建议用 STM32CubeMX 加 HAL 库。理由很实际编码器接口模式和 PWM 生成都涉及好几个寄存器位图形化配置能省掉大量查手册的时间而且引脚冲突会直接报错。2.1 芯片选型为什么我不推荐最小系统板 F103C8T6很多人手上第一块板子是 F103C8T6 最小系统板便宜、资料多。但做麦轮底盘它的定时器数量会很紧张。STM32F103C8T6 只有 TIM1 到 TIM4 四个定时器其中 TIM1 是高级定时器TIM2/3/4 是通用定时器四个都支持编码器接口模式。这意味着如果四个编码器全占了 TIM1 到 TIM4那 PWM 就没定时器可用了。所以我的建议是直接上 F103RCT6 或 ZET6。它们有 TIM1 到 TIM8RCT6 有 8 个ZET6 也是可以这样分配功能定时器通道备注四路电机 PWMTIM1CH1~CH4高级定时器20 kHz左前轮编码器TIM2CH1/CH2编码器模式 TI1TI2右前轮编码器TIM3CH1/CH2编码器模式 TI1TI2左后轮编码器TIM4CH1/CH2编码器模式 TI1TI2右后轮编码器TIM5CH1/CH232 位计数器不溢出速度环节拍TIM6—5 ms 定时中断TIM5 是 32 位计数器这点很关键。其他 16 位定时器做编码器时计数范围是 0 到 65535一旦正负溢出就得手动处理。TIM5 用 32 位就不会有这个问题能省掉一段容易写错的溢出逻辑。2.2 时钟树和几个必须改的默认项CubeMX 里新建工程之后按顺序配置RCC 里把 HSE 设为 Crystal/Ceramic Resonator外部 8 MHz 晶振。F103 的 PLL 可以倍频到 72 MHz这是它的满血频率别留在默认 8 MHz 上。SYS 里的 Debug 一定要选 Serial Wire。这一项如果忘了勾下载器就只接 SWDIO 和 SWCLK 两根线但芯片默认可能把 SWJ 复用成普通 IO导致下次再也连不上。我身边至少三个人在这上面卡过半天最后靠按住复位键上电的土办法才救回来。Project Manager 里的 Toolchain 按你的习惯选 MDK-ARM 或者 STM32CubeIDECode Generator 里勾上Generate peripheral initialization as a pair of .c/.h files代码会清爽很多。生成工程之后先别急着写逻辑编译一次烧进去用调试器看几个变量。我习惯在 SystemClock_Config 之后打断点确认 SystemCoreClock 变量是 72000000这一步能过滤掉一大类明明代码没错但就是跑不对的问题。2.3 用户代码只能写在指定区域CubeMX 生成的代码里到处是 USER CODE BEGIN 和 USER CODE END 注释对。只有写在这两行之间的内容重新生成代码时才不会被覆盖。我自己被坑过一次在 main.c 顶部手写了一个数组后来加了个外设重新生成数组直接没了编译报一堆未定义。后来我养成了习惯除了 main 函数里主循环那一小块其他所有自己写的东西都放到单独的 .c/.h 文件里只留函数调用在 USER CODE 区。3. 让轮子转起来PWM、编码器接口与接线里那些不写在手册上的细节工程骨架有了接下来是外设。这一层配错后面闭环调得再好也白搭。3.1 电机驱动选型TB6612、DRV8833 还是别的东西小功率麦轮底盘上最常见的是 TB6612FNG一块芯片带两路电机持续电流 1.2 A峰值 3.2 A。四轮底盘用两块就够了。它的好处是逻辑电源和电机电源分开逻辑侧 3.3V 兼容不会因为 12V 电机电源烧掉 MCU 引脚。DRV8833 也常用特点是压降小、驱动能力强一点但逻辑电平和 STM32 的兼容性要看一下版本。至于 A4988 和 HR4988 这类它们是步进电机驱动器不是给直流减速电机用的别买错。同理网上卖的两轮差速小车套件里的驱动板和麦轮底盘不是一个东西套件里的编码器线序也各不相同。电机和 MCU 之间必须共用一套电源地。很多底盘用 3S 锂电标称 11.1V供电通过一块 DC-DC 降到 5V 给驱动板逻辑侧和传感器再经 LDO 降到 3.3V 给 MCU。这里有个容易忽略的点AMS1117 这类老式 LDO 的输出电容需要一定的等效串联电阻ESR来保证环路稳定如果你把原来的钽电容换成纯陶瓷电容ESR 太低反而可能出现输出振荡甚至自激。换成陶瓷电容不是不行但要注意选带阻尼的或者在输出串一个小电阻。3.2 PWM 频率的选择不是随便定的假设系统时钟 72 MHzTIM1 挂在 APB2 上。要出 20 kHz 的 PWM预分频和自动重装寄存器的组合是这样算的f_pwm 72 000 000 / ((PSC 1) × (ARR 1))取 PSC 0那么 ARR 1 72 000 000 / 20 000 3600所以 ARR 填 3599。这样 CCR 的取值范围是 0 到 3599对应 0% 到 100% 占空比。为什么选 20 kHz一组对比就能说明问题。低于 8 kHz 时电机会有明显的可听啸叫而且电流纹波大高于 20 kHz 之后啸叫进入人耳听不到的区间但 H 桥的开关损耗会明显上升TB6612 这类芯片发热会增加。20 kHz 是个折中实际用下来电机声音干净驱动芯片也只是温热。还有一点PWM 模式里我一般选 PWM 模式 1、高电平有效计数值小于 CCR 时输出有效电平。这样 CCR 越大转速越快写代码时直觉更顺。3.3 编码器接口模式的两个隐藏坑CubeMX 里把定时器设为 Encoder ModeCombined Channels 选 Encoder Mode TI1 and TI2这样 A/B 两相都参与计数自动实现四倍频。ARR 设 65535然后有个细节Polarity 要保持一致否则计数方向会反。第一个坑是滤波。编码器在电机旁边工作导线又长很容易收到干扰脉冲。CubeMX 里的 Input Filter 参数IC1Filter / IC2Filter建议设成 6 到 10 之间这会开启输入端的数字滤波能滤掉大部分窄脉冲干扰。我之前遇到过编码器静止时计数器却在小幅跳变的情况就是干扰导致的加上滤波之后彻底解决。第二个坑是上拉。很多通用编码器模块自带上拉电阻可以直接接如果你用的是裸的霍尔或光电编码器A/B 相必须外接 4.7 kΩ 到 10 kΩ 上拉否则输出是开漏的MCU 读到的电平会飘。走线最好用双绞线并且和电机动力线分开走别捆在一起。3.4 上电前必须做的方向标定这一步是我认为整个项目里最值得花时间的地方。做法很简单写一个临时的测试函数手动给某个轮子一个固定的正占空比比如 30%观察两件事轮子实际的旋转方向从车身外侧看或者用一张贴纸标记编码器计数值的变化方向也就是读 CNT 前后的差值是正还是负。把四个轮子的结果记成一张表。如果某个轮子电机转向和预期相反改 H 桥的 IN1/IN2 电平如果编码器符号和电机转向不对应改编码器的 A/B 相接法或者在代码里给这路计数乘一个 ±1 的系数。四个轮子都标定完你就得到了一组宏比如#define ENC_SIGN_FL ( 1) #define ENC_SIGN_FR (-1) #define ENC_SIGN_RL (-1) #define ENC_SIGN_RR ( 1) #define MOT_SIGN_FL ( 1) #define MOT_SIGN_FR (-1) #define MOT_SIGN_RL (-1) #define MOT_SIGN_RR ( 1)这几个宏看起来不起眼但它们能让你后面所有的调试都建立在同一个可信的基础上。我现在的习惯是任何新的底盘到手第一件事就是标定这套符号绝不跳过。4. 轮速闭环定时采样、M/T 测速与一个抗饱和的位置式 PID到这一步硬件通了轮子能转编码器能计数。接下来是让轮子按照给定速度稳定旋转也就是速度环。4.1 开环 PWM 为什么一定会翻车开环控制下同样的占空比电池满电和电量剩一半时轮速能差 20% 以上。四个轮子还会因为电机个体差异、机械装配公差、地面摩擦不同出现明显的不一致。表现就是指令是直走实际走出来的是一条弧线指令是原地转 90 度转出来是 100 度。更麻烦的是负载变化。麦轮底盘在加速、过坎、轮子打滑时负载差异巨大开环根本没有补偿能力。所以速度闭环不是锦上添花而是能不能用的问题。4.2 测速方法M 法、T 法和 M/T 法的取舍方法原理优点缺点适用区间M 法固定时间内数脉冲个数实现简单算力占用低低速时脉冲少量化误差大中高速T 法固定脉冲数测所需时间低速分辨率高高速时时间太短误差大低速M/T 法同时测脉冲数和时间全速域都能兼顾逻辑略复杂需要两个定时器全速域我的做法是固定周期 M 法打底加低速补偿。定时器 TIM6 每 5 ms 中断一次中断里读取四个编码器定时器的 CNT 值减去上次的值得到增量然后把 CNT 清零。速度换算v Δcount / (PPR × 4 × GearRatio × Ts) × π × D其中 PPR 是编码器线数乘 4 是四倍频GearRatio 是减速比Ts 是采样周期秒D 是轮径。拿一组实数算一下。编码器 13 线减速比 30那么输出轴转一圈的计数是 13 × 4 × 30 1560 个。5 ms 采样轮径 60 mm。当轮速是 1 m/s 时输出轴转速 5.31 r/s5 ms 内的计数是 1560 × 5.31 × 0.005 ≈ 41 个量化误差约 2.4%可以接受。但当轮速降到 0.1 m/s 时5 ms 内只有 4 个计数单个数量的误差就代表 25% 的速度误差。这时候速度环会抖得厉害。解决办法有三个我都试过。一是加大采样周期到 10 ms 甚至 20 ms代价是速度环带宽下降二是做滑动平均用最近 N 次采样的和除以总时间等效于加长窗口但更新率不变三是上 M/T 法当脉冲数少于阈值比如 10 个时切换到用另一个高频定时器计时间。我实际项目里用的是第二种简单可靠5 ms 采样加 4 次滑动平均低速抖动能压住同时 200 Hz 的更新率对小车来说完全够用。另外一定要加一阶低通滤波/* 一阶低通alpha 越小越平滑延迟越大 */ speed_filt 0.3f * (speed_raw - speed_filt);alpha 取 0.2 到 0.4 之间具体看你的噪声水平。滤波太狠会让速度环响应变慢出现加速慢半拍的感觉。4.3 位置式 PID 的抗饱和写法速度环我一般用位置式 PID因为它的输出直接对应 PWM 占空比物理意义清晰限幅也直观。但位置式 PID 最大的敌人是积分饱和当目标速度和实际速度差很大时比如上电瞬间积分项会一路涨到天花板等速度追上来了积分还得慢慢退回去表现就是冲过头然后慢慢回落。我用的写法是积分分离加输出限幅typedef struct { float Kp, Ki, Kd; float err, err_last, err_prev; float integral; float integral_max; /* 积分限幅 */ float out_max; /* 输出限幅 */ float deadband; /* 误差死区 */ float out; } PID_t; float PID_Calc(PID_t *p, float target, float actual, float dt) { float d_out; p-err target - actual; /* 死区误差很小时不再累加积分避免低速抖动 */ if (p-err p-deadband || p-err -p-deadband) { p-integral p-Ki * p-err * dt; if (p-integral p-integral_max) p-integral p-integral_max; if (p-integral -p-integral_max) p-integral -p-integral_max; } /* 微分项用增量形式避免设定值突变时的微分冲击 */ d_out p-Kd * (p-err - p-err_last) / dt; p-err_last p-err; p-out p-Kp * p-err p-integral d_out; if (p-out p-out_max) p-out p-out_max; if (p-out -p-out_max) p-out -p-out_max; return p-out; }积分限幅的取值有个经验公式integral_max 不要超过 out_max 的 60%否则积分项在退出饱和的过程中还是会把输出顶到限幅等于没限。out_max 对应 PWM 的最大 CCR 值但我一般只给到满量程的 90%留点余量。另外还有一点PID 输出到 PWM 之间要处理电机死区。占空比低于 8% 左右的时候电机根本不转但积分项会一直往上加直到越过死区电机突然窜一下。解决办法是在输出上叠加一个死区补偿输出不为零时把绝对值小于 8% 的输出抬到 8%符号保持一致。4.4 参数整定的顺序和手感整定顺序是先把 Ki 和 Kd 置零慢慢加 Kp加到轮子能快速响应目标速度但开始有小幅振荡然后回退到振荡消失的 60% 左右。接着加 Ki一次加很小的量观察稳态误差是否消除。Ki 加太多会出现低频摆动就是速度缓慢地来回晃这时候要往回退。最后加 KdKd 主要抑制超调和振荡但会对编码器噪声敏感如果加了 Kd 之后电机开始高频嗡嗡响说明 Kd 太大把噪声放大了。四路轮子的参数最好分别整定因为电机个体差异是客观存在的。不要图省事用同一组参数尤其是 Kp 和 Ki差个 20% 都能感觉出来。5. 位置控制把速度环当执行器塞进外环里速度环调好之后位置控制其实就顺理成章了。核心思想是位置环不直接控制 PWM而是输出一个目标速度给速度环速度环再去驱动电机。5.1 单轮位置闭环最简单的练手场景先做单轮。编码器读到的累计计数乘以换算系数就是轮子转过的角度。位置环的输入是目标角度和当前角度的差输出是目标轮速。这里有个关键参数关系外环的带宽必须明显低于内环一般是 5 到 10 倍。速度环跑 200 Hz5 ms位置环跑 20 Hz 到 50 Hz 就够了。如果位置环跑得和速度环一样快两个环会互相打架表现为低频振荡怎么调参数都压不住。我一般这样分层环频率定时器说明速度环200 HzTIM6 中断读编码器、算速度、跑 PID、写 PWM位置环50 Hz主循环计数分频读里程计、算偏差、给速度环设目标通信上报50 Hz主循环向上位机发波形数据5.2 底盘位姿闭环里程计正解是必备技能单轮位置闭环只是练手真正要做的是控制底盘在平面上的位姿 (x, y, θ)。这就需要里程计也就是正解。从四个轮子的线速度反推底盘速度用前面那套矩阵的伪逆结果/* v1..v4 为四个轮子的线速度单位 m/s */ /* 顺序左前、右前、左后、右后 */ #define HALF_LW (L W) void Chassis_Forward(const float *v, float *vx, float *vy, float *wz) { *vx ( v[0] v[1] v[2] v[3]) * 0.25f; *vy (-v[0] v[1] v[2] - v[3]) * 0.25f; *wz (-v[0] v[1] - v[2] v[3]) / (4.0f * HALF_LW); }要注意这里的 vy 定义和逆解里保持一致。如果逆解里你把 vy 符号取反了正解里也必须跟着改否则里程计会跑偏。然后按位置环的周期 dt 做积分odom.x (vx * cosf(odom.theta) - vy * sinf(odom.theta)) * dt; odom.y (vx * sinf(odom.theta) vy * cosf(odom.theta)) * dt; odom.theta wz * dt;里程计的精度取决于三件事轮径参数的准确性、轮距轴距参数的准确性、以及打滑程度。轮径和轮距这两个参数我建议实测标定不要用说明书上的数字。方法很简单让小车直走 2 米看里程计读数是多少比值就是轮径的修正系数让小车原地转 10 圈看 theta 的累计值比值就是轮距轴距的修正系数。5.3 位姿控制器外环用 P 还是 PID对于底盘位姿我的经验是外环用纯 P 加一个很小的 I 就够了D 项基本不需要。原因是位置信号本身就经过积分比较平滑加 D 反而容易把里程计的累积误差放大。三个自由度的控制器可以分开设计x 方向的偏差经过 P 控制器输出 vx 指令y 方向的偏差经过 P 控制器输出 vy 指令θ 方向的偏差经过 P 控制器输出 ω 指令。这三个输出再一起送给逆解得到四个轮子的目标速度。这个结构的好处是解耦得很干净每个方向单独调参数互不干扰。有个细节要注意θ 的误差要归一化到 −π 到 π 之间。否则当目标角度是 179 度、当前角度是 −179 度时误差算出来是 358 度小车会绕一大圈。这个坑我第一次做的时候踩得很实小车在原地转了将近一整圈看着特别傻。5.4 定点停车和原地转向的实测手感位置环的 Kp 决定冲劲。Kp 太大小车会冲过目标点然后倒回来来回震荡Kp 太小接近目标点时速度已经很慢了最后一段爬得特别慢看起来没劲。我通常会在接近目标点时加一个减速策略当位置误差小于某个阈值时把最大速度限下来让小车缓慢贴合目标。同时给位置误差设一个小死区比如 3 毫米误差进入死区就不再输出速度避免在目标点附近抖动。原地转向的精度比平移高得多因为四个轮子的位移都被机械约束住了不容易打滑。实测下来转 90 度能稳定在 ±1.5 度以内而平移 1 米的误差通常在 ±1 厘米左右主要来源是打滑和轮径误差。6. 调试手记麦轮底盘最容易翻车的七个地方这部分是我这几年踩过的坑里最有价值的也是常规教程里基本不会写的。6.1 轮子转的方向和预期相反现象给前进指令小车往后退或者给横移指令小车自转。根因通常有三个层次。第一层是电机接线H 桥的 IN1/IN2 接反了改线或者改代码里的 MOT_SIGN。第二层是编码器符号A/B 相接反导致计数方向反了速度环会形成正反馈表现为电机疯转改 ENC_SIGN。第三层是运动学符号即前面说的 vy 或 ω 的整体符号改解算函数。排查顺序很重要先用开环测试确认电机转向再确认编码器符号最后才是运动学符号。顺序反了你会在错误的层面上反复修正越修越乱。6.2 麦轮辊子装反怎么快速识别麦轮装错方向是最隐蔽的坑因为它不影响单轮测试只有整车联调才暴露。识别方法是看地面的接触线抓住轮子从车头正上方往下看四个轮子的辊子接触地面的那条线如果构成一个 X 形就是 X 型装法构成一个菱形或者说朝内的 O就是 O 型。两种装法对应的运动学符号不同。我的做法是四个轮子都按同一朝向装都用 X 型然后只用一套解算公式。混装是绝对不行的会导致某个方向的运动学关系无解。6.3 编码器计数只增不减或者干脆不动第一种情况是 A/B 相只接了一路编码器接口模式必须两路都接才能判方向。第二种情况是上拉缺失信号电平在 1.5V 到 2V 之间飘着MCU 有时读 0 有时读 1。第三种情况是定时器通道映射错了CubeMX 里编码器模式只能映射到 CH1/CH2如果你把 A 相接到了 CH3就不工作。还有一种很隐蔽的编码器和电机共用 5V 电源电机启动瞬间把 5V 拉下来编码器供电电压跌到工作下限以下出现丢计数。解决办法是给编码器单独走一路 LDO或者在编码器电源脚旁边并一个 100 μF 的电解电容。6.4 PID 一上电就疯转九成是速度反馈的符号错了。速度环的输入是 (目标 − 实际)如果实际的符号是反的那么目标为正时实际为负误差会翻倍输出越来越大电机越转越快。验证方法手动转动轮子看速度反馈的符号和转向是否对应。往前走反馈应该是正的往后推反馈应该是负的。6.5 直线跑偏得厉害三个原因按概率排序。第一是四路 PID 参数不对称尤其是 Kp 差得多。第二是轮径有差异特别是用便宜的橡胶麦轮四个轮子直径能差 1 到 2 毫米2 毫米在 60 毫米轮径上就是 3% 的差异跑 1 米偏 3 厘米。第三是机械装配的公差比如某个轮子的轴套偏了导致它转动时有额外的阻力。对策是给每个轮子单独标一个轮径系数用实测的方式确定。我的做法是让小车每次只驱动一个轮子转一圈测实际前进距离反算轮径。四个轮子标完跑直线的偏差能压到 1 厘米以内。6.6 串口打印把整个系统卡死这个是新手最容易踩的在 200 Hz 的中断里调用 printf 或者 HAL_UART_Transmit 的阻塞版本。串口波特率 115200 时发送 20 个字节大约需要 1.7 ms在 5 ms 的中断里占用了三分之一的时间而且这期间如果被更高级中断打断还会出各种玄学问题。正确做法是用 DMA 加环形缓冲。中断里只往缓冲区里塞数据DMA 在后台自动发送。如果只是调试阶段可以简单点在中断里只置一个标志位把数据存到全局数组主循环里再统一发送。还有个更土的坑有人写延时函数用空的 for 循环结果编译器开 O2 优化之后循环被整个优化掉了延时时间变成零紧接着就出现各种时序问题。正确做法是用 __NOP() 或者 SysTick 做延时别指望空循环。6.7 电源和布线的隐形问题电机是强干扰源。我遇到的典型症状是底盘跑起来之后串口偶尔收到乱码或者编码器某个通道突然跳变。处理措施有几个层次驱动板电源输入端并一个 470 μF 的电解电容加 0.1 μF 的陶瓷电容吸收电机启停的电流冲击电机引线用双绞或者屏蔽线编码器信号线不要和电机动力线走同一个线槽MCU 的电源脚每个都要有 0.1 μF 的去耦电容位置尽量贴近引脚。如果空间和预算允许电机电源和 MCU 电源在电路板上从不同分支走线最后在电源入口处单点共地能明显改善干扰。7. 从能跑起来到好用波形、参数持久化与后续扩展底盘能按指令走了接下来能做的事还很多。7.1 用上位机看波形别靠猜调 PID 的时候肉眼看小车动作只能判断好不好判断不了是哪里不好。我强烈建议在代码里加一个数据上报通道把目标速度、实际速度、PWM 输出、位置误差这些量按固定格式打包发出去用 VOFA 或者类似的串口波形工具画出来。数据格式我一般用最简单的文本格式每一帧一行字段用逗号分隔末尾加换行。虽然比二进制浪费一点带宽但调试起来方便出问题一眼能看出哪个字段异常。上报频率 50 Hz 就够了太高的话串口带宽吃紧。看着波形调参数效率比盲调高一个数量级。你能清楚看到超调是多少、积分恢复要多久、有没有稳态误差这些信息在调参时非常关键。7.2 PID 参数掉电保存每次上电都要重新输参数太痛苦了。F103 内部有 Flash可以拿最后几页来存 PID 参数和轮径标定系数。做法是把参数结构体整体写入读的时候校验一个魔数魔数不对就用默认值。要注意 Flash 写入前必须先擦除而且擦除粒度是一个页F103 中容量型号是 1 KB 或 2 KB。别每次都写只在你确认参数满意之后触发一次保存。频繁擦写会缩短 Flash 寿命虽然十万次的擦写次数对调试来说够用但也没必要浪费。7.3 后面还能往哪走如果底盘已经稳定可以往几个方向扩展。加一个六轴 IMU用陀螺仪积分出角度和里程计做卡尔曼融合能显著改善打滑情况下的航向精度。麦轮底盘打滑是很常见的纯编码器里程计的航向误差累积得很快。再往上就是接上位机或者 ROS把底盘封装成一个速度指令接口接收 vx/vy/ω 三个量回传里程计。这样底盘就能和激光雷达、视觉模块配合做导航。通信层用串口或者 CAN 都行CAN 在抗干扰和多节点扩展上更有优势。如果后面要加机械臂或者其他执行机构建议从一开始就把状态机设计好把底盘运动和机构动作分成独立的模块通过一个简单的调度器协调。我见过太多项目后期因为主循环里堆了几百行 if-else加一个功能就要重构一次。最后再分享一个小技巧每次改完代码烧录之前先用调试器看一眼四个编码器的原始计数和四个速度反馈值确认它们在你手动转动轮子时是合理变化的。这一步花不了半分钟但能帮你排掉一半的烧进去没反应的问题。我自己到现在还保持着这个习惯尤其是换了新板子或者重新生成过工程之后。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →