寻迹小车————循迹主线
进入循迹主线——这是整辆车的大脑也是你调参时 KP8/KD30 为什么有效的原理所在。先看完整代码从三层逐渐讲透代码第一层传感器 → 误差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。丢线处理active0一个传感器都没压线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。如果直接e0车会以为居中直着冲出去。所以短暂丢线沿上次方向追持续丢线原地旋转找。易疑惑点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 还是 LOWHAL 库读 GPIO 电平的函数ports[i]pins[i] 第 i 个传感器的端口引脚比如 GPIOA GPIO_PIN_8 IR_LLINE_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~4last_eint8_t上一拍的 e算 D 项要用d_termint16_te - last_e误差变化量差分kp_x10int16_tKP * 10P 系数放大10倍KP8 → 80kd_x10int16_tKD * 10D 系数放大10倍KD30 → 300/10—计算后除以10把存大10倍的系数换回真实值corrint16_t上面整个表达式算出的修正量就是给轮子的转向力度base_speedint16_t基础速度你调 S车直线行驶的基础 PWMm1/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_sensors5 路加权{-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误差越大修正越大。e2, KP8→corr 8*2 16调 KP8 的意义KP 决定了车反应多猛。太大会冲过头蛇形太小跟不上弯冲线。我实测 KP8 是甜点。每辆车的车重和对地板的摩擦力不同最好自己实测D 项微分kd_x10 * (e - last_e) / 10e - last_e 误差这一拍变了多少 车偏的速度车正在偏 → D 项反向对抗刹车→ 抑制蛇形P 项只看现在偏了多少D 项看偏的势头——车正在偏离D 提前踩刹车车正在回正D 拉住它别冲过头为什么 corr 要 /10kp_x10 是放大 10 倍存的小数KP8 → kp_x1080支持小数如 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/m2int16_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右轮解释一加一减保持平均速度不变只产生转向举例e2 → corr0 → 左轮加速右轮减速 → 车右转看一个具体数字KP8 举例假设e 2线偏右d_term 1还在往右偏KP8, KD30 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);一加一减保持平均速度不变只产生转向。线偏右corr0→ 左轮加速右轮减速 → 车右转 ✅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 值0DIR1RESET, DIR2SETSPEED正转0DIR1SET, DIR2RESET-SPEED反转绝对值0都 RESET0停止三件事讲明白这是本篇干货① 先设方向再设 PWM顺序感每个分支都是先写方向脚(WritePin)→再写PWM(__HAL_TIM_SET_COMPARE)。方向决定转哪边PWM 决定转多快——两件事缺一不可。② 为什么SPEED0要传-SPEED因为 PWM 值不能是负数0~99 占空比。负数只用来表示方向实际给__HAL_TIM_SET_COMPARE的必须是绝对值。SPEED-30→ 传-(-30)30。③__HAL_TIM_SET_COMPARE是什么设置 PWM 占空比 电机转速。填的值越大占空比越高平均电压越高电机转越快。你调S 40就是把这个值设成 40。为什么 change1 / change2 方向脚电平不同/* change1: 正转 DIR1RESET, DIR2SET */ /* change2: 正转 DIR3SET, DIR4RESET ← 反的(注释: 电机2实测反转方向电平对调) */两个电机接线方向不同方向电平要分开定义。我实测发现电机2接反了所以单独把DIR3/DIR4电平对调代码里留有2026-08-19注释。千万别想当然给两个电机用同一组方向电平——焊完一定要单电机实测转向。