资讯详情

资讯详情

STM32F407VET6灰度循迹优化:从位置式PID到模糊PID的工程实践

1. 为什么F407VET6跑灰度循迹总感觉力不从心很多人第一次拿到STM32F407VET6做智能巡检小车第一反应是这芯片性能过剩了吧——168MHz主频、512KB Flash、192KB SRAM跑个灰度循迹不是杀鸡用牛刀但实际调车的时候你会发现车要么在直道上画龙要么在弯道直接冲出去PID参数怎么调都不对劲。问题往往不在算法本身而在于采样链路、执行链路和控制周期这三者没有对齐。灰度循迹的本质是一个位置式闭环控制问题灰度传感器阵列给出偏差量MCU算出控制量电机执行。听起来简单但F407VET6上跑这套逻辑有几个容易被忽略的坑。第一灰度传感器的采样如果走普通GPIO轮询一次完整的8路或16路扫描可能要几十微秒加上ADC转换时间实际控制周期会被拉长到5ms甚至10ms以上。第二电机驱动如果用的是PWM加普通定时器占空比更新和传感器采样之间没有同步机制会出现用旧偏差算新PWM的时序错位。第三很多人直接把PID写在主循环里主循环里还塞了OLED刷新、串口打印、按键扫描控制周期抖动非常大。我实测过一组数据同样一套PID参数控制周期稳定在2ms时小车能跑完全程周期抖动到2ms±1.5ms时直道就开始出现明显摆动。所以这篇内容的核心不是教你PID公式而是围绕F407VET6这颗芯片的特性把灰度循迹从能跑优化到跑得稳、跑得快、弯道不丢线。关键词里的灰度循迹、PID、模糊控制是三个层次灰度循迹是感知层PID是基础控制层模糊控制是优化层。热词里出现的位置式PID、增量式PID、串级PID、模糊PID都会涉及但我会重点讲清楚在F407VET6这个具体平台上哪种方案最合适、为什么。注意本文所有代码和参数基于STM32F407VET6最小系统板8路灰度传感器TB6612电机驱动的常见配置其他硬件平台需要根据实际情况调整。2. 灰度传感器阵列的采样时序与F407的ADC配置陷阱2.1 灰度传感器的两种输出形式与选型逻辑灰度传感器分模拟输出和数字输出两类。模拟输出给的是连续电压值能反映偏了多少数字输出只给0或1只能反映偏没偏。做循迹优化必须用模拟输出否则PID的偏差量只有离散的几个档位控制精度上不去。常见的8路灰度模块模拟输出接F407的ADC通道。这里第一个坑就来了F407VET6有3个ADC但ADC1和ADC2共用一部分通道ADC3的通道分布又不一样。如果你用8路模拟灰度需要8个ADC通道建议全部挂在ADC1上用扫描模式DMA搬运。为什么不用ADC1和ADC2各挂4路因为两个ADC的采样时间、校准状态可能不一致导致同样反射率下两路读数有系统性偏差PID会把这个偏差当成真实偏差去修正车就会往一边偏。具体配置上F407的ADC时钟最高36MHz12位分辨率下最小采样时间3个周期加上12个周期的转换时间一次转换约15个ADC时钟周期。8路扫描一轮下来如果采样时间设15周期总时间约(1512)×8/36MHz≈6μs。这个时间完全可以接受关键是用DMA搬运不要让CPU去等EOC标志。// ADC1扫描模式DMA配置核心代码 void Gray_ADC_Init(void) { ADC_CommonInitTypeDef ADC_CommonInitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); DMA_InitStructure.DMA_Channel DMA_Channel_0; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_Memory0BaseAddr (uint32_t)gray_adc_value; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize 8; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_Init(DMA2_Stream0, DMA_InitStructure); DMA_Cmd(DMA2_Stream0, ENABLE); ADC_CommonInitStructure.ADC_Mode ADC_Mode_Independent; ADC_CommonInitStructure.ADC_Prescaler ADC_Prescaler_Div4; // 84MHz/421MHz ADC_CommonInitStructure.ADC_DMAAccessMode ADC_DMAAccessMode_Disabled; ADC_CommonInitStructure.ADC_TwoSamplingDelay ADC_TwoSamplingDelay_5Cycles; ADC_CommonInit(ADC_CommonInitStructure); ADC_InitStructure.ADC_Resolution ADC_Resolution_12b; ADC_InitStructure.ADC_ScanConvMode ENABLE; ADC_InitStructure.ADC_ContinuousConvMode ENABLE; ADC_InitStructure.ADC_ExternalTrigConvEdge ADC_ExternalTrigConvEdge_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfConversion 8; ADC_Init(ADC1, ADC_InitStructure); // 配置8个通道的采样时间 for(uint8_t i 0; i 8; i) { ADC_RegularChannelConfig(ADC1, gray_channel[i], i1, ADC_SampleTime_15Cycles); } ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_SoftwareStartConv(ADC1); }这段代码的关键点DMA用循环模式ADC用连续转换模式这样ADC会不停地把8路数据搬到gray_adc_value数组里CPU随时读到的都是最新值不需要主动触发转换。控制周期里只需要读数组、算偏差、更新PWM。2.2 采样窗口与电机PWM的同步问题灰度传感器对光线敏感尤其是室内巡检场景日光灯有100Hz的闪烁如果采样频率和灯闪频率耦合读数会周期性波动。解决办法有两个一是在控制周期里连续采多次取中值二是让采样时刻避开PWM开关瞬间。TB6612的PWM频率一般设10kHz到20kHz开关瞬间会在电源线上产生尖峰如果灰度传感器的供电和电机驱动共用一路电源尖峰会串到ADC读数里。我实测过PWM频率10kHz时ADC读数在电机换向瞬间会有±30LSB的跳变。这个跳变如果直接进PID输出就会抖。处理方式在ADC采样和电机PWM之间加一个RC低通滤波硬件同时在软件里做滑动平均滤波。滑动窗口取4到8个点窗口太大响应会滞后太小滤波效果不够。我一般取6个点配合2ms控制周期等效延迟约12ms对于巡检小车的速度一般0.3到0.8m/s来说完全可以接受。提示如果你发现小车在特定位置总是误判先检查灰度传感器的离地高度。常见8路模块的最佳检测距离是8到15mm太高灵敏度不够太低容易蹭地。用白纸和黑胶带实测确保黑白差值在ADC读数上至少有800LSB12位下满量程4096。3. 位置式PID还是增量式PIDF407上的实测对比3.1 两种PID形式在循迹场景下的本质差异位置式PID的输出直接对应PWM占空比公式是u(k) Kp·e(k) Ki·Σe(i) Kd·[e(k)-e(k-1)]增量式PID的输出是占空比的增量Δu(k) Kp·[e(k)-e(k-1)] Ki·e(k) Kd·[e(k)-2e(k-1)e(k-2)]在循迹场景下位置式更适合原因有三个。第一循迹的偏差量e(k)是有界且连续的位置式输出直接对应转向量物理意义清晰。第二位置式的积分项可以做成抗积分饱和的形式弯道丢线时不会因为积分累积导致出弯猛冲。第三增量式在偏差突变时比如从白区进入黑线输出增量会很大容易造成电机冲击。但位置式有个问题积分项需要累加如果控制周期抖动积分效果会变。所以位置式PID必须配合稳定的定时器中断不能放在主循环里。3.2 在F407上用定时器中断跑PID的完整配置F407的TIM2到TIM5都是32位定时器用来做控制周期定时非常合适。我一般用TIM3配置成1ms或2ms中断在中断里做三件事读灰度偏差、算PID、更新PWM。// TIM3配置为2ms中断 void PID_Timer_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 84MHz / (4200 * 40) 0.5Hz? 不对重新算 // 目标2ms周期84MHz下预分频4200-1计数40-1 // 84,000,000 / 4200 / 40 500Hz 2ms TIM_TimeBaseStructure.TIM_Period 40 - 1; TIM_TimeBaseStructure.TIM_Prescaler 4200 - 1; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM3, ENABLE); } // 中断服务函数 void TIM3_IRQHandler(void) { if(TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); int16_t error Gray_GetError(); // 获取偏差 int16_t output Position_PID(error); // 位置式PID Motor_SetPWM(output); // 更新PWM } }这里有个细节中断优先级要设高但不要设成最高。F407的NVIC有4位优先级我一般把PID中断设成抢占优先级1串口中断设成2SysTick设成3。这样PID不会被串口打印打断但也不会阻塞系统滴答。3.3 位置式PID的抗积分饱和实现循迹小车最容易出的问题弯道丢线后偏差量突然变成最大值积分项疯狂累积等车回到线上时积分项已经大到让车猛冲出去。解决办法是积分分离输出限幅。typedef struct { float Kp; float Ki; float Kd; float integral; float prev_error; float output_max; float integral_max; } PID_TypeDef; int16_t Position_PID(int16_t error) { float output; float p_term, i_term, d_term; // 积分分离偏差大时不积分 if(fabs(error) 50) { // 偏差小于阈值才积分 pid.integral error; // 积分限幅 if(pid.integral pid.integral_max) pid.integral pid.integral_max; if(pid.integral -pid.integral_max) pid.integral -pid.integral_max; } else { pid.integral 0; // 偏差大时清零积分 } p_term pid.Kp * error; i_term pid.Ki * pid.integral; d_term pid.Kd * (error - pid.prev_error); output p_term i_term d_term; // 输出限幅 if(output pid.output_max) output pid.output_max; if(output -pid.output_max) output -pid.output_max; pid.prev_error error; return (int16_t)output; }积分分离的阈值怎么定我的经验是取最大偏差的30%到50%。8路灰度传感器偏差范围一般是-7到7每路对应一个单位最大偏差7阈值取2到3比较合适。积分限幅取输出限幅的50%到70%防止积分项单独就能把输出顶到最大。注意fabs在F407上如果开了FPU单精度浮点运算很快但如果你用的是软浮点建议把PID参数放大100倍用整数运算。F407VET6带FPU直接开硬件浮点就行在Keil里勾选Use FPU。4. 模糊PID在弯道和直道切换时的实际效果4.1 为什么固定参数PID在复杂赛道上不够用固定参数PID有个根本矛盾直道上希望Kp小一点、Kd大一点抑制高频摆动弯道上希望Kp大一点、Kd小一点快速响应偏差。一套参数不可能同时满足两种工况。我实测过一组典型数据直道最优Kp25弯道最优Kp45。如果用Kp25跑弯道车会冲出去用Kp45跑直道车会画龙。折中取Kp35直道和弯道都勉强能跑但速度上不去。模糊PID的思路就是根据偏差和偏差变化率实时调整Kp、Ki、Kd。偏差大时加大Kp加快响应偏差小时减小Kp抑制超调偏差变化率大时加大Kd抑制振荡。4.2 模糊规则表的简化实现完整的模糊PID需要模糊化、规则推理、解模糊三个步骤在F407上跑完全没问题但代码量不小。对于循迹小车我推荐用简化模糊规则把偏差e和偏差变化率ec各分成3个模糊集负、零、正规则表用查表法实现。e \ ec负零正负Kp大,Kd小Kp中,Kd中Kp小,Kd大零Kp中,Kd小Kp小,Kd中Kp中,Kd大正Kp小,Kd小Kp中,Kd中Kp大,Kd大这个表看起来简单但实际调车时非常有效。具体实现时把Kp、Kd的调整量做成三个档位根据e和ec的符号查表选择。// 简化模糊PID调整 void Fuzzy_PID_Adjust(int16_t error, int16_t ec) { int8_t e_sign (error 20) ? 1 : ((error -20) ? -1 : 0); int8_t ec_sign (ec 10) ? 1 : ((ec -10) ? -1 : 0); // 基础参数 float Kp_base 30.0f; float Kd_base 15.0f; // 根据模糊规则调整 if(e_sign 0 ec_sign 0) { // 直道稳定区小Kp中Kd pid.Kp Kp_base * 0.7f; pid.Kd Kd_base * 1.0f; } else if(e_sign ! 0 ec_sign 0) { // 弯道入口大Kp小Kd pid.Kp Kp_base * 1.5f; pid.Kd Kd_base * 0.5f; } else if(e_sign 0 ec_sign ! 0) { // 直道摆动中Kp大Kd pid.Kp Kp_base * 1.0f; pid.Kd Kd_base * 1.8f; } else { // 弯道中中Kp中Kd pid.Kp Kp_base * 1.2f; pid.Kd Kd_base * 0.8f; } }这段代码在2ms中断里跑浮点运算量很小F407的FPU完全跟得上。实测下来模糊PID比固定PID的圈速能快15%到20%而且直道摆动明显减小。4.3 模糊PID的调试顺序与参数整定调模糊PID不能一上来就调规则表必须按顺序来先调固定PID找到一组能跑完全程但速度不快的参数作为基础参数。确定模糊集的边界e的阈值一般取最大偏差的20%到30%ec的阈值取e阈值的50%左右。先调Kp的模糊规则只让Kp随e变化Kd固定跑几圈看弯道响应。再调Kd的模糊规则让Kd随ec变化抑制直道摆动。最后微调Ki模糊PID里Ki一般不动或者只做很小的调整。提示调试时用串口把e、ec、Kp、Kd、输出值实时传到上位机用波形显示。我常用的是VOFA或者简单的串口示波器能直观看到参数调整的效果。F407的串口波特率可以开到9216002ms周期传5个float完全够用。5. 电机执行链路从PID输出到PWM的映射细节5.1 差速转向的PWM分配逻辑循迹小车的转向靠左右轮差速。PID输出的是一个转向量需要映射到左右轮PWM上。常见做法左轮PWM 基础速度 PID输出右轮PWM 基础速度 - PID输出但这里有个坑基础速度不能是固定值。直道和弯道的基础速度应该不同弯道要减速。我的做法是用PID输出的绝对值来反推速度衰减void Motor_SetPWM(int16_t pid_output) { int16_t base_speed 600; // 基础PWM范围0-1000 int16_t turn pid_output; // 转向越大基础速度越低 int16_t speed_reduction abs(turn) / 2; if(speed_reduction 300) speed_reduction 300; base_speed - speed_reduction; int16_t left_pwm base_speed turn; int16_t right_pwm base_speed - turn; // 限幅 if(left_pwm 1000) left_pwm 1000; if(left_pwm -1000) left_pwm -1000; if(right_pwm 1000) right_pwm 1000; if(right_pwm -1000) right_pwm -1000; // 设置PWM if(left_pwm 0) { TIM_SetCompare1(TIM1, left_pwm); TIM_SetCompare2(TIM1, 0); } else { TIM_SetCompare1(TIM1, 0); TIM_SetCompare2(TIM1, -left_pwm); } // 右轮同理 }这个逻辑的关键是速度衰减系数。系数太大弯道太慢系数太小弯道冲出去。我一般从0.5开始试根据赛道曲率调整。5.2 PWM频率与电机响应的匹配TB6612的PWM频率建议在10kHz到20kHz之间。太低比如1kHz电机会有啸叫太高比如50kHzTB6612的开关损耗会增加。我一般用15kHz兼顾噪音和效率。F407的TIM1是高级定时器适合做电机PWM。配置时注意死区时间虽然TB6612内部有死区但外部再加一点更安全。死区时间设100ns到200ns就够了。// TIM1 PWM配置15kHz void Motor_PWM_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB, ENABLE); // PA8, PA9, PA10, PA11作为PWM输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_PinAFConfig(GPIOA, GPIO_PinSource8, GPIO_AF_TIM1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_TIM1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_TIM1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource11, GPIO_AF_TIM1); // 84MHz / 1 / 5600 15kHz TIM_TimeBaseStructure.TIM_Period 5600 - 1; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, 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(TIM1, TIM_OCInitStructure); TIM_OC2Init(TIM1, TIM_OCInitStructure); TIM_OC3Init(TIM1, TIM_OCInitStructure); TIM_OC4Init(TIM1, TIM_OCInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }注意TIM1的PWM输出需要调用TIM_CtrlPWMOutputs使能普通定时器不需要。这个坑我踩过PWM配置全对但就是没输出查了半天才发现少了这一句。6. 实测中遇到的三个典型问题与排查过程6.1 问题一直道高速时小车突然往一边偏现象直道加速到PWM 800以上时小车会突然往左或往右偏减速后又恢复正常。排查过程先怀疑是PID参数问题把Kp从35降到20现象减轻但没消失。用串口打印灰度ADC值发现高速时某一路灰度读数会突然跳变。用示波器看电源发现电机加速瞬间3.3V电源上有200mV的跌落。根因灰度传感器和MCU共用3.3V LDO电机加速时电流突增LDO响应不过来导致ADC参考电压波动。解决给灰度传感器单独加一个LDO或者在灰度模块的VCC和GND之间并一个100μF电解电容加0.1μF陶瓷电容。同时把ADC的参考电压改成外部基准如果板子支持没有外部基准就在软件里做电源电压补偿。6.2 问题二弯道丢线后出弯猛冲现象急弯处灰度全白丢线小车冲出赛道回到线上后猛冲一段才恢复。排查过程打印PID的积分项发现丢线期间积分项从0累积到了限幅值。虽然做了积分限幅但限幅值设得太大输出限幅的100%。解决把积分限幅降到输出限幅的40%同时加丢线保护连续3个控制周期检测到全白直接清零积分项并把输出置为上一次有效偏差的方向让车往回找线。// 丢线保护 if(Gray_IsAllWhite()) { lost_count; if(lost_count 3) { pid.integral 0; // 用最后一次有效偏差的方向找线 Motor_SetPWM(last_valid_output 0 ? 400 : -400); } } else { lost_count 0; }6.3 问题三串口打印导致控制周期抖动现象开串口打印PID数据时小车运行明显不稳关掉打印就正常。排查过程用GPIO翻转示波器测控制周期不开打印时2ms很稳开打印时周期在2ms到4ms之间跳。根因串口打印放在主循环里但主循环和定时器中断有资源竞争而且串口发送本身耗时。115200波特率下打印20个字节要1.7ms如果主循环正好在中断前开始发送中断就会被推迟。解决串口打印改成DMA发送主循环只负责把数据填到缓冲区DMA在后台搬运。同时把打印频率降到每10个控制周期打一次减少数据量。// 串口DMA发送 void UART_SendData_DMA(uint8_t *data, uint16_t len) { while(DMA_GetCmdStatus(DMA1_Stream6) ! DISABLE); // 等待上一次发送完成 DMA_ClearFlag(DMA1_Stream6, DMA_FLAG_TCIF6); DMA_SetCurrDataCounter(DMA1_Stream6, len); DMA_Cmd(DMA1_Stream6, ENABLE); }提示F407的串口DMA发送要注意清除标志位否则第二次发送会直接返回。这个坑很隐蔽表现是第一次打印正常后面就没数据了。7. 从能跑到跑得快的几个调参经验调车这件事理论是一回事手上功夫是另一回事。我把自己反复试出来的几个经验列一下不一定通用但方向可以参考。第一先调机械再调算法。灰度传感器的离地高度、轮子的摩擦力一致性、车体重心位置这些机械因素对循迹效果的影响比PID参数大得多。我见过有人花一周调PID最后发现是左右轮摩擦力不一致换个轮子就好了。第二控制周期不是越短越好。2ms是我试下来最合适的1ms时CPU负载太高串口和OLED都受影响5ms时响应明显滞后弯道容易冲出去。2ms配合15kHz的PWM一个PWM周期对应约33个控制周期足够精细了。第三Kd的作用被很多人低估。循迹小车直道摆动大部分人第一反应是减小Kp其实加大Kd更有效。Kd抑制的是偏差变化率直道摆动时偏差变化率很大Kd能直接把这个变化压下去。我一般先把Kd加到车不抖再回头调Kp。第四模糊PID的规则表不要写太复杂。我试过5个模糊集、25条规则的完整版效果和3个模糊集、9条规则的简化版差不多但代码量和调试时间翻倍。对于循迹这种单一场景简化版足够了。第五一定要做数据记录。每次调参把e、ec、Kp、Kd、输出、PWM都记下来跑完一圈回看数据比凭感觉调快得多。我用的是SD卡记录2ms周期记一次一圈下来也就几百KB事后用Excel或者Python分析。最后说一个我自己的习惯每次改参数只改一个改完跑三圈取最好成绩。同时改两个参数你根本不知道是哪个起了作用。调车是个耐心活急不得。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →