ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F103C8T6智能小车三合一:蓝牙遥控、避障循迹与PWM电机控制实战

STM32F103C8T6智能小车三合一:蓝牙遥控、避障循迹与PWM电机控制实战 简介面向STM32入门学习者和智能小车爱好者这份源代码实现手机蓝牙遥控、红外避障与循迹三个典型实验主控采用STM32F103C8T6电机驱动使用L293D搭配TT直流减速电机整个工程基于KEIL4开发环境搭建作者已在自制的智能小车上完成实测验证可直接参考使用。资源压缩包内共计四十五个文件包含C语言源程序、头文件、汇编启动文件、KEIL工程配置文件、编译链接生成的中间文件以及可直接烧写的HEX固件同时提供程序必看说明、实验指导文档、手机APP使用说明和安卓安装包还附带一张L293D电机驱动电路接线图方便学习电路连接与故障排查。压缩包整体体积为二点八三兆字节目录按系统、用户、输出等模块组织结构清晰便于快速定位相关代码。目前已有六千八百七十二人学习下载比较适合需要从零搭建智能小车控制程序、理解蓝牙串口通信、红外避障和循迹逻辑的开发者参考。1. 同时拿到蓝牙遥控、避障、循迹先想清楚电机由谁接管同时拿到“手机 APP 蓝牙遥控”“超声波避障”“红外循迹”三个需求第一次写的人多半会建三个工程分开调调完再合并。真正难的不是三段代码量而是它们共用两个电机、一块 stm32f103c8t6 最小系统板和同一根串口线避障刚要转向蓝牙又发来前进命令电机听谁的下面这套方案围绕 stm32f103c8t6 的 64KB Flash / 20KB SRAM 展开先把引脚分配、电机驱动和供电接线定死再把 HC-05 蓝牙指令帧收下来最后用状态机把遥控、避障、循迹统一进一个主循环。适合手里已有底盘和传感器、想从零把三合一工程理清并在 Keil 或 IAR 下直接改代码的读者。2. 板级硬件搭建stm32f103c8t6 引脚功能分配、电机驱动与供电拓扑2.1 stm32f103c8t6 引脚功能分配从串口、PWM 到 GPIO 的一次性排表stm32f103c8t6 是 48 脚 LQFP 封装可用 GPIO 有 37 个外设不冲突的前提下足够容纳蓝牙串口、两组电机 PWM、超声波和五路循迹。多数引脚带有多个复用功能接线前最好先画一张表否则很容易出现 PA9 既要接蓝牙 TX 又要当定时器输出的尴尬局面。外设推荐引脚引脚复用说明HC-05 蓝牙PA9(TX) / PA10(RX)USART13.3V 逻辑电平调试串口可选PA2(TX) / PA3(RX)USART2输出日志用左电机 PWMPA0 / PA1TIM2_CH1 / CH2接 L298N ENA、IN1/IN2右电机 PWMPA6 / PA7TIM3_CH1 / CH2接 L298N ENB、IN3/IN4HC-SR04 超声波PB8(TRIG) / PB9(ECHO)无ECHO 需分压到 3.3V五路循迹传感器PB0、PB1、PB2、PB10、PB11无数字输出低电平有效板载 LEDPC13无模式指示用 STM32CubeMX 生成工程时注意在 Pinout 视图里把 PA9/PA10 锁定为 USART1_TX/RX 以后不要再勾选同一引脚的 TIM1_CH2 复用否则生成的 GPIO 初始化代码会互相覆盖。如果习惯标准外设库重点检查 RCC 时钟树APB1 定时器时钟来自 PCLK1默认 36MHzTIM2 的预分频要按实际总线时钟算不能拿 72MHz 直接套。2.1.1 电机 PWM 初始化预分频和自动重装载值的配合左电机用 TIM2 的 CH1/CH2右电机用 TIM3 的同构代码。PWM 频率我一般选 10kHz 到 20kHz既能避开人耳可闻噪声又不会让 L298N 的续流二极管发热。低于 1kHz 时电机会发出明显啸叫这是预分频数设错最常见的表现。// motor.c —— 左电机 TIM2 的 PWM 初始化 void motor_pwm_init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); gpio.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; gpio.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); tim.TIM_Prescaler 71; // APB172MHz 时, 72/721MHz tim.TIM_Period 999; // 1MHz/1000 1kHz PWM tim.TIM_CounterMode TIM_CounterMode_Up; tim.TIM_ClockDivision 0; TIM_TimeBaseInit(TIM2, tim); oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 0; // 初始占空比 0 oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, oc); TIM_OC2Init(TIM2, oc); TIM_Cmd(TIM2, ENABLE); }这段代码里决定 PWM 频率的是预分频 71 和周期 999 两个参数计数频率 1MHz 除以 1000 得到 1kHz。要把频率提到 10kHz周期改成 99 即可预分频不动。占空比由 TIM_SetCompare1(TIM2, value) 控制value 范围 0 到 999映射到 0% 到 100%。2.2 L298N 电机驱动接线和电平整流的三个关键点L298N 每个 H 桥需要三根控制线一个使能脚和两个方向输入脚。使能脚如果只接高低电平电机只有全速和停止两种状态所以必须接 PWM 输出。IN1/IN2 决定转向典型逻辑是 IN11、IN20 正转反过来反转。电机不转的第一个排查点就是使能脚悬空。ENA/ENB 内部有下拉悬空时桥臂截止定时器输出再正常电机也不动。第二个排查点是逻辑电源地和电机电源地没接在一起表现为蓝牙偶尔断连、串口数据随机乱码。L298N 板载 5V 稳压输出给 stm32f103c8t6 最小系统板供电时两边的 GND 必须短接。第三个关键点是 HC-SR04 的 ECHO 电平。ECHO 在 5V 供电时输出高电平接近 5V直接拉到 PB9 会超过 stm32 引脚耐压。常规做法是在 ECHO 到 PB9 之间串 1kΩ 电阻同时对地并 2kΩ得到约 3.3V 的分压值。很多模块标称低功耗版本仍可能输出 5V实测一下最稳。如果手头买不到原厂 ST 芯片GD32E103C8T6、APM32F103C8T6 这类国产替代型号引脚兼容固件一般可以直接烧录只是烧录器 ID 识别和 Flash 等待周期设置要按对应厂商手册调整。2.3 供电顺序与上电瞬间的电压跌落整车用三节 18650 串联给 L298N 供电电机启动瞬间母线压降可能超过 1V。如果 L298N 板载 5V 稳压同时给单片机和蓝牙供电压降传导过去就会造成复位现象是车刚起步、蓝牙就掉线。常见做法是在 L298N 的 5V 输出端并联 470uF 电解电容和 104 陶瓷电容电机电源端再并一颗 100uF。HC-05 的 VCC 上串一个 100Ω 电阻模块旁边放一颗 10uF 电容把电机母线上的高频噪声滤掉一部分。上电顺序建议先接电池等板载 LED 稳定后再给蓝牙上电能避免模块在电源爬升阶段进入异常状态。3. 蓝牙遥控链路HC-05 的 AT 配置、指令帧协议与手机 APP 对接3.1 HC-05 配置方法与“连接不上”的四类排查方向HC-05 是经典蓝牙 SPP 模块出厂默认波特率 9600配对密码多数固件是 1234 或 0000。进入 AT 命令模式有两种方式按住模块上的开关再上电或者上电前把 KEY/EN 引脚拉高。AT 模式波特率固定 38400与透传模式的 9600 不一致换模块、换代码后最容易在这里翻车。所谓“HC05 蓝牙模块连接不上”按现象可以拆成四类排查方向完全不同。现象原因处理方向手机完全搜不到模块供电电压太低或天线被金属遮挡提高供电到 3.6V 以上天线远离电机线能搜到但配对失败手机里残留旧配对记录删除配对记录重新搜索配对配对成功但 APP 提示未连接APP 只支持 BLE不支持 SPP换支持“蓝牙串口/SPP”的 APP连接成功但小车无反应波特率不匹配核对单片机串口初始化是否与模块一致逐个排查前先用 USB-TTL 验证模块本身PC 端串口助手打开 AT 模式回显 OK再接到单片机上避免拿一块坏模块反复查程序。3.2 带校验的指令帧协议与 STM32 端解析实现遥控指令不要用单字节否则串口噪声干扰下很难判断是否收到完整命令。我习惯用五字节帧结构固定为帧头、帧头、命令、速度、校验和0xAA 0x55 CMD SPEED CHECKSUMCMD 取值约定如下0x00 停止、0x01 前进、0x02 后退、0x03 左转、0x04 右转。SPEED 是百分比 0 到 100CHECKSUM 等于 CMD 加 SPEED 的低 8 位。这样设计的好处是接收端校验简单处理速度足够快。// app_bt.c —— USART1 中断接收五字节遥控帧 #define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 #define CMD_STOP 0x00 #define CMD_FORWARD 0x01 #define CMD_BACKWARD 0x02 #define CMD_LEFT 0x03 #define CMD_RIGHT 0x04 static uint8_t bt_rx[5]; static uint8_t bt_idx 0; void USART1_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART1, USART_IT_RXNE) SET) { ch USART_ReceiveData(USART1); if (bt_idx 0) { if (ch FRAME_HEAD1) bt_rx[bt_idx] ch; } else if (bt_idx 1) { if (ch FRAME_HEAD2) bt_rx[bt_idx] ch; else bt_idx 0; // 第二字节不对重新等帧头 } else { bt_rx[bt_idx] ch; if (bt_idx 5) { if ((bt_rx[2] bt_rx[3]) bt_rx[4]) { remote_handle_cmd(bt_rx[2], bt_rx[3]); } bt_idx 0; } } } }接收状态机的思路是逐个字节等待只有出现 0xAA 0x55 才进入命令接收状态。如果第二个字节不是 0x55索引直接回零防止后续字节错位。校验和只用加法和低 8 位截断覆盖不了人为的恶意数据但对付电机产生的随机噪声足够。命令处理函数里要加一层速度保护void remote_handle_cmd(uint8_t cmd, uint8_t speed) { if (speed 30) speed 30; // 低于死区电机不转 if (speed 100) speed 100; switch (cmd) { case CMD_FORWARD: set_motor_direction(FWD, speed); break; case CMD_BACKWARD: set_motor_direction(REV, speed); break; case CMD_LEFT: set_motor_turn(LEFT, speed, 0); // 左轮速度0,右轮speed break; case CMD_RIGHT: set_motor_turn(RIGHT, speed, 0); break; case CMD_STOP: set_motor_direction(STOP_M, 0); break; default: break; } }左转和右转也可以做成差速比如左轮 30%、右轮 80%比单边刹车的转向更平滑但调试时常数要多标几组先用一边全速、一边停止验证协议再往细调。3.3 手机 APP 控制端通用蓝牙串口工具的发送约定调试阶段最快的方式是手机装一个支持 SPP 的蓝牙串口助手发送十六进制数据。注意 APP 里要选“十六进制发送”而不是把字符串“AA55”直接发出去。字符串模式下发送的是 0x41 0x41 0x35 0x35单片机收到后对不上 0xAA 0x55 帧头。Android 端如果自己写控制 APP核心代码就是蓝牙 SPP Socket// Android 端发送一帧前进命令的简化代码 BluetoothSocket sock device.createRfcommSocketToServiceRecord( UUID.fromString(00001101-0000-1000-8000-00805F9B34FB)); sock.connect(); byte[] frame { (byte)0xAA, (byte)0x55, 0x01, 0x50, (byte)(0x01 0x50) }; sock.getOutputStream().write(frame);这里 UUID 必须用 SPP 标准值随便填一个自定义 UUID 会导致 connect 失败。发送时以 byte[] 形式写入不要经 String.getBytes() 转码否则中文注释环境下的默认编码会改变字节值。iOS 端对经典蓝牙 SPP 支持较弱大部分 HC-05 在 iPhone 上连不上如果产品定位必须兼容 iOS要换 BLE 方案或者增加 MFi 认证模块。顺带说一句HC-05 是 SPP 透传模块不像 iBeacon 那样持续广播 RSSI所以“蓝牙测距”这种需求不要指望它做它更适合数据通道而不是定位。4. 避障与循迹控制五路传感器采集、超声波动态避障和模式仲裁4.1 五路循迹传感器为什么比三路好用以及数字信号怎么读五路循迹传感器的优点在于中线对准的判定裕度比三路大。三路只能给出“偏左、居中、偏右”三个离散结论五路可以分辨轻微偏移和严重偏移转向执行更平滑。它一般集成五个 TCRT5000 红外对管数字输出低电平表示该路压到黑线。接线定义为最左到最右依次接 PB0、PB1、PB2、PB10、PB11读取函数把所有引脚一次读进来组装成一个字节// track.c #define TRACK_PORT GPIOB #define TRACK_LEFT GPIO_Pin_0 // 最左 #define TRACK_L GPIO_Pin_1 #define TRACK_MID GPIO_Pin_2 #define TRACK_R GPIO_Pin_10 #define TRACK_RIGHT GPIO_Pin_11 // 最右 uint8_t track_read_status(void) { uint8_t raw 0; raw | (GPIO_ReadInputDataBit(TRACK_PORT, TRACK_LEFT) RESET) 4; raw | (GPIO_ReadInputDataBit(TRACK_PORT, TRACK_L) RESET) 3; raw | (GPIO_ReadInputDataBit(TRACK_PORT, TRACK_MID) RESET) 2; raw | (GPIO_ReadInputDataBit(TRACK_PORT, TRACK_R) RESET) 1; raw | (GPIO_ReadInputDataBit(TRACK_PORT, TRACK_RIGHT) RESET) 0; return raw; // 位值为 1 表示该路在黑线上 }因为传感器数字输出低电平有效所以用 RESET来判定压线。返回值的 bit4 对应最左路bit0 对应最右路查表时更直观。原始状态含义执行动作0b00100中间压线直行0b01100 / 0b11100线偏右车偏左向右修正0b00110 / 0b00111线偏左车偏右向左修正0b00000脱线或压全黑停车 / 找线循迹要跑得稳不能只在满速和停止两个状态间切换。用比例控制更好把线位置换算成偏差量中间为 0偏右一个通道加 1偏左一个通道减 1转向修正量等于 Kp 乘偏差。现实中高速循迹的最大问题是弯道丢线修正量给晚了车就冲出去。一个实用做法是把比例项输出限幅在占空比 30% 以内避免修正过大造成反方向甩尾。Kp 的经验起点在 25速度越高 Kp 越小基本逻辑是低速时转向灵敏些高速时转向动作收敛些。4.2 超声波避障HC-SR04 测距、转向决策与动态避障的边界HC-SR04 有 VCC、GND、TRIG、ECHO 四根线。TRIG 上给一个 10us 以上高电平触发模块发出八个 40kHz 脉冲并拉高 ECHO收到回波后拉低 ECHO。距离计算公式是时间除以 58得到厘米数。// avoid.c —— ECHO 高电平时间通过 TIM4 计时 float hc_sr04_get_cm(void) { uint32_t t, timeout 0; GPIO_SetBits(GPIOB, GPIO_Pin_8); // TRIG 拉高 delay_us(10); GPIO_ResetBits(GPIOB, GPIO_Pin_8); // TRIG 拉低 while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9) RESET) { if (timeout 30000) return 99.0f; // 没等到回波,按远距离处理 } TIM_SetCounter(TIM4, 0); // ECHO 上升沿开始计时 while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9) SET); t TIM4-CNT; // 单位微秒 return (float)t / 58.0f; // 距离单位厘米 }这里用 TIM4 而不是 TIM3 计时是因为 TIM3 的通道已经分给右电机 PWM 了两个功能共用计数器会互相乱套。TIM4 预设分频为 71计数频率 1MHzCNT 直接等于微秒数。超时返回 99.0f 是防死等的手段量程外或模块线松时不会让程序卡在 while 里。单个超声波模块只能知道前方距离无法同时获知左右两侧因此避障逻辑通常是启发式正前方小于 30cm 就左转 90 度再前进无法左转再右转。这个逻辑在小规模场地上够用但不要把它说成完整的路径规划。工程上更可靠的做法是给超声波加一个舵机云台扫描左、中、右三个方向各测一次选最远方向转向。所谓动态避障小车路径规划在 stm32f103c8t6 上并不适合跑 A 星或 DWA 这类需要地图内存的算法本地维护一个 3x3 的栅格方向表记录最近三次扫描的可走方向已经能应付多数竞赛场景。还有一点容易被忽略超声测距周期太短结果会跳。一次测量本身要几毫秒到几十毫秒加上舵机云台转动时间避障主循环建议用 50ms 到 100ms 周期而不是每 10ms 测一次。4.3 三种模式的状态机仲裁遥控、避障、循迹的优先级怎么定三个功能不能同时跑谁说了算需要明确。APP 端加一个模式切换命令0x05 切避障、0x06 切循迹、任意遥控命令切回遥控用一个枚举变量存当前模式。// main_control.c typedef enum { MODE_REMOTE 0, MODE_AVOID, MODE_TRACK } car_mode_t; volatile car_mode_t g_mode MODE_REMOTE; void main_loop(void) { while (1) { switch (g_mode) { case MODE_REMOTE: // 蓝牙命令在串口中断里已完成电机控制 break; case MODE_AVOID: if (app_tick 50) { // 50ms 周期测距 app_tick 0; avoid_obstacle_step(); } break; case MODE_TRACK: if (app_tick 10) { // 循迹 10ms 周期 app_tick 0; track_step(); } break; default: motor_set_stop(); break; } delay_ms(1); } }模式切换瞬间要先停电机再进新循环否则电机从当前占空比直接跳到新目标值冲击电流大小车还会突然窜一下。切换函数里把两个 PWM 输出清成 0延时 100ms 后再允许新一轮控制。循迹和避障的执行周期相差五倍是因为传感器性质不同数字输出的循迹传感器响应快10ms 周期就能跟上弯道变化超声测距一次要几毫秒测太频繁只会读到跳动值。周期参数分开维护模式切换时才不会互相拖累。5. 联调与参数收敛串口日志、速度斜坡和 PWM 占空比标定5.1 printf 重定向到调试串口让每个决策可追溯先把调试串口用起来。Keil 工程里勾选 MicroLIB重定向 fputc 到 USART2printf 就可以直接输出到串口终端。// debug.c int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); USART_SendData(USART2, (uint8_t)ch); return ch; }勾选 MicroLIB 很关键标准 libc 的 printf 会占掉大量 FlashMicroLIB 的实现精简到不到十分之一。IAR 环境下则不用勾 micro在编译器选项里把 stdout 配置成“通过 UART 输出”即可。日志格式我习惯带毫秒时间戳和模式枚举值printf([%lu] mode%d dist%5.1f track0x%02X\r\n, (unsigned long)uwTick, g_mode, obstacle_cm, track_raw);uwTick 在 SysTick 中断中每 1ms 加一拿到示波器或串口绘图工具里看能直接对应上某个时刻是哪种模式在控制电机。5.2 PWM 占空比死区与加减速斜坡PWM 占空比和轮速不是线性关系。L298N 驱动下占空比低于 25% 时电机静摩擦占主导轮子基本不动高于 85% 后转速趋于饱和继续加大占空比几乎没用。标定方法是在空载状态下从 0 逐步加大 PWM记下轮子开始转的最小值和转速不再上升的拐点这两点之外的区间才是有效控制范围。转向和直行对斜坡的需求不同。循迹时突然给满速会让红外采样跟不上高速弯道必然丢线遥控时陡变又有冲击感。斜坡函数限制每次调速的步长void motor_set_speed_smooth(int16_t target, uint8_t ramp_ms) { int16_t cur motor_current_speed; while (cur ! target) { if (target cur) { cur (target - cur 3) ? 3 : (target - cur); } else { cur - (cur - target 3) ? 3 : (cur - target); } set_motor_pwm(cur); delay_ms(ramp_ms); } }步长 3 对应约 0.3% 占空比ramp_ms 取 2 到 3ms从停止到 80% 大概需要 270ms体感上平顺且响应足够快。转弯时的斜坡要比直行时陡否则转向动作拖沓会错过弯道窗口。5.3 按现象查参数的对照表现象原因调整方向遥控前进时电机抖动PWM 频率低于 1kHz周期改 99提到 10kHz循迹高速过弯丢线转向比例太小或速度过高提高 Kp弯道前降速到 60%避障反复左右摆头转向延时不足 400ms加长转向持续时间关掉多余测距蓝牙偶发断帧电噪声或命令发送过频降低发送频率加帧校验和蓝牙断帧的隐蔽原因多半是 APP 端用了字符串模式发送单字节毛刺把状态机打乱。收到乱帧后解析失败会自动回到等帧头状态所以偶发丢帧不影响长期运行不用为一次失败去改协议。循迹振荡时优先把 Kp 降下来不要把巡线速度提上去。避障死转向时看一下超声波返回值确认不是 ECHO 电平超限导致每次测距都读到 99。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进