尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
STM32智能小车实战:PWM调速、循迹、避障、跟随与红外遥控设计
简介这是一份基于STM32F103ZET6单片机的智能小车系统设计论文系统讲解PWM调速、红外循迹、红外避障、超声波避障、障碍物跟随以及红外遥控等功能的实现方法适合单片机、嵌入式方向的在校学生以及需要完成课程设计或毕业设计的开发者参考。资源以单个Word文档形式提供文件总数为1个文档篇幅约65页、约1.58万字压缩包大小为34.15MB。文中从硬件电路设计、软件代码说明到程序流程图均有详细叙述并重点介绍了Keil开发环境与mcuisp烧录工具的使用流程同时结合芯片引脚功能、电机驱动电路和传感器接口等关键细节完整呈现智能小车的设计思路、调试过程与问题处理方法。文档对每种功能模块都给出了独立的实现分析与程序流程图例如PWM占空比调节电机速度的控制策略、红外传感器黑白线检测逻辑、超声波模块测距的触发时序等能够帮助读者从原理到代码层面掌握完整闭环便于快速复现或二次开发。无论用于毕业设计参考还是竞赛备赛都能从中获得完整的系统设计框架。该资源已有1357人学习下载是学习STM32综合应用、掌握PWM控制与多传感器融合设计的实用参考资料。1. 一个标题装下五种功能这台STM32智能小车设计到底在做什么拿到“基于STM32F103ZET6单片机的PWM调速、循迹、避障、跟随、红外遥控智能小车设计”这个标题时我第一反应是这大概率是一份课程设计或毕业设计里反复出现的老题目很多平台都能搜到各种版本。但真正动手做过的人会告诉你难点从来不在“把五个模块分别点亮”而在于让它们在同一块主控上稳定协作。PWM调速管着前进后退是否顺滑循迹管着贴线走还是左右画龙避障管着会不会撞墙跟随管着能不能咬住目标红外遥控管着模式切换是否即时——任何一个环节没处理好整车行为都会很难看。适合谁看正准备做课设、搞电子设计竞赛、或者想从点灯进阶到“让小车跑起来”的开发者。2. 先把车架起来硬件选型、模块划分与接线顺序很多人拿到这类题目就急着写代码结果车都还没在地上跑起来就先被杜邦线干掉半天。我习惯先把整车按信号流向拆开再确定每一层的模块型号和接口最后才动手接线。这样出问题时能快速定位是感知、决策还是执行层的锅而不是整块板子一起做一次大排查。2.1 整车按信号流向拆成四个模块这辆车的本质是一个闭环系统感知层采集环境信息决策层STM32F103ZET6根据模式跑逻辑执行层通过PWM调速驱动电机供电层给所有部分提供能量。这个分层思路对后续调试非常重要。感知层有三类输入循迹用灰度传感器常见是3路或5路独立探头模拟输出或者带比较器输出避障和跟随都用同一个超声波测距模块常见的是HC-SR04这一类一个TRIG引脚发脉冲一个ECHO引脚回收波高电平持续时间红外遥控则用一个一体化接收头输出解调后的NEC协议波形。这三类信号的共同点是电平都在0~3.3V或者0~5V范围内可以直接接STM32的GPIO、ADC或者EXTI引脚不需要额外做电平转换。决策层就是主控板本身。STM32F103ZET6是144引脚的增强型型号主频72MHz片上资源很充裕定时器有TIM1~TIM8ADC有3个外部中断线也有16条。做这台小车资源基本不会有瓶颈。要注意的是选主板时尽量选已经把晶振、复位电路、USB转串口都做好了的最小系统板不要自己从零搭。之前有人图便宜用了一片空芯片自己焊最小系统结果系统时钟不稳超声波距离计算直接漂了几十厘米最后花了很长时间排查通信问题才发现是晶振负载电容没加。执行层的核心是两个带减速箱的直流电机加上一个双路电机驱动模块。市面上常见的方案是针对L298N这一类驱动板通过两个PWM引脚控制左右轮转速四个方向引脚控制正反转。供电层则需要注意电机驱动板需要从电池组直接供电而主控、传感器、接收头需要稳定的5V或3.3V。如果全部挤在一个小降压模块上电机启动瞬间会把电压拉低主控直接复位。这也是联调阶段最典型的翻车点后面详写。2.2 一份不用来回返工的接线顺序清单接线顺序比很多人想象的重要。我建议按“先电机驱动、再传感器、最后电源”的顺序操作具体分配如下表所示功能引脚分配备注左轮PWMPB6TIM4_CH1定时器通道引脚固定不能随意改右轮PWMPB7TIM4_CH2同上左轮方向PA4、PA5接驱动板IN1、IN2右轮方向PA6、PA7接驱动板IN3、IN4循迹左/中/右PA0、PA1、PA2复用为ADC输入超声波TRIGPA8普通GPIO输出超声波ECHOPB5普通GPIO输入可复用外部中断红外接收头PC13下降沿触发外部中断引脚分配这里有几个容易踩的硬约束。首先是PWM引脚必须查定时器通道表比如TIM4的通道1默认在PB6通道2在PB7你如果想挪到其他引脚需要开AFIO重映射而且重映射后不能和别的外设冲突。其次PA0、PA1、PA2既是ADC通道也是外部中断源但在这台小车里它们更适合做ADC因为循迹采集的是连续模拟量。最后ECHO引脚最好选一个支持输入捕获的定时器通道这样后续可以不用阻塞等待回波。如果只当普通GPIO读也可以但测距周期会被拉长。接线顺序我一般这样安排先把电机接到驱动板输出端再把方向引脚和PWM引脚从主控连到驱动板然后接传感器信号线最后才接电源线。为什么电源要放在最后一步因为如果信号线接错、或者电机线接反上电瞬间就会出现电流异常驱动器或者主控可能直接烧掉。接完每一根线通电前用万用表量一下信号脚对地电压确认没有短路再开电这是低成本高收益的动作。3. 用定时器输出PWM调速从寄存器到带缓冲的驱动层在这一类小车设计里电机的控制质量直接决定了整车的行驶质感。PWM调速的方案几乎是唯一选择但真正让车走得顺、不突然点头的不是PWM本身而是它的缓冲逻辑和加减速策略。3.1 为什么选PWM而不是模拟电压调速给直流电机调速有几种思路直接改变电枢电压、串联电阻分压、用PWM占空比控制平均电压。前两种在嵌入式小车里基本没人用因为效率低、控制精度差而且需要额外的模拟电路。PWM的优势在于主控只输出数字波形占空比修改等效于改变电机两端平均电压管子工作在开关状态损耗小响应速度快因为系统频响受限于开关频率而不是电源响应。因此STM32只需要用定时器产生两路频率和占空比可调的方波就能完成速度控制。PWM频率的选择有一个经验区间。太低几十Hz电机会发出明显啸叫声电流纹波大太高几十kHz以上MOS管开关损耗增大驱动模块发热明显而且低速段会感觉启动力矩不够。常见做法是取1kHz到10kHz之间。我在这台车上习惯用1kHz因为1kHz的定时器计算最简单同时听不到明显噪声。要说明的是F103的定时器计数频率由72MHz主频决定所以下面代码里的预分频和周期参数不是随手写的而是把总频率一步步分到想要的PWM频率。3.2 最小可用的PWM输出代码与参数解释以TIM4通道1、通道2为例初始化代码如下void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; /* 1. 开启GPIOB时钟和AFIO时钟因为PB6/PB7要复用为定时器输出 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); /* 2. TIM4在APB1总线上注意别开错时钟 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); /* 3. 配置PB6/PB7为复用推挽输出 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); /* 4. 定时器时基72MHz/72 1MHz计数频率 */ TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; /* 5. 自动重装载值1000PWM频率 1MHz/1000 1kHz */ TIM_TimeBaseStructure.TIM_Period 1000 - 1; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseInit(TIM4, TIM_TimeBaseStructure); /* 6. PWM模式1计数小于比较值时输出有效电平 */ TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInitStructure.TIM_Pulse 0; /* 初始占空比0% */ TIM_OC1Init(TIM4, TIM_OCInitStructure); TIM_OC2Init(TIM4, TIM_OCInitStructure); TIM_Cmd(TIM4, ENABLE); }这段代码里最关键的参数是预分频器和自动重装载值。72MHz经过72分频得到1MHz计数频率也就是说计数器每1us加一。自动重装载值设置为999即计数器从0数到999一个周期正好1000us 1ms所以PWM频率为1kHz。比较值寄存器CCR的取值范围是0~999写入多少占空比就是千分之几。例如写入500占空比50%对应电机平均电压约等于电源电压的一半。这个整数关系在后续调速度时非常好用直接以500代表50%的占空比。逻辑上初始化本身只是把引脚和定时器工作模式配置好真正对外表现是不断修改CCR寄存器。所以还需要提供一个速度设置函数并且把占空比换算暴露给上层调用者void set_motor_speed(uint8_t motor, int16_t speed_pct) { uint16_t ccr_value; if (speed_pct 1000) speed_pct 1000; /* 上限保护 */ if (speed_pct -1000) speed_pct -1000; /* 下限保护 */ ccr_value (speed_pct 0) ? (uint16_t)(-speed_pct) : (uint16_t)speed_pct; if (motor MOTOR_LEFT) { TIM_SetCompare1(TIM4, ccr_value); } else { TIM_SetCompare2(TIM4, ccr_value); } }这里用正负号表示电机方向正数前进负数后退但实际PWM模块输出不了负电压真正的方向切换靠方向引脚的电平组合。所以调用这个函数时还需要配合一组设置方向引脚状态的函数。你会发现我故意把PWM数值范围写成-1000到1000而不是-100到100主要是为了和CCR寄存器范围对齐减少换算出错。3.3 占空比映射与加减速曲线直接把某个目标速度写到CCR车会很难受启动瞬间电流冲击大、容易打滑速度切换时车头会猛点头。更合理的方式是软件上做一个“缓冲层”让目标速度一步步逼近而不是一步到位。我见过不少人直接用一个延时循环做缓冲最简单做法是这样void set_motor_speed_smooth(uint8_t motor, int16_t target) { int16_t current get_motor_current_speed(motor); /* 记录当前值 */ while (current ! target) { if (current target) current 10; else current - 10; set_motor_speed(motor, current); delay_us(200); /* 每步间隔200us步进10个单位 */ } }这段代码的逻辑是每次只变化10个单位也就是占空比的1%每隔200us变化一次。假设当前是0目标是500完成整个加速过程需要50步总耗时约10ms。如果整车质量偏大或者地面摩擦小可以把步进改成5、间隔改成500us把加速时间拉到50ms以上起步会更平缓。这个“逐步逼近”的思路被不少人称为打死方向突变本质上是一个简易梯形加减速算法。更复杂的S型曲线在这台小车上没必要运算开销大不说肉眼也基本分辨不出来。参数上需要调的地方有两个步进大小和步进间隔。步进越小速度变化越细腻但达到目标速度的时间越长步进间隔越大同理。如果发现电机低频震动还要注意PWM频率是否太低把它调高到5kHz再试。这部分我一般是通电后拿手持转速表验证没有仪器的话听电机啸叫、看启动是否打滑也能判断个大概。4. 循迹、避障、跟随与红外遥控三种传感策略共用一个决策框架功能多了以后最常见的误区是把每一种模式写成一套独立的代码各跑各的循环最后挤在一起处理不过来。我从一开始就按“统一读取传感器数据统一计算目标速度再由模式开关决定用哪条策略”的思路来做。这样做的好处是代码结构清晰切模式时不会出现某一时刻两套逻辑同时对电机发指令。4.1 循迹三路灰度传感器的阈值判断循迹的原理很简单黑线的反射率比白色地面低灰度传感器输出的电压会随反射光变化。每个传感器探测一个点把三个点的黑/白状态组合起来就能判断小车相对于黑线的位置。用ADC读取比直接读数字输出更灵活因为你可以边跑边观察阈值而不需要拿螺丝刀去拧模块上的电位器。void tracking_loop(void) { uint16_t left read_adc(CH_GRAY_LEFT); /* 0~4095 */ uint16_t mid read_adc(CH_GRAY_MID); uint16_t right read_adc(CH_GRAY_RIGHT); uint8_t pattern 0; if (left TRACK_THRESHOLD) pattern | 0x04; /* 压线置1 */ if (mid TRACK_THRESHOLD) pattern | 0x02; if (right TRACK_THRESHOLD) pattern | 0x01; int16_t base 500; /* 基准差速基准50%占空比 */ int16_t diff 0; switch (pattern) { case 0x02: /* 010 中间压线直线 */ diff 0; break; case 0x04: /* 100 左传感器压线车偏右 */ diff 150; /* 左轮减速、右轮加速 */ break; case 0x01: /* 001 右传感器压线车偏左 */ diff -150; break; case 0x00: /* 丢线 */ if (last_pattern 0x04) diff 200; else if (last_pattern 0x01) diff -200; else diff 0; break; default: if ((pattern 0x06) 0x06) diff 120; /* 110 左弯 */ else if ((pattern 0x03) 0x03) diff -120; /* 011 右弯 */ break; } last_pattern pattern; set_motor_speed_smooth(MOTOR_LEFT, base - diff); set_motor_speed_smooth(MOTOR_RIGHT, base diff); }这里的核心理解点是左侧传感器压线说明车头已经偏到黑线右边所以要把车头往左拉回来。怎么拉左轮减速、右轮加速产生一个向右的转向力矩细心看代码就会发现逻辑是“左轮减、右轮加”车头其实是向右偏的。错了纠正一下左侧压线说明车身整体右偏黑线在车的左边这时需要向右转回赛道不对让我们重新想左侧轮压到黑线说明黑线位置在左传感器下方且车头整体偏右。这时候应该让车头向左转也就是左轮加速、右轮减速。但很多代码用了相反的处理导致越修越偏。我把上面代码修正为case 0x04左压线时左轮加速、右轮减速即diff赋值要保证左轮收到basediff、右轮收到base-diff。让命名与实际作用一致避免误导。继续说明丢线处理当三个传感器都离开黑线时不能原地不动要按最近一次有效状态继续向原方向修正。这个逻辑对直角弯尤其重要否则车到弯道一丢线就直冲出去。灰度传感器还有一个绕不开的麻烦环境光和地面材质不同同样的黑线在不同光照下读出数值变化很大。所以阈值不能写死在程序初始化时做一次“白底采样黑线采样取中值”或者支持串口在线标定。第六章会讲怎么在跑动中调这个参数。4.2 避障超声波测距和转向策略避障模式的核心是测距和转向决策。测距用超声波模块过程就是发一串脉冲然后测回波高电平的持续时间。因为声速约340m/s回波是往返路程所以距离 时间 × 0.34 / 2单位毫米。uint32_t measure_distance_mm(void) { uint32_t t0, t1; GPIO_SetBits(TRIG_PORT, TRIG_PIN); /* 拉高TRIG */ delay_us(12); /* 大于10us即可 */ GPIO_ResetBits(TRIG_PORT, TRIG_PIN); while (GPIO_ReadInputDataBit(ECHO_PORT, ECHO_PIN) 0) { /* 等待回波高电平 */ } t0 timer_get_us(); /* 记录高点开始时间 */ while (GPIO_ReadInputDataBit(ECHO_PORT, ECHO_PIN) 1) { /* 等待波结束 */ } t1 timer_get_us(); /* 记录高点结束时间 */ return (uint32_t)((t1 - t0) * 0.34f / 2.0f); }这段代码有一个明显的坑两个while等待属于阻塞等待如果前方没有障碍物ECHO不会返回高电平程序就会卡死在这里。标准做法是加超时保护例如超过50ms就退出并返回最大距离。代码里timer_get_us这个函数在F103上可以用一个自由运行的定时器实现或者直接用SysTick做微秒计时。建议让这个函数每50ms调用一次同时连续采3次取中位数避免偶尔的尖峰毛刺导致误判。避障的转向策略不要写得太死板。常见做法是距离大于40cm直行小于40cm停车原地右转1秒然后继续前进如果转完后还是近再左转。这个逻辑在开阔场地够用但遇到墙角很容易原地打转。改进思路是记录最近两次的转向方向如果上一次右转后障碍依然存在下一次就尝试左转。这个行为很像一个线性搜索过程虽然笨但可靠。4.3 跟随把避障逻辑反过来用的死区控制跟随和避障共用超声波测距但决策逻辑正好相反。避障是“距离近了远离”跟随是“距离远了靠近太近了退后”。如果处理成粗暴的开关控制比如小于20cm后退、大于40cm前进小推车会在边界处来回耸动体验非常差。更好的做法是让速度随距离差值线性变化。假设期望跟随距离为30cm上下各留10cm死区void follow_loop(void) { uint32_t dist measure_distance_mm(); int16_t speed 0; if (dist 200) { speed -200; /* 太近后退 */ } else if (dist 300) { speed (int16_t)((dist - 200) * 2); /* 20~30cm速度从0线性增加 */ } else if (dist 400) { speed (int16_t)((300 - dist) * 2); /* 30~40cm速度从0反向线性增加? */ } else { speed 200; } set_motor_speed_smooth(MOTOR_LEFT, speed); set_motor_speed_smooth(MOTOR_RIGHT, speed); }让我重新理清跟随的线性映射。设目标为30cm低于20cm后退固定速度-20020~30cm之间距离越近需要后退距离越接近30cm速度越接近030~40cm之间距离越远越要加速前进超过40cm固定200前进。所以正确逻辑是dist 200speed -200200 dist 300speed -(300 - dist) * 2距离290时speed-20接近目标时几乎停止300 dist 400speed (dist - 300) * 2距离310时speed20dist 400speed 200这样速度曲线在距离维度上是连续的不会出现20cm处-20021cm处直接跳0的突变。这个线性化思想是整个跟随模式的核心比傻乎乎的开关控制实用得多。4.4 红外遥控NEC协议解码与模式切换红外遥控通常用NEC协议。一体化接收头输出的波形里引导码是一段9ms低电平4.5ms高电平数据位通过脉宽区分“0”是560us低560us高总周期约1.125ms“1”是560us低1.69ms高总周期约2.25ms。解码思路就是测量每个数据位的高电平或低电平持续时间。用外部中断加定时器计数来解void EXTI15_10_IRQHandler(void) { uint32_t now TIM_GetCounter(TIM2); uint32_t gap now - last_count; if (EXTI_GetITStatus(EXTI_Line15) ! RESET) { if (GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13) 0) { if (gap 13000) { /* 引导码结束开始收数据 */ ir_bit_idx 0; ir_data 0; } else if (gap 2000) { /* 高电平持续时间约1.7ms判为1 */ ir_data | (1UL ir_bit_idx); ir_bit_idx; } else if (gap 1000) { /* 高电平持续时间约560us判为0 */ ir_bit_idx; } if (ir_bit_idx 32) { ir_ready 1; } } last_count now; EXTI_ClearITPendingBit(EXTI_Line15); } }这里最关键的是gap的计算。我用的定时器计数频率是1MHz所以计数差值就是微秒数。如果定时器是16位的并且没有开启更新中断计数到65535会回绕到0gap就会变成一个很小的值导致解码全乱。解决方法是把TIM2设置为自由运行并在更新中断里把溢出次数累加计算gap时考虑溢出。这段代码里13000、2000、1000三个阈值对应NEC协议的引导码4.5ms、逻辑1高电平1.7ms、逻辑0高电平560us中间留了足够裕量。解码出32位数据后其中低8位是命令码高16位是设备地址码还有8位反码用于校验。主循环里一旦发现ir_ready置位就解析这32位按不同命令码切换运行模式。要注意的是不要在主循环里一遍遍地打印调试信息串口打印会占用几十毫秒可能导致解码中断缓冲区被覆盖漏掉下一次按键。5. 联调阶段高频坑位排查跑不直、乱转向、电机启动就复位写完整车代码后真正耗时间的往往是联调。这部分我整理了五个高频踩坑点每一条都按现象、原因、解决的顺序写方便你对照排查。5.1 现象电机一启动单片机就复位如果小车接通电源一切正常但只要电机开始转屏幕或调试器立刻断连大概率是供电问题。电机启动瞬间电流可以达到正常工作电流的几倍如果主控和电机共用同一条细导线取电导线上的压降会瞬间把主控供电拉到低于复位阈值。解决方法是电源分层电池输出先到电机驱动板的电源输入端再从驱动板或另接一个降压模块给主控板供电。电机电源和主控电源在电池端汇合但要尽量各自用独立的线束回到电池避免大电流路径穿过主控电路。还有一个辅助手段是软件上的PWM缓冲也就是第三章讲的逐步加速能显著降低启动冲击。5.2 现象循迹车到十字路口直接冲出赛道三路循迹传感器在普通弯道表现还可以但到了十字路口三个探头可能同时压在横向黑线上或者全部丢线如果策略里没有对“全黑”和“全白”做特殊处理车就会按直行继续跑直接冲出赛道。这是因为三路传感器的视野太窄无法区分“正在过十字路口”和“前方大范围黑线”。解决办法有三个方向第一升级到五路传感器用左右外侧探头检测横向线第二在代码里对全黑或全白状态增加记忆逻辑比如进入全黑后继续按上一个有效状态行驶直到重新压线第三从机械结构上把传感器往车头两侧拉开一些让检测覆盖宽度略大于黑线的横向宽度。最省钱的还是第二种改几行代码的事。5.3 现象避障的时候小车原地打转越转越乱超声波模块的波束有一定扩散角当障碍物位于车头正前方偏左或偏右约30°的位置时测量结果会在有障碍和无障碍之间跳动。如果测距结果跳变转向策略就会一会儿判断有障碍要转一会儿判断无障碍要前进结果车头来回摆动看起来就是原地打转。原因是单帧测距不够稳定且转向时间固定。解决方法是把测距结果做中位滤波连续测5次取中间值消除偶然的毛刺同时转向动作完成后强制前进一小段时间让传感器离开障碍物的边缘区域再进行下一次测距。从制度上避免“测一次就决策一次”带来的抖动。5.4 现象跟随模式忽快忽慢一冲一停跟随模式下如果车在40cm处还全速前进到了30cm处立刻变020cm处立刻后退那么车就在死区边界反复横跳。原因是底层控制是离散的二值开关没有在目标距离附近做过渡。解决方法是把速度映射改为线性按上一章的公式调整。还有一点容易被忽略左右轮电机参数不完全一致同样50%占空比左轮可能比右轮快一些导致跟随过程中车不是直进直退而是慢慢偏转。解决方法是做一次直道标定给转速慢的一侧加一个小PWM偏移量。5.5 现象红外遥控距离只有20cm遥控器要凑到眼前才有反应一体化接收头的灵敏度对供电电压非常敏感。如果接收头供电取自电机驱动板上的5V而电机转动时纹波很大接收头的解调阈值会被噪声干扰导致接收距离急剧缩短。另一个原因是解码时定时器溢出没处理数据位宽度测量错误接收到的命令码不对主控直接丢弃了。解决方法是给接收头单独供电用一个小LDO从电池降压到5V或者3.3V并在电源引脚就近放10uF和0.1uF的电容。解码逻辑上把定时器升级为带溢出计数的32位计时或者改用输入捕获模式来测量脉宽并在解完一帧后校验地址码避免误码被当成有效按键。6. 把整车调稳的关键技巧跑动中在线标定三组参数整车联调到最后你会发现剩余问题基本都是参数问题。循迹阈值不合适、基础速度太快、跟随死区太窄这些参数不实测很难一次调到位。我习惯在工程里留一个串口调试命令接口跑动中直接改参数不用反复烧录。先定义一个全局结构体保存三组参数再用串口中断解析简单的文本命令例如void parse_debug_cmd(char *cmd) { if (strncmp(cmd, TH, 3) 0) { g_params.track_threshold atoi(cmd 3); } else if (strncmp(cmd, BASE, 5) 0) { g_params.base_speed atoi(cmd 5); } else if (strncmp(cmd, DEAD, 5) 0) { g_params.follow_deadzone atoi(cmd 5); } }然后在主循环里周期性打印当前传感器的关键值格式建议用逗号分隔方便之后用串口上位机画曲线。比如这样printf(ADC_L,%d,ADC_M,%d,ADC_R,%d,DIST,%d,SPD,%d\n, gray_l, gray_m, gray_r, dist, target_speed);调参时先让小车静止在白底上记下三个灰度ADC值再推到黑线上记下值阈值取两个值的中间数。基础速度BASE从400开始每调高50试跑一圈观察直道是否稳定、过弯是否推头。跟随死区则是在距离较近时观察速度过渡是否平滑。标定完以后我会把最终参数直接写进工程头文件并把日期和车架号注释在旁边。这个习惯救过我很多次因为换了一块电池或者换了一套车轮后同样的参数要重新修正留一份记录能省掉大量重复劳动。另外提醒一句这种串口在线调参的方式也适合移植到其他电机小车项目上它本质上是在给实物系统开一扇小窗。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

