☰
STM32巡线小车PID控制实战:从传感器布局到参数整定
2026/10/15 16:40:36 网站建设 项目流程

简介:基于STM32F103C8T6的巡线小车PID算法完整工程代码,覆盖从传感器采集到电机驱动的全链路,面向嵌入式学习者、智能车竞赛和课程设计人群,用于解决循迹过程中的转向迟滞与抖动问题。工程采用L298N驱动双路直流电机,三路反射式红外传感器经ADC与DMA将赛道数据送入控制器,PID闭环计算后输出PWM调节电机;同时集成超声波测距、1.8寸LCD显示、SG90舵机等外设驱动,C语言模块化编写,数据流向清晰,便于理解外设分层与中断响应。压缩包共85个文件,以C源文件、H头文件为主,附带hex烧录文件、s启动文件以及map和lst链接映射文件,整体仅610KB,结构紧凑,适合直接编译烧录或对照分析。目前已有5978人学习,资源提供完整的PID调参框架与巡线路径制作思路,可在此基础上快速适配不同赛道,实现从原理理解到实际落地的实践闭环。

1. 巡线小车最常翻车的地方不在PID,而在你把算法当成开关

你大概也遇到过这种场景:小车明明按别人的代码拼出来了,直线段跑得笔直,一进弯道就直冲出去,把速度调低了又慢得像蜗牛,加个P参数又开始左右画龙。这不是传感器坏了,也不是电机不行,而是你手里拿的是“指令”不是“算法”——要么偏了就猛打方向,要么不知道提前回正。标题里的PID,本质上就是给这个小车一个“能商量”的控制策略:偏差多大,回正多少,回正多快,提前多久。这篇笔记针对的是正在做STM32巡线小车、想把PID代码真正调到能跑的开发者,我不谈那些教科书式的系统框图,只讲从硬件布局到参数整定的完整落地路径,你照着复现完,会发现巡线这件事的关键真的不在代码量,而在偏差怎么构造、采样周期怎么固定、限幅有没有做。

2. 先定硬件再写PID:传感器布局与执行机构决定算法形态

2.1 灰度传感器布局:从三路到八路的选型逻辑

很多刚开始做巡线小车的同学,第一反应是先抄一段PID代码,结果代码拿过来根本跑不了。原因很简单:PID吃的不是“黑线在哪一格”,而是一个连续的偏差值。三路红外传感器只能输出“左偏、居中、右偏”三个离散状态,PID拿到这种信号等于拿开关量喂算法,输出自然只有抽搐式的猛打方向。所以我建议至少用五路灰度传感器,中间三路负责覆盖黑线,外侧两路负责预警,这样构造出来的偏差是连续的,PID才能真正发挥“比例、微分、积分”各自的作用。

传感器间距直接影响偏差的分辨率。间距太宽,比如超过20mm,黑线在两个传感器之间游走时偏差会出现台阶式跳变,小车在直线段会高频微抖;间距太窄,比如小于10mm,中间几路传感器同时压线时偏差会直接跳到最大值附近,让PID输出饱和。我一般把五路灰度传感器做成间距15mm左右的红外反射式方案,排成一排,安装高度控制在10mm到15mm之间。这个高度很关键,太高了反射光散射严重,阈值没法稳定,太低了安装结构和赛道摩擦会干涉。

2.2 用ADC把模拟传感器变成归一化偏差

巡线小车的传感器输出到了STM32这一侧,不管你的灰度模块是模拟电压还是数字IO,最终都会转成一个或多个0到4095的ADC读数。常见做法是每个传感器接一个ADC通道,也可以用一个74HC4051模拟多路开关做通道扩展来省引脚,但只要是实时巡线,我建议直接上多通道ADC,省去额外的切换延时。下面的代码示意了五路传感器的读取与偏差构造:

