STM32F407VET6灰度循迹优化:从位置式PID到模糊PID的工程实践
2026/9/19 14:12:15 网站建设 项目流程

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时钟最高36MHz,12位分辨率下最小采样时间3个周期,加上12个周期的转换时间,一次转换约15个ADC时钟周期。8路扫描一轮下来,如果采样时间设15周期,总时间约(15+12)×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/4=21MHz 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], i+1, 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读数上至少有800LSB(12位下满量程4096)。

3. 位置式PID还是增量式PID:F407上的实测对比

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,串口中断设成2,SysTick设成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小一点,快速响应偏差。一套参数不可能同时满足两种工况。

我实测过一组典型数据:直道最优Kp=25,弯道最优Kp=45。如果用Kp=25跑弯道,车会冲出去;用Kp=45跑直道,车会画龙。折中取Kp=35,直道和弯道都勉强能跑,但速度上不去。

模糊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不能一上来就调规则表,必须按顺序来:

  1. 先调固定PID,找到一组能跑完全程但速度不快的参数,作为基础参数。
  2. 确定模糊集的边界,e的阈值一般取最大偏差的20%到30%,ec的阈值取e阈值的50%左右。
  3. 先调Kp的模糊规则,只让Kp随e变化,Kd固定,跑几圈看弯道响应。
  4. 再调Kd的模糊规则,让Kd随ec变化,抑制直道摆动。
  5. 最后微调Ki,模糊PID里Ki一般不动,或者只做很小的调整。

提示:调试时用串口把e、ec、Kp、Kd、输出值实时传到上位机,用波形显示。我常用的是VOFA+或者简单的串口示波器,能直观看到参数调整的效果。F407的串口波特率可以开到921600,2ms周期传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)电机会有啸叫,太高(比如50kHz)TB6612的开关损耗会增加。我一般用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分析。

最后说一个我自己的习惯:每次改参数只改一个,改完跑三圈,取最好成绩。同时改两个参数,你根本不知道是哪个起了作用。调车是个耐心活,急不得。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询