
简介本资源是一套面向嵌入式初学者与课程设计者的STM32智能小车综合实验方案聚焦STM32F103C8T6核心板在蓝牙遥控与自动循迹双模控制中的工程实践。资源提供完整KEIL4工程涵盖L293D电机驱动、HC-05类蓝牙模块通信、红外循迹传感器数据采集与PID调速逻辑等关键功能支持安卓手机APP远程操控与自主路径识别切换适用于电子竞赛备赛、单片机课程设计及毕业设计原型开发。压缩包共43个文件含uvproj工程配置、C/H源码与头文件、编译生成的axf/hex/obj中间文件、电路参考图JPG、APP安装包及三份详细文档含程序说明、实验指导与APP使用指南整体大小2.83MB结构清晰便于理解软硬件协同逻辑。目前已有3912人学习下载所有代码经作者实车验证可直接烧录运行附带典型问题说明与模块接线提示显著降低调试门槛。 我做了个STM32F103C8T6智能小车最终效果是手机APP通过蓝牙遥控同时把循迹跑线也整合进去了整套程序源码打包放了出来。这篇文章会把硬件选型、蓝牙通信协议、循迹算法、电机控制这些关键的坑和思路全部拆开讲提供一个可以直接复现的完整方案给正在做课设、毕设或者单纯想玩STM32的朋友做个参考。先交代一下背景。我做这个小车的直接动机是当时有个朋友要参加一个校内电子设计竞赛题目就是做一辆能够“手动遥控自动循迹”的智能车。而市面上能找到的大部分例程要么只写了蓝牙要么只写了循迹很少有把两者完整融合而且在同一块板子上稳定跑的。我干脆自己从头搭了一套方案把两块功能全部打通跑完比赛之后把代码整理成了这个压缩包。下文所有内容都会对应压缩包里的源码结构来讲。1. 项目整体设计与硬件选型1.1 为什么选STM32F103C8T6作为主控如果你接触过单片机开发应该知道STC89C52、Arduino Uno这类入门板。它们做简单流水灯、超声波测距没问题但要同时处理蓝牙串口数据、五路循迹传感器采样、两路电机PWM输出而且还要保证响应速度不卡顿8位机的算力和外设资源其实很紧张。STM32F103C8T6则完全不一样它就是一颗Cortex-M3内核的32位处理器主频最高72MHzFlash有64KBRAM有20KB片上集成了USART、I2C、SPI、ADC、PWM定时器等常用外设。做小车这类项目它有几个明显的优势外设资源足够。你需要两个串口的话它有一个USART1和一个USART2可以用一个接蓝牙、一个留给调试互不干扰。循迹传感器输出模拟量也能直接进ADC通道不需要额外扩展。PWM输出精度高。定时器可以输出多路PWM电机调速用16位定时器生成脉冲信号速度调起来比软件延时模拟出来的细腻得多。中断响应快。蓝牙数据一条一条进来串口中断能保证你不丢字节循迹传感器状态变化也可以做成外部中断实测下来整体系统的响应时间是毫秒级的。最便宜的C8T6最小系统板只要十几块钱实验室废板子上也能拆。对于学生项目来说成本非常友好。1.2 硬件清单和接线方案这个项目的硬件清单并不复杂整体加起来几十块钱。我实际用的是硬件型号/规格备注主控板STM32F103C8T6最小系统板蓝色那块即可电机驱动L298N模块如果用小功率电机换TB6612也完全没问题电机TT马达带减速箱4个两两对应左右轮蓝牙模块HC-05或HC-06建议用HC-05支持主从一体调试方便循迹模块五路循迹传感器TCRT5000比单路、双路抗干扰能力好很多电池18650锂电池两节7.4V不能只用3.3V供电否则电机带不动车架四驱亚克力小车底盘随便买关键是四个轮子转速一致杜邦线公对公、公对母若干建议多备几种颜色方便查线接线方案比较关键不要接错。我的接法如下电机驱动L298N的IN1、IN2接PA0、PA1控制左轮正反转IN3、IN4接PA2、PA3控制右轮正反转。ENA和ENB接PB0、PB1用PWM调速。蓝牙模块HC-05的TXD接STM32的PA10USART1_RXRXD接PA9USART1_TX。注意交叉连接这个很多人接反了收不到数据。五路循迹传感器从左到右分别接PA4、PA5、PA6、PA7、PB6。传感器模块VCC接5VGND接GND输出口直接接GPIO输入。STM32的3.3V不能直接驱动L298N的逻辑电平需要共地但VCC要从电池电压经过降压模块给这个后面会在电源部分细说。实际调试过程中我踩过两次接线坑一个是蓝牙的TXD和RXD接反APP发数据一直收不到另一个是L298N没有共地导致PWM信号乱跳。这两处建议新手在通电之前反复核对一遍。2. 蓝牙遥控功能拆解协议、APP与调参2.1 HC-05蓝牙模块的关键配置蓝牙模块这部分我先说连接方式。HC-05用串口和STM32通信默认波特率是9600但不同商家预烧固件可能不一致所以第一步建议先做模块配置。拔掉蓝牙模块的VCC按住模块上的小按键再重新上电会进入AT模式这时模块指示灯慢闪大概2秒一次。通过USB转TTL连接到电脑发送AT指令配置AT // 测试响应返回OK ATNAMESMART_CAR // 修改蓝牙名称便于手机识别 ATROLE1 // 设为主机模式若作为从机模式可设为0 ATUART9600,0,0 // 设置波特率9600停止位1无校验 ATPSWD1234 // 设置配对密码关键点在于ATUART这个指令。波特率必须和STM32端串口初始化代码保持一致否则APP发送来的数据全是乱码。我工程里的usart.c配置的就是9600所以模块也是9600两边匹配好一次调通。2.2 手机APP控制方案选型手机APP控制这块有两条路线可选现成的串口调试助手和自己写APP。如果你希望项目“开箱即用”可以直接在手机上装一个“蓝牙串口助手”类的软件搜索HC-05配对之后用界面上的按键发送指定的字符即可。工程代码里默认解析以下指令指令字符功能F前进B后退L左转R右转S停止0-9挡位调速0最慢9最快自己做安卓APP的话原理也很简单调用系统的BluetoothSocket和服务端建立SPP连接然后通过输出流发送字节数据。界面就是几个按钮按下发送对应字符松开发送停止字符。这里比较推荐新手用QT或者MIT App Inventor快速做一个不用从零啃Java图形化积木方式能让你把精力放在逻辑上。如果是毕设或者需要答辩展示自己写APP会更有亮点代码量其实也不大核心就是Socket通信。2.3 串口中断接收与指令解析蓝牙数据接收是工程里非常关键的一块不能在主循环里用堵塞方式等待串口数据否则小车会僵住。我采用的是USART1全局中断接收void USART1_IRQHandler(void) { uint8_t ch; if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { ch USART_ReceiveData(USART1); if(ch \n) { rx_complete_flag 1; } else { rx_buffer[rx_index] ch; } } }这里有个小技巧很多APP发送字符时后面会带换行符所以用换行作为一帧结束的标志。主循环检测rx_complete_flag置位后集中解析这次收到的完整指令数据。指令解析部分我写成了switch-case比较清晰void parse_command(void) { switch(rx_buffer[0]) { case F: car_forward(speed); break; case B: car_backward(speed); break; case L: car_left(speed); break; case R: car_right(speed); break; case S: car_stop(); break; default: if(rx_buffer[0] 0 rx_buffer[0] 9) { speed (rx_buffer[0] - 0) * 10; } break; } memset(rx_buffer, 0, sizeof(rx_buffer)); rx_index 0; rx_complete_flag 0; }速度调节逻辑注意一点用数字字符转成数值后乘10作为占空比百分比。比如发送5speed就是50对应PWM占空比50%。实测下来占空比低于20%电机基本转不动所以在调速函数里加了一个下限保护低于20直接按20处理通过软件避免电机堵转。3. 循迹系统设计与控制算法3.1 五路循迹传感器的工作原理循迹的原理并不复杂。黑色跑道和白色底面对红外光的反射能力不同TCRT5000传感器发射红外线接收管根据反射光的强度输出不同电平。白底反射强接收管导通输出低电平黑线吸收红外光输出高电平。五路循迹传感器的优势在于中间三路用来判断车体是否偏离两边的两路用来提前感知大弯道和十字路口。相比单路和双路五路不会在遇到岔路时直接丢失目标线。我用的模块数字输出每个探头输出0或1五路组合起来正好可以形成一个5位的状态码uint8_t track_state 0; track_state | (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4) 0); track_state | (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5) 1); track_state | (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) 2); track_state | (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7) 3); track_state | (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_6) 4);五个探头从左到右编号为1到5规定探头压在黑线上输出1否则输出0。我按这个规则把常见情况做了查表处理。循迹的核心其实就是查表状态码和动作一一对应没有太多复杂的数学计算。3.2 循迹查表策略与PWM差速我定义的巡线状态表如下状态码二进制对应情况动作00100中间探头压线车体正直行01100 / 00110稍微偏右/偏左轻微左修/右修11100 / 00111偏得较多较大幅度左转/右转11000 / 00011严重偏离原地左转/右转10100 / 00101进入十字或遇到断点直行继续00000全部丢线保持上次动作11111全部压线停车在这个表里核心处理技巧是“保持上次动作”和“继续直行”。循迹小车有个很常见的现象在十字路口五个探头会同时压到黑线如果程序不加判断直接按某个方向转弯就会跑出路径。遇到全0的情况一般是车子冲出弯道或者探头悬空这时不能乱打方向保持上次动作让小车的惯性把它带回来这是我在赛道上试了很多次之后总结出来的经验。动作执行依赖PWM差速。左轮和右轮分别用两个PWM通道控制void set_motor_speed(int left_speed, int right_speed) { if(left_speed 0) { GPIO_SetBits(GPIOA, GPIO_Pin_0); GPIO_ResetBits(GPIOA, GPIO_Pin_1); TIM_SetCompare2(TIM3, left_speed); } else { GPIO_ResetBits(GPIOA, GPIO_Pin_0); GPIO_SetBits(GPIOA, GPIO_Pin_1); TIM_SetCompare2(TIM3, -left_speed); } if(right_speed 0) { GPIO_SetBits(GPIOA, GPIO_Pin_2); GPIO_ResetBits(GPIOA, GPIO_Pin_3); TIM_SetCompare3(TIM3, right_speed); } else { GPIO_ResetBits(GPIOA, GPIO_Pin_2); GPIO_SetBits(GPIOA, GPIO_Pin_3); TIM_SetCompare3(TIM3, -right_speed); } }比如左转修偏时设置左轮速度30、右轮速度50两轮速度差越大转弯半径就越小。因为STM32的定时器比较寄存器是16位的我设置ARR为999所以比较值设为0到999和上面代码的速度值含义直接对应。3.3 循迹速度换挡与赛道适应性循迹不是一味求快就行。我测试下来速度参数分两档比较合理——直线用快速档弯道用慢速档。判断逻辑是从当前状态码的二进制位可以看出是否有外侧探头压线如果只有中间那一路或中间偏左右一路压线说明是直线可以直接全速如果有外侧探头加入说明前方有弯道要提前降速。这样处理可以让小车在进入弯道前就完成减速而不是到了弯道中心才急刹车。具体控制函数我简化成void line_follow_run(void) { uint8_t s read_track_sensor(); switch(s) { case 0x04: // 00100 set_motor_speed(LEFT_SPEED, RIGHT_SPEED); break; case 0x06: // 00110 偏左 set_motor_speed(LEFT_SPEED - ADJUST, RIGHT_SPEED); break; case 0x0C: // 01100 偏右 set_motor_speed(LEFT_SPEED, RIGHT_SPEED - ADJUST); break; case 0x07: // 00111 偏左较多 set_motor_speed(LEFT_SPEED - ADJUST*2, RIGHT_SPEED); break; case 0x1C: // 11100 偏右较多 set_motor_speed(LEFT_SPEED, RIGHT_SPEED - ADJUST*2); break; case 0x00: // 丢线保持 set_motor_speed(LEFT_SPEED, RIGHT_SPEED); break; default: set_motor_speed(LEFT_SPEED, RIGHT_SPEED); break; } }上面代码里LEFT_SPEED、RIGHT_SPEED是基础速度ADJUST是修正幅度。具体数值需要根据你的电机、电池电压、地面摩擦系数来调。我用的是40%占空比基础速度修正幅度8%这个参数在普通瓷砖地面上效果最好。如果你用木地板或者地毯摩擦系数不同这个值要重新标定。4. 源码工程结构与核心代码实现4.1 工程目录和你拿到的压缩包里都有什么解压这个.rar文件之后里面是标准的STM32标准外设库工程结构。我用的是固件库3.5版本开发环境是Keil MDK5。如果你是第一次打开工程建议先装好Keil MDK5和对应的STM32F1系列器件支持包然后直接打开USER目录下的.uvprojx文件即可。主要目录结构如下smart_car/ ├── USER/ │ ├── main.c // 主函数、系统初始化、模式切换 │ ├── stm32f10x_it.c // 中断服务函数 │ └── system_stm32f10x.c // 系统时钟配置默认72MHz ├── HARDWARE/ │ ├── motor.c / motor.h // 电机驱动、PWM初始化 │ ├── bluetooth.c/.h // 串口1初始化与蓝牙数据接收 │ ├── tracking.c/.h // 五路循迹传感器读取与巡线策略 │ └── delay.c/.h // 延时函数 ├── CORE/ └── LIB/修改硬件的时候只需要修改HARDWARE目录下对应文件就行。比如把L298N换成TB6612只需要改motor.c里GPIO和PWM通道的宏定义不需要动main.c。4.2 main函数里的模式切换设计主函数采用了一个简单的模式标志位默认处于蓝牙遥控模式但可以通过蓝牙发送特定指令切换为循迹模式。这样一台车既能平时手动遥控玩又能放到赛道上跑自动循迹。int main(void) { delay_init(); GPIO_Config(); TIM3_PWM_Init(999, 71); // PWM频率 72M/(711)/(9991) 1kHz USART1_Config(9600); Tracking_GPIO_Init(); while(1) { if(work_mode MODE_BLUETOOTH) { if(rx_complete_flag) { parse_command(); } } else if(work_mode MODE_TRACKING) { line_follow_run(); delay_ms(10); } } }PWM频率1kHz听起来简单但实际选择是有讲究的。L298N这类模块内部是完全由H桥开关控制的频率过低会听到明显的电机啸叫频率过高又会增大开关损耗1kHz到10kHz是首选范围。我用1kHz是靠耳朵听没有噪声后定下来。4.3 电机PWM初始化的具体参数计算PWM初始化代码里TIM3_PWM_Init(999, 71)的两个参数值得解释一下。STM32F103C8T6的定时器时钟来自APB1通过时钟树倍频后到达72MHz。PWM频率的计算公式为PWM频率 72MHz / (PSC 1) / (ARR 1)我把预分频器PSC设为71ARR设为999代入公式就是72000000 / (711) / (9991) 72000000 / 72 / 1000 1000Hz这样得到的PWM频率是1kHz周期1ms。占空比就是比较值除以1000比如设置TIM_SetCompare2(TIM3, 500)就是50%占空比电机半速转动。这个参数组合同样方便换算临时想改占空比直接在比较值上加减数字就行。4.4 五路循迹初始化与采样周期循迹传感器模块只要把五个GPIO配置成上拉输入然后周期读取IO电平即可。我用的GPIO配置方式如下以PA4为例GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);上拉输入是为了确保传感器输出端悬空时IO口能保持高电平避免误判。循迹的采样周期我控制在了10ms左右也就是每10ms读一次传感器并调整一次方向这个速度在普通黑白赛道上是绰绰有余的。如果你遇到的赛道弯道特别急可以把这个周期缩短到5ms但要注意主循环里其他逻辑的耗时不能过长避免采样抖动。5. 常见问题与排查技巧实录5.1 蓝牙模块连不上手机或者收不到数据这个现象在社区里问得最多。我总结下来分为三类原因第一模块没有进入配对模式。HC-05上电后指示灯快慢闪不同如果一直是慢闪说明没有进入可配对状态需要重新上电或按模块上的复位信号。第二波特率不匹配。模块默认9600但你的串口助手或者STM32代码里设置的是115200两边频率不一致就收不到正确数据。可以先用USB转TTL连接电脑打开任意串口调试助手以9600发送AT测试确认模块还能响应。第三接线问题。TXD和RXD必须交叉连接如果模块TXD接到板子的TXD数据收发就全乱了。另外一定要检查是否有共地如果不共地信号参考电位不一致串口通信就会失败。我建议上电后用万用表量一下模块TXD引脚的电压正常空闲状态应该在3.3V左右。常见问题定位速查表现象可能原因排查方法手机搜不到蓝牙模块没进入配对模式按模块按键重新上电看指示灯是否快闪搜索到但连接不上配对密码错误恢复默认密码1234或重新AT设置连上但发送APP无反应TXD/RXD接反或波特率不对用USB转TTL单独测试模块再用串口监视器看数据偶发丢数据电源纹波大或共地不良检查电源给单片机单独滤波电容5.2 循迹小车出线、跑飞、原地打转循迹跑飞是最让人头疼的。原因通常有两个传感器安装高度不合适或者速度太快。TCRT5000传感器的有效检测距离一般是1cm到2cm安装时探头离地面高度应控制在1.5cm以内并且要保持水平。如果你把传感器装得偏高检测灵敏度下降黑白边界判断就会模糊车就容易冲出赛道。我实际测试时用手按压传感器支架找到刚好能稳定识别黑线的临界高度然后固定在这个位置。速度方面如果你的赛道有大量直角弯和S弯全速跑必飞。解决思路是两级速度直线速度可以高但检测到外侧探头压线时立即降低到弯道速度。这个策略在代码里已经写好关键在于LEFT_SPEED、RIGHT_SPEED和ADJUST三个宏要重新标定。一个简单粗暴的标定方法先把基础速度调低到能稳定过直角弯然后慢慢增加直到入弯时第一次跑飞记下这个速度上限留出10%的余量作为最终设定值。原地打转的问题则是左右电机基准速度差异大。两个TT马达即使型号相同实际转速也可能有偏差可以通过增加一个直行校准参数解决。最简单的办法是在代码里对某一侧的PWM比较值加一个修正偏置比如左侧电机偏快就把LEFT_SPEED减去5%反复调整直到直线段不跑偏。5.3 电机不转、转动无力、芯片发烫电机不转优先排查L298N供电是否正常。L298N的12V和5V跳线帽要注意如果只靠STM32的3.3V给逻辑部分供电那没问题但电机工作电压必须外接电池。我用的两节18650电池串联约7.4V接在L298N的12V输入口上同时用模块自带的5V输出给单片机板供电这是一个经典方案。转动无力的问题通常出在PWM占空比太低或者电池电压跌落了。解决方法是把最低速度阈值设定在20%以上同时检查电池电量TT马达电压低于5.5V后扭矩明显下降。芯片发烫特别是L298N烫手可能是因为电机堵转电流过大。这种时候要检查轮子是否被卡住以及占空比是否长时间维持在满值。小车在停止状态下程序如果还输出PWM就会导致堵转电流长期存在。我在代码里做了处理收到停止指令时直接清零比较值而不是仅仅改变转向引脚电平。6. 项目扩展与个人经验总结这个基础方案跑通之后往下的想象空间很大。硬件不用大改只要在C8T6的空余引脚上外接传感器就能继续升级加一个超声波测距模块HC-SR04把避障功能做进去。在蓝牙模式下优先级最高前方障碍物小于30cm自动停车或绕行。加一个OLED显示屏实时显示当前速度、循迹状态码、蓝牙连接状态调试和答辩演示都非常直观。把蓝牙模块换成ESP8266或者ESP32就可以把控制指令换成WiFi网络下发手机APP不用蓝牙配对直接局域网连接。循迹做成参数可调增加一个按键切换不同赛道模式分别对应直线高速、弯道中速、S弯低速等场景。我个人实际做完这整个项目之后最大的体会是做嵌入式小车代码本身不是最难的难点在调试的耐心和系统化排查问题的思路。刚上电可能会遇到一堆奇怪问题——蓝牙连不上、电机不转、循迹出线但只要你按模块逐一排查每一步都确认无误再推进其实都能很快解决。踩过几次坑之后你会对GPIO配置、串口中断、PWM调制这些底层概念有完全不一样的理解。最后再说一个很多新手会忽略的小技巧调试的时候在main函数里加一个LED翻转指示进入蓝牙模式闪一下进入循迹模式闪两下这样你不用盯着串口打印光看板子上的LED状态就知道程序跑到哪里了。这个习惯帮我省了很多定位问题的时间建议你也试试。本文还有配套的精品资源点击获取