// 以某STM32系列HAL库为例,读取5路灰度传感器模拟输出 #define CNT_SENSOR 5 #define ADC_THRESHOLD 1800 // 黑白分界的ADC阈值,需实测标定 static uint32_t adc_raw[CNT_SENSOR]; static uint8_t line_state[CNT_SENSOR]; // 1=黑线,0=白底 // 权重按传感器物理位置配置,偏差输出范围[-100, 100] static const int16_t weight[CNT_SENSOR] = {-40, -20, 0, 20, 40}; void sensor_read_all(void) { for (uint8_t i = 0; i < CNT_SENSOR; i++) { adc_raw[i] = hal_adc_read(ADC_CHANNEL_BASE + i); // 阈值比较:高于阈值视为黑线,低于视为白底 line_state[i] = (adc_raw[i] > ADC_THRESHOLD) ? 1 : 0; } } int16_t calc_line_error(void) { int16_t error = 0; int16_t cnt = 0; for (uint8_t i = 0; i < CNT_SENSOR; i++) { error += line_state[i] * weight[i]; cnt += line_state[i]; } if (cnt == 0) // 全白:所有传感器都脱线,保留上次偏差 return error; return error / cnt; // 归一化到单个传感器权重级别 }

这段代码的核心逻辑是对命中黑线的传感器按位置加权求和,再除以命中个数,得到的就是一个连续的带符号偏差。error为正表示车偏右,为负表示车偏左,数值大小表示偏离程度。权重数组{-40,-20,0,20,40}不是唯一的,你完全可以根据赛道线宽改成{-50,-25,0,25,50},偏差范围会变化,但注意改了之后PID参数也要跟着重新整定。ADC阈值ADC_THRESHOLD别拍脑袋填,我习惯上电后让传感器悬空对着白色桌面读一组值、再放到黑线上读一组值,取两者中点作为阈值。另外注意,当cnt==0全白脱线时,我直接保留当前偏差不做更新,这样PID会按照脱线前的态势继续输出转向,争取把车拉回来,而不是突然跳到0。

2.3 执行端:把PID输出映射到电机PWM

传感器搞定之后,要看执行机构。巡线小车最常见的是两路直流减速电机加后轮万向轮,或者四轮双电机驱动,本质都是差速转向:左轮和右轮的速度差决定了车头的转角。PID算出来的输出量,一般有两种接法:一种是把PID输出直接当作PWM占空比,另一种是把PID输出当作转向修正量叠在基础速度上。巡线场景应该用第二种,因为你希望小车即便在直线上也有一个基础前进速度,转向量只负责“纠正方向”,而不是让电机从零开始走。

