ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于STM32与JGB-520编码电机的两轮自平衡小车:串级PID控制与TB6612驱动实战

2026/9/28 18:02:58 拓冰建站 浏览量
基于STM32与JGB-520编码电机的两轮自平衡小车:串级PID控制与TB6612驱动实战 1. 项目缘起与整体方案拆解1.1 为什么选择两轮自平衡这个方向两轮自平衡小车在嵌入式圈子里算是一个经典到不能再经典的项目但经典不代表简单。它几乎把嵌入式开发里所有容易踩坑的环节都串起来了传感器数据采集与滤波、电机闭环控制、实时性要求、机械结构装配、电源管理、参数整定。你很难找到第二个项目能在一个巴掌大的板子上同时塞进这么多知识点。我前后做过四五个版本的平衡小车从最早的L298N加直流减速电机到后来的TB6612配JGB-520编码电机中间翻过的车比跑通的多。这次把JGB-520编码电机和TB6612驱动模块这套组合单独拎出来讲是因为这套搭配在性价比、控制精度和上手难度之间找到了一个很好的平衡点特别适合想认真搞懂PID闭环控制、而不是只让小车勉强站起来的玩家。JGB-520这个型号520指的是电机外径约52mm实际是37mm减速箱体加电机整体尺寸它自带霍尔编码器能输出A/B两相正交脉冲。编码器的意义在于你能实时知道轮子转了多少、转多快这是做速度闭环的前提。没有编码器的平衡小车只能靠陀螺仪和加速度计盲猜姿态一旦遇到地面摩擦变化或者电池电压下降车就会越跑越飘。TB6612则是一颗双路H桥驱动芯片单路持续电流1.2A、峰值3.2A驱动JGB-520这种空载电流一两百毫安、堵转一两安的电机绰绰有余而且它比L298N效率高、发热小、体积小PWM频率支持到100kHz完全够用。1.2 整体控制架构是怎么搭的平衡小车的控制逻辑说白了就是一个倒立摆问题。车体相当于一根倒立的杆子轮子相当于杆子的支点。支点要不停地往杆子倾斜的方向移动才能让杆子保持不倒。这个移动就是靠电机正反转实现的。整个系统我把它拆成三个环从内到外依次是电流环可选进阶玩法控制电机绕组电流响应最快一般用不到除非你要做力矩控制。速度环控制轮子转速输入是目标速度输出是PWM占空比。这个环用编码器反馈。角度环控制车体倾角输入是目标角度通常是0度即竖直输出是目标速度交给速度环执行。这三个环串起来就是串级PID。角度环是外环速度环是内环。角度环的输出不作为PWM直接给电机而是作为速度环的目标值。这样设计的好处是当车体倾斜时角度环先算出一个我应该用多快的速度去追的目标速度环再根据当前实际转速去调节PWM两个环各司其职抗干扰能力比单环强得多。我见过不少新手直接把角度环的输出当PWM用车也能站但一推就倒或者在地上缓慢漂移停不下来。原因就是没有速度环兜底轮子转速不受控车体角度稍微偏一点PWM就给到底轮子猛转车冲出去然后角度反向超调来回振荡。加上速度环之后轮子转速被约束在一个合理范围内车体运动就平顺多了。1.3 硬件选型背后的取舍逻辑主控用STM32F103C8T6也就是常说的蓝板或最小系统板。选它的理由很直接72MHz主频、自带多路定时器和PWM输出、硬件I2C/SPI、ADC资源丰富价格便宜到离谱资料多到看不过来。对于平衡小车这种需要同时处理传感器读取、PWM输出、编码器计数、串口调试的任务F103的性能刚好够用不会像51单片机那样捉襟见肘也不至于像F4那样大材小用。电机驱动选TB6612而不是L298N除了前面说的效率和发热优势还有一个关键点TB6612的PWM频率可以设得很高。L298N内部是双极型晶体管开关速度慢PWM频率一般只能到10kHz左右再高就发热严重、效率暴跌。TB6612是MOSFET工艺PWM给到20kHz甚至更高都没问题。PWM频率高有什么好处人耳听觉范围是20Hz到20kHz如果PWM频率落在听觉范围内电机就会发出刺耳的啸叫声。把PWM设到20kHz以上电机运行就安静多了这对调试时的心情影响很大。编码器电机选JGB-520减速比一般有1:30、1:45、1:60等几种。减速比越大输出扭矩越大、转速越低、编码器分辨率越高因为电机转很多圈输出轴才转一圈。平衡小车需要的是响应速度不是大力气所以我推荐1:30或1:45的减速比。1:60的扭矩过剩转速偏低车体修正倾斜时反应会慢半拍。电源部分电机和主控要分开供电。电机启动瞬间电流冲击很大如果和主控共用一路LDO电压会被拉低STM32可能复位。我的做法是电池用2节18650串联7.4V一路直接给TB6612的VMOT供电另一路经过降压模块降到5V给STM32和传感器。TB6612的逻辑供电VCC用3.3V或5V都行接STM32的3.3V即可。2. 核心硬件连接与底层驱动实现2.1 TB6612与STM32的接线要点TB6612模块上一般有这些引脚VM电机电源、VCC逻辑电源、GND、STBY使能、AIN1/AIN2A路方向控制、BIN1/BIN2B路方向控制、PWMA/PWMBA/B路PWM输入、AO1/AO2A路电机输出、BO1/BO2B路电机输出。接线时最容易出错的地方是STBY引脚。这个引脚是芯片的使能端高电平芯片才工作低电平所有输出关闭。很多新手接完线发现电机不转查了半天PWM和方向都对最后发现STBY悬空了。我的习惯是直接把STBY接到STM32的一个GPIO上初始化时拉高这样还能在紧急情况下软件关断电机比接固定高电平灵活。方向控制引脚AIN1/AIN2的逻辑是这样的AIN1高、AIN2低电机正转AIN1低、AIN2高电机反转两个都低电机自由停止滑行两个都高电机制动刹车。注意TB6612没有内置刹车电阻制动时电机能量会通过H桥的下管续流制动效果比自由停止强但不如机械刹车。PWM输入PWMA接STM32的定时器通道。我一般用TIM3的CH1和CH2对应PA6和PA7或者TIM2的CH1/CH2。PWM频率设20kHz占空比0到100%对应电机从停到全速。这里有个细节TB6612的PWM是高电平有效也就是说PWMA给高电平时电机才通电给低电平时电机断电。所以如果你用互补PWM输出要注意极性。注意TB6612的VM和VCC不能接反VM接7.4V电池VCC接3.3V或5V逻辑电源。接反会烧芯片。另外电机输出AO1/AO2不要短路否则H桥直通芯片瞬间冒烟。2.2 JGB-520编码器的信号读取JGB-520电机尾部有一个6线接口通常是电机正、电机负、编码器VCC、编码器GND、编码器A相、编码器B相。编码器VCC一般接3.3V或5V具体看电机规格大部分JGB-520的霍尔编码器是5V供电但输出信号是开漏或推挽接STM32的3.3V GPIO也能识别STM32的IO大部分是5V容忍的但为了保险可以用电平转换或者直接接实测多数情况下没问题。编码器A/B两相输出正交脉冲相位差90度。通过判断A相和B相的先后顺序可以知道电机是正转还是反转通过计数脉冲个数可以知道转了多少角度。STM32读取编码器有两种方式软件计数把A相接到外部中断引脚B相接普通GPIO在A相上升沿中断里读B相电平判断方向并加减计数。这种方式占用CPU高速时可能丢脉冲。硬件编码器模式STM32的定时器自带编码器接口模式把A/B两相接到定时器的CH1和CH2硬件自动计数和判向CPU只需要定期读计数器值即可。这是推荐方式不丢脉冲不占CPU。我用TIM4的CH1PB6和CH2PB7接编码器A/B相。配置时把TIM4设为编码器模式计数模式选编码器模式3在TI1和TI2的上升沿和下降沿都计数这样四倍频分辨率最高。比如JGB-520的编码器是11线电机轴每转11个脉冲减速比1:30那么输出轴每转一圈电机轴转30圈编码器输出11×30330个脉冲四倍频后是1320个计数。也就是说输出轴转一圈TIM4计数器变化1320。这个数字很重要后面算速度要用。2.3 电机驱动的底层代码框架底层驱动我习惯分成三个文件motor.c负责PWM输出和方向控制encoder.c负责编码器读数pid.c负责PID计算。这样模块清晰调参时改哪个文件一目了然。先看motor.c的核心部分。初始化时配置TIM3的CH1和CH2为PWM输出频率20kHz占空比初始为0。然后配置AIN1/AIN2/BIN1/BIN2为推挽输出STBY为推挽输出并拉高。// motor.c 初始化片段 void Motor_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; TIM_TimeBaseInitTypeDef TIM_BaseStruct; TIM_OCInitTypeDef TIM_OCStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // PA6/PA7 复用推挽输出接PWMA/PWMB GPIO_InitStruct.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStruct); // PB0/PB1 接AIN1/AIN2PB10/PB11 接BIN1/BIN2PB12接STBY GPIO_InitStruct.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12; GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOB, GPIO_InitStruct); GPIO_SetBits(GPIOB, GPIO_Pin_12); // STBY拉高使能TB6612 // TIM3 时基72MHz / (01) / (35991) 20kHz TIM_BaseStruct.TIM_Prescaler 0; TIM_BaseStruct.TIM_Period 3599; TIM_BaseStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_BaseStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, TIM_BaseStruct); // CH1/CH2 PWM模式1 TIM_OCStruct.TIM_OCMode TIM_OCMode_PWM1; TIM_OCStruct.TIM_OutputState TIM_OutputState_Enable; TIM_OCStruct.TIM_Pulse 0; TIM_OCStruct.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCStruct); TIM_OC2Init(TIM3, TIM_OCStruct); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }设置电机速度的函数输入是左右轮的目标PWM值范围-3600到3600对应-100%到100%占空比。负值代表反转。void Motor_SetPWM(int left, int right) { // 左轮 if (left 0) { GPIO_SetBits(GPIOB, GPIO_Pin_0); GPIO_ResetBits(GPIOB, GPIO_Pin_1); TIM_SetCompare1(TIM3, left); } else { GPIO_ResetBits(GPIOB, GPIO_Pin_0); GPIO_SetBits(GPIOB, GPIO_Pin_1); TIM_SetCompare1(TIM3, -left); } // 右轮同理注意方向引脚对应BIN1/BIN2 if (right 0) { GPIO_SetBits(GPIOB, GPIO_Pin_10); GPIO_ResetBits(GPIOB, GPIO_Pin_11); TIM_SetCompare2(TIM3, right); } else { GPIO_ResetBits(GPIOB, GPIO_Pin_10); GPIO_SetBits(GPIOB, GPIO_Pin_11); TIM_SetCompare2(TIM3, -right); } }这里有个细节左右轮的PWM值要限幅不能超过3600否则TIM_SetCompare会写入超过ARR的值实际输出还是100%但逻辑上容易混乱。我在函数入口加一个限幅判断。2.4 编码器数据的读取与速度计算编码器用TIM4的编码器模式。初始化时把PB6/PB7设为浮空输入或上拉输入配置TIM4为编码器模式3。void Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; TIM_TimeBaseInitTypeDef TIM_BaseStruct; TIM_ICInitTypeDef TIM_ICStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); GPIO_InitStruct.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOB, GPIO_InitStruct); TIM_BaseStruct.TIM_Prescaler 0; TIM_BaseStruct.TIM_Period 65535; TIM_BaseStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, TIM_BaseStruct); TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_ICStruct.TIM_Channel TIM_Channel_1; TIM_ICStruct.TIM_ICFilter 10; // 滤波防抖动 TIM_ICInit(TIM4, TIM_ICStruct); TIM_ICStruct.TIM_Channel TIM_Channel_2; TIM_ICInit(TIM4, TIM_ICStruct); TIM_Cmd(TIM4, ENABLE); }读取速度时我一般用定时器中断每5ms或10ms读一次计数器然后清零这样得到的是这段时间内的脉冲增量除以时间就是速度。注意TIM4是16位计数器正转到65535会溢出反转到0会下溢。由于我每5ms清零一次5ms内电机最多转多少假设电机空载转速300RPM输出轴5ms转0.025圈编码器计数0.025×132033个脉冲远小于65535所以不会溢出。但如果电机高速旋转且不清零累计值可能溢出所以定期清零是必要的。int16_t Encoder_GetDelta(void) { int16_t delta (int16_t)TIM_GetCounter(TIM4); TIM_SetCounter(TIM4, 0); return delta; }这里把计数器值强制转为int16_t是因为如果电机反转计数器从0减到65535再减到65534……直接读是无符号的65535转成有符号就是-1正好表示反转一个脉冲。这个技巧很实用省去了手动判断方向的麻烦。速度的单位我习惯用脉冲数/10ms因为PID计算时不需要换算成物理单位直接用脉冲增量作为速度反馈即可。如果你要显示实际转速RPM可以换算RPM (脉冲数/10ms) × 100 × 60 / 1320。但PID调参时用脉冲数更直观因为参数范围好把握。3. PID控制器的设计与调参实战3.1 位置式PID与增量式PID的选择PID控制有两种常见实现位置式和增量式。位置式PID的输出直接是控制量比如PWM值公式是输出 Kp × 误差 Ki × 误差积分 Kd × 误差微分增量式PID的输出是控制量的增量公式是增量 Kp × (本次误差 - 上次误差) Ki × 本次误差 Kd × (本次误差 - 2×上次误差 上上次误差)平衡小车的角度环和速度环我用的都是位置式PID。原因很简单角度环需要输出一个绝对的目标速度速度环需要输出一个绝对的PWM值用位置式更直观。增量式适合执行机构带积分特性的场景比如步进电机平衡小车不太适用。但位置式PID有一个坑积分项会累积。如果车体长时间倾斜一个方向积分项会越积越大导致输出饱和车体反向超调。解决办法是加积分限幅和积分分离。积分限幅就是限制积分项的最大值比如限制在±500积分分离就是当误差很大时比如车刚启动角度偏差30度先不积分等误差小了再积分避免启动时积分爆掉。3.2 角度环的参数整定步骤角度环是平衡小车的核心它决定了车能不能站起来。角度环的输入是车体倾角由MPU6050的陀螺仪和加速度计融合得到输出是目标速度。目标角度设为0度竖直。调角度环时先把速度环去掉也就是角度环的输出直接当PWM用。然后按以下步骤来只给PKp从0开始慢慢加大。你会看到车体开始有抵抗倾斜的趋势。继续加大直到车体能在小范围内来回振荡频率较高像抖动。记下这个Kp值然后取它的60%到70%作为最终Kp。比如振荡时Kp100最终取65左右。加DD的作用是抑制振荡给系统阻尼。Kd从0开始加你会看到振荡幅度减小车体变得稳重。但Kd太大会引入高频噪声因为微分对噪声敏感。如果车体出现高频抖动像电机在嗡嗡响但车不动说明Kd太大了要减小。Kd一般取Kp的0.5到2倍具体看车体惯量和电机响应。加II的作用是消除稳态误差。如果车体总是往一个方向偏一点才能平衡说明有稳态误差需要加一点I。但I不能大大了会振荡。角度环的I一般很小甚至为0因为角度环的稳态误差可以通过机械调平来消除。我实测的一组参数供参考不同车体差异很大Kp65Ki0.5Kd1.2。注意这里的Kd单位是每度每秒因为微分项是误差的变化率。如果你的角度更新频率是100Hz每10ms一次Kd的实际作用会被放大因为误差变化率是每10ms的变化量数值上比每秒变化量小100倍。所以Kd的数值看起来很小但实际作用不小。注意角度环调参时一定要把车体悬空或者放在支架上不要让轮子着地。否则车体一倾斜轮子就转车冲出去你根本来不及调。我一般用两个书夹把车轴架起来轮子悬空这样调角度环最安全。3.3 速度环的参数整定与串级调试速度环的输入是目标速度来自角度环输出反馈是编码器测得的实际速度输出是PWM值。速度环的参数整定要在角度环调好之后进行。先把角度环的参数固定然后加上速度环。速度环的Kp从0开始加你会看到车体开始有抵抗移动的趋势你推它一下它会自己走回来。继续加大Kp直到车体出现低频振荡频率比角度环的振荡低像车体前后缓慢摆动。然后取振荡时Kp的50%到60%。速度环的Ki用来消除速度稳态误差。如果车体总是往一个方向缓慢漂移说明速度环有稳态误差加一点Ki。但Ki大了会导致积分饱和车体漂移更严重。我的经验是Ki取Kp的0.1到0.3倍。速度环的Kd一般不用因为速度反馈本身有噪声微分会放大噪声。如果非要用Kd取很小比如Kp的0.05倍。串级调试时有一个关键点角度环的输出限幅。角度环输出的目标速度不能太大否则车体一倾斜目标速度给到几百速度环拼命加速车冲出去。我一般把角度环输出限幅在±100单位是脉冲数/10ms对应轮子转速大约±4.5RPM输出轴这个速度足够平衡修正了。还有速度环输出限幅也就是PWM限幅。我限在±3000对应约83%占空比留一点余量避免电机长时间满占空比运行发热。3.4 PID调参中的常见现象与对策调PID时你会遇到各种症状我整理了一个速查表现象可能原因对策车体高频抖动电机嗡嗡响Kd太大或角度噪声大减小Kd给角度加低通滤波车体低频前后摆动速度环Kp太大减小速度环Kp车体往一个方向缓慢漂移速度环Ki太小或有稳态误差增大Ki检查机械中值车体一推就倒没有抵抗角度环Kp太小增大角度环Kp车体启动时猛冲一下积分饱和加积分限幅或积分分离车体站住但轮子来回蹭地角度环Kd太小适当增大Kd车体倾斜角度很大才修正角度环输出限幅太小增大角度环输出限幅这里重点说机械中值。平衡小车有一个机械中值就是车体真正竖直时角度传感器的读数。这个值不是0因为传感器安装有偏差。你需要在车体竖直时读一次角度值把它作为目标角度。如果机械中值不准车体就会往一个方向偏。我一般用串口把角度值打印出来手动把车扶正记下读数写进代码里。另外角度融合也很关键。MPU6050的陀螺仪积分会漂移加速度计噪声大但长期稳定。我用互补滤波角度 0.98 × (角度 陀螺仪角速度 × dt) 0.02 × 加速度计角度。这个0.98和0.02是权重陀螺仪信任度高但会漂加速度计信任度低但能纠正漂移。实际调的时候如果车体静止时角度缓慢变化说明陀螺仪漂移没被纠正增大加速度计权重如果车体抖动时角度噪声大说明加速度计权重太大减小它。4. 调试工具与实战避坑经验4.1 串口调试与数据可视化调PID最痛苦的是看不见车体内部的状态。你只能看到车倒了但不知道是角度环的问题还是速度环的问题。所以串口调试是必须的。我的做法是用STM32的USART1波特率115200每10ms发一次数据格式是角度、目标速度、实际速度、PWM值。然后在电脑上用串口助手或者Python脚本接收画成曲线。这样你就能看到车体倾斜时角度环输出的目标速度是多少速度环跟上了没有PWM有没有饱和。如果你不想写Python脚本可以用现成的串口示波器软件比如VOFA或者SerialPlot。这些软件支持简单的协议比如把数据用逗号分隔加个换行就能自动画曲线。我一般用VOFA的FireWater协议数据格式是角度,目标速度,实际速度,PWM\n直接就能看。注意串口发送数据会占用CPU时间如果发送频率太高比如每1ms一次会影响PID计算的实时性。我一般每10ms发一次和PID计算频率一致这样对实时性影响最小。4.2 电机死区与PWM限幅JGB-520电机有一个死区PWM占空比低于某个值时电机不转。这个值一般是5%到10%具体看电机和驱动。如果你不给死区补偿PID输出在死区附近时电机时转时不转车体会抖动。解决办法是加死区补偿当PWM值不为0但小于死区阈值时直接给到死区阈值。比如死区是200对应5.5%那么PWM在1到200之间时统一输出200。这样电机就能克服静摩擦启动。但死区补偿会引入一个问题车体在平衡点附近微调时PWM本来应该很小但被补偿到200电机就会突然转一下车体反而抖动。所以死区补偿要配合小信号死区当PID输出绝对值小于某个阈值比如50时直接输出0让电机停转靠车体惯性维持平衡。这个阈值需要实测太小了电机抖太大了车体站不住。我实测JGB-520配TB6612死区大约在180到220之间小信号死区设在60左右比较合适。4.3 电源管理与电机干扰抑制平衡小车调试时经常遇到一个诡异现象车体站着站着突然复位或者角度数据突然跳变。这多半是电源问题。电机启动和换向时电流突变会在电源线上产生尖峰通过共地耦合到STM32的电源导致复位或ADC采样错误。解决办法电机电源和主控电源分开走线最后在电池处单点共地。在TB6612的VM和GND之间并一个大电容100uF到470uF吸收电机换向的尖峰。在STM32的电源引脚附近并0.1uF和10uF电容滤高频和低频。编码器信号线用双绞线或屏蔽线远离电机电源线。我踩过最坑的一次是电机电源线和编码器信号线捆在一起走结果编码器计数乱跳车体速度反馈全是噪声PID根本没法调。后来把线分开编码器计数立刻稳定了。4.4 常见问题速查与排查思路问题排查方向解决方法电机不转STBY是否拉高、PWM是否输出、方向引脚是否正确用万用表测TB6612输出逐步排查电机只往一个方向转方向引脚接线错误或PWM极性错误交换AIN1/AIN2或修改代码编码器计数不变编码器供电、A/B相接反、TIM配置错误用示波器看A/B相波形角度数据漂移陀螺仪零偏未校准、互补滤波权重不对静止时校准零偏调整权重车体振荡不停PID参数太大、角度噪声大减小参数加滤波车体往一边倒机械中值不准、电机安装不对称重新校准中值检查机械串口无数据波特率不对、TX/RX接反、串口被占用检查接线和配置这里重点说陀螺仪零偏校准。MPU6050的陀螺仪在静止时输出不是0而是一个小偏置这个偏置会随时间温度变化。如果不校准角度会一直漂。我的做法是上电后先让车静止2秒采集1000次陀螺仪数据求平均作为零偏后续读数都减去这个零偏。这个校准过程要放在车体完全静止时进行否则零偏不准。还有一个坑MPU6050的I2C地址。默认是0x68如果AD0引脚接高电平变成0x69。如果你用现成的模块AD0一般接地地址是0x68。但有些模块AD0悬空地址不确定导致I2C通信失败。我一般直接在代码里写0x68如果读不到数据再试0x69。4.5 从能站到站得稳的进阶技巧车能站起来只是第一步要站得稳、抗干扰还需要一些进阶技巧。第一速度环的前馈。角度环输出目标速度时可以加一个前馈项目标速度 角度环输出 Kff × 角度。这个Kff的作用是当车体倾斜时除了PID修正额外给一个与倾斜角度成正比的速度指令加快响应。前馈不参与反馈不会引起振荡但能显著提升动态性能。Kff一般取角度环Kp的0.3到0.5倍。第二角度环的微分先行。标准PID的微分项是对误差微分但误差 目标角度 - 实际角度目标角度是0所以误差微分 -实际角度微分。如果目标角度突变比如你用手推车微分项会突变引起冲击。微分先行是把微分项改成只对实际角度微分不对目标角度微分这样目标突变时微分项平滑。平衡小车的目标角度固定为0所以微分先行和标准微分效果一样但如果你要做遥控转向目标角度随遥控变化微分先行就很重要了。第三电机的速度前馈。速度环输出PWM时可以加一个前馈项PWM 速度环输出 Kff_v × 目标速度。这个前馈补偿了电机的反电动势和摩擦让速度环的Kp可以取小一点减少振荡。Kff_v一般取0.5到1.0具体看电机特性。第四转向环。如果你要做遥控平衡小车还需要一个转向环。转向环的输入是遥控的转向指令反馈是左右轮速度差输出是左右轮的差速PWM。转向环一般只用P不用I和D因为转向不需要消除稳态误差也不需要阻尼。转向环的Kp取速度环Kp的0.5到1倍。我实测下来加了前馈之后车体的抗推能力明显提升原来推一下要晃三四下才稳加了前馈之后晃一两下就稳了。但前馈参数不能太大太大了车体会过冲推一下反而往反方向冲。5. 代码组织与实时性优化5.1 定时器中断的任务调度平衡小车的实时性要求很高角度环和速度环的计算必须在固定周期内完成不能被打断太久。我一般用TIM2或TIM6做1ms定时中断在中断里做任务调度每1ms读取MPU6050数据更新角度互补滤波。每5ms读取编码器计算速度。每10ms计算角度环PID输出目标速度。每10ms计算速度环PID输出PWM。这样分频的好处是角度更新频率高1kHz保证姿态估计的实时性速度环和角度环频率低100Hz减少CPU负担。实际上角度环和速度环的频率可以一样都是100Hz因为角度环的输出是速度环的输入两个环同步计算更简单。中断里的代码要尽量短不要做浮点运算除非STM32带FPUF103没有硬件FPU浮点运算是软件模拟很慢。我一般用定点数或者整数运算。比如角度用0.1度为单位存成int16_tPID参数用整数最后除一个缩放因子。这样中断执行时间能控制在几十微秒不影响其他任务。5.2 浮点运算与定点运算的取舍STM32F103没有硬件浮点单元浮点运算靠软件库一次浮点乘法大约几十个时钟周期一次浮点除法上百个周期。在1kHz中断里做浮点PIDCPU占用率会很高。所以我在中断里用定点运算。具体做法角度用int16_t单位0.1度比如竖直是0倾斜10度是100。PID参数也用整数比如Kp65实际计算时是Kp × 误差 / 100。这样所有运算都是整数乘除速度快很多。但定点运算有一个坑整数除法会截断损失精度。比如误差是10.1度Kp6565×1/1000输出就是0相当于死区。解决办法是先把误差放大比如误差用0.01度为单位存成int32_t这样精度就够了。或者用移位代替除法比如除以100改成乘以655再右移16位精度和速度都兼顾。我一般用int32_t存中间结果最后输出时再限幅和转成PWM值。这样既保证了精度又避免了浮点运算。5.3 代码模块化与参数在线调整调PID时如果每次改参数都要重新编译下载效率太低了。我一般把PID参数放在全局变量里通过串口命令在线修改。比如串口收到P65就把Kp设为65I0.5就把Ki设为0.5。这样调参时不用反复下载直接在串口助手里改改完立刻生效。代码模块化方面我习惯把PID封装成一个结构体typedef struct { float Kp; float Ki; float Kd; float integral; float prev_error; float integral_limit; float output_limit; } PID_TypeDef; float PID_Calc(PID_TypeDef *pid, float error) { pid-integral error; if (pid-integral pid-integral_limit) pid-integral pid-integral_limit; if (pid-integral -pid-integral_limit) pid-integral -pid-integral_limit; float derivative error - pid-prev_error; pid-prev_error error; float output pid-Kp * error pid-Ki * pid-integral pid-Kd * derivative; if (output pid-output_limit) output pid-output_limit; if (output -pid-output_limit) output -pid-output_limit; return output; }这样角度环和速度环各用一个PID_TypeDef实例参数独立互不干扰。调参时只改对应实例的参数即可。注意PID_Calc里的积分项在误差很大时会快速累积即使有限幅也可能在限幅值附近停留很久。更好的做法是加积分分离当误差绝对值大于某个阈值时不累积积分。这个阈值一般取输出限幅的10%到20%。5.4 系统上电与安全保护逻辑平衡小车上电时如果车体是倾斜的角度环会立刻输出很大的目标速度车体猛冲。所以上电时要有一个启动保护上电后前2秒电机不使能只做传感器校准和角度初始化。2秒后如果车体角度在±5度以内才使能电机如果角度超出范围继续等待直到车体被扶正。另外如果车体倾斜超过一定角度比如30度说明已经倒了这时候要立刻关断电机避免电机堵转烧毁。这个保护逻辑放在1ms中断里检测到角度超限就拉低STBY。还有一个保护是堵转保护如果PWM输出很大但编码器速度为0说明电机堵转持续超过500ms就关断电机。这个保护能防止车体卡住时电机烧毁。我踩过一次坑车体倒下后电机还在转轮子蹭地电机电流很大TB6612发热严重最后芯片热保护了。后来加了倾倒保护再也没出现过这个问题。6. 机械结构与装配的隐藏影响6.1 重心高度与轮距的匹配平衡小车的机械结构对控制效果影响极大但很多新手只关注代码忽略了机械。重心高度和轮距是两个关键参数。重心越高车体越容易倒需要更快的修正速度对电机响应要求更高。重心越低车体越稳但修正力度不够时容易站不住。我一般把电池放在最底部STM32和驱动板放在中间MPU6050放在顶部但尽量靠近重心垂线。这样重心低且集中车体稳定性好。轮距两个轮子之间的距离影响转向和侧倾。轮距太窄车体容易侧翻轮距太宽转向不灵活。我一般取轮距等于车体高度的0.6到0.8倍。比如车体高15cm轮距取10到12cm。还有一个细节电机安装要对称。如果左右电机安装位置有偏差车体直线行驶时会跑偏速度环会一直修正导致车体抖动。我一般用游标卡尺量电机轴到车体中线的距离确保左右对称。6.2 轮胎材质与地面摩擦轮胎材质对平衡小车的影响经常被忽略。硬塑料轮在地砖上打滑车体修正时轮子空转编码器有速度但车体不动PID会误判。橡胶轮抓地力好但摩擦力太大车体微调时轮子粘在地上电机需要更大扭矩才能转动死区变大。我试过几种轮胎普通橡胶轮、硅胶轮、海绵轮。最后发现硅胶轮在木地板和地砖上表现最好抓地力适中死区小。海绵轮太软车体重量压上去变形滚动阻力大。普通橡胶轮在光滑地面上打滑。如果你要在不同地面上跑建议准备两套轮胎硅胶轮用于室内光滑地面橡胶轮用于室外粗糙地面。换轮胎后要重新调PID因为摩擦特性变了。6.3 传感器安装与减震MPU6050对振动非常敏感。电机转动时的高频振动会传到车体被陀螺仪采集到导致角度噪声大PID输出抖动。解决办法是在MPU6050和车体之间加减震垫比如海绵双面胶或者硅胶垫。减震垫的硬度要适中太软了传感器会晃角度漂移太硬了减震效果差。我一般用3M的泡棉双面胶剪成小块垫在MPU6050四个角然后用尼龙柱固定。这样既有减震效果又不会晃。另外MPU6050要尽量安装在车体中心线上远离电机减少振动耦合。还有一个坑MPU6050的安装方向。如果你把传感器横着装角度轴就变了代码里的角度计算要对应修改。我一般把MPU6050的X轴指向车体前进方向Y轴指向左侧Z轴指向上方。这样俯仰角pitch就是车体前后倾斜的角度直接用于平衡控制。7. 从调通到优化的个人体会平衡小车这个项目我最大的体会是代码只占三成调参和机械占七成。你可以在网上找到无数份平衡小车的代码但每辆车的机械结构、电机特性、传感器安装都不一样直接抄代码大概率站不起来。必须理解每个参数背后的物理意义然后根据自己的车体去调。我调第一辆车的时候花了整整一周才让它站起来中间无数次想放弃。后来发现问题是电机死区没补偿PWM在死区附近时电机不转车体抖动我以为是PID参数不对调来调去都没用。加了死区补偿之后车立刻就稳了。所以遇到问题不要只盯着PID先检查底层驱动和机械。另一个体会是数据比感觉可靠。调参时不要凭感觉觉得车稳了要用串口把角度、速度、PWM都打出来看曲线。曲线会告诉你真相是角度环振荡还是速度环振荡是积分饱和还是微分噪声。我后来养成了习惯每次调参都开着串口示波器看着曲线调效率高很多。最后分享一个小技巧如果你调了很久都站不稳不妨把车体拿在手里感受电机的反应。车体往前倾电机应该往前转车体往后倾电机应该往后转。如果方向反了说明角度环的极性错了车永远站不起来。这个检查只需要几秒钟但能排除最常见的问题。这个项目后续还可以扩展加蓝牙或WiFi做遥控加OLED显示角度和速度加超声波避障甚至加摄像头做视觉循迹。但前提是先把平衡控制调稳基础不牢加再多功能都是空中楼阁。