☰
寻迹小车————循迹主线
2026/10/10 5:56:59 网站建设 项目流程

进入循迹主线——这是整辆车的"大脑",也是你调参时 KP=8/KD=30 为什么有效的原理所在。先看完整代码:

从三层逐渐讲透代码

第一层:传感器 → 误差(read_sensors)

目标:把"5 个传感器压线状态"变成一个误差数 e,告诉车"线偏了、偏多少、往哪偏"。

int8_t error = 0; uint8_t active = 0; for (int i = 0; i < 5; i++) { if (HAL_GPIO_ReadPin(ports[i], pins[i]) == LINE_ACTIVE_LEVEL) /* 这个传感器压到线了? */ { error += weights[i]; /* 把它对应的权重加进误差 */ active++; /* 记一下"有传感器压线" */ } }

关键:weights 是"位置权重"—— 每个传感器代表"线偏了多少":

const int8_t weights[5] = {-2, -1, 0, 1, 2}; /* 左→右 */
传感器权重含义
左1 (IR_L)-2线在最左边 → 误差-2
左2 (IR_LM)-1线偏左 → -1
中 (IR_C)0居中 → 0
右2 (IR_RM)+1线偏右 → +1
右1 (IR_R)+2线在最右边 → +2

举例:只有中间压线 → 只有权重 0 加进去 →e = 0(完美居中)。只有左1压线 →e = -2(线偏左)。

为什么中间是 0:中间压线 = 车正好在线正上方,不用修。权重代表"偏差方向+大小",居中=0。

丢线处理(active==0,一个传感器都没压线)

if (active == 0) /* 全都没压到线 = 眼前没线 */ { lost_cycles++; if (lost_cycles <= LOST_SPIN_DELAY) /* 短暂丢线(前2个周期) */ { error = last_error * 2; /* 沿上次方向加强修正(防传感器瞬断误判) */ } else /* 持续丢线 */ { line_lost = 1; /* 挂旗子,让 line_follow 去原地旋转 */ } } else { last_error = error; /* 记住这次误差(丢线时要用) */ lost_cycles = 0; /* 清了,不是丢线 */ }

为什么要有丢线处理:直角弯/锐角弯时,线会瞬间从传感器视野消失(00000)。如果直接e=0,车会以为"居中"直着冲出去。所以:短暂丢线沿上次方向追,持续丢线原地旋转找。

易疑惑点:

HAL_GPIO_ReadPin(ports[i], pins[i]) == LINE_ACTIVE_LEVEL这个啥意思last_error是哪来的??

为什么丢线的时候要用的last_error 在没有丢线的时候要errer来提供基础数值,这不是只能证明上一次修正的时候的偏差吗,和下一个弯有何关系??

一,HAL_GPIO_ReadPin(ports[i], pins[i]) == LINE_ACTIVE_LEVEL是什么

这句 ="读这个引脚的电平,判断它是不是'检测到线'的状态":

  • HAL_GPIO_ReadPin= 读某个引脚当前是 HIGH 还是 LOW(HAL 库读 GPIO 电平的函数)
  • ports[i]+pins[i]= 第 i 个传感器的端口+引脚(比如 GPIOA + GPIO_PIN_8 = IR_L)
  • LINE_ACTIVE_LEVEL= 这个模块"压到线时输出什么电平"(你的模块黑线=高电平,所以是GPIO_PIN_SET)
二,last_error是哪来的
static int8_t last_error = 0; /* 模块级(static)变量,存"上一次有误差时"的值 */ ... else /* 有传感器压线(没丢线) */ { last_error = error; /* ★ 每拍都更新:把这次算出的误差存进 last_error */ }

last_error= 记录"最近一次没丢线时的误差"。它只在传感器正常压线时被更新(else分支)。丢线时它已经是"上一次正常时的值"了。

三,重点"上次的偏差和下一个弯有什么关系?"

说实话:没直接关系,甚至可能是错的。这正是丢线处理的先天局限,但还是得用,因为:

① 丢线瞬间,车只有一个"合理猜测"——继续朝刚才的方向转

  • 上一拍车在"往右修"(线在右边),说明线在右侧。现在线"消失了"(00000),车最合理的动作就是继续往右转去找线——毕竟线刚刚还在这边。
  • last_error就是"最后确认的方向",用它 = "朝刚才的趋势继续走一段"。

