资讯详情

STM32智能小车:五路红外巡线、三路避障与差速控制实战

📅 2026/10/26 3:54:23 | 华诺云谱 👁 阅读
STM32智能小车:五路红外巡线、三路避障与差速控制实战
简介围绕黑线循迹与红外避障展开的STM32智能小车综合实验工程面向刚接触STM32、准备验证小车基础功能的学生与初学者帮助理解循迹、避障及电机控制的代码组织方式与调试思路。压缩包共192个文件约6.49MB以34个.c源文件和35个.h头文件为核心辅以编译中间文件、Keil工程配置及hex、axf烧录文件形成可编译、可下载验证的完整项目。代码强调浅显易懂与详细注释将电机控制封装为前进、刹车、左转、左旋、右转、右旋、后退等函数每个函数均带速度与持续时间参数并通过定时器配置电机便于读者按入口函数梳理运动逻辑再结合红外避障与巡线模块完成综合实验。已有1187人学习适合课堂实验、课程设计或智能小车二次开发也便于在现有工程上继续调试、对照硬件现象调整参数与扩展功能。1. 一辆会画圈的小车红外避障与巡线寻迹到底难在哪把车放地上、按下开关它不往前走先原地画圈——这是我拆这份 ZYSTM32-A1 智能小车工程时看到的第一个画面。工程包做的是两件事五路红外对管读黑线做巡线寻迹三路红外避障模块做前向遮挡判断两路直流减速电机差速转向。它不复杂但足够完整GPIO 输入、定时器 PWM、状态机仲裁、电机方向控制一条最小可用的嵌入式控制链路全在里面。反直觉的地方在于这类小车跑得稳不稳八成不取决于算法写得多漂亮而取决于探头离地多高、间距多宽、电位器拧到什么位置。我见过把 PID 调到参数爆表还是画龙的车最后发现是两个探头间距比黑线宽度还窄。这份工程适合刚摸到 STM32 外设、想找一个能把“读传感器→算偏差→出 PWM”这条链走通的实物项目熟手也能拿它当骨架把权重法换成闭环控制。2. 硬件拓扑与工程骨架一块最小系统板怎么带起两套传感先看它到底由哪些东西拼起来。ZYSTM32-A1 这类实验车的思路很一致主控用一块 STM32F103C8T6 最小系统板电机用 TB6612 或 L298N 双路驱动循迹靠 TCRT5000 红外对管阵列避障靠三只一体化的红外反射模块带比较器、输出数字量、板上带电位器。整套系统没有编码器、没有陀螺仪全靠开环控制加传感器反馈这也是它便宜、好上手的原因。2.1 接线拓扑把外设按“输入/输出/功率”分三组拆开看接线可以归成三组分组清楚以后排查故障会快很多。分组器件典型引脚电气特性传感器输入五路 TCRT5000 循迹PB12~PB15、PA8数字量黑线高/低可选传感器输入三路红外避障PA4、PA5、PA11数字量检测到障碍拉低控制输出TB6612 PWMA/PWMBPA6、PA7TIM3_CH1/CH210kHz PWM控制输出TB6612 AIN1/AIN2/BIN1/BIN2PB0、PB1、PB5、PB6推挽 GPIO功率电机电源 VM电池正极6~9V与 MCU 电源分开走调试串口 / SWDPA9、PA10 / PA13、PA14打印状态用提示表中的引脚是我按这份工程的常见分配整理的一套实际板子上如果跳线不同只要把后面的宏改掉就行代码逻辑一行都不用动。循迹探头一定要接成上拉输入。这些模块大多是开漏或带上拉的比较器输出如果配成浮空输入车一震动读数就乱跳你会以为是算法问题其实是引脚在“飘”。/* 循迹探头五路上拉输入避免悬空抖动 */ #define TR_L2_PIN GPIO_Pin_12 /* 最左 */ #define TR_L1_PIN GPIO_Pin_13 #define TR_M_PIN GPIO_Pin_14 /* 中间 */ #define TR_R1_PIN GPIO_Pin_15 #define TR_R2_PIN GPIO_Pin_8 /* 最右 */ static void Track_GPIO_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Pin TR_L2_PIN | TR_L1_PIN | TR_M_PIN | TR_R1_PIN | TR_R2_PIN; gpio.GPIO_Mode GPIO_Mode_IPU; /* 上拉输入模块开漏时也不会乱跳 */ gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); }这段初始化只做一件事把五个引脚配成上拉输入并打开 GPIOB 时钟。参数里唯一需要留意的是GPIO_Mode_IPU它决定了引脚在模块未驱动时的默认电平如果换成GPIO_Mode_IN_FLOATING车轮一震动读到的位图就会在0x00和0x1F之间反复横跳。2.2 上电初始化顺序时钟在前定时器在最后工程的主函数基本是固定的三段式时钟与中断分组 → 各外设初始化 → 主循环。顺序不能随便换尤其是TIM_Cmd必须放在电机方向引脚初始化之后否则 PWM 会先于方向信号输出上电瞬间两个轮子可能朝反方向各冲一下。int main(void) { /* 1. 中断分组先定后面配 NVIC 才有效 */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); /* 2. 外设逐个初始化顺序GPIO - 定时器 - 串口 */ Track_GPIO_Init(); IR_GPIO_Init(); Motor_GPIO_Init(); /* 方向引脚先给低电平保证电机静止 */ Motor_PWM_Init(); /* 最后才开定时器输出 */ Uart1_Init(115200); /* 调试口用来打印位图 */ /* 3. 主循环按固定节拍跑不在这里做延时等待 */ while (1) { Car_Loop(); /* 内部由 SysTick 的 5ms 标志位驱动 */ if (g_tick_5ms) { g_tick_5ms 0; Debug_Print(Track_ReadRaw(), IR_ReadStable()); } } }逻辑上Motor_GPIO_Init里会先把 AIN1/AIN2/BIN1/BIN2 全部写成低电平这时 TB6612 处于刹车态电机不会动。等 PWM 定时器起来以后占空比初值也是 0所以整车上电是静止的。参数上唯一要改的是串口波特率115200 是为了在调试助手里看位图刷新不卡。2.3 PWM 频率与最小启动占空比一个绕不开的玄学电机 PWM 频率常见做法是 10kHz 到 20kHz 之间。低于 1kHz 电机啸叫明显高于 20kHz 又会让驱动芯片发热变严重。TIM3 挂 APB1时钟 72MHz预分频 71 得到 1MHz 计数时钟自动重装值 99正好 10kHz。static void Motor_PWM_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); /* PA6/PA7 复用推挽接 TB6612 的 PWMA/PWMB */ gpio.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); tb.TIM_Prescaler 71; /* 72MHz / 72 1MHz 计数时钟 */ tb.TIM_Period 99; /* 1MHz / 100 10kHz PWM */ tb.TIM_CounterMode TIM_CounterMode_Up; tb.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, tb); oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_OCPolarity TIM_OCPolarity_High; oc.TIM_Pulse 0; /* 上电占空比 0车不动 */ TIM_OC1Init(TIM3, oc); TIM_OC2Init(TIM3, oc); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE); }参数怎么定TIM_Prescaler和TIM_Period是一对满足72M / (PSC1) / (ARR1) 目标频率即可。TIM_Pulse是占空比的比较值范围 0~100后面所有电机控制都把它当百分比的整数来用。玄学在这里占空比 10% 时电机大概率不转只有轻微嗡嗡声。这不是代码问题是直流减速电机的启动死区。我的经验是从 15%~20% 开始试低于这个值一律提到死区以上否则会出现“小车在该转弯的时候不动、过了弯才猛地窜出去”的怪现象。3. 循迹模块拆解从五路位图到差速转向循迹是这份工程的主线。它的实现思路非常朴素五个探头读出一个五位位图位图算出线相对车身的偏移量偏移量乘以增益变成左右轮的速度差。难点不在控制律在于你怎么把“线偏了多少”这件事量化成一个有符号的数。3.1 探头间距、离地高度与阈值标定先说物理层这决定了后面一切算法的上限。常见排布是等间距 12~15mm探头中心距黑线边缘留一点余量让车在正对线时中间那路压在黑线上、两侧两路在白色背景上。离地高度一般 8~15mm太高会受环境光干扰太低会蹭到地面。标定过程我一般这么做把车架空一路一路拿黑纸片挡在探头下面看串口打印的对应位是不是翻转五个都确认无误后再放地面上看整体位图。正常压线的位图应该只有一个或两个相邻的位为 1如果出现0x1F全黑或0x00全白说明阈值或者高度不对先别急着改代码。/* 读原始五位位图bit4最左 ... bit0最右 */ #define TR_L2() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_12) #define TR_L1() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13) #define TR_M() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) #define TR_R1() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_15) #define TR_R2() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8) static uint8_t Track_ReadRaw(void) { uint8_t v 0; if (TR_L2()) v | 0x10; if (TR_L1()) v | 0x08; if (TR_M ()) v | 0x04; if (TR_R1()) v | 0x02; if (TR_R2()) v | 0x01; return v; }这里把五路读成一个字节而不是五个变量好处是后面查表、打日志、接状态机都方便。参数上的关键点是如果模块上的电位器反相了黑线输出低电平那就把if (xxx)改成if (!xxx)或者干脆在硬件上拧回来别在代码里到处加非。3.2 权重法把五路位图压成一个有符号偏差位图本身没法直接给电机用需要压成一个数。工程里用的是权重法给五个探头各自分配一个固定的转向权重左边负、右边正中间为零然后把所有“看到线”的探头的权重取平均。探头L2L1MR1R2位序bit4bit3bit2bit1bit0权重-2-1012#define TRACK_LOST 0x7F static const int8_t weight[5] { -2, -1, 0, 1, 2 }; /* 输入五路位图输出负数线在左正数线在右TRACK_LOST丢线 */ static int16_t Track_GetError(uint8_t raw) { int16_t sum 0; uint8_t cnt 0; for (uint8_t i 0; i 5; i) { if ((raw (4 - i)) 0x01) { /* bit4 对应 weight[0] */ sum weight[i]; cnt; } } if (cnt 0) return TRACK_LOST; /* 全白车已经冲出线外 */ return sum / cnt; /* 取平均多路同时压线时平滑过渡 */ }这段是整个循迹的核心。为什么要取平均而不是直接求和因为当两个相邻探头同时压在线上的交界处时求和会让偏差突然翻倍车就会一顿一顿地抖取平均后(-1 0)/2 0.5和( 0 1)/2 0.5之间是连续的转向自然顺滑。返回值范围是 -2 到 2正好对应“线在最左”到“线在最右”。TRACK_LOST用 0x7F 是随手选的一个不可能出现的值实际工程里更稳妥的做法是定义一个独立的枚举避免和正常值混淆。3.3 执行层把误差变成两个轮子的占空比偏差算出来以后剩下的就是差速左轮 基础速度 转向量右轮 基础速度 − 转向量。#define BASE_SPEED 55 /* 直道基础占空比按电池电压微调 */ #define KP_TURN 8 /* 转向增益从 8 开始试抖就往下调 */ #define PWM_MIN 18 /* 死区低于此值电机带不动车 */ #define PWM_MAX 100 static void Drive_By_Error(int16_t err) { int16_t turn err * KP_TURN; int16_t l BASE_SPEED turn; int16_t r BASE_SPEED - turn; /* 内轮不要压到 0否则出弯瞬间变成原地转很肉 */ if (l PWM_MIN) l PWM_MIN; if (r PWM_MIN) r PWM_MIN; Motor_Set(l, r); }KP_TURN是这套方案里唯一需要反复试的参数。开到 15 以上车在直道上会左右画龙因为你把探头的离散跳变放大得太狠开到 4 以下遇到急弯根本拐不过来车会直接冲出去。我一般从 8 开始先把BASE_SPEED压到 40 试稳定了再把速度提上去。PWM_MIN这个钳位很关键。如果不加转弯时内轮占空比会算成负数Motor_Set收到负值就会反转车在弯道上会出现“原地打转”而不是“顺畅过弯”这个现象我调了两天才反应过来是钳位没做。4. 红外避障与电机驱动两套逻辑怎么不打架避障是第二套逻辑和循迹是并行运行的互相冲突。正前方有障碍就得停下来但循迹又要求它一直往线上贴。这两套逻辑如果写成if (循迹) {...} if (避障) {...}的并列结构车的行为会变得完全不可预测——有时候碰到墙还往前顶有时候明明线在左边却突然右转。4.1 红外模块的电平、遮挡与去抖这类一体化红外避障模块内部是“发射管 接收管 比较器 电位器”检测到反射物时输出拉低。它的量程通常只有 2~30cm靠电位器调很吃被测物的颜色和反光率白墙能测到 25cm黑色哑光布可能 8cm 就开始误报。场景表现处理正对白墙稳定拉低距离远正常可适当调小量程深色物体距离明显变短降低速度留更大余量强光直射一直拉低不恢复加遮光筒探头下沉安装地面反光间歇性误触发抬高安装角度别对着地面去抖不能省。红外模块输出的是比较器的开关量边缘会有几十微秒的抖动直接进主循环会被当成连续多次触发。我用的是最简单的三次采样取与/* 返回 bit0左 bit1前 bit2右某个位为 1 表示该方向确认有障碍 */ #define IR_LEFT() (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4) Bit_RESET) #define IR_FRONT() (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5) Bit_RESET) #define IR_RIGHT() (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_11) Bit_RESET) static uint8_t IR_ReadStable(void) { uint8_t m 0xFF; for (uint8_t i 0; i 3; i) { uint8_t s 0; if (IR_LEFT ()) s | 0x01; if (IR_FRONT()) s | 0x02; if (IR_RIGHT()) s | 0x04; m s; /* 三次采样取“与”只有持续检测到才认 */ Delay_ms(2); } return m; }这里用“与”而不是“或”是因为避障误报的代价比漏报高——漏报顶多撞一下车速不快误报会让车在路上莫名其妙地停住或者乱转。三次采样间隔 2ms加起来 6ms对 5ms 的主循环节拍来说稍微长了点实际工程里我会把这个函数拆成非阻塞的状态机不过对实验车来说这点延迟无所谓。4.2 驱动层一个 Motor_Set 收口所有方向逻辑方向控制最容易翻车的地方是“正负号约定不统一”。循迹层认为正数是前进避障层可能认为正数是后退最后车的行为就变成玄学了。解决办法只有一个所有电机操作都必须走同一个函数。#define AIN1_H() GPIO_SetBits(GPIOB, GPIO_Pin_0) #define AIN1_L() GPIO_ResetBits(GPIOB, GPIO_Pin_0) #define AIN2_H() GPIO_SetBits(GPIOB, GPIO_Pin_1) #define AIN2_L() GPIO_ResetBits(GPIOB, GPIO_Pin_1) #define BIN1_H() GPIO_SetBits(GPIOB, GPIO_Pin_5) #define BIN1_L() GPIO_ResetBits(GPIOB, GPIO_Pin_5) #define BIN2_H() GPIO_SetBits(GPIOB, GPIO_Pin_6) #define BIN2_L() GPIO_ResetBits(GPIOB, GPIO_Pin_6) /* 约定正值前进负值后退绝对值是占空比 0~100 */ static void Motor_Set(int16_t left, int16_t right) { if (left PWM_MAX) left PWM_MAX; if (left -PWM_MAX) left -PWM_MAX; if (right PWM_MAX) right PWM_MAX; if (right -PWM_MAX) right -PWM_MAX; /* A 路TB6612 的 AIN1/AIN2 决定方向PWMA 决定快慢 */ if (left 0) { AIN1_H(); AIN2_L(); } else { AIN1_L(); AIN2_H(); } TIM_SetCompare1(TIM3, (uint16_t)(left 0 ? left : -left)); /* B 路同理 */ if (right 0) { BIN1_H(); BIN2_L(); } else { BIN1_L(); BIN2_H(); } TIM_SetCompare2(TIM3, (uint16_t)(right 0 ? right : -right)); }这个函数的价值在于方向由符号决定速度由绝对值决定硬件细节被完全封装。后面无论是循迹、避障还是停车调用的都是它符号约定只有一套。如果某天发现左轮方向反了只需要把 AIN1/AIN2 两行对调其他代码一行都不用动。参数说明left/right的绝对值上限是PWM_MAX因为TIM_SetCompare1的比较值范围就是 0~TIM_Period超了会被硬件截断成满占空比反而掩盖了逻辑错误。4.3 仲裁用状态机而不是 if 套 if两套逻辑必须定优先级避障 循迹。我一般用一个显式状态机来写而不是靠if的先后顺序因为状态机能表达“避障动作要持续多久”这种时间维度的信息。typedef enum { MODE_TRACK 0, MODE_BACK, MODE_AVOID } car_mode_t; static car_mode_t s_mode MODE_TRACK; static uint16_t s_tick 0; /* 每 5ms 调用一次 */ void Car_Loop(void) { uint8_t ir IR_ReadStable(); switch (s_mode) { case MODE_TRACK: if (ir 0x02) { /* 正前方有障碍优先级最高 */ Motor_Set(-PWM_MIN, -PWM_MIN); /* 先退一点拉开距离 */ s_tick 30; s_mode MODE_BACK; /* 后退 150ms */ } else { int16_t err Track_GetError(Track_ReadRaw()); if (err ! TRACK_LOST) Drive_By_Error(err); else Motor_Set(0, 0); /* 丢线就地停车等人工干预 */ } break; case MODE_BACK: if (--s_tick 0) { s_mode MODE_AVOID; s_tick 40; } break; case MODE_AVOID: if (ir 0x01) Motor_Set( 60, -40); /* 左边堵往右拐 */ else if (ir 0x04) Motor_Set(-40, 60); /* 右边堵往左拐 */ else Motor_Set( 55, -55); /* 都堵原地掉头 */ if (--s_tick 0) s_mode MODE_TRACK; break; } }MODE_BACK那一小段后退是血泪经验换来的。如果检测到障碍立刻转向车头离障碍物太近转弯半径扫过去还是会蹭到先退 15cm 左右再转成功率高很多。s_tick的计数值按 5ms 节拍换算30 就是 150ms40 就是 200ms这两个数按车速和障碍距离调。TRACK_LOST的处理我选了停车而不是继续搜索。继续搜索的版本在网上很常见——原地转圈找线——但在有桌沿的地面上这很危险车会一边转一边前进。实验车更稳妥的做法是停下来串口打一行日志人过去把车放回线上。想做成搜索版本把Motor_Set(0, 0)换成带超时的转向即可。5. 避坑与排查五条我真实踩过的坑这一章是按“现象 → 原因 → 解决”整理的排障记录。这几条基本覆盖了这类实验车 90% 的现场问题遇到车不听话从上往下对一遍比反复改参数快得多。5.1 现象上电后两个轮子一正一反车原地打转原因左右电机的接线顺序反了或者Motor_Set里 B 路的BIN1/BIN2写反了。TB6612 的两路通道物理上是对称的但电机引线焊上去的时候很容易把正负搞混。解决把车架空串口发一条固定指令Motor_Set(50, 50)看两个轮子是不是同向转。如果反向先把该路电机的两根线对调或者把BIN1_H(); BIN2_L();改成BIN1_L(); BIN2_H();。别用代码里的负号去“修”它一旦开始这么干后面所有正负约定都会乱掉。5.2 现象串口打印的位图全是 0x1F 或全是 0x00原因阈值没标定。全 0x1F 是所有探头都认为自己在黑线上全 0x00 是都认为在白底上。常见诱因是环境光变了白天和晚上差异很大、探头离地太高、模块上的电位器被碰过。解决架空车拿一张黑纸片逐个挡探头看对应位是否翻转。如果某个探头始终不翻转拧模块上的电位器直到指示灯在挡与不挡时明确变化。五个都通过后再放地面上验证整体位图正常压线应该只有一到两个相邻位为 1。5.3 现象直道上左右画龙越调参数抖得越厉害原因转向增益太大或者采样周期太慢。5ms 一次和 20ms 一次的表现完全不同——周期长了车已经偏出去很远才反应过来一打方向就过头然后再反向修正形成振荡。解决先把BASE_SPEED降到 40把KP_TURN从 8 往下调到 4~5确认车能勉强走直线再逐步把增益加回去找到刚开始抖的那个值的 70% 作为最终参数。如果降到 3 还抖那就是采样周期的问题把Car_Loop的调用节拍从 20ms 改成 5ms。5.4 现象避障模块在空旷地面上一直报有障碍原因安装角度朝下地面反光被当成了障碍物。红外反射式模块对光滑地面、浅色瓷砖特别敏感尤其是探头下沉安装的时候。解决把探头改成水平略微上扬 5°~10°或者在探头外面套一截黑色热缩管当遮光筒。改完还是误报就把模块上的电位器往“灵敏度低”的方向拧牺牲一点量程换稳定。别指望软件滤波能救回来——如果物理层一直输出低电平任何去抖算法都会把它当成真实信号。5.5 现象电机一转程序就跑飞或者复位原因电机电流走了 MCU 的地线启动瞬间的大电流在地线上产生压降MCU 的供电被拉低触发复位。这是这类小车最典型的电源干扰问题而且表现很随机容易让人误以为是代码 bug。解决三件事一起做——电机电源和 MCU 电源分开走线只在地线单点汇聚电机电源端并一个 100μF 以上的电解电容吸收启动冲击MCU 供电端加 0.1μF 去耦。如果还不行就在电机电源串一个肖特基二极管防止反灌。改完这些那种“调试器一停程序就正常、一跑就挂”的怪现象基本就消失了。6. 二次开发把权重法换成闭环控制改动只有三处权重法够用但它有个天花板偏差量只有 -2 到 2 五个离散值控制输出也是阶跃的车速一快就跟不上。想往上走把Drive_By_Error换成增量式 PID改动点其实只有三个。第一处是误差来源。Track_GetError的返回值仍然是 -2 到 2但不再是直接乘增益而是作为 PID 的输入。第二处是控制律加一个 PID 结构体和一个计算函数。typedef struct { float kp, ki, kd; float err_last; float integral; float out_max; } pid_t; static float PID_Calc(pid_t *p, float err, float dt) { p-integral err * dt; /* 积分限幅防止长时间丢线后积分饱和回线时猛冲 */ if (p-integral 50.0f) p-integral 50.0f; if (p-integral -50.0f) p-integral -50.0f; float d (err - p-err_last) / dt; p-err_last err; float out p-kp * err p-ki * p-integral p-kd * d; if (out p-out_max) out p-out_max; if (out -p-out_max) out -p-out_max; return out; }第三处是调用点。在Car_Loop里把Drive_By_Error(err)换成Drive_By_Error(PID_Calc(g_pid, err, 0.005f))dt传 0.005 对应 5ms 节拍。参数整定顺序我一般按 kp → kd → ki 来先只开 kp 让车能大致跟上线再加 kd 压振荡最后加一点点 ki 消除直道上的静态偏移。kp 从 6 左右起kd 大约是 kp 的 5~10 倍因为误差是离散小整数微分项本身就小ki 给 0.1 以内就够。验证方法别靠肉眼。工程里的串口打印这时候就派上用场了把每拍的err、PID 输出、左右占空比一起打到上位机用串口助手存成 CSV看误差曲线是收敛还是发散。我个人的习惯是任何一次改了控制参数都先架空跑十秒看占空比有没有异常饱和再放地上跑。最后一个提醒也是我调这类小车最深刻的一课不要在没有日志的情况下改参数。有一年冬天我在实验室地板上反复调KP_TURN改了二十几版都觉得是玄学最后插上串口一看五个探头里有两个根本没焊好位图一直是 0x0C。从那以后我每次动控制参数之前都强制先把原始位图打出来确认一遍再谈算法。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