基于STM32与PID闭环的麦克纳姆轮小车控制实战 📅 发布时间:2026/9/18 13:18:10 👁 浏览次数: 1. 从四个轮子到四个麦克纳姆轮这个项目到底在做什么很多人第一次听到四麦小车会愣一下其实四麦就是四个麦克纳姆轮Mecanum Wheel的简称。普通小车用四个橡胶轮只能前进后退加转弯而麦克纳姆轮每个轮子表面都有一圈呈45度角排列的辊子四个轮子转速和转向的不同组合能让车体在平面上做出前后、左右平移、斜向移动、原地旋转这些动作。这个项目要干的事情就是用STM32F103这块经典到不能再经典的单片机配合ST官方的HAL库通过蓝牙接收遥控指令再对四个电机做PID闭环控制让小车按照遥控的意图精准运动。为什么选STM32F103说白了就是便宜、资料多、够用。F103C8T6这颗芯片主频72MHz有3个通用定时器能输出PWM有多个USART接口做四路电机PWM加一路蓝牙串口绰绰有余。HAL库虽然被不少人吐槽效率低、代码臃肿但它的好处是移植性强、CubeMX配置方便对于这种中小规模的控制项目开发效率远比那点性能损耗重要。PID闭环控制是这个项目的灵魂。如果只是开环控制你给电机发个PWM占空比电机转是转了但地面摩擦力变化、电池电压下降、四个电机个体差异都会让实际转速偏离预期。表现就是小车走不直、平移时画弧、原地旋转时中心漂移。PID闭环就是给每个电机装上编码器测速把实测转速反馈回来和设定值比较通过比例、积分、微分三项运算动态调整PWM输出让实际转速紧紧咬住目标转速。这个内容适合谁看如果你已经玩过STM32点灯、串口收发想进阶做一个综合性的控制项目那这篇就是写给你的。如果你正在做毕设或者课程设计涉及电机控制和无线遥控这里面的坑和思路也能直接参考。我会把CubeMX配置、编码器测速原理、PID参数整定、蓝牙协议设计这些环节拆开讲透并且重点说那些文档里不会写、只有实际调过才知道的细节。2. 硬件选型与接线别等烧了芯片才后悔2.1 主控与电机驱动板的搭配逻辑STM32F103最小系统板负责逻辑控制但它的GPIO输出电流只有20mA左右根本驱动不了电机。中间必须加驱动板。常见的选择有TB6612、L298N、A4950这几类。TB6612效率高、发热小单路持续电流1.2A峰值3.2A驱动这种小车用的直流减速电机完全够用而且它支持PWM频率范围宽我一般设在10kHz到20kHz之间避开人耳能听到的啸叫频段。L298N便宜但压降大大约每路要吃掉1.4V到2V电池电压本来就不高再被它吃掉一截电机转速会明显偏软。如果你用的是7.4V锂电池经过L298N之后电机端可能只剩5.5V左右PID调起来会很别扭因为执行器的动态范围被压缩了。所以我的建议是优先选TB6612或者A4950这类MOS管驱动的方案。四麦小车需要四路独立的电机控制每路需要两个方向控制引脚加一个PWM引脚。TB6612一块芯片能驱动两路电机所以需要两块。接线的时候注意STM32的PWM引脚要选带定时器通道的那些比如PA0、PA1、PA2、PA3对应TIM2的四个通道或者PB6、PB7、PB8、PB9对应TIM4的四个通道。我习惯用TIM4的四个通道因为TIM2有时候会被其他功能占用。2.2 编码器的类型与接线陷阱编码器分增量式和绝对式这个项目用增量式就够了。常见的是霍尔编码器和光电编码器电机尾部自带的通常是霍尔型输出A、B两相方波分辨率一般是13线或者11线经过四倍频之后每转能拿到几百个计数。比如13线编码器电机减速比1:30四倍频后每输出轴转一圈的计数是13×4×301560个脉冲。这个数字直接决定了测速的分辨率。接线的时候有个大坑编码器的A、B相必须接到STM32带编码器模式的定时器通道上。STM32F103的TIM1、TIM2、TIM3、TIM4都支持编码器模式但每个定时器的CH1和CH2才能组成编码器接口。也就是说TIM4的CH1PB6和CH2PB7可以接一个编码器CH3PB8和CH4PB9不能组成编码器接口。所以四个电机需要四个定时器TIM1、TIM2、TIM3、TIM4各管一个编码器。这就跟PWM输出产生了资源冲突因为TIM4的四个通道你本来想用来输出四路PWM的。解决方案有两种一是PWM用TIM1的CH1到CH4编码器用TIM2、TIM3、TIM4再加TIM1的编码器模式——但TIM1的CH1到CH4已经用于PWM了没法同时做编码器。所以更实际的做法是PWM用TIM1的四个通道编码器用TIM2、TIM3、TIM4第四个编码器怎么办可以用外部中断计数的方式但那样CPU开销大。或者换一块引脚更多的芯片比如F103RCT6它有TIM5、TIM6、TIM7、TIM8资源就宽裕多了。我实际做的时候用的是F103RCT6PWM用TIM8的四个通道编码器用TIM2、TIM3、TIM4、TIM5各不冲突。如果你手头只有C8T6那就得在PWM和编码器之间做取舍比如用两个定时器各输出两路PWM另外两个定时器做编码器剩下两个编码器用外部中断。这个取舍在项目初期就要想清楚不然后面改起来很痛苦。2.3 蓝牙模块与电源的注意事项蓝牙模块常用HC-05或HC-06前者主从一体后者只能从机。做遥控的话HC-05更灵活。接线就是VCC、GND、TXD、RXD四根线注意模块的TXD接STM32的RX模块的RXD接STM32的TX交叉连接。波特率默认9600可以AT指令改成115200提高遥控指令的刷新率。电源部分要特别小心。STM32和蓝牙模块用3.3V电机驱动和电机用电池直接供电。千万不要用STM32的3.3V输出给电机驱动板供电电流不够会拉低电压导致单片机复位。正确做法是电池经过降压模块给STM32供电电机驱动板直接接电池。另外电机启动瞬间电流很大会在电源线上产生尖峰建议在电机驱动板的电源输入端并一个大电容比如470uF到1000uF能有效吸收这些尖峰减少对单片机的干扰。注意编码器的电源也要接3.3V或5V具体看编码器规格。如果编码器输出是5V电平而STM32的IO是3.3V容忍的那可以直接接如果不是5V容忍的引脚就要加电平转换。F103的大部分IO都是5V容忍的但还是要查一下数据手册确认。3. CubeMX配置那些默认值不能直接用3.1 时钟树与调试接口的基础设置打开CubeMX选好芯片型号之后第一件事是配置时钟。F103的外部晶振一般是8MHz经过PLL九倍频得到72MHz的系统时钟。在Clock Configuration页面把HSE选为Crystal/Ceramic ResonatorPLL Source选HSEPLL Mul选9倍频System Clock Mux选PLLCLKAPB1分频选2得到36MHzAPB2分频选1得到72MHz。这样定时器的时钟就是72MHzPWM频率计算和编码器计数都基于这个。调试接口一定要开。在SYS选项卡里Debug选Serial Wire否则下载一次程序之后SWD引脚可能被禁用下次就下载不进去了。这个坑我踩过当时只能把BOOT0拉高用串口下载来救折腾了半天。3.2 定时器PWM输出的参数计算假设用TIM8的CH1到CH4输出四路PWM时钟72MHz。要得到20kHz的PWM频率预分频器Prescaler设为0自动重装载值ARR设为72M/20k - 1 3599。这样计数器从0数到3599就是一个周期频率正好20kHz。PWM模式选PWM Mode 1脉冲宽度通过CCR寄存器设置占空比就是CCR/(ARR1)。在CubeMX里配置的时候Channel选PWM Generation CHx然后在下方的Parameter Settings里设置Prescaler和Counter Period。Counter Period就是ARR的值。Pulse就是初始的CCR值先设0后面在代码里动态调整。有个细节四个通道的PWM频率必须一致所以它们共用同一个定时器的ARR和Prescaler。如果你把四个通道分到不同定时器上那每个定时器的ARR都要单独算确保频率一致。频率不一致会导致四个电机在相同占空比下转速不同PID整定会很麻烦。3.3 编码器模式的配置要点以TIM2为例Combined Channels选Encoder Mode然后Channel1和Channel2都选Input Capture direct mode。在Parameter Settings里Encoder Mode选Encoder Mode TI1 and TI2这样A、B两相的上升沿和下降沿都会计数就是四倍频。Counter Period设为6553516位最大值因为编码器计数是双向的用满量程能获得最大的计数范围。Polarity都选Rising EdgeInput Filter可以设一个小的值比如4到8用来滤除编码器信号上的毛刺。编码器的计数方向跟A、B相的接线顺序有关。如果发现电机正转时计数减少可以把A、B相接线对调或者在代码里对计数取反。这个在调试的时候用串口打印计数值就能看出来。3.4 串口与中断的配置蓝牙模块接在USART1上波特率1152008位数据位1位停止位无校验。开启USART1的全局中断这样每收到一个字节就进一次中断在中断里把数据存到缓冲区。注意不要在中断里做耗时操作只负责收数据解析和处理放到主循环里。还需要一个定时器来做PID的周期调用。可以用TIM6或TIM7这种基本定时器配置成1ms或5ms中断一次。PID控制周期一般取5ms到10ms比较合适太快了没必要太慢了响应迟钝。我一般用5ms也就是200Hz的控制频率。4. 编码器测速与PID算法的代码实现4.1 从编码器计数到实际转速的换算编码器定时器在后台自动计数你只需要每隔固定时间读一次计数值然后清零就得到了这个周期内的脉冲数。比如5ms读一次读到156个脉冲那转速就是156/(1560×0.005)20转/秒换算成RPM就是1200。这个换算关系要提前算好写成宏定义或者常量。#define ENCODER_PPR 13 // 编码器线数 #define GEAR_RATIO 30 // 减速比 #define QUADRATURE 4 // 四倍频 #define COUNTS_PER_REV (ENCODER_PPR * GEAR_RATIO * QUADRATURE) // 1560 #define CONTROL_PERIOD 0.005f // 5ms float counts_to_rpm(int16_t counts) { return (float)counts / COUNTS_PER_REV / CONTROL_PERIOD * 60.0f; }读编码器计数用__HAL_TIM_GET_COUNTER(htim2)读完之后用__HAL_TIM_SET_COUNTER(htim2, 0)清零。注意要先读再清顺序反了会丢计数。另外如果计数超过16位范围会溢出但因为每5ms就读一次正常转速下不会溢出。如果电机转速特别高可以把控制周期缩短或者用32位定时器。4.2 位置式PID还是增量式PID电机速度控制我推荐用增量式PID。位置式PID的输出直接对应PWM占空比积分项会累积如果电机堵转或者电池电压突变积分项会饱和恢复起来很慢。增量式PID输出的是PWM的增量积分项体现在相邻两次输出的差值里不容易饱和而且不需要在切换模式时重置积分项。增量式PID的公式是Δu Kp×(e[k]-e[k-1]) Ki×e[k] Kd×(e[k]-2e[k-1]e[k-2])。输出u[k] u[k-1] Δu。代码实现如下typedef struct { float Kp, Ki, Kd; float error[3]; // error[0]当前, error[1]上次, error[2]上上次 float output; // 当前PWM输出 float output_max; // 输出限幅 float output_min; } IncPID; float IncPID_Calc(IncPID *pid, float target, float actual) { pid-error[2] pid-error[1]; pid-error[1] pid-error[0]; pid-error[0] target - actual; float delta pid-Kp * (pid-error[0] - pid-error[1]) pid-Ki * pid-error[0] pid-Kd * (pid-error[0] - 2*pid-error[1] pid-error[2]); pid-output delta; if (pid-output pid-output_max) pid-output pid-output_max; if (pid-output pid-output_min) pid-output pid-output_min; return pid-output; }4.3 四个电机的PID结构体管理四个电机各需要一个独立的PID结构体因为每个电机的负载特性、摩擦力、编码器精度都有差异。用数组管理最方便IncPID pid[4]; float target_rpm[4] {0, 0, 0, 0}; float actual_rpm[4] {0, 0, 0, 0};在5ms定时器中断里依次读取四个编码器的计数换算成RPM调用PID计算更新PWM的CCR寄存器。注意中断里不要用浮点运算太多次F103没有硬件浮点单元浮点运算是软件模拟的比较耗时。如果四个电机的PID计算加在一起超过5ms就会影响实时性。优化方法是用定点数运算或者把控制周期放宽到10ms。我实测下来四个电机的浮点PID计算大约耗时200us左右5ms周期完全够用。4.4 电机方向控制与死区处理每个电机需要两个方向引脚加一个PWM引脚。方向引脚控制H桥的导通方向PWM控制速度。在代码里封装一个函数void Motor_SetOutput(uint8_t motor_id, float output) { int16_t pwm (int16_t)output; if (pwm 0) { // 正转 HAL_GPIO_WritePin(dir_port[motor_id], dir_pin_a[motor_id], GPIO_PIN_SET); HAL_GPIO_WritePin(dir_port[motor_id], dir_pin_b[motor_id], GPIO_PIN_RESET); } else { // 反转 HAL_GPIO_WritePin(dir_port[motor_id], dir_pin_a[motor_id], GPIO_PIN_RESET); HAL_GPIO_WritePin(dir_port[motor_id], dir_pin_b[motor_id], GPIO_PIN_SET); pwm -pwm; } __HAL_TIM_SET_COMPARE(htim8, pwm_channel[motor_id], pwm); }死区处理很重要。电机在PWM占空比很低的时候比如低于5%可能转不起来但PID还在积分输出会一直涨直到超过死区突然转动造成顿挫。解决办法是在输出上加一个死区补偿如果输出不为零但绝对值小于死区阈值就把它抬到死区阈值。或者更简单粗暴地在PID输出后面加一个判断if (pid-output 0 pid-output DEAD_ZONE) pid-output DEAD_ZONE; if (pid-output 0 pid-output -DEAD_ZONE) pid-output -DEAD_ZONE;5. 蓝牙遥控协议与麦轮运动学5.1 遥控指令的编码设计蓝牙遥控最简单的做法是发单个字符比如F前进、B后退、L左转、R右转、S停止。但这样只能控制方向不能控制速度。更好的做法是发一个结构化的数据包包含速度值和方向值。比如定义一个5字节的协议帧头0xAA、速度值0到100、方向值0到8对应八个方向加停止、校验和、帧尾0x55。在STM32端串口中断收到数据后存入环形缓冲区主循环里解析。解析的时候要处理粘包和断帧的情况用状态机最稳妥typedef enum { STATE_HEADER, STATE_SPEED, STATE_DIRECTION, STATE_CHECKSUM, STATE_TAIL } ParseState; void ParseByte(uint8_t byte) { static ParseState state STATE_HEADER; static uint8_t speed, direction, checksum; switch (state) { case STATE_HEADER: if (byte 0xAA) state STATE_SPEED; break; case STATE_SPEED: speed byte; state STATE_DIRECTION; break; case STATE_DIRECTION: direction byte; state STATE_CHECKSUM; break; case STATE_CHECKSUM: checksum byte; if (checksum (speed direction)) state STATE_TAIL; else state STATE_HEADER; break; case STATE_TAIL: if (byte 0x55) { // 解析成功更新目标速度 UpdateTarget(speed, direction); } state STATE_HEADER; break; } }5.2 麦克纳姆轮的速度分解麦克纳姆轮的运动学是这个项目里最数学的部分。四个轮子的转速和车体运动的关系可以用一个矩阵表示。假设车体坐标系下前进速度为Vx横移速度为Vy旋转角速度为ω轮子半径为R车体半长为a半宽为b那么四个轮子的线速度是左前轮Vx - Vy - ω(ab)右前轮Vx Vy ω(ab)左后轮Vx - Vy ω(ab)右后轮Vx Vy - ω(ab)这个公式的推导基于辊子与地面接触点的速度合成这里不展开。实际用的时候把遥控指令映射成Vx、Vy、ω三个量然后代入公式算出四个轮子的目标速度再换算成RPM送给PID。void MecanumKinematics(float vx, float vy, float omega, float *wheel_rpm) { float v[4]; v[0] vx - vy - omega * (A B); // 左前 v[1] vx vy omega * (A B); // 右前 v[2] vx - vy omega * (A B); // 左后 v[3] vx vy - omega * (A B); // 右后 for (int i 0; i 4; i) { wheel_rpm[i] v[i] / (2 * PI * R) * 60.0f; } }遥控端发过来的方向值可以映射成八个方向前、后、左、右、左前、右前、左后、右后再加上原地左转和原地右转。每个方向对应一组Vx、Vy、ω的值。速度值则作为这些量的缩放系数。5.3 遥控端的实现思路遥控端可以用另一块STM32加摇杆模块也可以用手机APP。用手机APP的话在APP里画一个虚拟摇杆把摇杆的X、Y坐标映射成Vx、Vy再通过蓝牙串口发送数据包。用硬件的话双轴摇杆模块输出两路ADC值STM32读取后做同样的映射。不管哪种方式关键是要保证发送频率和控制周期匹配。如果遥控端每100ms发一次而PID每5ms算一次那中间95ms的目标值是不变的这没问题。但如果遥控端发送间隔太长比如500ms操作起来就会有明显的延迟感。我一般让遥控端每50ms发一次兼顾流畅性和蓝牙带宽。6. PID参数整定从震荡到稳定的实战过程6.1 先调一个轮子再复制到四个很多人一上来就四个轮子一起调结果四个轮子互相干扰根本看不出哪个参数有问题。正确做法是先让小车悬空四个轮子都不接触地面然后只调一个轮子的PID。把目标转速设为1000RPM观察实际转速的响应曲线。通过串口把目标值和实际值打印出来用上位机画曲线或者用OLED屏实时显示。调参的顺序是先P后I再D。Kp从0开始慢慢加加到实际转速开始震荡然后退回到震荡临界点的60%左右。这时候系统会有稳态误差实际转速达不到目标值。然后加Ki从小到大直到稳态误差消除。Ki太大会引起低频震荡表现为转速缓慢地上下波动。最后加Kd用来抑制超调和震荡Kd太大会放大编码器噪声表现为转速高频抖动。6.2 空载和负载的参数差异空载调好的参数放到地面上跑大概率会出问题。因为地面摩擦力和车体重量相当于给电机加了负载同样的PWM占空比产生的转速会下降。如果PID的积分项不够强稳态误差会变大。解决办法是在地面上重新微调适当增大Ki。但Ki增大会降低系统的相位裕度容易震荡。这时候可以适当增大Kp来补偿。我实测的一组参数供参考空载时Kp0.8Ki0.15Kd0.02地面负载时Kp1.2Ki0.25Kd0.03。当然这跟电机型号、减速比、车重都有关系不能照搬但数量级可以参考。6.3 电池电压下降带来的参数漂移锂电池从满电8.4V放到7.0V的过程中同样的PWM占空比对应的电机转速会下降大约15%到20%。如果PID的积分项足够强它能自动补偿这个下降维持转速稳定。但如果积分项不够强就会看到小车越跑越慢。这就是闭环控制的价值所在——它不依赖电池电压的稳定而是依赖反馈。不过积分项太强也有副作用。当电池电压突然下降时积分项会快速累积导致PWM输出猛增可能触发驱动板的过流保护。所以输出限幅很重要把PID的输出限制在驱动板能承受的最大值以内。6.4 用串口绘图工具辅助调参调PID的时候光看数字很难判断震荡频率和超调量。用串口把数据发到电脑上用绘图工具画出来就直观多了。我常用的是VOFA或者SerialPlot把目标转速和实际转速两路数据发过去实时看曲线。数据格式用简单的文本就行比如target,actual\n每5ms发一行。115200的波特率下每行大约20个字节5ms发一次就是4000字节/秒完全在带宽范围内。如果不想用电脑也可以在OLED屏上画一个小型的波形图虽然分辨率低但看趋势够了。OLED用I2C接口的话HAL库有现成的驱动刷屏速度也够快。7. 调试中那些让人抓狂的问题7.1 编码器计数方向不一致四个编码器装上去之后发现有的电机正转时计数增加有的减少。这是因为编码器的A、B相接线顺序不同或者电机安装方向不同。解决办法是在代码里给每个编码器加一个方向系数正转计数增加的系数为1减少的为-1。在读计数的时候乘上这个系数。int16_t encoder_dir[4] {1, -1, 1, -1}; int16_t counts __HAL_TIM_GET_COUNTER(encoder_tim[motor_id]) * encoder_dir[motor_id];这个系数在调试的时候确定确定之后写死。如果换了电机或者重新接线要重新确认。7.2 蓝牙数据丢包和延迟HC-05在115200波特率下如果周围有WiFi路由器或者其他2.4G设备丢包是常有的事。表现就是遥控指令偶尔不响应或者响应延迟。解决办法有几个一是降低波特率到9600牺牲刷新率换稳定性二是加校验和重传机制收到错误数据包就丢弃等下一个三是在遥控端做数据平滑不要每次摇杆动一点就发而是等摇杆稳定在一个位置再发。还有一个坑是HC-05的缓冲区溢出。如果STM32端处理不及时蓝牙模块的缓冲区满了会丢数据。所以串口中断里只做数据搬运解析和处理放到主循环确保中断快速返回。7.3 PID输出震荡但找不到原因有时候PID参数明明调得很温和但电机还是周期性地震荡。这种情况大概率不是PID的问题而是机械或者电气的问题。检查几个地方编码器联轴器是否松动松动会导致计数跳变电机电源线是否接触不良接触不良会导致转速突变PWM频率是否和电机产生共振试着改变PWM频率看看震荡是否消失。我遇到过一次震荡频率大约2Hz调PID怎么都消不掉。后来发现是电池到驱动板的线太细电机启动瞬间压降太大导致驱动板欠压保护反复重启。换了粗线之后问题消失。这种问题从PID参数上是看不出来的要从硬件上找原因。7.4 小车走不直的系统性偏差即使四个轮子的PID都调好了小车还是可能走不直。原因可能是四个轮子的直径有微小差异或者车体重量分布不均导致某个轮子抓地力不同。这种系统性偏差靠PID是消除不了的因为PID只保证每个轮子的转速准确不保证车体直线运动。解决办法是在运动学层面加补偿。比如发现小车总是往左偏就在Vx的基础上给右侧轮子加一点速度补偿。这个补偿值需要实际测试确定没有理论公式。或者更高级的做法是加陀螺仪做航向闭环但那就超出这个项目的范围了。8. 从能跑到好用几个提升体验的细节8.1 加减速斜坡让操作更顺滑遥控端突然从停止跳到全速PID会瞬间输出最大PWM小车猛地窜出去体验很差。加一个斜坡函数让目标速度逐渐变化而不是阶跃变化。比如每5ms目标速度增加不超过50RPM这样从0到1000RPM需要100ms操作起来就顺滑多了。float ramp_limit(float target, float current, float step) { if (target current step) return current step; if (target current - step) return current - step; return target; }这个斜坡加在遥控指令解析之后、PID计算之前。step的值根据实际测试调整太小了响应慢太大了还是猛。8.2 堵转保护防止烧电机小车撞到障碍物时轮子被堵住PID会疯狂增加输出试图达到目标转速电流急剧上升时间长了会烧电机或者驱动板。加一个堵转检测如果PID输出已经达到限幅值但实际转速仍然低于目标值的20%持续超过500ms就判定为堵转强制把目标速度设为0并置一个标志位等遥控端发来新的指令再恢复。这个保护逻辑放在PID计算之后用计数器实现。堵转保护触发后可以在OLED上显示一个提示方便调试。8.3 用DMA减轻CPU负担串口接收可以用DMA加空闲中断的方式这样CPU不用每个字节都进中断。配置USART1的RX为DMA模式循环接收然后在空闲中断里计算收到的数据长度交给解析函数处理。这样即使遥控端发送频率很高CPU也不会被串口中断拖累。编码器读取也可以用DMA但编码器是定时器外设不经过DMA。不过PWM的CCR寄存器更新可以用DMA把四个CCR的值组成一个数组用DMA传输到定时器的CCR寄存器这样更新PWM不需要CPU干预。不过对于四个通道来说直接写寄存器也就几条指令的事DMA的收益不大。8.4 参数存储与上电自恢复调好的PID参数如果每次上电都要重新烧录那就太麻烦了。可以把参数存在STM32的Flash里上电时读出来。HAL库有Flash操作函数F103的Flash页大小是1KB找最后一页存参数就行。存之前先擦除然后按半字写入。读的时候直接指针访问。#define PARAM_ADDR 0x0801FC00 // 最后一页起始地址 typedef struct { float Kp[4], Ki[4], Kd[4]; uint32_t magic; // 用于判断是否已初始化 } PIDParams; void SaveParams(PIDParams *p) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase; erase.TypeErase FLASH_TYPEERASE_PAGES; erase.PageAddress PARAM_ADDR; erase.NbPages 1; uint32_t page_error; HAL_FLASHEx_Erase(erase, page_error); uint16_t *src (uint16_t*)p; for (int i 0; i sizeof(PIDParams)/2; i) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, PARAM_ADDR i*2, src[i]); } HAL_FLASH_Lock(); }magic值设一个固定的数比如0x5A5A1234上电时检查这个值如果不对就加载默认参数。8.5 用OLED做现场调试界面没有电脑的时候OLED屏就是最好的调试工具。显示四个电机的目标转速、实际转速、PID输出值一眼就能看出哪个轮子有问题。用0.96寸的I2C OLEDHAL库的I2C驱动加上SSD1306的驱动库刷一屏大约20ms不影响控制周期。显示内容可以分页第一页显示四个电机的转速第二页显示PID参数第三页显示电池电压和堵转标志。电池电压检测用ADC分压之后接到ADC引脚。F103的ADC是12位的参考电压3.3V。分压电阻选两个10k的测7.4V电池的话分压后是3.7V超过3.3V了所以要用两个电阻比如20k和10k分压比3:17.4V分压后2.47V在量程内。9. 关于这个项目的一些个人体会做这个项目最大的感受是PID参数整定没有捷径必须动手试。网上看到的参数只能作为起点实际效果跟电机、车重、地面材质都有关。我一般会花一整个下午专门调参数用串口绘图工具反复看曲线直到四个轮子的响应基本一致。另一个体会是硬件问题往往比软件问题更难查。软件问题至少可以通过打印调试信息定位硬件问题比如接触不良、电源干扰表现是随机的很难复现。所以焊接和接线的时候一定要仔细该用粗线的地方不要省该加电容的地方不要漏。前期多花半小时把线理好后期能省半天排查时间。麦克纳姆轮的运动学公式看起来复杂但实际用的时候只需要把遥控指令映射成Vx、Vy、ω三个量剩下的交给公式就行。关键是四个轮子的方向要统一编码器计数方向要一致否则运动学算出来的结果跟实际运动对不上。最后说一个容易被忽略的点蓝牙遥控的延迟。从摇杆动到小车响应中间经过遥控端采集、蓝牙传输、STM32解析、PID计算、电机执行整个链路下来大约50ms到100ms。这个延迟在低速下感觉不明显但高速平移或者快速旋转时会有明显的滞后感。如果对实时性要求高可以考虑用2.4G专用遥控器加接收机延迟能降到10ms以内。不过那就不是蓝牙方案了看具体需求取舍。