ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32智能小车实战:PWM调速、循迹避障与蓝牙遥控全解析

STM32智能小车实战:PWM调速、循迹避障与蓝牙遥控全解析 1. 项目概述与整体设计思路说到STM32智能小车很多朋友一开始都觉得是个“玩具级”项目但真正动手做一遍就会发现它几乎涵盖了嵌入式开发最核心的底层能力GPIO控制、定时器PWM、外部中断、串口通信、传感器数据采集、状态机逻辑。我也是在折腾过好几块板子、烧过无数遍固件之后才敢说“入门神器”这四个字配得上它。这里把我最近整理的一套基于STM32F103C8T6的智能小车完整方案分享出来硬件选型、引脚分配、核心代码、踩坑记录都写清楚了代码可以直接抄走用。这套方案能做什么最基本的就是让小车动起来前进、后退、左转、右转、调速往上走可以做红外循迹让小车自己沿着黑线跑加一个超声波模块就能实现自动避障再加一块HC-05蓝牙模块手机就能遥控。适合刚学完单片机基础、正想做第一个完整项目的同学也适合参加工创赛、智能车校赛这类比赛时拿去做原型参考。整车的逻辑并不复杂但把每个模块单独调通再整合能把C语言功底和调试能力都练扎实。1.1 系统整体架构智能小车本质上是一个典型的“传感器-控制器-执行器”闭环系统。控制器用的是STM32F103C8T6属于M3内核主频72MHzFlash 64KBRAM 20KB做循迹避障这种体量的任务绰绰有余。执行器是四个直流减速电机编码器可以后面再加基础版本不接码盘也一样能跑。传感器部分我选了最主流的三个TCRT5000红外循迹传感器、HC-SR04超声波测距模块、HC-05蓝牙串口模块。数据流大概是这样的红外传感器检测地面反射光强度判断当前压在黑线上还是白地上超声波通过声波往返时间算出前方障碍物距离蓝牙模块接收手机下发的指令字符。这些信息汇总到STM32由主控判断此时该直行、转弯、后退还是停下然后通过PWM波控制电机驱动模块输出对应的电压最终让电机按指定速度和方向转动。驱动模块选的是经典的L298N虽然效率不如DRV8833这类集成MOS驱动但胜在皮实、便宜、资料多入门阶段不容易烧。1.2 功能设计与需求拆解我把功能拆成两个等级。第一级是“必须跑通”的基础功能电机正反转控制、PWM调速、按键或串口调试。第二级是“锦上添花”的扩展功能红外循迹、超声波避障、蓝牙遥控。建议新手一定按这个顺序推进不要上来三块传感器一起接出了问题很难定位。需求拆解下来整个系统的引脚需求并不紧张。C8T6有37个IO可用扣除SWD烧录占用的PA13/PA14还剩35个。四路红外用PA0-PA3四路电机控制两个PWM输出加八个方向IO用PA6-PA11加PB0-PB3超声波Trig和Echo用PA4/PA5或者PB4/PB5蓝牙串口默认接USART1的PA9/PA10OLED如果加的话用I2C的PB6/PB7这么分配完还留有富余。关键是提前规划好别等焊完线才发现引脚被占用那拆线比调代码还痛苦。2. 核心硬件细节解析与实操要点2.1 主控选型与最小系统说明ST官方其实有现成的开发板比如淘宝十几块钱的“蓝板”或者“红板”直接用没问题。但如果你打算把控制板和小车集成在一起、尽量减小体积那就要自己画最小系统。最小系统看着简单但有几个坑必须注意首先是BOOT0引脚必须通过10K电阻下拉到地不然上电后进不了Flash启动其次是复位电路NRST接10K上拉加100nF电容到地还有VDDA模拟电源脚很多人直接悬空这是不对的VDDA和VREF必须接3.3V并加滤波电容否则ADC读数会漂。晶振部分我建议用8MHz无源晶振两个20pF负载电容然后在STM32CubeMX里把PLL倍频到72MHz。HSE旁路模式对晶振参数要求比较苛刻新手直接用外部晶振加PLL最稳。如果你手头只有内部HSI也不是不行但系统时钟只有64MHz而且精度略差串口波特率会偏后续通信容易出问题。2.2 电机驱动模块与PWM调速原理L298N内部是两个H桥电路每个H桥可以控制一路直流电机的正反转和调速。它的逻辑非常简单使能端ENA/ENB接高电平时IN1和IN2的逻辑组合决定电机转不转、往哪转。IN11、IN20正转IN10、IN21反转IN1IN20或者同为1就刹车。调速的原理是让使能端输入PWM波通过占空比控制平均电压占空比越高电机转速越快这就是所谓的“斩波调压”。实际操作时我最常犯的错误是把ENA直接接3.3V以为“只要使能就行”。但大部分L298N模块的ENA跳线帽默认是把5V引到EN脚的去掉跳线帽后输入PWM才有作用。这里还牵扯一个电平匹配问题STM32的GPIO是3.3V电平L298N的逻辑输入一般兼容3.3V但稳妥起见IN1-IN4直接接STM32引脚没问题ENA/ENB接PWM输出共地后逻辑电平就能正确识别。另外一定要看清L298N的VS端子接电池正极VSS端子供电给芯片内部逻辑这块板子自带5V稳压输出可以从这里取电给单片机省一个降压模块。2.3 传感器模块接线与信号说明先看红外循迹。TCRT5000本质是一个反射式光电开关发射管发出红外光接收管根据反射光强度改变输出电压。地面是白色的时候反射光强模块输出低电平压到黑色引导线时反射光弱输出高电平。模块上的电位器可以调节灵敏度阈值这个必须实车调不要凭感觉拧。传感器与地面的高度建议控制在8-15mm太近了容易抖动太远了黑白区分度下降。四路传感器采集赛车线信息实际可以归约为五种状态全白丢线、左偏、居中、右偏、全黑十字或停车点。然后是HC-SR04超声波模块。它的原理是Trig脚给一个10us以上的高电平脉冲模块内部自动发出8个40kHz方波并等待反射Echo脚输出高电平高电平持续时间就是声波来回的时间。距离 高电平时间 × 声速340m/s / 2换算成厘米就是distance_cm time_us * 0.017。这个模块号称探测范围2cm-400cm精度比较高但在近距离小障碍物面前容易误判使用时要加滤波。蓝牙模块HC-05要重点说一下接线。它兼容3.3V和5V供电但TX、RX引脚最好用3.3V直接接STM32的RX、TX时要记住交叉连接STM32的TX接到HC-05的RXSTM32的RX接到HC-05的TX。很多人第一次没反应先查是不是交叉反了。HC-05默认波特率9600蓝牙配对PIN码通常是1234用AT指令可以改名字和波特率后面我会展开讲。2.4 电源系统设计电源是新手项目翻车率最高的环节没有之一。电机启动瞬间电流可以达到800mA甚至更高如果电池供电能力不足电压会瞬间跌落导致单片机复位表现就是小车一动就重启。我的方案是电池用两节18650锂电池串联标称7.4V满电8.4V。正极接L298N的VS端子GND必须和单片机、传感器全部共地。L298N板载5V稳压输出给STM32供电注意不能同时接USB的5V和L298N的5V两路电源并联会打架。如果是为了让电机跑得更稳可以在电机两端并联一个100nF高频瓷片电容外加一个220uF电解电容吸收换向火花产生的毛刺。电源线尽量用粗一点的杜邦线或者直接焊接细线在大电流下压降严重。还有一点STM32的3.3V稳压一般用AMS1117-3.3输入5V没问题但如果你某些模块需要从3.3V取电就要算一下总电流AMS1117最大输出1A超声波加OLED加蓝牙同时跑的时候余量不大最好让模块直接用5V。3. 实操过程与核心代码实现3.1 开发环境搭建与STM32CubeMX初始化开发工具我用的是Keil MDK5加STM32CubeMX另外ST-Link驱动必须装好。CubeMX图形化配置完自动生成HAL库工程省去手写寄存器初始化的时间但不能只依赖它生成的代码还是要能看懂、能改。CubeMX里需要配置的内容如下时钟树RCCHSE选择Crystal/Ceramic ResonatorPLLCLK倍频到72MHz。DebugSerial Wire否则烧录一次后第二次就No target connected。两个定时器TIM2和TIM3都选择PWM Generation CH1和CH2预分频72-1计数周期1000-1这样PWM频率就是72MHz / 72 / 1000 1kHz占空比CCR范围为0-999调节精度够用。USART1异步模式波特率9600开启全局中断。GPIO红外四路输入模式上拉输入或者浮空输入取决于模块输出类型一般推挽输出模块直接输入即可。超声波Trig输出、Echo输入都设为普通GPIOEcho加EXTI外部中断也可以但基础版用定时器轮询测量更简单。生成工程后把生成方式选为“Copy only necessary files”避免把整个库都复制一遍。代码风格我建议保持HAL库后面的例程都以HAL库为准。3.2 电机驱动核心代码电机驱动需要两个东西方向IO和PWM比较寄存器。定义一组宏方便写代码// 电机A 左后轮电机B 右后轮电机C 左前轮电机D 右前轮 // 这里以两路PWM四路方向IO控制一个双电机底盘为例 // IN1-IN4控制左电机和右电机方向ENA/ENB接PWM #define AIN1_PIN GPIO_PIN_0 #define AIN1_PORT GPIOB #define AIN2_PIN GPIO_PIN_1 #define AIN2_PORT GPIOB #define BIN1_PIN GPIO_PIN_2 #define BIN1_PORT GPIOB #define BIN2_PIN GPIO_PIN_3 #define BIN2_PORT GPIOB然后写低速控制的几个函数核心就是设置CCR的值// left_speed和right_speed范围: -1000 ~ 1000负值表示反转 void motor_set_speed(int16_t left_speed, int16_t right_speed) { if (left_speed 0) { HAL_GPIO_WritePin(AIN1_PORT, AIN1_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(AIN2_PORT, AIN2_PIN, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, left_speed); } else { HAL_GPIO_WritePin(AIN1_PORT, AIN1_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(AIN2_PORT, AIN2_PIN, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, -left_speed); } // 右侧同理使用TIM3_CH2 if (right_speed 0) { HAL_GPIO_WritePin(BIN1_PORT, BIN1_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(BIN2_PORT, BIN2_PIN, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, right_speed); } else { HAL_GPIO_WritePin(BIN1_PORT, BIN1_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(BIN2_PORT, BIN2_PIN, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, -right_speed); } }这里有个细节__HAL_TIM_SET_COMPARE传入的 compare 值不能超过 ARR也就是不能超过1000。所以调用这个函数时入参最好做一次限幅防止哪天写了一个超大值导致PWM输出变成恒低电平看起来像“电机没反应”其实是PWM异常。可以在函数开头加一段夹逼if (left_speed 1000) left_speed 1000; if (left_speed -1000) left_speed -1000;3.3 循迹功能实现循迹逻辑是整车的“大脑”之一。四路传感器从左到右编号为S1、S2、S3、S4读数结果用一个8位变量保存每一位代表一路传感器的电平。实际判断时不用很严格核心思想是“偏差修正”小车应该沿着黑线走当车偏左时右侧轮胎应该加速或左侧减速把车拉回中心。简单版代码如下uint8_t sensor_read(void) { uint8_t val 0; val | (HAL_GPIO_ReadPin(S1_GPIO_Port, S1_Pin) ? 1 : 0) 3; val | (HAL_GPIO_ReadPin(S2_GPIO_Port, S2_Pin) ? 1 : 0) 2; val | (HAL_GPIO_ReadPin(S3_GPIO_Port, S3_Pin) ? 1 : 0) 1; val | (HAL_GPIO_ReadPin(S4_GPIO_Port, S4_Pin) ? 1 : 0); return val; } void track_run(void) { uint8_t s sensor_read(); switch (s) { case 0b0110: // 中间两路压线直行 case 0b1110: case 0b0111: motor_set_speed(600, 600); break; case 0b0100: // 偏左车身压在线的左侧需要右转回正 case 0b1100: motor_set_speed(300, 700); break; case 0b0010: // 偏右需要左转回正 case 0b0011: motor_set_speed(700, 300); break; case 0b0000: // 全白丢线默认刹车或者原地转弯 motor_set_speed(0, 0); break; case 0b1000: // 最左传感器压线急右转 motor_set_speed(200, 800); break; case 0b0001: // 最右传感器压线急左转 motor_set_speed(800, 200); break; default: motor_set_speed(600, 600); break; } }判断逻辑中我用了“左侧比右侧慢、右侧快”来实现右转这核心是差速转向。很多同学误以为转向就要让一侧反转其实在小车速度不高、转向半径不大的时候差速就够了反转反而容易把传感器甩离黑线。循迹跑高速的时候还可以引入简单的PID把传感器的偏差值映射成PWM差速量不过那是后面优化的方向基础版本用状态查表法已经能稳定跑完一个小赛道。3.4 超声波避障状态机避障功能不一定要和循迹同时跑更多时候是单独模式。HC-SR04的驱动时序不复杂主要坑是延时。用HAL库的时候HAL_Delay基于SysTick容易和中断冲突测量时尽量避免在中断服务函数里调用长时间延时。float hcsr04_get_distance(void) { // 拉高Trig 20us触发 HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); delay_us(20); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); // 等待Echo拉高 uint16_t timeout 0; while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) GPIO_PIN_RESET) { delay_us(1); if (timeout 30000) return -1; // 超时约30ms } uint32_t start 0; TIM2-CNT 0; // 清空计数器直接用TIM2计us HAL_TIM_Base_Start(htim2); timeout 0; while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) GPIO_PIN_SET) { if (timeout 40000) { HAL_TIM_Base_Stop(htim2); return -1; } } start TIM2-CNT; HAL_TIM_Base_Stop(htim2); return start * 0.017f; // 单位cm }避障的决策我习惯用一个简单的状态机找到目标方向、直行、右转/左转、后退这几个状态之间通过距离阈值切换。伪代码如下void avoid_mode(void) { float dist hcsr04_get_distance(); if (dist 0) dist 100; if (dist 25) { motor_set_speed(700, 700); // 前方安全直行 } else { // 前方有障碍先原地右转一个角度 motor_set_speed(500, -500); HAL_Delay(300); } }这版只是“遇到障碍就右转”的简单策略实际跑的时候容易在障碍物旁边来回绕圈。更稳的做法是结合循迹传感器判断边界或者加一个状态变量记录上次的转向方向如果本次距离还是近就反向转可以显著减少卡死概率。3.5 蓝牙控制与串口中断解析HC-05蓝牙模块用USART1通信手机端的蓝牙串口助手发一个字符STM32通过串口中断接收。中断回调里只做一件事收字符并存到全局变量不去处理业务逻辑。处理逻辑放到主循环里轮询这样不容易丢数据。uint8_t bt_cmd 0; uint8_t bt_flag 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { bt_cmd rx_buf[0]; bt_flag 1; HAL_UART_Receive_IT(huart1, rx_buf, 1); } } void bluetooth_control(void) { if (bt_flag) { bt_flag 0; switch (bt_cmd) { case F: motor_set_speed(800, 800); break; case B: motor_set_speed(-800, -800); break; case L: motor_set_speed(500, 800); break; case R: motor_set_speed(800, 500); break; case S: motor_set_speed(0, 0); break; default: motor_set_speed(0, 0); break; } } }注意初始化的时候要调用一次HAL_UART_Receive_IT(huart1, rx_buf, 1)这样串口一直处于接收状态否则中断回调永远不会触发。HC-05如果一直连不上把它上面的按键按住上电模块会进入AT模式模块指示灯慢闪此时波特率固定38400发送ATNAMECar可以改名ATUART9600,0,0可以改通信波特率改完重新上电生效。3.6 主函数整合与模式切换主函数的思路很直白把所有模块初始化完后进入while循环用一个mode变量决定当前走哪套子程序。我预留了按键PA0切换模式同时用蓝牙字符‘1’‘2’‘3’也能切。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); MX_TIM3_Init(); MX_USART1_UART_Init(); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); HAL_UART_Receive_IT(huart1, rx_buf, 1); uint8_t mode 1; while (1) { if (mode 1) { track_run(); // 循迹模式 } else if (mode 2) { avoid_mode(); // 避障模式 } else if (mode 3) { bluetooth_control(); // 蓝牙遥控模式 } HAL_Delay(10); } }10ms的延时让主循环频率大概100Hz对循迹和避障来说足够对蓝牙遥控也足够灵敏。如果你后续加OLED显示参数主循环还可以抽空刷新屏幕注意别在中断回调里做耗时操作就行。4. 常见问题与排查技巧实录4.1 电机不转或抖动电机不转先不要怀疑单片机坏了90%是硬件连线问题。用万用表量L298N输入端和输出端的电压如果输出端没有电压查ENA/ENB是不是接了PWM且PWM占空比不为0如果ENA悬空电机肯定死活不转。还有就是把PWM频率设得太低会听到明显的“嗡嗡”声电机一顿一顿这时候把ARR从1000改成1000以内分频系数调小一点比如把PWM频率提到10kHz抖动会明显改善。4.2 循迹跑偏或丢线菜鸟最常见的做法是把全白当成“停车”但赛道上确实存在大弧度弯道四路全白其实是冲出赛道了。我调车时发现如果循迹速度超过某个阈值丢线后直接刹车容易甩尾导致传感器离地弹跳反而更难回线。更好的策略是丢线后保持上一次的转向方向继续转一会设一个超时时间超过20ms还没检测到线再停车。另外传感器安装间距不能太大四个TCRT5000一字排开最左和最右探头的间距控制在60-70mm左右刚好覆盖常规黑线一般线宽15-20mm太宽容易漏检中间位置。4.3 超声波测距数据跳动HC-SR04在一个测量周期还没结束时如果又触发了一次TrigEcho回波会互相干扰。我踩过最明显的一个坑是HAL_Delay(50)放在测距函数里导致主循环在等待时无法响应其他输入。正确的做法是保证两次测量间隔大于60ms同时给Echo等待循环加超时否则在空旷场地测距容易卡死在while循环里。数据跳动还可以做简单的中位值滤波取连续5次距离排序后取中间值稳定很多。4.4 蓝牙连不上或乱码蓝牙模块连不上的原因第一查波特率第二查配对状态第三查TX RX交叉。手机蓝牙搜不到设备把HC-05按住复位进入AT模式用USB转TTL单独测试排除STM32端干扰。串口打印出现乱码多半是系统时钟不对导致波特率实际值和9600有偏差重新检查CubeMX时钟树配置。另外注意HC-05和HC-06的差异很大HC-05默认是双向透传HC-06只是从机直接用HC-06做手机遥控足够了代码和接线完全一样。4.5 烧录失败No target connected这个实在太常见了。用ST-Link下载失败时按住STM32板上的复位键在点下载的瞬间松开成功率非常高。还有一个原因是CubeMX生成工程时没选Serial Wire导致第一次下载后SWD引脚被禁用这种情况只能按住复位再下载或者用串口ISP清掉Flash。如果完全检测不到芯片检查BOOT0有没有跳到1跳成1之后芯片进入ISP模式FLASH启动不执行用户代码但不影响SWD连接。4.6 常见问题速查表现象可能原因解决办法电机不转ENA/ENB没接PWM或占空比为零检查使能引脚示波器或万用表量PWM输出电机转但无力电池电量低或电源线过细换锂电池组加粗电源线小车一启动就复位电机瞬间拉低电压主控和驱动共地用稳压模块分开供电循迹一直向左偏传感器灵敏度不一致逐个调节电位器用串口打印传感器值校准超声波数值为0Trig/Echo接反或模块供电不足检查接线模块独立5V供电蓝牙遥控延迟大串口中断业务逻辑过多主循环轮询处理中断只收数据烧录识别不到芯片SWD引脚被占用或没选Serial Wire按住复位下载CubeMX勾选Debug-Serial Wire4.7 调车避坑经验说实话智能小车这种项目调车花的时间比写代码多得多。我总结几条自己的经验第一拿到套件先把每个模块单独用串口打印数据确认传感器读数符合预期再接下一块别一口气全接完再排查第二调试时写一个串口助手输出函数把PWM占空比、传感器状态、当前模式都打出来能看清小车内部的“想法”第三PWM调速一定要做限幅保护防止非法参数第四电池电量对性能影响极大满电和快没电时候的循迹速度参数完全不同最好在比赛或演示前充满电再调参数。从最开始只会点灯到后来能独立调通循迹避障蓝牙三合一这个项目让我对嵌入式开发有了整体认知。代码写完只是第一步真正把它放在地上跑起来、处理各种跑偏和翻车才是能力的体现。如果后续想继续扩展可以加MPU6050做惯性导航加OpenMV做摄像头识别甚至把控制算法换成完整的PID这块板子和这套框架都撑得住。
返回列表