最近总有人问我同一个问题:想用STM32做一辆能遥控的小车练手,但又想同时把串口通信、红外通信和蓝牙都摸一遍,不知道该先去搞哪个。我去年用一块STM32F103C8T6做过一辆蓝牙遥控小车,后来又把串口通信和红外通信一起整合了进来,三个通道都能控制同一辆小车跑。今天干脆把这台车的完整思路、引脚规划、关键代码和调试记录整理出来,给正在做类似项目的朋友一个能直接参考的改动路径。
这篇文章适合两类人:一是刚把STM32点灯跑通、想做个稍微完整点的项目的初学者,二是已经会基本外设使用、但没把几种通信方式整合到同一个工程里的人。我会把通信协议设计、硬件接线的坑、代码解耦的思路都讲清楚,你拿到手就能照着改。
1. 项目总览与选型思路:一辆车上为什么要装三种通信方式
1.1 车架、电机与驱动板的搭配
先说硬件底子。我用的车底盘是常见的四驱TT马达底盘,但实际控制时只用了两路电机,也就是左侧两轮并联成一路、右侧两轮并联成另一路,这样L298N驱动板只需要两路输出就能完成差速转向。TT马达的额定电压一般是3V到6V,配合4节AA电池或者一块7.4V锂电池都有问题要处理,因为直接把电池电压给电机,转速会很高而且不受控,所以必须通过PWM调速。
驱动板我选了L298N,这个模块虽然体积大、效率也不算高,但胜在皮实抗造,非常适合新手阶段。它的每个通道有两个逻辑输入引脚IN1/IN2和IN3/IN4,还有一个使能引脚ENA/ENB。使能引脚接PWM信号就能调速,逻辑引脚决定电机正反转。如果你手头用的是DRV8833或者TB6612这类低压驱动板,代码逻辑完全一样,只需要改一下电源连接方式。
1.2 为什么选STM32F103C8T6这个型号
STM32F103C8T6在淘宝上十几块钱一块,资源却一点不寒酸:72MHz主频、64KB Flash、20KB RAM、三个USART、四个定时器、丰富的中断和GPIO。做遥控小车需要的资源它全都有,而且网上资料多得看不完,踩坑了基本都能搜到答案。
选择它的另一个原因是通信接口足够分散。USART1留给蓝牙模块,USART2作为备用调试口,红外接收头用PA0的外部中断输入,PWM信号从TIM3的两个通道输出,这样每个外设都有独立的引脚,不会出现复用冲突。做这类项目最怕的就是调着调着发现两个功能抢同一个引脚,这个规划能避免一大半麻烦。
1.3 三种通信方式在小车上分别扮演什么角色
串口通信、红外通信、蓝牙通信这三种方式在小车上承担的任务完全不同:
- 串口通信:既负责调试输出,也是车载最稳定的控制通道。调试阶段要打印各种状态信息,算法验证阶段可以直接从串口助手发指令看小车响应,它是一切调试的基础。
- 红外通信:老式遥控器方案,成本只要几块钱,却拥有完全不依赖网络的单向控制能力。NEC协议解码本身就是很好的编程练习,能逼着你理解定时器和外部中断配合的精髓。
- 蓝牙通信:最终产品形态,让手机能无线遥控小车。HC-05模块本质上就是无线串口,它把电脑/手机的串口数据无线转发给STM32,所以在协议层面可以和串口共用同一套解析逻辑。
三段控制通路最终都会汇聚到同一个指令解析函数,小车本质上只认识“前进、后退、左转、右转、停止”这几个动作,通信方式只是搬运工。
1.4 供电与电平的几个隐性坑
电源是整个小车最容易出诡异问题的地方。电机启动瞬间电流很大,同一时间如果单片机也在工作,电压跌落会导致复位重启。我的做法是电机电源和逻辑电源分开:电池直接给L298N的12V输入口供电,L298N的5V输出端单独给STM32主板供电,红外接收头和蓝牙模块也从STM32的3.3V引脚取电。这种供电方式在地线处要连接在一起,也就是所有模块的GND必须共地,这个条件不满足,串口和蓝牙都会出现数据丢失或完全不通的症状。
另外要注意TT马达并联时总电流较大,L298N芯片发热明显,尤其长期堵转时温度会很高。使用3S锂电池(11.1V)时建议给L298N的12V输入串接一段降压或者降低PWM占空比上限,否则电机转速过高会让小车原地打转,还容易烧驱动芯片。
2. 串口通信:printf调试与文本指令接收的完整套路
2.1 USART1初始化与printf重定向
串口是嵌入式开发的基础设施,也是我这台小车调试的“眼睛”。USART1对应PA9(TX)和PA10(RX),PA9负责把数据发送出去,PA10负责接收外部发来的指令。初始化部分用标准外设库写,配置好波特率、停止位、校验位和中断使能即可:
void UART1_Init(uint32_t baud) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // PA9 TX: 推挽复用输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // PA10 RX: 浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = baud; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, &USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); USART_Cmd(USART1, ENABLE); }波特率我固定用115200还是9600,取决于蓝牙模块的设置。HC-05出厂默认是9600,所以MCU这边也用9600,两边必须严格一致。调试阶段串口助手如果出现乱码,第一反应就是波特率没对上。
printf重定向是串口调试的灵魂。在C语言工程里重写fputc函数,把标准输出终端到USART1,这样以后随便printf一行“当前速度: 150”就能在串口助手看到实时状态:
int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); return ch; }重定向之后还要在keil里勾选微库(Use MicroLIB),否则printf会引出一大堆文件系统代码,Flash直接爆掉。
2.2 文本行指令的下发与接收逻辑
小车接收控制指令,我采用的协议是用户友好的文本行格式:每条指令以\r\n结尾,内容就是单个字符加一个速度数值,格式如下:
F,180表示前进,速度180B,150表示后退L,120表示左转R,120表示右转S,0表示停止
这个设计最大的好处是手机蓝牙串口助手和电脑串口助手都能直接输入,调试时肉眼可读,不需要搞二进制帧解析。团队协作或自己复盘时也不会因为忘了协议而重新翻代码。
文本行接收在中断里做,思路是累积字符直到收到换行符,然后把完整的字符串交给指令解析函数:
char cmd_buffer[16]; uint8_t cmd_len = 0; volatile uint8_t cmd_ready = 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE)) { uint8_t data = USART_ReceiveData(USART1); if (data == '\n') { cmd_buffer[cmd_len] = '\0'; cmd_ready = 1; cmd_len = 0; } else if (data != '\r') { if (cmd_len < 15) { cmd_buffer[cmd_len++] = data; } } } }解析函数在主循环中处理cmd_ready标志。因为中断只是搬运数据,解析和下发的实际动作放在主循环里执行,避免在中断上下文做耗时操作:
void Execute_Cmd(char *cmd) { char action = cmd[0]; int speed = 0; if (cmd[1] == ',') { speed = atoi(&cmd[2]); } switch (action) { case 'F': Car_Forward(speed); break; case 'B': Car_Backward(speed); break; case 'L': Car_TurnLeft(speed); break; case 'R': Car_TurnRight(speed); break; case 'S': Car_Stop(); break; default: printf("Unknown cmd: %s\r\n", cmd); break; } }注意这里速度值范围我限制在0到255之间,因为PWM寄存器是8位精度。
2.3 串口调试时最容易迷惑人的三个现场
串口通信看似简单,实际调试时踩坑点却很多。第一个是乱码,表现是串口助手显示一堆看不懂的符号,原因通常是波特率不匹配,或者USB转串口模块的晶振精度不够。CH340在常见的9600波特率下很稳定,但如果你把波特率拉高到115200以上,且使用了精度差的晶振,误码率会迅速上升。实测下来小车项目用9600最省心。
第二个是发送没反应但接收正常。这种问题我用过一次就长记性了,八成是TX/RX接反了。STM32的TX要接模块的RX,STM32的RX接模块的TX,别一看名字是R就都接R。如果是板载CH340或者通过杜邦线接USB转串口模块,还要检查两边的GND有没有连在一起,不共地时信号根本没有参考电平,数据自然传不过去。
第三个是“为什么我一发数据单片机就重启”。这经常是电源问题而不是串口问题——发送瞬间USB转串口模块会给你的板子灌一点电流,压降一来,电源保护就触发复位了。解决办法是老老实实把电源系统分开,别让通信模块和电机共用同一个脆弱电源。
3. 红外通信:用外部中断加定时器解出NEC协议
3.1 红外接收头的反相输出与时序拆解
红外遥控方案我用的是最常见的VS1838B接收头和普通NEC协议遥控器,成本人民币几元钱。很多新手第一次遇到红外接收头都会愣一下:为什么没有红外信号时输出是高电平,有信号时反而变低?这是因为VS1838B内部集成了放大、滤波和解调电路,输出信号与发射端是反相的。
NEC协议发射端的时序是:先发9ms载波加4.5ms空闲作为引导码,后面跟着32位数据位。每一位由560us载波脉冲加不同长度的空闲时间组成,空闲560us代表逻辑0,空闲1680us代表逻辑1。经过接收头反相之后,STM32看到的信号变成:9ms低电平加4.5ms高电平、560us低电平加560us高电平或者560us低电平加1680us高电平。
这就有意思了——我们不需要去解码载波频率(38kHz的载波已经被接收头解调掉了),只需要精确测量高低电平的持续时间就能还原NEC协议数据。
3.2 用外部中断加16位定时器做软件解码
解码方案我选的是外部中断加定时器计时。红外接收头输出引脚接PA0,配置成双边沿触发的外部中断,同时把TIM2配置成一个自由的微秒级计数器。中断发生时读取TIM2的计数值,与前一次记录的时间戳做差,就能得到本次电平持续的时间。
TIM2配置很简单,72MHz时钟源分频72倍后得到1MHz计数频率,也就是计数器每增加1就代表1微秒:
void TIM2_Init(void) { TIM_TimeBaseInitTypeDef TIM_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStructure.TIM_Period = 0xFFFF; TIM_InitStructure.TIM_Prescaler = 72 - 1; TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_InitStructure); TIM_Cmd(TIM2, ENABLE); }中断处理函数里维护一个last_rise变量记录最近一次上升沿的时间。每次触发下降沿时,用当前时间减去last_rise,得到刚结束的高电平宽度,从而判断引导码、逻辑1和逻辑0:
volatile uint8_t ir_bit_cnt = 0; volatile uint32_t ir_data = 0; volatile uint8_t ir_recv_done = 0; static uint16_t last_rise = 0; void EXTI0_IRQHandler(void) { uint16_t now = TIM_GetCounter(TIM2); uint8_t level = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); if (level == SET) { // 上升沿:记录高电平开始时刻 last_rise = now; } else { // 下降沿:高电平刚结束 uint16_t high_us = now - last_rise; if (high_us > 4000 && high_us < 5000) { // 引导码的4.5ms高电平 ir_bit_cnt = 0; ir_data = 0; } else if (high_us > 1600 && high_us < 1900) { // 逻辑1的空闲时间约1.68ms ir_data = (ir_data << 1) | 1; ir_bit_cnt++; } else if (high_us > 400 && high_us < 800) { // 逻辑0的空闲时间约560us ir_data = (ir_data << 1) | 0; ir_bit_cnt++; } if (ir_bit_cnt >= 32) { ir_recv_done = 1; ir_bit_cnt = 0; } } }这里用了uint16_t计时,TIM2计数器溢出时会自动回绕。C语言里无符号数减法有自动取模的特性,所以即使在溢出边界做时间差也能得到正确结果,不需要手动处理溢出。
主循环中检测到ir_recv_done后,把32位数据拆成地址码、地址反码、数据码、数据反码四段,做一次反码校验:
if (ir_recv_done) { uint8_t addr = (ir_data >> 24) & 0xFF; uint8_t addr_inv = (ir_data >> 16) & 0xFF; uint8_t cmd = (ir_data >> 8) & 0xFF; uint8_t cmd_inv = ir_data & 0xFF; if ((addr + addr_inv == 0xFF) && (cmd + cmd_inv == 0xFF)) { printf("IR cmd: 0x%02X\r\n", cmd); switch (cmd) { case 0x45: /* 遥控器按键值,自己映射到小车动作 */ break; default: break; } } ir_recv_done = 0; }这段代码不依赖任何红外解码库,纯粹用定时器和中断就能解出完整数据,逻辑上也很好理解。
3.3 红外实测中的重复码、距离和环境光问题
红外遥控器在持续按住按键时不会重新发一帧完整数据,而是发引导码加2.25ms高电平的重复码。上面的代码里2.25ms的高电平宽度既不落在引导码区间,也不落在数据位的区间,所以会被自动忽略。控制小车时,你按一下它动一下,如果你想让小车持续前进,需要自己做一个超时自动停止的机制——比如你期望的是“按下红外按键期间保持前进,松开后小车继续跑500ms再停下”。
实测距离和环境光的影响也值得提。VS1838B在室内无遮挡情况下遥控距离轻松超过8米,但把接收头阳光直射或者靠近荧光灯管时,它会收进大量红外噪声,导致解码偶尔失败或误触发。摆放接收头时尽量让它朝向遥控器,用热缩管遮住侧面光线,能明显提高解包成功率。
另外在中断里频繁调用GPIO_ReadInputDataBit是非常快的,但是当定时器溢出发生时可能出现一次误判,我在工程里加了简单的噪声过滤(连续收到两次相同的高电平时序才认为是有效),把误判率降到了可接受范围。
4. 蓝牙模块:HC-05的AT配置流程与手机遥控链路排障
4.1 HC-05接线与AT模式入口
蓝牙部分用的是老牌经典模块HC-05,它内部就是一组UART转蓝牙的透传功能,所以接线方式和串口模块几乎一样:模块的TXD接STM32的PA10(RX),模块的RXD接STM32的PA9(TX),模块的VCC接5V或3.3V,GND共地。需要注意HC-05的逻辑电平是3.3V,STM32 F103的GPIO也是3.3V,可以直接连;如果你用的是5V单片机,就得加电阻分压,否则容易把模块引脚打坏。
HC-05有两种工作模式:正常透传模式和AT指令配置模式。进入AT模式的经典方法有两个,一是按住模块上的那个小按钮再上电,二是把模块的EN引脚或KEY引脚拉高后上电。我推荐按住按钮上电这种方式,因为不需要额外飞线。
进入AT模式后,模块的LED会变成慢闪(约1秒一次),此时模块的串口波特率固定为38400,这个很重要——很多人用默认9600发AT指令,结果怎么发都没响应。
4.2 常用AT指令与参数配置清单
下面是配置遥控小车蓝牙模块时最常用的一套AT指令,配合电脑的串口助手或者USB转TTL模块就能操作:
| 指令 | 功能 | 典型设置 |
|---|---|---|
| AT | 测试通信 | 返回OK |
| AT+NAME=Car_BLE | 修改蓝牙广播名称 | Car_BLE |
| AT+PSWD=1234 | 修改配对密码 | 1234 |
| AT+UART=9600,0,0 | 设置波特率9600,停止位1,无校验 | 9600,0,0 |
| AT+ROLE=0 | 设为从机模式 | 0 |
| AT+CMODE=1 | 允许任意设备连接 | 1 |
我的习惯是把波特率设为9600,与STM32保持一致,手机端配对完成后用蓝牙串口助手下发“F,180”这类指令就能直接驱动小车。蓝牙名称建议改成一个容易识别的名字,否则车多的时候会把别人的车也连上。
AT指令配置过程中注意每条指令以回车换行结尾,也就是串口助手里要选“发送新行”模式。AT模式下模块不会转发蓝牙数据,它只响应AT指令,配置完成后断电重新上电才能回到透传模式。
4.3 蓝牙连不上、数据乱码的排查顺序
蓝牙模块是我使用中排查最久的一个环节,也是我在热词里看到很多人在问“HC05连接不上”的原因。根据我的经验,连接不上时按这个顺序排查:
- 模块LED状态:上电后如果LED慢闪(约1秒一次)说明模块在待配对状态,此时手机搜“HC-05”或者自定义的名称,配对密码输1234或0000。如果LED一直双闪(约2Hz),说明模块已经连上了一个设备,新手机搜不到它,要先断开原连接。
- 进入了AT模式没退出:AT模式下模块不响应手机配对请求,断电重新上电就好。
- 手机搜不到设备:可能是模块名称里带了不可见字符,或者模块供电不足导致射频发射功率降低。我遇到过一次是USB供电给HC-05,连线过长,压降太大,换成独立3.3V电源立刻解决。
- 连接成功但数据乱码:模块和STM32的波特率不一致,用AT+UART指令统一到9600即可。
- 连接成功后一断开就再也连不上:这种情况先检查CMODE是否设置为1,如果CMODE=0,模块会自动连接上次绑定的设备,其他设备无法接入。
HC-05透传模式下的数据流非常好理解:手机蓝牙串口助手发出的字符,经过蓝牙无线链路到达HC-05,HC-05通过TX引脚发到STM32的UART接收寄存器,中断函数收到后跟电脑串口走完全相同路径。这也是为什么我可以让蓝牙和串口共用同一套指令解析函数的根本原因——它们本质都是给USART1喂数据。
功耗方面,HC-05平时电流大约30mA左右,连上手机后会到40mA以上。小车由L298N的5V输出供电时完全没问题。如果电池电压较低,蓝牙模块会频繁断连,最好在模块电源引脚并联一个100uF电解电容,实测能明显改善掉线问题。
5. 电机驱动与整车联调:L298N接线、PWM调速与真实调车经验
5.1 驱动板接线与PWM初始化方法
L298N驱动板作为整车的执行核心,接线相当直观。我把两组逻辑输入接到STM32的PB0-PB3,两组使能引脚接到PA6和PA7,这两个引脚刚好是TIM3的通道1和通道2,可以输出两路独立PWM。
| L298N引脚 | 接STM32 / 电源 | 功能 |
|---|---|---|
| IN1 | PB0 | A路正转逻辑 |
| IN2 | PB1 | A路反转逻辑 |
| IN3 | PB2 | B路正转逻辑 |
| IN4 | PB3 | B路反转逻辑 |
| ENA | PA6 (TIM3_CH1) | A路PWM调速 |
| ENB | PA7 (TIM3_CH2) | B路PWM调速 |
| 12V输入 | 电池正极 | 电机供电 |
| GND | 电池负极+STM32 GND | 共地 |
| 5V输出 | 可选,给STM32供电 | 稳压输出 |
L298N内部本质上是两路H桥。H桥四个开关管组合起来,可以控制电机两端电压的极性:IN1=1、IN2=0时电机正转,反过来就是反转,都等于0时刹车。PWM信号改的是开关管的通断占空比,从而实现调速。简单理解就是水龙头开关频率不变、但每次开的时间占比不同,水流平均大小就不同,电机速度也就跟着变。
PWM初始化代码如下,周期我设成1000个计数单位,也就是1kHz的PWM频率,对TT马达来说足够平滑:
void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); 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(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 999; TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_OC2Init(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }L298N模块出厂默认在ENA、ENB位置插着跳线帽,这时使能端内部被拉高,电机永远全速转,必须拔掉跳线帽才能用PWM控制速度。
5.2 统一控制逻辑:把三种输入汇成一个动作
整车控制逻辑可以整理成一张动作表,看完就明白代码在做什么:
| 动作 | IN1 | IN2 | IN3 | IN4 | PWM1/PWM2 |
|---|---|---|---|---|---|
| 前进 | 1 | 0 | 1 | 0 | 速度值 |
| 后退 | 0 | 1 | 0 | 1 | 速度值 |
| 原地左转 | 0 | 1 | 1 | 0 | 速度值 |
| 原地右转 | 1 | 0 | 0 | 1 | 速度值 |
| 停止 | 0 | 0 | 0 | 0 | 0 |
串口、红外、蓝牙三种输入最终统一调用同一个运动函数。我在工程里把串口解析函数写的指令,和蓝牙通过USART1发的数据完全共享同一个解析流程,红外遥控器按键则通过一个映射表转换成同样的“F/B/L/R/S”命令。这样代码结构非常清晰,将来再加一个WiFi控制,只需要新增一个数据源映射到同一套动作函数就完事了。
void Car_TurnLeft(uint8_t speed) { GPIO_WriteBit(GPIOB, GPIO_Pin_1, Bit_SET); // IN2=1, 左路反转 GPIO_WriteBit(GPIOB, GPIO_Pin_0, Bit_RESET); GPIO_WriteBit(GPIOB, GPIO_Pin_2, Bit_SET); // IN3=1, 右路正转 GPIO_WriteBit(GPIOB, GPIO_Pin_3, Bit_RESET); TIM_SetCompare1(TIM3, speed); TIM_SetCompare2(TIM3, speed); }原地转向最直观,两侧轮子等速反向旋转,小车以中心为轴原地转圈,在桌上调车时非常方便。如果场地空间大、希望转向平滑,可以把左右轮速差调成不同的值,比如左轮50%、右轮100%,就能画出弧线弯道。
5.3 整车联调阶段实测遇到的几个典型问题
调试小车是最花时间的一环,因为问题往往不是单一原因。我列出几个实际遇到的现象和解决方法,可以当作你的排查手册。
第一个现象:一上电小车猛地向前冲一下,然后所有指示灯正常,但电机不动。原因通常是电机启动瞬间电流太大导致L298N或单片机电压跌落,单片机复位了。解决方法是给电池输出端并联一个大容量电解电容(我用的是470uF),同时把PWM初始值设为0,主函数里等系统稳定后再允许接收指令。
第二个现象:蓝牙发送的指令经常丢,尤其是“F,180”这类带数值的命令。排查下来是波特率配置有问题——HC-05默认9600,但我之前用USB转TTL配置AT时给它设成了115200,MCU这边还是9600,两边通信自然断断续续。统一到9600之后问题彻底消失。每次改完HC-05参数后,MCU端和调试助手都确认统一波特率再上电测试。
第三个现象:红外遥控能收到码,但小车动作和遥控器按键对应错乱。这通常是遥控器厂商对数据码位的定义不同,我直接用一个打印函数把收到的cmd值打印出来,然后逐个按键记录,再做映射表,比猜测快得多。如果你用的遥控器解码地址和标准NEC不同,也可以通过打印地址码来判断是否需要修改地址校验逻辑。
第四个现象:小车无法直行,总是往右偏。这不是软件问题,而是TT马达的个体差异导致左右转速不同。解决办法有两个:在代码里给慢的一侧加一个速度补偿系数,或者直接在硬件上调整左右轮的橡皮圈松紧。我实测发现用速度补偿代码更灵活,调整系数0.9到1.1之间就能让小车跑出直线。
最后说说个人习惯。我现在做这类整合性项目,一定会先做最小验证板——只测串口printf,再测PWM输出,再跑红外解码,最后连蓝牙。每加一个模块都在笔记本上记录当天改了什么引脚、什么参数,这习惯帮我省掉了无数排查时间。串口通信、红外通信、蓝牙通信落到一辆小车上,核心不在于单个外设怎么用,而在于怎么让它们协同、共用一套逻辑,这才是做一个完整嵌入式小项目最有价值的部分。