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~TIM8,ADC有3个,外部中断线也有16条。做这台小车,资源基本不会有瓶颈。要注意的是,选主板时尽量选已经把晶振、复位电路、USB转串口都做好了的最小系统板,不要自己从零搭。之前有人图便宜用了一片空芯片自己焊最小系统,结果系统时钟不稳,超声波距离计算直接漂了几十厘米,最后花了很长时间排查通信问题才发现是晶振负载电容没加。

执行层的核心是两个带减速箱的直流电机,加上一个双路电机驱动模块。市面上常见的方案是针对L298N这一类驱动板,通过两个PWM引脚控制左右轮转速,四个方向引脚控制正反转。供电层则需要注意:电机驱动板需要从电池组直接供电,而主控、传感器、接收头需要稳定的5V或3.3V。如果全部挤在一个小降压模块上,电机启动瞬间会把电压拉低,主控直接复位。这也是联调阶段最典型的翻车点,后面详写。

2.2 一份不用来回返工的接线顺序清单

接线顺序比很多人想象的重要。我建议按“先电机驱动、再传感器、最后电源”的顺序操作,具体分配如下表所示:

功能引脚分配备注
左轮PWMPB6(TIM4_CH1)定时器通道引脚固定,不能随意改
右轮PWMPB7(TIM4_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. 自动重装载值1000,PWM频率 = 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赋值要保证左轮收到base+diff、右轮收到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后退固定速度-200;20~30cm之间,距离越近需要后退,距离越接近30cm速度越接近0;30~40cm之间,距离越远越要加速前进;超过40cm固定200前进。

所以正确逻辑是:

  • dist < 200:speed = -200
  • 200 <= dist < 300:speed = -(300 - dist) * 2,距离290时speed=-20,接近目标时几乎停止
  • 300 <= dist <= 400:speed = (dist - 300) * 2,距离310时speed=20
  • dist > 400:speed = 200

这样速度曲线在距离维度上是连续的,不会出现20cm处-200,21cm处直接跳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会回绕到0,gap就会变成一个很小的值,导致解码全乱。解决方法是把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处立刻变0,20cm处立刻后退,那么车就在死区边界反复横跳。原因是底层控制是离散的二值开关,没有在目标距离附近做过渡。

解决方法是把速度映射改为线性,按上一章的公式调整。还有一点容易被忽略:左右轮电机参数不完全一致,同样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试跑一圈,观察直道是否稳定、过弯是否推头。跟随死区则是在距离较近时观察速度过渡是否平滑。

标定完以后,我会把最终参数直接写进工程头文件,并把日期和车架号注释在旁边。这个习惯救过我很多次,因为换了一块电池或者换了一套车轮后,同样的参数要重新修正,留一份记录能省掉大量重复劳动。另外提醒一句:这种串口在线调参的方式也适合移植到其他电机小车项目上,它本质上是在给实物系统开一扇小窗。希望帮到你。

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

相关新闻

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

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

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

2026/11/13 1:58:30 阅读更多 →
STM32串口通信原理与Proteus仿真实践:从USART配置到虚拟终端调试

STM32串口通信原理与Proteus仿真实践:从USART配置到虚拟终端调试

简介&#xff1a;针对STM32串口通信学习与Proteus虚拟仿真验证的嵌入式入门资源包&#xff0c;适合正在学习STM32标准库/HAL库开发及硬件仿真调试的开发者&#xff0c;帮助解决串口初始化、数据收发逻辑与电路虚拟联调中的常见问题。压缩包共136个文件&#xff0c;约19.9MB&…

2026/11/13 1:58:30 阅读更多 →
Python函数设计与装饰器实战:掌握参数、闭包与异常处理的核心技巧

Python函数设计与装饰器实战:掌握参数、闭包与异常处理的核心技巧

1. 函数设计的核心思路&#xff1a;先把职责划分清楚1.1 为什么函数值得认真设计很多人写 Python 函数&#xff0c;第一反应是“把一段重复代码包起来”&#xff0c;这个直觉没错&#xff0c;但如果只停留在这一步&#xff0c;函数很快会退化成一堆纠缠在一起的过程性代码。我有…

2026/11/13 1:58:31 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年&#xff0c;我大部分时间都在跟浏览器标签页打交道&#xff0c;直到第一次把桌面应用跑起来&#xff0c;才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词&#xff0c;最开始是在某段时间频繁出现在同事的讨论里&#xff0c;一个用HTML…

2026/11/14 5:13:29 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介&#xff1a;亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档&#xff0c;主要面向智能锁安装人员、终端用户及售后维护者&#xff0c;帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件&#xff0c;大小4.37MB&#xff0c;内容结构清晰&#xff…

2026/11/14 5:13:29 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统&#xff0c;整车要求空载跑到8m/s&#xff0c;电机转速已经拉到额定点&#xff0c;驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令&#xff0c;转速纹丝不动&#xff0c;三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/14 5:13:29 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年&#xff0c;用它写过嵌入式驱动、做过游戏服务端&#xff0c;也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词&#xff0c;我其实挺感慨的——这门语言的难点从来不是语法多&#xff0c;而是知识点之间的耦合太深…

2026/11/13 22:08:41 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介&#xff1a;亚太天能T109滑盖智能锁产品说明书以PDF格式发布&#xff0c;适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开&#xff0c;先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能&#xff0c;再给出完整安装步骤&…

2026/11/14 2:41:37 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历&#xff1a;翻开源码注释、论文摘要或者一份专业报告&#xff0c;每个字都认识&#xff0c;连在一起却像天书。想找人聊聊&#xff0c;身边要么没人懂&#xff0c;要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型&#xff0c;几秒钟之后&a…

2026/11/14 5:13:29 阅读更多 →

日新闻

周新闻

月新闻