波形和文稿对不齐,播客剪辑台怎样让字幕跟着声音走

波形和文稿对不齐,播客剪辑台怎样让字幕跟着声音走

编辑 EP.028 那期《城市夜航》的时候,我把播放头拖到 08:24,“智能文稿"里显示的是"我们开始意识到,声音不只是内容的载体……”,可波形上对应的却是两秒前嘉宾的尾音。字幕和声音之间始终差着一小段,而且越…

📅 2026/10/21 7:26:04
C#火车订票系统:WinForm+SQL Server全链路实战指南

C#火车订票系统:WinForm+SQL Server全链路实战指南

简介:这是一套基于C#与SQL Server开发的完整火车订票系统实战项目资源,面向C#初学者及.NET桌面应用入门学习者,聚焦GUI开发、数据库交互与多模块业务逻辑实现。资源包含128个文件,主体为50个C#源码文件(.cs&#xff09…

📅 2026/10/21 7:21:04
C# WinForms火车订票系统:完整CRUD+余票校验+并发控制实战

C# WinForms火车订票系统:完整CRUD+余票校验+并发控制实战

简介:本资源是一个基于C#与SQL Server开发的完整火车订票系统桌面应用,面向C#初学者及.NET入门实践者,聚焦GUI编程、数据库交互与多模块业务逻辑整合,适用于课程设计、毕业设计或小型项目实训。压缩包共128个文件,含50…