② 对"瞬间丢线"(短暂丢线)特别有效

  • 很多时候线只是被颠簸/传感器瞬间没采到,1~2 个周期就回来了。这种"假丢线",沿上次方向追一下就找回来,不用真的转圈。
  • 这就是LOST_SPIN_DELAY(前 2 个周期用 last_error,之后才转圈)的原因——先用 last_error 快速处理"假丢线",只有真丢线才转圈。

③ 确实不准,所以才有"升级方案"

  • 你说得对:到了下一个弯,last_error是旧弯的信息,可能完全没用,甚至让你往错误方向转。
  • 所以我们的代码只让它管"短暂丢线"(2 拍内),一旦确认是真丢线,就改用原地旋转找线(line_lost分支)——不再依赖 last_error,而是主动旋转去找线,直到重新看到线。

第二层:误差 → 速度差(line_follow 的 PD)

一、先标清所有"代数"定义(速查表)

名字类型定义作用
eint8_tread_sensors()返回的误差线偏多少、往哪偏(-4~+4)
last_eint8_t上一拍的 e算 D 项要用
d_termint16_te - last_e误差变化量(差分)
kp_x10int16_tKP * 10P 系数放大10倍(KP=8 → 80)
kd_x10int16_tKD * 10D 系数放大10倍(KD=30 → 300)
/10—计算后除以10把存大10倍的系数"换回真实值"
corrint16_t上面整个表达式算出的修正量就是给轮子的"转向力度"
base_speedint16_t基础速度(你调 S)车直线行驶的基础 PWM
m1/m2int16_tbase ± corr左/右轮的最终 PWM

传感器数据接受逻辑别忘

int8_t error = 0; uint8_t active = 0; for (int i = 0; i < 5; i++) { if (HAL_GPIO_ReadPin(ports[i], pins[i]) == LINE_ACTIVE_LEVEL) /* 这个传感器压到线了? */ { error += weights[i]; /* 把它对应的权重加进误差 */ active++; /* 记一下"有传感器压线" */ } }

误差到底怎么算(read_sensors)

  • 5 路加权{-2,-1,0,1,2},一个传感器压线就加它的权重
  • 中间=0(居中不用修)、偏左负、偏右正
  • 举具体例子:「只有左1压线 → e = -2」

二,目标:用前面的e,算出两个轮子的速度差 corr。

int16_t d_term = (int16_t)(e - last_e); /* 误差变化率(微分) */ int16_t corr = (kp_x10 * e) / 10 /* P项 */ + (kd_x10 * d_term) / 10; /* D项 */ last_e = e;

P 项(比例):kp_x10 * e / 10

  • 误差越大,修正越大。e=2, KP=8→corr = 8*2 = 16
  • 调 KP=8 的意义:KP 决定了车反应多猛。太大会冲过头(蛇形),太小跟不上弯(冲线)。我实测 KP=8 是甜点。(每辆车的车重和对地板的摩擦力不同,最好自己实测)

D 项(微分):kd_x10 * (e - last_e) / 10

  • e - last_e= 误差这一拍变了多少= 车"偏的速度"
  • 车正在偏 → D 项反向对抗("刹车")→ 抑制蛇形
  • P 项只看'现在偏了多少',D 项看'偏的势头'——车正在偏离,D 提前踩刹车;车正在回正,D 拉住它别冲过头

为什么 corr 要 /10:kp_x10 是"放大 10 倍存的小数"(KP=8 → kp_x10=80,支持小数如 12.5),所以计算完要 /10 换回真实值。这是定点数存小数的技巧。

常见疑惑:

你说P项是kp_x10 * e / 10为什么乘10??那么corr是啥意思corr = (kp_x10 * e) / 10 + (kd_x10 * d_term) / 10??

这些数字的正负性同时代表着方向性,但是我没有看见你如何调用corr值,怎么使用这些数据呢??

为什么有_x10:

系统支持小数系数(P 12.5),但单片机整数运算快,所以系数×10存成整数,算完÷10——"用整数算小数"的定点数技巧。

corr到底是什么:

corr = 修正量 / 转向力度 / "例如:该往左偏多少"——它告诉车"两个轮子差速要差多少"。