void motor_set_speed(int16_t speed_left, int16_t speed_right) { // 输入为带符号速度值,正为前进,负为后退 speed_left = clamp_int16(speed_left, -PWM_MAX, PWM_MAX); speed_right = clamp_int16(speed_right, -PWM_MAX, PWM_MAX); motor_set_dir(LEFT_MOTOR, speed_left >= 0 ? DIR_FWD : DIR_REV); motor_set_dir(RIGHT_MOTOR, speed_right >= 0 ? DIR_FWD : DIR_REV); __HAL_TIM_SET_COMPARE(&htim_pwm, TIM_CHANNEL_1, abs(speed_left)); __HAL_TIM_SET_COMPARE(&htim_pwm, TIM_CHANNEL_2, abs(speed_right)); }

控制量叠加的公式很简单:left_speed = base_speed + turn,right_speed = base_speed - turn。turn就是PID的输出。这里有两个隐藏问题:第一,base_speed和turn的数值量纲必须一致,如果你base_speed用0到100的百分比,turn也得是百分比,你让PID输出一个几百的整数直接加到百分比上,车会一直往一边打死;第二,直流电机的PWM占空比和实际转速不是线性关系,在低占空比区电机根本转不动,在高占空比区转速趋于饱和。这就是为什么很多成品方案里在PID外面又套了一圈“电机速度闭环”,但那是后话,先用开环占空比把控制和参数跑通,已经足够应付大部分巡线任务。

3. 把PID写进STM32:位置式与增量式的代码骨架

3.1 位置式PID:为什么巡线小车常用它

PID按计算方式最常见的是位置式和增量式两种。位置式PID的输出是控制量的绝对值,它直接告诉你“转向修正量应该是多少”,对巡线小车来说非常直观,而且它天然带记忆,可以通过积分项消除长时间的小偏差。另一个原因是位置式在代码里容易调试:你把输出打印出来,看一眼就知道当前控制量是正还是负、有没有打到限幅。我见过不少代码把PI、PD、PID都揉在一个函数里用宏开关切换,跑起来逻辑混乱,我建议直接写一个独立的PID结构体,把各项系数、误差历史上限、输出限幅都封进去。

typedef struct { float kp; // 比例系数 float ki; // 积分系数 float kd; // 微分系数 int32_t integral; // 积分累计值 int32_t last_error; // 上一次误差,用于微分计算 int32_t integral_limit; // 积分限幅 int32_t out_limit; // 输出限幅 } pid_ctrl_t; int16_t pid_positional_update(pid_ctrl_t *pid, int16_t current_error) { // 目标值为0,所以error越大表示偏离越严重 pid->integral += current_error; pid->integral = clamp_int32(pid->integral, -pid->integral_limit, pid->integral_limit); int32_t p_out = (int32_t)(pid->kp * current_error); int32_t i_out = (int32_t)(pid->ki * pid->integral); int32_t d_out = (int32_t)(pid->kd * (current_error - pid->last_error)); pid->last_error = current_error; int32_t out = p_out + i_out + d_out; out = clamp_int32(out, -pid->out_limit, pid->out_limit); return (int16_t)out; }

逻辑说明:这里把target直接设为了0,也就是期望偏差为零,所以error就是当前偏差本身。积分项每次累加当前误差,在直线段误差很小所以积分量增长缓慢,但在弯道里误差持续较大,积分会迅速累积,这就是后面要做积分限幅的原因。微分项是当前误差减上次误差,它体现的是偏差的变化趋势,注意这里没有除以采样周期dt,因为只要你的主循环是固定周期调用,dt就是个常数,可以直接被并进kd里,这是一个很常见的工程简化,写代码时心里要有数。

3.2 增量式PID:什么时候值得用

增量式PID输出的不是控制量本身,而是控制量的增量,也就是说每次算出来的是“在上一轮控制量基础上加多少”。它的好处是积分效应隐含在增量里,不容易出现积分饱和,输出限幅也不需要额外做积分限幅。但对巡线小车这个场景,增量式不仅没有优势,反而多了一堆麻烦:你要在外部维护上一次输出,还要处理累加溢出,而且输出天然没有“绝对值概念”,调试时你根本不知道当前转向量到底多大。所以除非你的执行机构是步进电机这种“按脉冲增量运动”的部件,否则巡线小车直接用位置式就好。

int16_t pid_incremental_update(pid_ctrl_t *pid, int16_t current_error) { // 增量式:结果是对上次输出的修正增量 int32_t delta = (int32_t)(pid->kp * (current_error - pid->last_error)) + (int32_t)(pid->ki * current_error) + (int32_t)(pid->kd * (current_error - 2 * pid->last_error + pid->last_last_error)); pid->last_last_error = pid->last_error; pid->last_error = current_error; return (int16_t)delta; }

如果你真的要用增量式,注意这里微分项变成了error(t) - 2*error(t-1) + error(t-2),也就是二阶差分,而不是简单的一阶差分。很多学习者在这里抄错代码,写出来的“增量式PID”实际上是位置式的差分版本,控制效果完全不对。我不建议你在巡线车上用增量式,把这个代码当成理解参考就行。

3.3 主循环里的采样周期与控制节奏

PID参数的整定离不开“采样周期”这个概念。很多人巡线车抖动,调了半天PID都没用,最后发现是主循环里用了HAL_Delay,每次延时之间还穿插了OLED刷新、串口打印、传感器扫描,导致PID的调用间隔忽长忽短。微分项对时间间隔非常敏感,这次间隔5ms下次间隔15ms,同样的误差变化率算出来的kd贡献差了三倍,车不抖才怪。

uint32_t last_tick = 0; const uint32_t PID_PERIOD_MS = 10; // 固定10ms调用一次PID while (1) { sensor_read_all(); // 循环内读取传感器,刷新最新偏差 int16_t err = calc_line_error(); if (HAL_GetTick() - last_tick >= PID_PERIOD_MS) { int16_t turn = pid_positional_update(&pid, err); motor_set_speed(BASE_SPEED + turn, BASE_SPEED - turn); last_tick = HAL_GetTick(); } // 其他非控制任务放这里,比如按键扫描、OLED显示 }

逻辑说明:传感器读取放在循环外、PID调用前,这样PID每次拿到的都是最新偏差,而不是上一次循环的旧数据。用HAL_GetTick()判断周期而不是用HAL_Delay()主动阻塞,是因为前者能保证控制周期相对稳定,哪怕主循环里其他任务偶尔耗时超标,也不会阻塞控制节奏太久。采样周期我默认取10ms,也就是PID控制频率100Hz,对巡线小车这个级别的慢速系统完全够用。如果你跑高速赛道,可以压缩到5ms,但注意微分项kd要按比例放大,因为误差差分的时间基数变小了。

3.4 必须做的限幅:输出限幅、积分限幅与转向优先级

我在带人做这个项目时发现,很多人写PID在功能上完全正确,但跑起来还是翻车,翻车的点几乎都在“限幅”上。输出限幅是最基本的,PID计算出来的turn可能会大到几百上千,直接加到PWM占空比上会让小车满舵打死,这时候再强的比例项也救不回来。积分限幅则更隐蔽,它限制的是积分项的累计值,防止小车在长直线或者长弯道中把积分憋成一个非常大的值,等偏差方向一变,这个积分残值会让转向瞬间冲向反方向极限。

#define INTEGRAL_LIMIT 300 // 积分项累计不超过±300 #define OUTPUT_LIMIT 400 // PID输出不超过±400(PWM占空比绝对值) int32_t clamp_int32(int32_t val, int32_t min, int32_t max) { if (val < min) return min; if (val > max) return max; return val; }

这两个限幅值不是随便拍的,它们的上限和你base_speed的大小直接相关。如果base_speed是600,PID输出限幅设在400,那么小车的转向范围是左轮200到1000、右轮1000到200,仍然能差速转向。但如果你把输出限幅设成800,那么叠加后左右轮会频繁出现一侧完全反转的情况,小车会在大幅度修正时剧烈摇摆。我常用的经验是:输出限幅取base_speed的60%到80%,积分限幅取输出限幅的三分之一到一半。至于转向优先级,意思是当偏差超过某一个大值(比如脱线预警)时,应该主动降低base_speed,把输出的大部分留给转向修正,这个策略我放在后面的进阶部分细讲。

4. PID参数整定:从比例到微分的调车顺序与现场手感

4.1 只用比例:先让小车“知道回正”

参数整定的顺序不要跳。第一步是把ki和kd全部设成0,只留kp,让小车在赛道上跑直线。这时候的kp是一个很小的值起步,比如误差范围是-100到100,kp先从0.5开始。你会发现小车在直线段反应迟钝,偏差早就出现了,它还在慢悠悠地回正,进弯道后拉不回来。然后逐步加大kp,每次加0.3左右,记录临界现象。加到某个值后,直线段会开始出现周期性的蛇形摆动,这说明kp已经大过头了。

kp偏小时的表现是过弯向外侧冲出,因为修正力度不够;kp偏大时则是过弯内切甚至原地掉头,因为修正量已经超过需要的角度。我调整时喜欢让小车跑一个“直线+大半径弯道”的组合赛道,观察它在直线末端进弯时车头的切入姿态。下面这个表格是经验值参考,注意它依赖你的偏差范围、PWM量程和采样周期,不要直接照抄。

现象kp范围参考处理方式
直线回正太慢,弯道冲出去0.5 ~ 1.5加大kp,步进0.3
直线轻微摆头,弯道能过2.5 ~ 4.0保持当前值,进入调kd阶段
直线蛇形明显,弯道内切5.0以上减小kp,寻找临界点再回落20%

这个阶段的经验是,找到直线段开始蛇形摆动的临界kp值,然后回退20%到30%,作为后续调kd的起点。这里不要追求直线跑得“绝对直”,因为真正让直线变稳的是微分项。

4.2 加入微分:抑制抖动,也放大噪声

kp定下来之后,开始在临界值基础上加kd。微分项在误差变化快的时候起作用——也就是刚进弯道那一刻和出弯回正的那一刻。它的本质相当于一个阻尼器,压制比例项造成的过冲。kd从0开始慢慢加,每次加0.2左右,观察直线段蛇形摆动的幅度和过弯时车头的收敛速度。

kd加太多会带来另一个问题:传感器读数本身的噪声被放大了。灰度传感器的阈值比较结果在黑白边缘抖动是很正常的,这种高频抖动会让微分项连续输出大的修正量,表现为小车在直线段“微颤”,频率很高但幅度不大。这时候你会觉得无论怎么降kp都没用,因为问题出在kd上而不是kp上。比较好的kd状态是:直线段几乎不见摆动,进弯时车头快速平滑地切入,出弯时没有明显的反向过冲。如果条件允许,用串口把PID输出和误差值打印出来,拉到示波器上看曲线,稳定的状态应该是误差曲线和输出曲线都没有高频率的毛刺。

4.3 积分项:直线赛道上的“欠债”

积分项在巡线小车里的使用场景远没有比例和微分多。很多人把标准PID公式背得很熟,上来三个参数都设成非零,结果车跑起来远比PD控制差,转头又觉得是代码有问题。实际上,积分项解决的是“系统存在固定偏差”的问题,比如机械装配导致左右轮转速不一致、赛道表面摩擦系数不对称,这些会让小车在直线段长期偏向一侧。但在巡线场景里,传感器已经把“是否偏离”检测得很直接,比例项本身就是消除偏差的主力,积分项能帮上忙的场合非常有限。

我倾向于在巡线小车里默认ki=0,先用PD把车调到稳定,然后跑一个长直线段观察,如果小车总是缓慢偏向同一侧、而且每次都需要靠弯道修正拉回来,再尝试加一个很小的ki,比如0.05起步。ki过大的典型表现是,小车刚上赛道时还好,跑了几圈后越来越“贼”,甚至进入一段长直线后开始自己往一边打方向,这就是积分项在长时间无有效误差的情况下慢慢积累然后爆发。这种现象我们在避坑里还会再提。所以积分项的定位应该是“用于消除固定偏差的补充手段”,而不是PID的主干。

4.4 参数整定的现场流程:先纸上算,再上赛道试

我的整定习惯是,先把误差范围、PWM量程、采样周期三个数写出来,估算初始参数。比如误差范围是-100到100,PWM基础速度是600,输出限幅400,那么kp初始值可以按“输出限幅/最大误差=4”再打个五折,取2.0左右。这样第一轮上赛道不会出现车直接飞出去的情况。然后用4.1到4.3的顺序逐步调整,每次只改一个参数,改完在同样的赛道上跑三圈,记录现象再改下一个。不要同时动两个参数,否则翻车了你根本不知道是哪个参数引起的。

赛道环境也是变量。真实比赛场地因为灯光不同、反光程度不同,同一个参数上午能稳定跑,下午就开始画龙,原因多半在传感器的阈值漂移。这方面我建议把阈值标定做成一个菜单功能,上电按键一次自动采集黑白值并写入Flash,比在代码里改常量重新烧录要高效得多。关于参数,还有一个常被忽略的点是BASE_SPEED本身。BASE_SPEED越高,同样的kp和kd实际效果越剧烈,因为同样的轮速差作用在更高的基础速度上,转向角速度变化的物理时间更短。这意味着你调好低速参数后直接加高基础速度,小车会重新开始抖动,这是正常现象,不是参数丢了。

5. 巡线小车PID调试的5个常见坑与排查顺序

5.1 上午能跑下午就冲线,传感器阈值漂移

现象:同一套参数和赛道,上午测试时小车稳定巡线,下午换了个灯光环境后小车频繁冲出赛道,尤其在黑白交界处左右摇摆。原因非常直接:灰度传感器本质是反射式光电检测,环境光中的红外成分会叠加在反射信号上,导致同样的黑线在ADC读数上比上午高出一截。你代码里写死的ADC_THRESHOLD是上午标定的,下午的自然光和灯光一变,黑线和白底的ADC区间整体上移,原本低于阈值的白底变成了“假黑线”。

解决:不要用固定阈值,改成动态标定。上电时让传感器分别对着白色区域和黑色区域采集10到20次取平均,把两个平均值的中点作为当前阈值。如果嫌标定麻烦,可以换数字接口的灰度传感器,它在模块内部做了比较器,输出就是0或1,但要注意这类模块往往也有一个电位器需要调,本质上还是要把阈值调准。我自己的做法是上电后自动快速扫一遍,看5个传感器的ADC最大值和最小值,然后按(max+min)/2动态设置阈值,这样即便环境变了,只要黑白对比度还在,就能自动跟上。

5.2 电池电压一降,参数就变玄学

现象:满电时小车参数很稳定,跑了几分钟或者电量降到一定值后,原本调好的PID开始出现过弯迟钝、直线抖动,甚至电机明显变慢,感觉所有参数都“失效”了。原因:我们用的是开环PWM占空比控制,占空比对应的是平均电压,不直接对应电机转速。电池电压从8.4V降到7.2V,同样的占空比下电机实际转速掉了很大一截,而小车的前进速度和转向响应都变了,但PID参数还是原来那套,自然不匹配。

解决:分两步。第一步,给控制板的电源单独加稳压,避免电机大电流拉低单片机电压导致ADC参考电压漂移,否则传感器读数也会一起变。第二步,如果条件允许,给电机加编码器做速度闭环,把PID输出的转向修正量作用在“期望速度”上而不是“PWM占空比”上。但这一步会放大项目复杂度,如果你只是做课程设计或者入门练手,我建议采用保守做法:把BASE_SPEED设定在电池电压变化不大的区间,比如PWM占空比不超过70%,同时定期给电池充电,承认这是一门手艺活而不是一个放之四海而皆准的算法。

5.3 积分饱和:长直线之后的第一个弯道直接甩出去

现象:小车在长直线段平稳跑着,PID输出很小几乎为零,但一进入弯道,转向突然猛烈到无法控制,车头大幅度内切甚至原地掉头。原因就是积分饱和在作祟。在长直线段,如果传感器因为轻微偏移或阈值问题始终检测到很小的误差,积分项就在悄悄累积。等弯道出现,真实大误差来了,比例和微分项本来可以正常输出,但因为积分项已经憋在一个很大的值上,三者叠加直接冲到输出限幅,小车表现为猛打方向。

解决:代码里做积分限幅是最基本的一层保护,INTEGRAL_LIMIT要设得足够小。更彻底的做法是偏差归零时清掉积分项,或者只在偏差小于某个范围内才允许积分累加,超出范围就禁用积分。还有一个常用技巧是积分分离,当误差绝对值大于某个阈值时,把ki直接当作0处理,误差小到阈值内再恢复积分。巡线赛道弯道多,我建议直接关掉积分项,用PD参数跑完整个场,你会发现效果反而更好。

5.4 采样周期不定,微分项疯狂输出

现象:代码逻辑没毛病,PID输出在串口监视器上看全是高频毛刺,电机发出明显的噪声,小车在小幅范围内快速抖动,可调参数怎么调都压不下来。原因:你的主循环里PID调用周期不稳定。可能是用了HAL_Delay配合OLED刷新,也可能是传感器读取函数里HAL_ADC_Start和HAL_ADC_PollForConversion的等待时间被其他中断打断。微分项的计算是error(t) - error(t-1),这个差值默认了两次调用之间时间间隔是固定的,实际间隔忽快忽慢时,微分项的输出就会随机跳动。

解决:用定时器中断作为PID的时钟源,比如配置一个10ms的定时器中断,在中断回调里只做“读取当前误差、调用PID、设置电机输出”这三件事,其他一切任务全部放主循环。如果不想用中断,也可以用HAL_GetTick()判断周期,但要注意主循环里不能有超过控制周期的阻塞操作。判断控制周期是否稳定的办法很简单,在PID调用处翻转一个GPIO,用示波器看这个方波的周期是否均匀,如果明显有抖动,就说明控制周期被干扰了。

5.5 传感器与赛道宽度不匹配,偏差计算失真

现象:你的赛道黑线比传感器排布宽度要宽得多,或者赛道是弯道特别急的折线,小车在过弯时车头冲着线外,但所有传感器都压在线上,偏差计算出0,小车完全失去方向感直行冲出。原因:当黑线宽度大于传感器阵列覆盖宽度时,在某个姿态下确实可能所有探头都落在黑线上,这时候算法认为“完美居中”,但实际上车已经骑在线上甚至偏向一边了。另一种常见情况是大弯道中,外侧的传感器全部超出赛道边线,采集到的是赛道外的地面颜色,和黑线的二值化结果混在一起。

解决:先量一下你的赛道实际线宽和传感器排布宽度,黑线宽度最好介于中间三个传感器的覆盖宽度和中间五个传感器的覆盖宽度之间。如果赛道线很宽,把权重数组改成只激活中间几路,或者增加传感器路数。如果赛道有急弯,可以在代码里加一个“全黑”与“全白”的异常分支,全白时保持上次偏差,全黑时按上一次偏差的方向继续加大转向输出,宁可转多也不要直冲出赛道,等偏差恢复后再回到正常PID控制。

6. 进阶:速度分层与前瞻量,把PID用得更稳

调完一轮PID之后,你会发现用固定参数和固定速度跑赛道,低速能稳过,高速就会在连续弯道里暴露极限。这里有个进阶技巧:把速度分成几个档位,每个档位用不同的PID参数。低速档用完整PD或PID,追求稳定通过;中速档加大kp、减小kd,因为速度上来后转向响应必须更积极;高速档采用“大偏差先降速、小偏差再精细修正”的策略。下面是一个速度分层与转向协调的代码骨架:

typedef enum { SPEED_LOW = 0, SPEED_MID, SPEED_HIGH } speed_level_t; typedef struct { int16_t base_speed; pid_ctrl_t pid_pd; // 高速档用PD pid_ctrl_t pid_full; // 低速档用PID } speed_strategy_t; int16_t calc_turn_with_speed(int16_t err, speed_level_t level, int16_t *base_speed) { if (abs(err) > 80) { // 偏差过大,优先降速并加大转向权重 *base_speed = (int16_t)(*base_speed * 0.6f); return pid_positional_update(&high_speed_pd, err); } if (level == SPEED_LOW) return pid_positional_update(&low_speed_pid, err); else if (level == SPEED_MID) return pid_positional_update(&mid_speed_pd, err); else return pid_positional_update(&high_speed_pd, err); }

这个分层做起来其实不难,关键在于给每个档位单独整定参数。我的习惯是先调低速档,再调中速档,最后调高速档;当你把高速档调好后,中速档和低速档可以直接从高速档参数往回推算,省掉一部分调试工作量。另外一个重要的“前瞻量”思想:传感器不要装在车头正中心,可以稍微向前探出一点,让小车在进入弯道前就能提前检测到黑线走向,相当于给PID算法一个“预感”。这个改动看起来不起眼,但对过弯稳定性影响非常大,我一度因为传感器装在车轴正上方,导致每次进弯都晚半拍,后来把传感器支架往前挪了20mm,同样的参数下过弯速度直接提升了一档。

还有一个小技巧值得提:PID输出不要直接作用于电机PWM,而是先经过一个平滑滤波。常见做法是把本次PID输出和上一次输出做加权平均,比如turn = turn * 0.7 + last_turn * 0.3,这样可以滤掉传感器跳变带来的瞬态冲击,代价是响应变慢。这个技巧在直线段效果明显,但在连续S弯里可能让车变迟钝,所以要注意平衡。我早年调车时吃过亏,把滤波系数设得太激进,小车在S弯里完全跟不上,后来改成只在偏差小于30时才做滤波,大偏差时直通,效果好很多。调巡线小车就是这样,你对控制周期的理解、对限幅的敬畏、对传感器布局的敏感,每一样都比多抄几行PID代码值钱,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询