📅 2026/10/21 7:21:04
MORE NEWS

更多资讯

📰

PostgreSQL 查询全部函数信息:从 pg_proc 到综合模板

1. 为什么需要把数据库里的函数全部查出来:几个真实场景先讲个实际例子。我接手过一个跑了三年的业务系统,里面几十个服务共用一个 PostgreSQL 实例。某天要做数据迁移前的合规盘点,DBA 要求把所有自定义函数、触发器和存储过程全部登记造册。…

📰

汽车推荐分析系统全链路实现:爬虫、线性回归与Flask可视化

本科阶段的计算机毕业设计里,“汽车推荐分析系统”几乎是长盛不衰的经典选题。但很多同学做出来的版本,往往停留在“爬了几百条数据 画两张图表 套一个线性回归模型”的层面,答辩时最怕老师追问两类问题:一是“你这些数据怎么来…

📰

平衡膳食指南文档解读:五大类食物与三大营养素供能比落地执行

简介:这是一份面向普通大众、营养学习者及健康管理从业者的科普文档,围绕平衡膳食的内涵、重要性与现代实践展开,帮助读者理解如何通过合理搭配食物满足人体近50种营养素的全面需求。文档共1个doc文件,压缩包约21KB,内…

📰

PL/0编译器添加运算符:词法、语法到P-code虚拟机的完整改动指南

简介:编译原理课程设计资料,面向计算机专业本科生,围绕PL/0编译器的修改扩充展开,重点集中在运算符扩展和循环语句实现。内容包含基本要求与选做内容:基本部分完成、-赋值运算以及Pascal FOR语句的TO/DOWNTO两种形式&a…

📰

arping 实战:链路层定位主机与回读 MAC 地址

简介:这是一份面向网络管理员、运维工程师及网络编程学习者的 arping 工具源码资源包,聚焦以太网环境下基于 ARP 协议探测主机在线状态并获取其 MAC 地址这一典型场景。资源共 19 个文件,以 cpp 与 h 源文件为主,辅以 vcxproj、vc…

📰

Python自然语言处理入门:从分词到深度学习完整指南

简介:这份资源面向希望系统掌握中文自然语言处理的Python开发者与初学者,围绕分词、词性标注、命名实体识别、情感分析、主题建模及深度学习模型等核心主题,提供从基础概念到实战案例的完整学习路径。压缩包共42个文件,约53.58MB&…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