所以D 项不是"延续",是"按变化方向加减"——这正是 P 有"过冲蛇形"、D 能压蛇形的原因。

方向的确认:d_term和e的正负号都代表方向(正=线偏右,负=线偏左),corr 带上符号 → m1/m2 一加一减 → 就转了。

差速转向——corr 怎么变成动作

corr 在line_follow里算出来,紧接着就被用来算 m1/m2:

int16_t corr = (kp_x10 * e) / 10 + (kd_x10 * d_term) / 10; /* ← 算 corr */ last_e = e; /* 记住e给下拍 */ /* 下面:用 corr 算两个轮子 */ int16_t m1 = base_speed + corr; /* 电机1=左轮 */ int16_t m2 = base_speed - corr; /* 电机2=右轮 */ ... change1(m1); /* ← 真正执行:左轮 */ change2(m2); /* ← 真正执行:右轮 */
  • m1 = base + corr(左轮)、m2 = base - corr(右轮)
  • 解释"一加一减":保持平均速度不变,只产生转向
  • 举例:e=+2 → corr>0 → 左轮加速右轮减速 → 车右转

看一个具体数字(KP=8 举例)

假设:e = +2(线偏右),d_term = +1(还在往右偏),KP=8, KD=30 P项 = 8 * 2 / 10 = 1.6 D项 = 30 * 1 / 10 = 3.0 corr = 1.6 + 3.0 = 4.6 m1 = base + 4.6 → 左轮加速 m2 = base - 4.6 → 右轮减速 → 车向右转 ✅

这一部分很考验思维贯通能力,可以多看多思考,画一个图来联系各个具体的参数

第三层:修正量 → 电机(change1/change2)⭐ 本片重点

差速的"关键一行"

int16_t m1 = base_speed + corr; /* 左轮 */ int16_t m2 = base_speed - corr; /* 右轮 */ change1(m1); change2(m2);

一加一减:保持平均速度不变,只产生转向。线偏右(corr>0)→ 左轮加速右轮减速 → 车右转 ✅

change1/change2 的内部:怎么把一个带符号的 int 变成转向+转速

void change1(int16_t SPEED) { if (SPEED > 0) { /* 正转 */ HAL_GPIO_WritePin(DIR1_GPIO_Port, DIR1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(DIR2_GPIO_Port, DIR2_Pin, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, SPEED); } else if (SPEED < 0) { /* 反转 */ HAL_GPIO_WritePin(DIR2_GPIO_Port, DIR2_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(DIR1_GPIO_Port, DIR1_Pin, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, -SPEED); } else { /* 停止 */ HAL_GPIO_WritePin(DIR1_GPIO_Port, DIR1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(DIR2_GPIO_Port, DIR2_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, 0); } }

核心思想:一个 int 用「正负号」编码方向、「绝对值」编码转速。

SPEED方向脚PWM 值
>0DIR1=RESET, DIR2=SETSPEED(正转)
<0DIR1=SET, DIR2=RESET-SPEED(反转,绝对值)
=0都 RESET0(停止)

三件事讲明白(这是本篇干货)

① 先设方向,再设 PWM(顺序感)每个分支都是:先写方向脚(WritePin)→再写PWM(__HAL_TIM_SET_COMPARE)。方向决定转哪边,PWM 决定转多快——两件事缺一不可。

② 为什么SPEED<0要传-SPEED?因为 PWM 值不能是负数(0~99 占空比)。负数只用来表示方向,实际给__HAL_TIM_SET_COMPARE的必须是绝对值。SPEED=-30→ 传-(-30)=30。

③__HAL_TIM_SET_COMPARE是什么?设置 PWM 占空比 = 电机转速。填的值越大,占空比越高,平均电压越高,电机转越快。你调S 40就是把这个值设成 40。

为什么 change1 / change2 方向脚电平不同?

/* change1: 正转 DIR1=RESET, DIR2=SET */ /* change2: 正转 DIR3=SET, DIR4=RESET ← 反的!(注释: 电机2实测反转,方向电平对调) */

两个电机接线方向不同,方向电平要分开定义。我实测发现电机2接反了,所以单独把DIR3/DIR4电平对调(代码里留有2026-08-19注释)。千万别想当然给两个电机用同一组方向电平——焊完一定要单电机实测转向。

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

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

立即咨询