ARTICLE DETAIL

资讯详情

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

STM32编码器闭环实现小车精确行驶距离:从PWM开环到PID位置控制

STM32编码器闭环实现小车精确行驶距离:从PWM开环到PID位置控制 简介基于 STM32 与 PID 算法的电机行驶距离控制项目资源面向嵌入式爱好者、电子竞赛参赛者及需要实现精准定位的开发者。内容围绕编码器反馈与 PID 闭环控制展开涵盖比例、积分、微分环节的原理、参数整定思路、STM32 与编码器接口方式以及 PWM 电机驱动细节适合用于课程设计或毕设参考。压缩包共 236 个文件以 121 个 h 头文件和 107 个 c 源文件为主包含 STM32F4 HAL 库底层驱动、PID 控制代码、Keil 工程配置、启动文件和可直接烧录的 hex 文件整体仅 1.65MB结构清晰便于查阅。目前已有 1020 人学习内容还涉及调试中的阶跃响应测试与安全保护设计既能帮助深入理解 PID 运动控制原理也能快速移植到实际项目中验证指定距离行驶效果性价比高。 去年我做了一个基于STM32的小车底盘需求听起来特别简单让车按设定距离直线行驶误差控制在2厘米以内。结果第一版被现实狠狠教育了——固定PWM跑固定时间十次里有八次超出误差范围最夸张的一次跑了1.5米实际停了1.72米差了整整22厘米。后来整套逻辑全部推翻换成编码器闭环才算把精度稳住。这篇就把整个思路、硬件选型、代码骨架和踩坑过程完整梳理一遍给要做“STM32控制行驶指定距离”的朋友当个参考。如果你只是想让车子大致走个距离不在乎那几厘米误差那这篇文章对你也有用至少能知道误差到底出在哪。1. 为什么“给个固定PWM跑一会”根本行不通很多人第一版方案都是这样定时器输出一个固定占空比的PWM让电机转然后用delay或者定时中断到时间就停。理论上只要转速稳定时间乘以速度就是距离但实际跑起来你会发现这个方案从原理上就站不住脚。1.1 开环控制的失败根源开环控制下你控制的是PWM占空比而不是车轮转速更不是行驶距离。占空比到转速之间隔着好几层不确定性电池电压会随着放电逐渐下降同样的PWM占空比满电时转速高亏电时转速低电机负载变化比如地面摩擦阻力、车身倾斜角度、轮胎气压都会导致转速波动启动瞬间存在死区电压PWM占空比低于某个值时电机根本不动高于某个值突然启动减速箱齿轮间隙和轮胎打滑更是完全不可控的因素。这里面最难处理的是电压波动。我用同一个PWM占空比做测试4.2V锂电池满电时电机转速约320转/分电压掉到3.7V时同样占空比转速只有260转/分差了接近20%。跑1米静止不动的距离开环就会有大约20厘米的误差。还有更隐蔽的问题电机启动是一个加速过程停止也有一个惯性滑行过程这两个过程的时间占整个运动周期越短误差占比就越大。跑1米时可能影响不大跑20厘米时就非常明显。1.2 闭环控制的核心逻辑要真正控制行驶距离核心思想就一句话把“车轮实际转了多少圈”测出来和目标值对比有偏差就加大PWM接近了就减小PWM到了就停止。这里有个关键传感器概念——增量式编码器。它装在电机尾部电机每转一圈会输出固定数量的脉冲通过脉冲计数可以精确知道电机转了多少圈。配合减速箱的减速比和轮子直径就能换算出车实际走了多少毫米。闭环的另外一个好处是自动补偿。地面阻力变大编码器发现转速慢了控制器自动加大PWM。电压下降编码器发现距离还没到控制器自动延长运行时间。这些在开环方案里根本做不到。2. 硬件层的地基编码器电机选型与接线要点做距离控制硬件选型直接决定系统精度上限。脉冲数太少控制再精细也白搭。2.1 编码器电机的基础参数我用的方案是常见的带霍尔编码器的直流减速电机型号是JGB37-520几个关键参数值得回头看额定电压6V空载转速约320转/分减速比1:30输出轴转速约10.7转/分霍尔编码器分辨率11线/转因为是双路输出且带倍频STM32用定时器编码器模式可以做到4倍频也就是电机轴每转44个脉冲轮子直径65mm周长约204.2mm。换算下来每个编码器脉冲对应的行驶距离约0.116mm204.2mm ÷ 30 ÷ 44 ≈ 0.155mm考虑实际减速比。这个分辨率下2厘米误差对应大约129个脉冲控制裕量很充足。2.2 电机驱动接线与供电驱动板我用的是TB6612FNG比L298N轻巧且压降小内置稳压可以给STM32供电但我不推荐共用。接线参考如下STM32的PB6、PB7接到驱动板AIN1、AIN2A电机方向控制STM32的PA8输出PWM到驱动板PWMASTM32的PB8、PB9TIM4的CH3、CH4接到编码器A相、B相6V电源接VM逻辑电压接3.3V注意TB6612逻辑电平支持3.3V电机线A01、A02接电机。供电是最大的坑。驱动板VM和STM32供电必须分开共地即可。STM32用稳压的3.3V电机单独用一节锂电池。我曾经图省事用同一块电源电机启动瞬间电压跌落直接导致STM32复位车直接原地重启。2.3 定时器编码器模式的初始化配置编码器模式是STM32定时器的特殊功能不需要外部中断硬件自动对A、B两相的上升沿和下降沿进行计数等于单片机零开销获取脉冲数。这是STM32做这个项目最顺手的地方。初始化配置核心代码如下void Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_3; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; TIM_ICInit(TIM4, TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_4; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_IndirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; TIM_ICInit(TIM4, TIM_ICInitStructure); TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM4, 0); TIM_Cmd(TIM4, ENABLE); }一个必须注意的细节TIM_Period设为0xFFFF计数器满65535会溢出。如果电机转速快、运行时间长必须在溢出中断里做处理否则距离会突然跳变。我实际用下来44个脉冲/轴转电机轴最高约10.7转/分一秒最多470个脉冲65535大约能撑139秒短距离运行问题不大。要是做成持续运行的小车就需要维护一个32位累计变量。3. 从编码器脉冲到距离的换算逻辑编码器返回的是脉冲数值距离控制需要的是毫米中间必须有一个可靠的换算层。3.1 脉冲到毫米的换算公式整个换算链路如下来电机齿轮箱输出轴每转一圈车轮转一圈直驱轮子电机轴每转一圈编码器输出44个脉冲11线4倍频电机轴转30圈输出轴转1圈对应脉冲数30 × 44 1320个轮子直径65mm周长65 × π ≈ 204.2mm每脉冲行程 204.2 / 1320 ≈ 0.155mm/脉冲。所以如果你要行驶500mm目标脉冲数 500 / 0.155 ≈ 3226个。这个目标值我会在代码里直接用浮点乘法得到避免手工取整损失精度。但实际工程的坑在于这个理论周长建立在轮胎标准气压、无负载形变的前提下。真实运行时轮胎压在地面上会有形变实际滚动半径比几何半径小轮径越软误差越大。处理方式后面说这里先记住一个概念——理论值只能当初始值最终要用实测标定校准。3.2 方向判断与脉冲累计编码器的两路输出相位差90度正转时A超前B反转时B超前A。TIM_EncoderMode_TI12模式下计数器可以自动完成加减计数正转计数增加反转计数减少方向判断完全交给硬件。不过在距离控制的场景下我会限制小车只前进或者只后退单一方向运行让逻辑更简单。如果需要回程只需要在接近目标时把PWM反向计数器从当前位置反向递减到达零即为回程终点。这里强烈建议用32位变量包装累计值volatile int32_t g_encoder_count 0; void TIM4_IRQHandler(void) { if (TIM_GetITStatus(TIM4, TIM_IT_Update) ! RESET) { // 根据计数器的变化方向更新累计值 uint16_t current TIM4-CNT; static uint16_t last; int16_t delta (int16_t)(current - last); g_encoder_count delta; last current; TIM_ClearITPendingBit(TIM4, TIM_IT_Update); } }这个中断函数写得好不好直接决定整个系统的可靠性。3.3 距离换算中的标定陷阱理论换算值0.155mm/脉冲一开始看着很精确实测时发现实际跑500mm编码器只数到约3070个脉冲反推真实每个脉冲对应0.163mm。差别来自三个地方轮胎形变软橡胶轮胎受压后滚动半径缩小电机减速比实际值和标称值有偏差JGB37-520标称1:30实测约1:29.7编码器安装公差导致霍尔元件和磁环间距偏大偶尔丢脉冲。我的解决办法是跑一次已知距离标定。贴一张卷尺在地上让车满载正常路面行驶1米记录实际脉冲数。标定公式实际每脉冲距离 1000mm / 实测脉冲数这个值替代理论值写入代码精度立刻从5%提升到1%以内。不同路面、不同胎压的系统标定结果完全不同换轮胎后也必须重新标定。4. 位置闭环的控制核心PID整定与限幅硬件和反馈链路就绪后剩下就是让控制器聪明起来。我用的是位置式PID中最简单的P比例控制加一点D微分效果已经很理想I项在这种一次到位的小车里反而容易惹麻烦。4.1 比例控制为什么能用控制逻辑不复杂当前脉冲数cur和距离目标脉冲数target作差得偏差err target - cur让PWM输出 Kp × err偏差越大电机转速越快偏差越小转速越慢偏差为零电机停转。生活化理解你开车快到目的地时会提前松油门滑行而不是一直全油门等到线前急刹。比例控制模拟的就是这个动作。Kp取值有个直观范围。如果设定目标距离500mm对应脉冲3226个。启动时偏差最大3226PWM调速范围0到1000对应约10kHz、0~100%占空比那么Kp 1000 / 3226 ≈ 0.31。这样起步就是全速随着接近目标PWM逐渐下降到偏差100个脉冲时PWM降低到100左右车子明显减速。4.2 位置式PID代码骨架下面是我实际跑通的精简版本基于标准库函数逻辑清晰可移植#define TARGET_DISTANCE_MM 500.0f #define PULSE_PER_MM 6.13f // 标定值0.163mm/脉冲的倒数 #define KP 0.25f #define KD 0.01f #define PWM_MAX 1000 float target_pulse TARGET_DISTANCE_MM * PULSE_PER_MM; float last_err 0; void Move_Distance(void) { while (1) { float cur (float)g_encoder_count; float err target_pulse - cur; if (err 0.2f) { set_motor(0); break; } float diff err - last_err; int pwm (int)(KP * err KD * diff); if (pwm PWM_MAX) pwm PWM_MAX; if (pwm 0) pwm 0; set_motor(pwm); last_err err; delay_ms(5); } }几点说明err 0.2f作为停机阈值。PWM死区导致车子在低速时可能停不下来必须设置一个极小的静差区间。0.2个脉冲几乎不影响精度但能避免电机在静止前反复抖动微分项对偏差变化速率起作用防止车子冲过头。实测KD设0.01时接近目标时PWM下降更平滑停车位置更稳定delay_ms(5)控制调节周期。周期太长反应迟钝太短微分非常敏感容易抖动。5ms是我试过比较稳定的值。4.3 参数整定的实操经验整定Kp有个快速方法论先从很小的Kp开始比如0.05观察车子是否到达目标附近如果停下时偏差很大就需要加大Kp逐步增加Kp直到车子在接近目标时出现明显的来回震荡此时记下临界值最终Kp取临界值的60% ~ 70%留出稳定裕量再加上微小的KD值来抑制超调。我实际整定过程中KP从0.1增加到0.3时500mm测试出现明显过冲最终定格在0.25。这个值跑20cm到150cm都有不错表现但如果你要覆盖很宽的距离范围建议做分段Kp或者加积分项处理。5. 完整代码骨架与运行注意上面的Move_Distance函数是整个控制的中枢但实际工程还包含PWM初始化、电机方向控制、编码器读取等外围代码。5.1 电机方向控制与PWM初始化方向控制用GPIO输出高低电平即可void set_motor_dir(uint8_t dir) { if (dir DIR_FORWARD) { GPIO_SetBits(GPIOB, GPIO_Pin_6); // AIN1 GPIO_ResetBits(GPIOB, GPIO_Pin_7); // AIN2 } else { GPIO_ResetBits(GPIOB, GPIO_Pin_6); GPIO_SetBits(GPIOB, GPIO_Pin_7); } } void set_motor(int pwm) { if (pwm 0) { set_motor_dir(DIR_FORWARD); TIM_SetCompare1(TIM1, pwm); // PA8的PWM输出 } else { set_motor_dir(DIR_BACKWARD); TIM_SetCompare1(TIM1, -pwm); } }PWM频率我用的是10kHz。低于1kHz能听到明显的电机线圈啸叫高于20kHz时驱动板开关损耗上升。10kHz是平衡点。5.2 主循环中的控制节奏主程序框架大致这样int main(void) { Motor_Init(); Encoder_Init(); delay_init(); while (1) { if (KEY_Pressed) { g_encoder_count 0; // 清零脉冲 TIM_SetCounter(TIM4, 0); Move_Distance(); // 走指定距离 } } }一个关键心得是每次运行前必须同时清零定时器计数器和软件累计值两层清零缺一不可。只清TIM4-CNT而忘了清g_encoder_count会在中断里叠加一个巨大突变直接导致控制异常。另一个容易出问题的点是中断优先级。编码器计数中断必须设为最高优先级因为它是距离反馈的唯一来源。PWM或者定时中断如果抢占了它脉冲就会丢失距离数据不准闭环就失效了。6. 实测中的意外情况与调参经验纸上谈兵结束说说真实调试中我踩过的几个坑每一个都花了不少时间。6.1 机械间隙带来的停车误差第一次完成闭环测试时设定500mm实测493mm误差7mm我觉得挺满意。但连续跑十次误差波动在486mm到498mm之间规律很不稳定。查了一晚上发现根源在减速箱齿轮间隙。齿轮啮合存在空行程电机正转时扭矩通过齿轮逐级传递每一级之间都有一点点可转动但没咬合的空隙。当PID把PWM逐渐减小到很低时最后几个齿的咬合状态不稳定电机轴微小的来回晃动被编码器视为真实位移导致计数跳动。解决办法一是增加停机阈值把err 0.2f改成err 1.0f避免在微小偏差区间反复切换电机方向二是在停车后加一个50ms的等待让电机彻底静止再读取一次编码器值做最终记录和校正。修改后十次测试极差压缩到4mm以内。6.2 电池电压下降导致的间接影响前面说过电压对开环的直接影响很大闭环后系统能自动补偿但电压下降改变了另一个东西PWM饱和区间。TB6612的VM从4.2V降到3.7V时同样PWM占空比下的电机转速明显下降。启动时err很大PID计算出1000的PWM看似是最大占空比但实际电压降低导致最大可达到的转速也降低。这造成的结果是低压时车在大部分路程上处于“全速但不够快”的状态接近目标时减速特性也不一样最终停车位置和高压时会有约1%的偏差。解决方案是在代码中根据电源电压做前馈补偿。6.3 打滑和碰撞时的识别与保护普通平地打滑不严重但如果地面有油渍或者车被障碍物挡住编码器计数还在增加但车子实际没动系统会一直输出PWM直到超时。这种情况下一个简单的保护逻辑是如果持续N个控制周期内编码器计数变化量小于某个阈值判定为堵转强制停车。int static_count 0; float last_pulse 0; float cur_pulse g_encoder_count; if (fabs(cur_pulse - last_pulse) 0.5f) { static_count; } else { static_count 0; } if (static_count 100) // 连续500ms没有有效位移 { set_motor(0); break; } last_pulse cur_pulse;这个防护在调试初期尤其有用它能防止你测试时车撞墙推着跑电机堵转烧驱动板。7. 后续还能往哪些方向扩展基础版做到位之后扩展方向非常多我按推荐顺序整理了几个7.1 速度环与位置环的双环控制单位置环在距离较长时启停加速度波动大。改成内环速度环、外环位置环的双闭环结构后距离控制会平顺很多。思想是位置环输出一个目标速度速度环通过实时编码器测速计算出PWM让实际速度跟随目标速度。速度环用PI控制位置环用P控制就足够了。7.2 S曲线加减速算法如果不做双环也可以把目标距离细分按梯形速度曲线或者S曲线规划目标位置曲线让PID去跟踪这个动态目标。比如前10%距离加速中间80%匀速最后10%减速。好处是机械冲击小车走起来不会点头适合拿来拍视频展示或者做些精密位置控制。7.3 多传感器协同纯编码器在打滑严重的场景依然不可靠。可以加一个MPU6050惯性传感器做辅助推算或者加一个红外/超声避障感应在前方有障碍时减速停车。我的建议是编码器作为主传感器其他传感器做安全保护和鲁棒性增强不要试图用IMU替代编码器因为积分漂移比编码器误差更可怕。7.4 双轮差速与转向控制两轮差速底盘需要控制左右轮分别行驶指定距离才能直线跑偏。常见的做法是加一个航向角传感器左轮右轮分别PID同时根据方向偏差微调两侧速度才能实现长距离直线行驶。如果只是短距离开环跑左右轮机械差异会导致走不了直线。我在做双轮控制时踩过另一个坑左右轮单独标定后的PWM参数并不通用因为两侧的机械阻力不一样。即使标定了静止距离控制动态加速阶段偏差也会放大最后我是在较短距离2米以内用位置PID加微调系数效果已经很够用。最后的一点实践经验这套闭环方案从立项到稳定运行差不多花了我两个周末大部分时间都耗在标定和参调上。回头看整个项目最核心的不是STM32代码写得多漂亮而是三件事编码器数据可靠、PD参数合理、机械传动的误差心中有数。如果你是自己做着玩预算有限用38块钱的JGB37-520电机加20块钱的TB6612加一块最小开发板就能搭出完整原型。如果想用在项目交付场景记得预留标定接口和堵转保护这两点会让你的设备在客户现场少挨很多骂。本文还有配套的精品资源点击获取
返回列表