首页 / 资讯中心 / 文章详情

STM32编码器+串级PID实现小车2米定点停车与误差标定

STM32编码器+串级PID实现小车2米定点停车与误差标定 ★ FEATURED ARTICLE
简介围绕STM32实现电机按指定距离精准行驶的完整工程面向已具备单片机基础、希望打通编码器测速与PID闭环控制的学习者与嵌入式开发者。工程以STM32F4系列HAL库为底层将编码器脉冲采集、位置偏差计算、PID运算与PWM占空比输出串联成闭环链路可用于小车定距行驶、丝杠定位等场景。压缩包共236个文件其中h头文件121个、c源文件107个主要为HAL外设驱动与用户控制逻辑另含Keil工程文件uvprojx/uvoptx、启动汇编文件、批处理脚本与可直接烧录的hex包体约1.65MB目录完整、开箱可编译。目前已有1023人学习。读者可从中获得编码器接口与中断计数写法、Kp/Ki/Kd整定与采样周期设置思路、定时器PWM调速实现以及超调、振荡、静差等常见问题的排查线索。1. 从走够 2 米这个需求说起开环定时为什么必然跑偏让一台小车不多不少正好走完 2 米是 stm32 控制行驶指定距离这类题目里最容易被低估的部分。多数人的第一版方案是给电机一个固定占空比延时 3 秒停车。这套做法在实验室新电池、空载、瓷砖地面上能跑到 2.05 米看起来成了然后换成木地板或者电池掉到 7.2V同一个 3 秒就变成 1.72 米到 2.38 米的随机分布。距离要求的本质不是控制时间而是控制位移必须引入反馈也就是编码器加 PID 闭环。这套方案面向三类人正在做单片机课程设计、需要交一台能定点停车小车的学生要做毕业设计、需要在答辩现场演示误差指标的人以及工作中要快速搭一台样机机构、验证运动控制逻辑的工程师。读完之后你应该能自己算清楚脉冲当量、定下定时器和外设的资源分配、把串级 PID 写进中断、并且拿一张误差表说服验收老师。2. 需求拆解与技术选型编码器、驱动与两张 PID 控制环2.1 把走 2 米翻译成脉冲数和误差带方案能不能落地第一步看的是指标是否可量化。行驶指定距离这句话里其实藏了三个参数目标距离比如 2000mm、允许误差工程上通常取满量程的 1% 左右即 ±20mm、重复精度同一目标连跑五次最大值与最小值之差。这三项定不下来后面 PID 调得再好也是玄学。把距离换算成编码器脉冲是整个方案的地基。以常见的带霍尔编码器的直流减速电机为例编码器本体 11 线减速比 1:30AB 两相在定时器编码器接口模式下按四倍频计数那么输出轴转一圈的计数是 11×4×30 1320 个脉冲。轮径 65mm周长约 204.2mm于是每个脉冲对应的位移是 204.2 / 1320 ≈ 0.155mm。2 米对应的脉冲数就是 2000 / 0.155 ≈ 12903。这个数字必须自己算一遍并写进代码的宏定义里不能大概估一个否则误差带根本压不住。参数取值说明编码器线数11 线电机尾部霍尔编码器减速比1:30输出轴转速约为电机轴 1/30倍频方式四倍频TIM 编码器模式 TI12轮径65mm实测而非标称磨损后需重标脉冲当量≈0.155mm/pulse周长 ÷ 每圈计数2m 对应脉冲≈12903目标值写入位置环提示脉冲当量不要用理论值一锤定音。装好轮子、加载整车重量之后用手推车走 1 米读编码器累计值反算真实当量这一步叫里程标定能把 3%~5% 的系统误差直接消掉。2.2 主控与外设选型定时器和 IO 怎么分配主控选 STM32F103 系列是这类题目性价比最高的选择主频 72MHz定时器资源足够标准库或 HAL 库资料都多。关键不是选哪颗芯片而是外设资源怎么切分才不打架这一步翻车的人特别多。我的分配习惯是这样TIM3 和 TIM4 工作在编码器接口模式分别接左右轮的 AB 相只做计数不做中断TIM1 的 CH1、CH2 输出两路 20kHz 的 PWM 给电机驱动TIM2 做 10ms 的固定周期中断作为整个控制系统的时基。电机驱动用常见的双 H 桥模块注意它需要三路信号两路方向加一路 PWM方向引脚切换时要有死区否则换向瞬间电流冲击会把驱动打保护。PWM 频率选 20kHz 是有原因的低于 8kHz 电机会发出明显啸叫而且电流纹波大、低速爬行严重高于 25kHz 又会让驱动模块的开关损耗上升、发热明显。20kHz 刚好在人耳听觉上限附近实测低速平稳性也最好。占空比分辨率方面72MHz 经预分频到 72kHz 计数、周期设为 3600分辨率就是 1/3600对低速微调足够用。/* TIM1_CH1/CH2 输出 20kHz PWM供左右轮调速 */ static void Motor_PWM_Init(void) { GPIO_InitTypeDef gpio {0}; TIM_TimeBaseInitTypeDef tb {0}; TIM_OCInitTypeDef oc {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1, ENABLE); gpio.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9; /* PA8-CH1, PA9-CH2 */ gpio.GPIO_Mode GPIO_Mode_AF_PP; /* 复用推挽输出 */ gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); tb.TIM_Period 3599; /* 72MHz/3600 20kHz */ tb.TIM_Prescaler 0; tb.TIM_ClockDivision TIM_CKD_DIV1; tb.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, tb); oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 0; /* 上电占空比为 0防窜车 */ oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, oc); TIM_OC2Init(TIM1, oc); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_CtrlPWMOutputs(TIM1, ENABLE); /* 高级定时器必须开主输出 */ TIM_Cmd(TIM1, ENABLE); }这段代码里有两个容易被忽略的点一是TIM_CtrlPWMOutputsTIM1 属于高级定时器不调用这一句引脚上永远没有波形很多人卡在这里以为是硬件坏了二是上电时TIM_Pulse必须置 0否则复位瞬间电机会全速冲出去把摆在桌边的车直接送下地。方向控制则用两个普通 GPIO配合一个Motor_Set(left, right)函数封装正数正转、负数反转、0 刹车上层控制代码只跟这个函数打交道。2.3 控制律选型单环、串级与位置式增量式的取舍只用速度环能不能走准 2 米能但很难。速度环的稳态误差会让小车一直差一点靠时间累积出来的距离误差随路况漂移。所以正确的结构是串级外环是位置环输入目标脉冲数输出目标速度内环是速度环输入目标速度输出 PWM 占空比。外环负责还差多少内环负责现在跑多快两层职责分开参数也好整定。具体到实现形式位置环用位置式 PID因为它需要保留积分项来消除稳态距离误差速度环用增量式 PID 更合适增量式输出的是占空比的变化量天然带抗积分饱和特性换向和突加负载时不容易产生大超调。这个组合是我试过最省调参时间的一套。还有一点值得提前说微分项要做微分先行也就是对测量值微分而不是对误差微分。目标距离是阶跃信号直接对误差微分会得到一个巨大的尖峰输出瞬间打满小车会窜一下。改成对反馈量微分冲击就没了。这个改动一行代码效果差别巨大。3. 总体架构设计三层闭环与五个模块的职责划分3.1 分层结构决策层、控制层、执行层把代码按谁负责什么切开是这套方案能不能改、能不能讲清楚的关键。我一般分成三层。执行层最贴近硬件包含电机 PWM 输出、方向控制、编码器读数、按键与显示这一层不含任何控制逻辑只做给定值到物理量的转换。控制层是核心跑 PID 运算包含位置环、速度环、限速曲线和停车判据。决策层管整个流程是一个状态机什么时候启动、什么时候减速、什么时候判定到位、异常时怎么停。五模块的划分对应下来就是encoder测距、motor驱动、pid算法、motion状态机与轨迹、debug串口数据上报。模块之间只通过明确的接口函数通信比如motion只能调用Motor_Set()绝不能直接写TIM_SetCompare1()。这条纪律看着啰嗦但当你要在答辩前一夜改控制参数时它会救你。整个数据流是单向的编码器 → 里程计算 → 位置环 → 目标速度 → 速度环 → PWM → 电机 → 编码器。闭环里任何一处出现符号错误表现出来都是车往后倒或者越跑越快所以第一次联调务必把轮子架空用手转轮子看反馈符号对不对。3.2 任务时序10ms 速度环与 20ms 位置环怎么排时序设计不合理是参数怎么调都抖的常见根源。我的做法是TIM2 每 10ms 进一次中断在中断里读取编码器增量、计算当前速度、跑速度环 PID、更新 PWM位置环不放在这个中断里而是在主循环里靠一个计数器每两次速度环执行一次也就是 20ms 一次。为什么位置环要慢因为位置环的输出是目标速度它的变化本来就该平滑跑太快会和速度环抢节奏形成两个环互相追着振荡。速度环 10ms 对应 100Hz 的带宽对一台几十瓦的小车足够了。另外中断里只做算术绝不放串口打印和浮点除法以外的东西printf塞进 10ms 中断是标准的翻车操作一条 115200 的串口日志能占掉好几毫秒。/* TIM2 中断10ms 控制节拍 */ void TIM2_IRQHandler(void) { static uint8_t div 0; if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); /* 1. 采样本周期脉冲增量 - 速度(mm/s) */ g_enc_delta Encoder_ReadDelta(); g_distance g_enc_delta * PULSE_TO_MM; /* 累计里程 */ g_speed g_enc_delta * PULSE_TO_MM / 0.01f; /* mm/s */ /* 2. 位置环20ms 跑一次输出目标速度 */ if (div 2) { div 0; if (g_state RUN) g_target_speed Position_Loop(g_target_dist - g_distance); } /* 3. 速度环每 10ms 跑一次输出 PWM */ float pwm Speed_Loop(g_target_speed - g_speed); Motor_Set(pwm, pwm); /* 差速模型下左右同值 */ /* 4. 到位判定 */ Motion_CheckArrive(); } }这段代码体现了三个设计决定里程累计用浮点毫米而不是脉冲数方便上层直接和2000mm比较位置环的节拍用软件分频实现不额外占用定时器Motor_Set接收的是 -1000~1000 的归一化值而不是原始比较寄存器值把硬件细节彻底挡在控制层之外。参数上PULSE_TO_MM就是前面算出的 0.155改成实测标定值即可。3.3 状态机待机、加速、匀速、减速、到位、制动没有状态机的小车行为是能跑但不可解释有状态机之后每一个时刻它在干什么都能一句话说清。我定义六个状态IDLE待机PWM 为 0里程清零、ACCEL加速目标速度斜坡上升到巡航速度、CRUISE匀速保持巡航、DECEL减速进入减速区按曲线降速、BRAKE制动短时反向 PWM 抵消惯性、ARRIVED到位锁定等待下一次触发。状态迁移的判据要写死从ACCEL到CRUISE看当前速度是否达到巡航速度的 95%从CRUISE到DECEL看剩余距离是否小于减速阈值从DECEL到BRAKE看剩余距离是否小于停车阈值且速度低于某个门限BRAKE持续固定时间比如 80ms后进ARRIVED。这些阈值全是可调参数集中放在一个config.h里答辩现场要改也就是改几个宏。加分项是加一个斜坡启动目标速度不是从 0 直接跳到 400mm/s而是每个控制周期增加固定步长比如每 10ms 涨 20mm/s这样 0.2 秒后到巡航速度起步不打滑。打滑是距离误差最主要的来源之一尤其在光滑地面斜坡启动能把这个误差压掉一大半。4. 关键模块实现编码器测距、串级 PID 与定点停车4.1 编码器四倍频与里程标定编码器接法定成败。STM32 的定时器有专门的编码器接口模式把 TIMx 配置成TIM_EncoderMode_TI12AB 两相都参与计数硬件自动完成四倍频和方向判别CPU 只需要定期读计数器的差值。这里有个细节计数器是 16 位的跑长距离会溢出但用补码相减(int16_t)(now - last)可以自动处理回绕不需要手动判溢出。#define PULSE_PER_REV 1320.0f /* 11线 × 4倍频 × 减速比30 */ #define WHEEL_MM 65.0f /* 实测轮径 */ #define PULSE_TO_MM (3.1415926f * WHEEL_MM / PULSE_PER_REV) /* ≈0.1547 */ /* 读取本周期脉冲增量带符号10ms 调用一次 */ int16_t Encoder_ReadDelta(void) { static uint16_t last 0; uint16_t now TIM_GetCounter(TIM3); int16_t delta (int16_t)(now - last); /* 16位减法自动处理溢出 */ last now; return delta; }标定的做法必须写进方案里否则误差永远是说不清。流程是把车放在起点车轮正对用直尺量出 1000mm 的直线软件里把PULSE_TO_MM临时改成 1.0让里程直接显示累计脉冲推车走完 1 米记下累计脉冲数 N真实当量 1000 / N。这个值会比理论值偏大一点因为轮子受压后有效半径变小通常差 2%~4%改回去之后系统误差立刻收敛。标定要重复三次取平均单次偶然误差不小。注意左右两个轮子要分开标定各用各的当量。加工误差和装配公差会让两个轮子的当量差 1% 以上走 2 米就是 2cm 的偏差正好压在误差带边缘。4.2 串级 PID 的实现细节积分分离与微分先行两个环的代码结构相近但参数和形态不同。位置环用位置式输出是目标速度且必须限幅速度环用增量式输出是 PWM 增量。下面这段是核心实现重点看三处积分分离、微分先行、输出限幅。typedef struct { float kp, ki, kd; float i_acc; /* 积分累加 */ float last_err; /* 上次误差增量式用 */ float last_meas; /* 上次测量值微分先行用 */ float out_max; /* 输出限幅 */ } PID_t; /* 位置式 PID外环位置环输入剩余距离(mm)输出目标速度(mm/s) */ float PID_Position(PID_t *p, float err, float dt) { /* 积分分离误差大时先不积分防止起步阶段积分饱和 */ if (fabsf(err) 200.0f) p-i_acc p-ki * err * dt; p-i_acc Constrain(p-i_acc, -p-out_max, p-out_max); float d -(p-last_meas - 0.0f) / dt; /* 微分先行此处仅示意 */ p-last_meas err; float out p-kp * err p-i_acc p-kd * d; return Constrain(out, -p-out_max, p-out_max); } /* 增量式 PID内环速度环输入速度误差(mm/s)输出 PWM 变化量 */ float PID_Increment(PID_t *p, float err, float dt) { float p_term p-kp * (err - p-last_err); float i_term p-ki * err * dt; float d_term p-kd * (err - 2.0f * p-last_err p-last_err2) / dt; p-last_err2 p-last_err; p-last_err err; return p_term i_term d_term; /* 返回的是增量需外部累加 */ }参数含义需要说清楚位置环的out_max就是巡航速度上限比如 400mm/s它同时充当限幅和积分上限防止还差很远就积分爆掉。速度环的kp影响响应快慢ki消除稳态速度差kd在小车上通常给 0因为编码器速度信号本身噪声就大加微分会把噪声放大成高频抖动。我一般的整定顺序是先把位置环参数设成纯比例kikd0把速度环调稳然后加位置环的 ki 消静差最后看情况补一点 kd。积分分离的阈值 200mm 是经验值起步时剩余距离 2000mm误差远超阈值积分不累积避免一开始就顶到饱和进入 200mm 以内开始积分正好用来吃掉最后的静差。这个设计能让停车精度从 ±30mm 提升到 ±10mm 量级。4.3 减速曲线与停车判据定点停车靠的不是刹车片很多人以为定点停车就是到了就断电结果车靠惯性冲出去十几厘米。正确的做法是提前减速让小车以低速接近目标点。减速区用一条基于运动学的限速曲线剩余距离 s 与允许速度 v 的关系是 v ≤ √(2·a·s)其中 a 是可用减速度实测取值一般在 800~1500 mm/s²。每个控制周期算一次这个允许速度和目标速度取小作为速度环的给定。这条曲线的意义在于剩余距离越短允许速度越低小车自然减速不会出现急刹和冲过头。实现只需要一行sqrtf但效果比任何经验阈值都稳。停车判据不能只看距离还要看速度两个条件同时满足才算到位/* 到位判定距离与速度双条件配合短时制动 */ void Motion_CheckArrive(void) { if (g_state ! DECEL) return; float remain g_target_dist - g_distance; /* 剩余距离(mm) */ if (fabsf(remain) ARRIVE_DIST_MM /* 距离门限例如 3mm */ fabsf(g_speed) ARRIVE_SPEED_MMPS) /* 速度门限例如 20mm/s */ { g_state BRAKE; Motor_Set(-200, -200); /* 短时反向制动 */ g_brake_tick 0; } }ARRIVE_DIST_MM建议设成误差带的 1/5 左右比如要求 ±20mm就设 3~5mmARRIVE_SPEED_MMPS设成巡航速度的 5%。制动阶段用一个计数器计满 8 个控制周期80ms就切断输出因为反向制动时间长了会让车倒回去反而引入新的误差。这个 80ms 是实测调出来的不同电机和整车重量差别很大别照抄。4.4 参数整定与波形调试把黑匣子变成看得见的曲线调参最大的效率提升来自可视化。我的做法是在debug模块里拼一个紧凑的数据帧每 20ms 通过串口发一次包含目标距离、当前距离、目标速度、当前速度、PWM 五路数据格式是逗号分隔加换行。上位机用任意支持串口波形的工具打开就能同时看到位置曲线和速度曲线。数据帧的字段顺序要固定并且加一个帧头校验否则偶发的丢字节会让波形整体错位看着像系统在剧烈振荡实际是解析错了这种冤枉路我走过不止一次。调参的具体顺序先架空车轮只调速度环给一个阶跃目标速度看响应有没有超调和稳态误差kp从 1.0 开始往上加加到开始振荡就回退 30%然后落地位置环kp从 2.0 开始观察 2 米行程的曲线如果接近目标时来回摆动说明位置环太强降到 1.0 左右最后加ki从 0.01 慢慢加直到停车后的静差收敛到 3mm 以内。波形上要盯两个特征一是速度曲线有没有台阶感有的话是 PWM 分辨率不够或者采样周期太长二是减速段是不是平滑下降的弧线如果出现直线掉到底再反弹说明减速曲线没生效还在用固定阈值。这两点看懂了调参基本就是照着图形改数字不再是碰运气。5. 避坑与常见问题排查从每次多跑 20cm到原地抖5.1 现象每次停车都多跑一段且距离很稳定原因通常是里程标定的脉冲当量偏小或者停车后没有制动、靠惯性滑行。稳定的偏差属于系统误差恰恰是最好治的。解决方式是重新做一次 1 米标定取三次平均同时把制动阶段的时长从 80ms 调到 120ms 试一次看残差往哪个方向走。如果标定之后仍然多跑固定距离那就是纯粹的机械惯性用减速曲线把接近速度压到 100mm/s 以下误差会明显收敛。5.2 现象低速时车身前后抖动串口波形速度值正负跳变原因是速度环kp过大叠加编码器低速分辨率不足。10ms 采样周期下速度 50mm/s 时一个周期内的脉冲增量不到 4 个量化误差直接变成噪声。解决办法有两个把低速段的 PWM 设一个最小死区补偿比如起步给 8% 的基础占空比以及在速度计算上做一阶低通滤波v 0.7*v_new 0.3*v_old。不要指望靠调参消除量化噪声那是物理限制。5.3 现象一上电小车就全速冲出去原因几乎都是 PWM 初始化顺序或者方向引脚默认电平不对。正确顺序是先初始化方向 GPIO 并全部拉低再初始化 PWM 且比较值置 0最后才使能定时器。另外检查Motor_Set里对 0 值的处理很多实现写成0 表示不动作结果上一次的占空比残留下来继续输出。这个坑的代价很高轻则撞坏轮子重则烧驱动第一次联调务必把车架在垫块上。5.4 现象中断里加了串口打印之后距离变得极不稳定原因很直接printf是阻塞式输出115200 波特率下发 40 个字节大约要 3.5ms占掉了 10ms 中断的三分之一控制周期被拉长所有积分项和微分项的时间基准都错了。解决办法是把数据先写进环形缓冲区在主循环里发或者干脆把调试输出放在主循环里通过一个标志位通知。控制中断里只允许做定长浮点运算和 GPIO 操作这是条硬规矩。5.5 现象同一目标连续跑五次误差能差出一倍重复性差通常指向三处电源电压波动、地面摩擦不一致、起步打滑。先量电池电压满电和亏电状态下同一组 PID 参数表现完全不同加一级电压补偿或者改用稳压供电能解决一大半再看起步把斜坡启动的加速度减小让轮子有个建立静摩擦的过程最后检查车轮是否偏心把车推着走一段看编码器数值是否线性增长非线性就说明轮子有问题。逐一排查下来重复精度一般能稳定在 ±10mm 以内。6. 验收与答辩拿一张误差表说话比讲十分钟原理管用到了验收环节老师或者评审最想看的只有两件事指标达没达到以及数据可不可信。所以演示的流程要提前排练成固定动作而不是现场随机操作。建议按这个顺序走先把车放在起点用卷尺在桌面上贴一条胶带做 2 米刻线上电复位屏幕或串口显示当前里程为 0按启动键车走完自动停用直尺量车头到刻线的距离并当场记录。这个动作重复五次把五次结果写进一张表现场填。序号目标距离实测距离绝对误差是否合格12000mm2008mm8mm合格22000mm1994mm-6mm合格32000mm2011mm11mm合格42000mm1992mm-8mm合格52000mm2006mm6mm合格这张表的价值远超任何原理讲解。它同时证明了系统误差五次均值约 2.2mm和重复精度极差 19mm而这两项正好对应评分标准里的准确性和稳定性。如果误差偏大也要敢当场分析误差主要来自哪一段、下一步打算怎么改这比藏起来强得多。评审最反感的不是误差大而是被问为什么的时候答不上来。答辩时被问到的问题高频集中在三处。第一是为什么用串级而不是单环答案要落到稳态误差和响应速度的分离上别背概念直接说单环速度控制在 2 米行程上会累积时间误差位置外环能把剩余距离直接反馈回来。第二是PID 参数怎么定的答题模板是先内环后外环先比例后积分参数由阶跃响应曲线读出来的超调量反推。第三是换一个距离还能不能跑这时候现场改成 1500mm 重跑一次如果还能停在误差带内前面所有的讲解就都立住了。还有一个很容易忽略的软性加分点把关键参数做成按键可调或者留下一个串口命令能改目标距离。评审临时提要求的时候你能在十秒内响应整个方案的可信度会完全不一样。最后说个我自己的习惯每次做这类定点停车的项目我都会在第一次联调前强制走一遍架空车轮、手转看反馈、单元测试三个函数的流程三分钟的事能省掉后面无数个怀疑人生的夜晚。距离控制的精度不是调出来的是标定和流程管出来的希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站