十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

STM32智能小车速度闭环:PID控制与编码器测速实战解析

STM32智能小车速度闭环:PID控制与编码器测速实战解析 简介这是一套面向STM32F103ZET6智能小车开发者的PID闭环速度控制程序源代码适合学习单片机PID算法、电机调速及测速反馈的入门与进阶用户。程序基于KEIL5开发运行于STM32F103ZET6处理器配合L293D电机驱动芯片、1602液晶显示屏、TT直流减速电机及测速模块实现带按键交互的实时速度闭环控制。压缩包共173个文件约3.47MB主要包括37个h头文件、36个c源文件、23个o目标文件以及hex、axf、uvprojx等工程与编译输出文件map、crf、lst等构建过程文件也一应俱全完整覆盖从源码编辑、编译链接到固件生成的整个开发链路。该程序已在作者自制的STM32F103ZET6智能小车上亲测可用源码结构清晰按外设驱动、控制算法、主逻辑分层注释详实适合直接移植或二次开发hex文件也可快速烧录验证效果。已有1053人学习下载对于仍在调不通速度环的开发者来说这是一份极具参考价值的实战范例。1. 为什么小车速度控制必须上PID闭环先看一个现象把同一个PWM占空比固定送给L293D小车刚充满电时跑得飞快电量下降后明显变慢放在光滑地板上和地毯上速度也不同。这就是开环控制的典型问题——电机转速取决于电压、负载和摩擦而你只给了“目标占空比”没有反馈回路去纠正偏差。解决方式是用测速模块把实际速度读回来和目标速度比较通过PID算法动态修正PWM输出形成闭环。这套STM32F103ZET6智能小车程序就干了这件事用1602显示速度按键调节目标速度PID实时调整电机驱动让小车在负载和电压变化时仍能保持设定速度。适合正在做循迹、搬运或者需要精确走速的智能车项目也适合刚接触STM32定时器编码器模式和数字PID的嵌入式初学者。2. 硬件解码STM32定时器编码器模式与测速模块2.1 测速原理从方波到速度值智能小车常用的TT直流减速电机自带霍尔编码器或光电码盘输出A、B两相互差90°的方波。STM32的定时器编码器模式可以同时捕获两路信号根据相位关系判断旋转方向并自动计数脉冲。计数值通过定时器寄存器直接读取硬件滤波还能省掉一部分毛刺。我用的F103ZET6上TIM1、TIM2、TIM3、TIM4都支持编码器接口但要注意TIM1和TIM8的编码器模式在F103上有限制推荐用TIM2~TIM4。本项目里测速模块输出接到TIM3的CH1和CH2即PA6、PA7配置成TI1TI2双沿采样这样每转一圈得到的脉冲数是码盘线数的4倍分辨率更高。2.1.1 编码器初始化代码void TIM3_Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, 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(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructInit(TIM_TimeBaseStructure); TIM_TimeBaseStructure.TIM_Period 0xFFFF; // 16位计数器最大值 TIM_TimeBaseStructure.TIM_Prescaler 0; // 不分频 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_ICStructInit(TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; 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(TIM3, TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_2; 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(TIM3, TIM_ICInitStructure); TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM3, 0); TIM_Cmd(TIM3, ENABLE); }逻辑说明先开TIM3和GPIOA时钟PA6、PA7设为浮空输入因为编码器输出是推挽信号让定时器自己采样更干净。配置TIM3的预分频为0计数周期0xFFFF这样每次脉冲都计数16位溢出后可软件处理。然后分别初始化CH1和CH2的输入捕获开启输入滤波0x0F可以滤掉电机换向产生的尖峰。最后调用TIM_EncoderInterfaceConfig把定时器设为TI1TI2双沿编码模式计数器随转速增减。这样读取TIM_GetCounter(TIM3)就能得到一段时间的总脉冲数。2.2 L293D驱动与PWM输出电机驱动用的是L293D它是双H桥芯片逻辑电源和电机电源分离。控制方式简单EN引脚用于使能接PWMIN1和IN2控制方向。如果电机标称6-9VL293D可以胜任缺点是压降接近1.2V把PWM占空比加到最大时电机实际电压会低于电源电压。本项目IN1、IN2接GPIOPWM用TIM4的CH1输出频率选择10kHz既能避免电机啸叫L293D也能跟得上。2.2.1 PWM输出初始化void TIM4_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; // TIM4_CH1 映射在PB6 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM4, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable); TIM_Cmd(TIM4, ENABLE); }参数说明arr是自动重装载值决定PWM频率psc预分频值。例如TIM4_PWM_Init(999, 71)APB1时钟72MHz得到PWM频率 72MHz / (72 * 1000) 1kHz分辨率1000级。实际项目里我用10kHz即arr7199, psc99对应频率72MHz/(100*7200)10kHz占空比调节范围0-7200。设置PWM占空比用TIM_SetCompare1(TIM4, duty)duty值限制在0~arr之间。注意L293D的EN引脚需要高电平才能输出如果直接把PWM接到ENIN1/IN2控制方向PWM低电平不会导致L293D关断因为EN已经拉高此时是靠占空比控制平均电压。如果反过来EN固定高、IN1接PWM也能转但L293D的开关损耗会略高我习惯把PWM接EN。2.2.2 硬件连接速查表部件引脚STM32 GPIO说明编码器A相霍尔输出APA6TIM3_CH1编码器B相霍尔输出BPA7TIM3_CH2电机驱动ENL293D EN1PB6TIM4_CH1 PWM电机方向IN1L293D IN1PC0高低电平电机方向IN2L293D IN2PC1高低电平LCD1602数据D0-D7PD0-PD78位并行LCD1602控制RS, RW, ENPD12, PD13, PD14标准控制线按键1目标速度PE0外部中断按键2目标速度-PE1外部中断这个表是项目里的典型接法具体以源码中的引脚定义为准。接线时注意编码器A/B不要接反否则计数器方向相反PID会变成正反馈。3. PID控制器的STM32实现与参数调整3.1 增量式PID公式推导速度环PID有两种形式位置式和增量式。位置式输出值是占空比绝对值计算时需要累加历史误差容易积分饱和增量式输出的是占空比的增量只和最近三次误差有关适合输出被限幅的执行机构。很多STM32小车项目用增量式PID因为输出限幅简单而且错误后恢复快。增量式PID公式 Δu(k) Kp * [e(k) - e(k-1)] Ki * e(k) Kd * [e(k) - 2*e(k-1) e(k-2)]其中e(k) 目标速度 - 当前速度。最终输出 u(k) u(k-1) Δu(k)这里Kp是比例系数Ki是积分系数Kd是微分系数。注意增量式里的Ki实际上是积分系数不是单独积分项所以整定时Ki不能太大否则输出增量会过冲。实际项目中速度环更关注稳态精度我一般不用微分项或者把Kd设得很小因为微分对测速噪声很敏感。3.2 代码实现PID结构体与计算函数typedef struct { float Kp; float Ki; float Kd; int target_speed; // 目标速度单位rpm int current_speed; // 当前速度单位rpm int error_last; // e(k-1) int error_prev; // e(k-2) int output_inc; // Δu(k) int output; // u(k)最终PWM占空比 int output_max; // 输出上限避免超调 } PID_TypeDef; void Speed_PID_Init(PID_TypeDef *pid, float kp, float ki, float kd, int out_max) { pid-Kp kp; pid-Ki ki; pid-Kd kd; pid-output_max out_max; pid-target_speed 0; pid-current_speed 0; pid-error_last 0; pid-error_prev 0; pid-output_inc 0; pid-output 0; } int Speed_PID_Calc(PID_TypeDef *pid, int target, int current) { int error target - current; int inc; inc (int)(pid-Kp * (error - pid-error_last) pid-Ki * error pid-Kd * (error - 2 * pid-error_last pid-error_prev)); pid-error_prev pid-error_last; pid-error_last error; pid-output inc; // 输出限幅 if (pid-output pid-output_max) pid-output pid-output_max; else if (pid-output -pid-output_max) pid-output -pid-output_max; pid-output_inc inc; return pid-output; }逻辑说明Speed_PID_Init保存PID参数并清零历史误差。Speed_PID_Calc每次调用计算一个增量累加到上次的输出上。代码里的分支判断是输出限幅防止积分饱和导致PWM值超过定时器允许范围。注意Kp、Ki、Kd是浮点数误差是整数强制转换时如果系数是0.3这种小数误差差值最大也就几百最好把系数放大100或1000倍用整数计算否则浮点运算在STM32F103上耗时且占用资源。我这里是直接浮点因为速度环的采样周期是20ms对F103ZET6来说压力不大。3.3 按键调整参数与1602显示调试PID时最烦的是每次改参数都要重新编译下载所以程序里加了按键实时调整参数和目标速度。按键1和按键2通过外部中断触发长按切换调整对象目标速度、Kp、Ki、Kd短按加减。1602显示当前模式和具体数值。3.3.1 按键中断服务函数extern PID_TypeDef speed_pid; extern u8 adjust_mode; extern int target_speed; void EXTI0_IRQHandler(void) { u8 press_count 0; if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { // 简单消抖连续读取10次每次间隔1ms while (press_count 10) { Delay_ms(1); if (GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_0) RESET) { press_count; } else { press_count 0; break; } } if (press_count 10) { // 长按进入参数切换模式短按调节目标速度 if (KEY1_LongPress()) { adjust_mode (adjust_mode 1) % 4; // 0:速度, 1:Kp, 2:Ki, 3:Kd } else { if (adjust_mode 0) target_speed 5; else if (adjust_mode 1) speed_pid.Kp 0.01f; else if (adjust_mode 2) speed_pid.Ki 0.005f; else if (adjust_mode 3) speed_pid.Kd 0.01f; } } EXTI_ClearITPendingBit(EXTI_Line0); } }参数说明长按检测通过记录按键持续时间我这里用了简单计数实际工程可以用定时器计时。adjust_mode是一个全局变量0表示调整目标速度1/2/3分别调整Kp、Ki、Kd。短按一次目标速度加5rpm调试时步进合适。注意外部中断优先级要设置好否则编码器读数会抖动我在初始化时把抢占优先级设为2。3.3.2 1602显示代码void LCD_Display_Speed(PID_TypeDef *pid, int target_speed) { char buf[16]; sprintf(buf, Tar:%drpm Cur:%d, target_speed, pid-current_speed); LCD_ShowString(0, 0, buf); sprintf(buf, Kp:%.2f Ki:%.2f, pid-Kp, pid-Ki); LCD_ShowString(0, 1, buf); sprintf(buf, Kd:%.2f Out:%d, pid-Kd, pid-output); LCD_ShowString(0, 2, buf); // 第三行可以显示模式 if (adjust_mode 0) LCD_ShowString(0, 3, Mode:Speed); else if (adjust_mode 1) LCD_ShowString(0, 3, Mode:Kp); else if (adjust_mode 2) LCD_ShowString(0, 3, Mode:Ki); else LCD_ShowString(0, 3, Mode:Kd); }这里用4行1602如果只有两行可以把Kd和Out放到第二行。每次刷新之前建议先清屏还是局部覆盖局部覆盖速度快但注意字符串长度不够时要补空格否则残留字符。我用的是8位并行模式初始化时序参照1602标准手册注意LCD1602是5V器件STM32是3.3V需要给LCD单独供电并用电平转换或者使用开漏输出加外部上拉。项目里直接用3.3V供电也能点亮但对比度会差一些属于非正常工作条件有条件还是5V供电加双向电平转换。3.3.3 PID参数初值参考参数作用初值建议过大表现过小表现Kp比例修正0.5~1.5震荡、电机啸叫响应慢、稳态偏差大Ki消除静差0.05~0.2积分过冲、低频振荡稳态速度与目标有偏差Kd抑制过冲0.01~0.1高频振荡、噪声放大对过冲抑制不足这个初值适用于减速比1:30、码盘线数20的TT电机配合20ms采样周期。如果你的电机不同Kp可能需要缩放。先只给Kp让它不震荡但方向正确然后加Ki最后根据动态响应加一点Kd。4. 速度闭环控制主程序集成4.1 系统初始化流程上电后要初始化所有外设顺序很重要先初始化系统时钟然后GPIO、定时器、按键、LCD、PID结构体。注意编码器计数器和PWM输出要分时序避免电机上电瞬间误动。我一般这样写int main(void) { delay_init(); // 系统时钟初始化F103ZET6运行在72MHz LED_Init(); KEY_Init(); // 按键PE0、PE1外部中断 LCD_Init(); // 1602初始化 TIM4_PWM_Init(7199, 99); // 10kHz PWM TIM3_Encoder_Init(); // 编码器接口 Speed_PID_Init(speed_pid, 0.8f, 0.1f, 0.02f, 7200); LCD_Clear(); printf(System Ready!\r\n); while(1) { Speed_Control_Task(); LCD_Display_Speed(speed_pid, target_speed); Delay_ms(20); // 控制周期20ms } }初始化顺序说明LCD和按键需要在系统时钟正常后初始化。PID参数初始化为Kp0.8Ki0.1Kd0.02输出上限7200对应PWM的ARR值。注意TIM3_Encoder_Init里需要清零计数器但必须放在PWM初始化之后否则上电瞬间PWM输出可能为0或未知电平影响电机启动。4.2 主循环中的PID调用与PWM输出速度控制任务里首先要根据编码器脉冲数计算当前速度。已知电机减速比为1:30码盘线数20则电机输出轴每圈有20*480个计数。采样周期20ms那么当前转速(rpm) 计数差值 / (80 * 时间分钟) 计数差值 / (80 * 0.02 / 60) 计数差值 * 37.5。注意计数差值可能有负数所以要用有符号数。另外编码器是16位计数器可能溢出需要处理。void Speed_Control_Task(void) { static short last_count 0; short cur_count TIM_GetCounter(TIM3); short diff cur_count - last_count; int current_rpm; int duty_new; // 处理16位溢出diff是有符号的能自动处理65535跳变 last_count cur_count; // 计算当前速度系数37.5由码盘线数和采样周期算出 current_rpm (int)(diff * 37.5f); speed_pid.current_speed current_rpm; // 设置目标速度默认值 if (target_speed 0) target_speed 30; // 调用PID计算 duty_new Speed_PID_Calc(speed_pid, target_speed, current_rpm); // 方向控制 if (duty_new 0) { GPIO_SetBits(GPIOC, GPIO_Pin_0); // IN1 1 GPIO_ResetBits(GPIOC, GPIO_Pin_1); // IN2 0 TIM_SetCompare1(TIM4, duty_new); } else if (duty_new 0) { GPIO_ResetBits(GPIOC, GPIO_Pin_0); // IN1 0 GPIO_SetBits(GPIOC, GPIO_Pin_1); // IN2 1 TIM_SetCompare1(TIM4, -duty_new); // 取反 } else { TIM_SetCompare1(TIM4, 0); // 停止输出 } }逻辑说明diff是两次采样间的编码器计数差值用short类型相减能自动处理溢出因为C语言的无符号溢出回绕对short也是同样道理只要差值绝对值不超过32767运算结果就是正确的。系数37.5怎么来的采样周期0.02秒脉冲数80所以转速 diff / 80 / (0.02/60) diff * 37.5。这里的转速单位rpm但实际电机转速有几百rpm注意diff如果是20ms内的计数20线码盘四倍频后是80电机一转是80如果转速300rpm那么每20ms的diff 300/600.0280 8所以数值不大用短整型完全够。方向控制PID输出正值表示正转负值表示反转。L293D的IN1和IN2决定方向PWM占空比取绝对值。注意如果目标速度始终为正一般不会出现负输出但遇到紧急制动或PID超调时负输出可以让电机制动这个处理是必要的。4.3 运行效果与波形观察接入1602后你会看到当前速度在目标速度附近波动。如果Kp和Ki合适波动范围在±5rpm以内。更好的调试方式是使用STM32的串口把运行数据发到PC端用串口示波器或者虚拟示波器画曲线。项目里可以加一个简单的UART输出每20ms发一帧目标速度、实际速度、PID输出。这样可以直观看到响应曲线和震荡情况。4.4 采样周期与控制周期的一致性PID参数整定依赖采样周期代码里Delay_ms(20)就是采样周期。注意不要用delay阻塞式延时否则按键中断和编码器计数会被打乱。我一般用定时器中断里设置标志位主循环检查标志位执行PID这样延时更准。如果执意用delay_ms要确保延时函数基于SysTick而且没有其他中断干扰。20ms的采样周期对TT电机是合适的采样太快会导致微分项放大噪声太慢会让响应迟钝。改变采样周期后Ki和Kd必须按比例调整。5. 调参实战与常见问题排查5.1 先P后I的调参顺序拿到这套代码后最稳的调参方法是把Ki和Kd先设为0只留Kp。从0.2开始一点一点加观察LCD上的实际速度曲线。当目标速度从0变成30rpm时如果实际速度不断上下冲就是Kp太大降一半再试。当速度最终稳定在一个接近目标的固定值但比目标低比如只有25rpm说明比例控制存在静差此时开始加Ki。每次加0.01直到速度稳定和目标差在1rpm以内。Kp和Ki协调好后如果发现启动瞬间超调太多可以加一点Kd我通常从0.01起试。注意Kd对测速噪声特别敏感如果看到PWM输出高频抖动说明Kd过头了。5.2 常见异常处理现象原因解决电机不转但PWM有值L293D的EN和IN逻辑不对确认PWM接ENIN1/IN2反向必须相反编码器数值一直为0A/B相没接对定时器通道确认PA6、PA7复用为TIM3速度显示忽正忽负编码器A/B接反或电机堵转交换A/B或检查机械部分卡阻装上负载后速度掉很多电机减速比小或电源功率不足换更大减速比电机或使用独立电源按键调整参数无效外部中断初始化失败或抖动看GPIO是否开时钟中断里做消抖还有几个容易踩的坑第一个是PWM的占空比上限。虽然定时器是16位的但L293D在高占空比下压降更大而且电机电流增大建议把PID输出上限设为ARR的80%左右这一套里默认7200的80%就是5760实际输出限幅在那个值附近。第二个是编码器计数和PID计算不同步如果在一个控制周期里多次读取TIM_GetCounter会读到不同时刻的值导致计算出的速度不均匀所以Speed_Control_Task里的读取和清零必须每个周期只做一次。第三个是1602显示更新太频繁会拖慢主循环我一般只在速度变化超过2rpm时才刷新或者每200ms刷新一次显示刷新本身也要几十毫秒会影响控制周期。5.3 验证闭环效果的简单方法找一段粗糙地面和一段光滑地面把小车放在上面固定目标速度观察LCD上的当前速度变化。如果闭环正常两种地面上实际速度基本一致开环则会相差超过20rpm。更精确的做法是用串口记录100个采样点画出速度曲线看稳态误差和超调量。我通常用蓝牙模块把数据发到手机串口工具方便在车跑动时实时观察。控制周期从20ms改成10ms后需要把Ki减半Kd也减半Kp不变因为积分和微分采样的时间片变短了。调好参数后把这组Kp、Ki、Kd直接写死到Speed_PID_Init里下次上电就不用按键重新调了。本文还有配套的精品资源点击获取
返回列表