
简介本资源是一套面向嵌入式初学者与课程实验者的STM32智能小车综合实践代码包聚焦蓝牙手机APP遥控与红外循迹双功能实现适用于STM32F103C8T6核心板搭配L293D驱动及TT减速电机的硬件平台。压缩包共43个文件含KEIL4工程文件uvproj/uvopt、C源码.c/.h、编译中间文件.o/.d/.axf/.hex、电路图参考JPG、详细实验文档DOCX/DOC及安卓APP安装包总大小2.83MB结构完整、模块清晰便于理解外设配置、串口通信协议与PID循迹逻辑。已有3912人学习下载配套《程序说明必看》《APP使用说明》《智能小车蓝牙遥控-循迹实验》三份指导文档覆盖硬件连接要点、Keil工程配置、蓝牙AT指令调试及循迹传感器阈值调节等实操难点可直接烧录验证显著降低入门门槛。 拿到这个标题的压缩包时我第一反应是挺有代表性的——stm32f103c8t6、蓝牙遥控、手机APP、循迹这几个词几乎是一个完整的嵌入式入门实战闭环。很多同学在学完串口、定时器、PWM、GPIO之后会做一个小车来把这些知识点串起来而这个小车恰恰就是最好的“缝合怪”项目既有底层寄存器配置又有通信协议设计还有闭环控制逻辑。如果你刚把源码下载下来烧进去发现车子不动、蓝牙连不上、循迹跑飞别急着怀疑代码有问题大概率是硬件接线、电源配置、引脚映射这些“看不见的细节”没对齐。这篇文章我就把这个项目从硬件到软件、从配置到调试的完整链路拆开讲透顺便把我自己调试过程中踩过的坑也一并交代清楚。1. 这个项目的本质不只是一辆会跑的小车先说一句可能有点反常识的话这个项目的核心难度不在于STM32本身而在于它把单片机里所有基础外设一次性都用上了。很多人以为循迹小车就是“传感器检测到黑线就转弯”实际上你打开源码之后会发现里面同时牵扯了定时器输出PWM、串口收发、外部中断或GPIO扫描、逻辑判断状态机这些东西每一样单独拿出来都不难但组合在一起就需要你真正理解它们之间的配合关系。1.1 从源码包结构反推工程内容拿到rar之后先别急着解压烧录先看里面有什么。一般情况下这类源码包会包含整个Keil MDK工程目录比如USER/目录存放main.c、stm32f10x_it.c、系统配置文件HARDWARE/或BSP/目录存放各个外设驱动常见的有motor.c、bluetooth.c、tracking.c、delay.cSYSTEM/目录可能是正点原子或者野火风格的sys、usart、delay文件工程文件后缀是.uvprojx对应Keil 5版本如果你的Keil版本太老可能打不开。打开工程后你第一件事应该是去stm32f10x_conf.h和main.c里看外设初始化调用顺序而不是直接搜“循迹”或者“蓝牙”关键字。因为小车的整体行为是“初始化外设 → 死循环轮询”的经典裸机模型这种结构下main函数里的每个初始化函数顺序就是整个系统的启动逻辑。1.2 从整体框图理解三种工作模式以我见过的大多数同类工程为例主程序里一般会有三种模式蓝牙遥控模式、循迹模式、待机/手动切换模式。模式切换的常见做法有两种通过板上按键切换或者通过蓝牙指令切换。如果你拿到的代码是用按键切换那main循环里通常会有类似if(KEY_Scan() MODE_BUTTON)的分支如果是蓝牙指令切换那串口中断接收里就会对特定指令做模式标记切换。我建议你把源码的主循环逻辑先画成一张流程图来看不用画出太细就画清楚初始化结束之后系统进入的是什么默认状态每个循环周期里是先判断模式还是先执行控制蓝牙数据是同步处理还是异步标记、下一轮循环再处理循迹传感器的读取是放在循环轮询里还是放在定时器中断里。这个分析过程可能比烧录后的跑车实验更有价值因为它是整个工程的“骨架”。1.3 为什么是stm32f103c8t6而不是其他型号顺带聊一下为什么这类项目基本都用stm32f103c8t6。C8T6属于F103系列里的中低密度型号72MHz主频、64KB Flash、20KB SRAM在F103里算入门但做一辆两轮小车完全够用。关键在于它便宜、封装小LQFP48、网上资料极其丰富而且几乎所有开发板的例程都是这个型号导致你遇到问题时搜索引擎一搜就有一堆答案。我个人的观点是如果你以后想做一些带屏幕带操作系统的嵌入式项目C8T6可能偏紧但做小车、做遥控、做循迹、做简单物联网终端这个芯片的性价比非常合适资源也够你折腾。2. 硬件选型与接线把每一根线都解释清楚很多初学者拿到源代码之后烧进去发现一些功能不正常第一反应是“代码错了吧”但实际有超过一半的情况是硬件接线和代码里的引脚定义对不上。所以我强烈建议先把代码里的引脚定义翻出来拿着原理图一根线一根线核对再通电。2.1 最小系统板与常见引脚分配STM32F103C8T6最小系统板是淘宝最常见的蓝色板子板载一个AMS1117稳压芯片把外部5V转成3.3V给芯片供电引出了几乎全部GPIO还有一个Type-C或者Mini USB口用于下载程序。你下载程序时用的是串口下载通过板载或外接CH340/CP2102还是SWD下载ST-Link会在工程配置里看到不同选项。就小车项目而言代码里高频用到的引脚一般是这几个功能模块常见引脚说明左电机PWMPA6TIM3_CH1或 PA0需要定时器复用功能打印PWM波右电机PWMPA7TIM3_CH2或 PA1与左电机使用同一个定时器更省事电机方向控制PB0、PB1、PB10、PB11等普通GPIO推挽输出控制IN1~IN4蓝牙串口PA9USART1_TX、PA10USART1_RX最常用的串口1注意交叉连接循迹传感器PB3~PB7 或 PA0~PA3等取决于代码初始化和你的接线按键PA0或PB5用于模式切换、复位上面这张表不是绝对标准不同代码差别很大。拿到源码后你需要在stm32f10x_gpio.c相关的初始化文件里逐个查GPIO_InitStructure配置把每个外设用到的引脚列出来再去接线。2.2 L298N电机驱动板接线与电源共地小车电机驱动方案一般两种L298N模块或者TB6612模块。L298N是比较老但很经典的方案缺点是压降大、功耗高、发热明显TB6612更现代体积小、效率高、而且PWM频率响应更好。但很多课程设计和例程仍然用L298N因为使用简单、不容易烧。源码里如果你的电机控制逻辑是IN1~IN4加ENA/ENB使能那大概率就是L298N。接L298N的时候有几个坑必须注意第一电源必须共地。L298N的GND和STM32的GND必须连在一起否则电机驱动的逻辑电平无法被单片机控制这是新手最容易忽略的。具体现象是电机不转、或者偶尔转、或者一片乱转。第二L298N的12V输入和5V输出。如果你用的是两节18650串联7.4V左右给L298N供电然后从L298N的5V输出口取电给STM32板供电要确认L298N的板上稳压输出能力是否够。有些L298N模块的5V输出电流有限给STM32用可以但如果你还挂了蓝牙模块、循迹传感器一起工作可能导致电压被拉低复位。第三电机电源和单片机电源最好分开。我个人的实际经验是如果条件允许电机直接用独立电池组供电单片机、蓝牙、传感器用另外一组电池或者稳压模块供电两组电源之间只共地。这样电机启停瞬间的电流尖峰不会干扰单片机尤其在循迹模式下电机频繁调速电源噪声很容易导致系统复位那种“车子一加速就重启”的经典问题八成就是电源问题。2.3 HC-05蓝牙模块接线与电平匹配蓝牙模块在小车项目里一般用HC-05或HC-06。HC-05支持主从一体并且可以通过AT指令配置但也因为模块本身逻辑电平是3.3V很多模块板载了电平转换电路可以直接接5V单片机的串口。而STM32的IO口是3.3V电平跟HC-05之间通信不需要额外电平转换但供电需要接5V还是3.3V要看具体模块——老的HC-05模块一般是5V供电板上自带稳压和电平转换而有些国产兼容模块是3.3V供电你用5V去喂它可能直接冒烟。接线方式记住一句话单片机TX接模块RX单片机RX接模块TX交叉连接。然后GND共地。如果你用的是USART1PA9/PA10那蓝牙模块的TX就接PA10RX就接PA9。蓝牙模块上电后一般会有一个LED指示灯没连接手机时快闪连接成功后变成两秒慢闪。这个状态灯是你判断蓝牙是否正常工作的最直观指标。2.4 五路循迹传感器的安装高度与供电循迹传感器常见的有两路、三路、四路、五路。标题里提到的五路循迹传感器本质上是一排红外对管发射管接收管某一路遇到黑线或者白线时因为反射率不同输出电平会变化。数字量输出的模块比较常用板上已经有电压比较器一般是LM393输出TTL电平可以直接接单片机GPIO。传感器供电一般也是5V或3.3V具体看模块说明。安装高度上传感器离地面约1~2厘米比较合适离太近地面轻微不平就会蹭到传感器甚至刮花离太远红外线在黑白交界处的采样精度下降容易误判。安装位置要保证五路传感器跨在黑线两侧中间一路大概对准黑线的正中间这样小车直线行驶时中间传感器正好在黑线上方。另外要特别注意红外传感器和蓝牙模块不要放太近因为红外和2.4G蓝牙在电磁兼容上虽然不互相干扰但蓝牙模块的供电纹波、传感器信号线的走线如果靠太近某些情况下会导致传感器误触发或者蓝牙掉线。我在一个项目里遇到过蓝牙每次在循迹模式下跑到转弯处就断连的情况排查了两天发现就是传感器线束和蓝牙天线缠在一起。3. 软件架构解析从PWM到串口协议的状态机设计硬件搞定之后再看软件。这个工程的代码量并不大但如果看代码的视角不对容易看得一头雾水。我的建议是别从头到尾一行行读而是按数据流的方向读电源和时钟初始化 → 电机控制 → 传感器读取 → 蓝牙通信 → 主逻辑决策。3.1 定时器PWM配置频率选择与占空比调节电机调速是靠PWM实现的STM32的PWM产生方式是把定时器的某个通道配置为PWM模式然后在比较寄存器里塞一个值。开启PWM后GPIO引脚会持续输出频率固定、占空比可调的方波。比如你用TIM3的CH1和CH2代码里大概会有类似这样的配置片段void Motor_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; // 自动重装载值决定PWM频率 TIM_TimeBaseStructure.TIM_Prescaler 71; // 预分频72MHz / (711) 1MHz 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; // 初始占空比0 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OC1InitStructure); TIM_OC2Init(TIM3, TIM_OC2InitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE); }这里的频率 72MHz / ((Prescaler1) * (Period1))上面这段代码里就是 72MHz / (72 * 1000) 1kHz。为什么选1kHz因为电机驱动板的响应速度和噪声控制比较平衡。频率太低比如几十Hz电机会发出“嗡嗡”的啸叫而且转速波动明显频率太高比如20kHz以上驱动芯片的开关损耗上升L298N这类大功率管子可能发热严重。1kHz~10kHz是电机PWM的常用区间。当然TB6612可以跑更高频率但代码里如果没有特别配置一般默认在1kHz~10kHz之间。占空比的调节就是改TIM_SetCompare1(TIM3, duty)里的 duty范围是0到ARR值上面是0~999。duty0是停duty999是全速duty500就是约50%速度。3.2 串口接收与指令解析协议设计才是关键蓝牙遥控的数据最终是通过串口进到STM32的所以核心问题变成手机端发什么数据MCU怎么解析。工程里最常见的做法是定义一帧协议比如帧头长度/命令数据校验0xAA0x01~0x08速度值等累加和或异或校验有的工程更简单粗暴直接在每个字符上做判断比如F表示前进、B表示后退、L表示左转、R表示右转、S表示停止。这种实现最简单但有个问题如果你手机APP发的是字符串每个命令对应一个字符那串口中断里只需要做字符比较就行非常易懂。如果你打算自己改协议我建议至少加一个帧头和校验位。比如uint8_t rx_buffer[8]; uint8_t rx_index 0; uint8_t frame_flag 0; void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); if(rx_index 0 data ! 0xAA) return; // 等待帧头 rx_buffer[rx_index] data; if(rx_index 8) { // 检查校验 uint8_t sum 0; for(uint8_t i 0; i 7; i) sum rx_buffer[i]; if(sum rx_buffer[7]) frame_flag 1; rx_index 0; } } }主循环里再根据frame_flag处理数据然后清标志位。注意串口中断里尽量不要做耗时操作比如调用delay、处理复杂的电机转向逻辑应该把数据存下来标记一下回主循环再处理。这是嵌入式尤其需要注意的编程习惯中断里代码越短越好。3.3 状态机思想遥控模式和循迹模式如何共用一套电机控制函数代码里电机控制部分一般会抽象出一个通用的控制函数比如void Set_Motor_Speed(int left_speed, int right_speed) { if(left_speed 0) { IN1 1; IN2 0; TIM_SetCompare1(TIM3, left_speed); } else { IN1 0; IN2 1; TIM_SetCompare1(TIM3, -left_speed); } // 右轮同理 }这样设计的好处是无论是手机发来的遥控指令还是循迹算法算出来的转向指令只要最终落到Set_Motor_Speed(left, right)上逻辑就统一了。我见过有些初学者在每个分支里重复写一堆电机引脚控制代码最后逻辑混乱改一个参数要改好几处。用这个接口去重构代码会清晰很多。主循环里的模式切换大概长这样while(1) { if(mode REMOTE_MODE) { Handle_Bluetooth_Command(); } else if(mode TRACKING_MODE) { Handle_Tracking(); } }这里的小技巧是模式切换不能在主循环里只改一个标记变量就完事通常在切换时还需要做收尾动作比如停车、清空串口缓冲、重新初始化传感器状态。否则从循迹模式切回遥控模式时小车可能会因为上一次的残余PWM值立刻冲出去吓人一跳。4. 手机APP控制链路HC-05配置与通信协议约定这一节聊蓝牙遥控端。手机APP控制小车这条链路是手机蓝牙 → HC-05 → 串口 → STM32 → 电机驱动 → 电机。任何一个环节断了遥控都没反应所以排查时要按链路逐段验证。4.1 HC-05的AT指令配置必须掌握的通信基础HC-05上电之前按住模块上的一个小按键enter/AT键再上电模块会进入AT模式此时串口波特率固定为38400。用USB转TTL接电脑打开串口助手发AT指令。常用AT指令我列一下指令作用AT测试通信返回OKATNAMECar_Bluetooth修改蓝牙名称ATPSWD1234设置配对密码ATUART9600,0,0设置串口波特率9600、停止位1、无校验ATROLE1设为主模式你只需要从模式可以忽略ATCMODE1任意设备可连接0表示只允许绑定地址的模块连接注意AT模式下波特率是38400设置完毕重启后会按你设定的新波特率工作。如果工程代码里初始化的串口波特率是9600那模块也要设置成9600如果代码是115200模块就设115200。波特率不一致是最常见的蓝牙收不到数据的原因而且现象很迷惑——手机和蓝牙已经连上了但小车没反应。4.2 手机APP的两种路线现成工具与自研APP手机上控制小车有两种方案第一种直接用现成的蓝牙串口APP比如“蓝牙串口”、“Serial Bluetooth Terminal”等。这类APP本质上是把手机变成一个串口终端你输入字符或点击按钮就发送对应的数据。如果你只验证功能能不能通用这个最快。打开APP连接HC-05然后发送F、B、L、R、S等字符看小车动没动。第二种自己做一个简单APP。如果你会Android开发可以用Android原生的BluetoothAdapter BluetoothSocket写一个简单的SPP客户端。核心代码并不复杂BluetoothAdapter adapter BluetoothAdapter.getDefaultAdapter(); BluetoothDevice device adapter.getRemoteDevice(00:1A:7D:DA:71:13); // HC-05的MAC BluetoothSocket socket device.createRfcommSocketToServiceRecord( UUID.fromString(00001101-0000-1000-8000-00805F9B34FB)); socket.connect(); OutputStream out socket.getOutputStream(); out.write(F); out.flush();这个 UUID 是SPP串行端口规范的标准UUIDHC-05用的是这个不要随便改。APP界面可以做成几个方向按钮每个按钮的onClick里发送对应指令。如果你想控制速度可以自定义协议比如S:0.8表示前进速度80%解析的时候用字符分割。对于大多数课程设计项目我不会一上来就推荐你写一个完整APP因为工作量不小而且调试起来也麻烦。先用现成APP验证链路确认蓝牙和串口没问题之后再考虑做自己的APP这样效率最高。4.3 指令映射与防抖为什么按一下按钮小车不走调试蓝牙遥控时新手最容易遇到一个问题手机APP按钮按一下小车只动一下或者根本不动。原因一般是两个一个原因是按键发送的是短脉冲比如按钮按下时发F松开时什么都不发但小车代码里只有收到F的那一刻执行了前进动作然后因为循环次数太快这个动作转瞬即逝。正确做法是按键按下时发一个持续前进指令松开时发停止指令或者APP按下时连续发送代码里收到一次指令就持续执行该动作直到收到下一个不同指令。另一个原因是代码里没有把串口收到的指令“保持”下来。很多小车的代码结构是串口中断里直接调用电机动作函数这样的问题是如果手机发来F发生在中断里电机确实转了但主循环下一轮可能并没有“重复执行前进”的逻辑于是小车蹭一下又停了。更好的做法是串口中断里只保存当前指令到全局变量current_cmd主循环里每个周期根据current_cmd执行动作。这样即使手机只发来一个字节小车也会持续执行这个指令直到收到新指令。类似的效果也可以手机端做按钮按下时周期性发送数据包比如每100ms发一次松开时发送停止指令。4.4 双模式切换的实际操作建议如果你需要同时支持蓝牙遥控和循迹我建议在按键切换或指令切换的基础上增加一个明确的状态指示比如用板载LED区分模式LED常亮表示遥控模式LED闪烁表示循迹模式。这样野外做测试时不用每次低头看手机你就能判断当前状态调试体验会好很多。而且硬件上如果板子空间允许还可以加一个蜂鸣器切换成功响一声确认感很强。5. 循迹算法的核心从传感器读数到转向决策循迹小车是嵌入式控制里非常经典的一个场景但它真正的技术含量不在“判断黑线”而在“怎么平稳地跟线转弯”。一个好的循迹算法应该做到小车在直道上不左右摇摆在弯道处不冲出去过十字路口不迷惑。5.1 五路传感器怎么读数字量与模拟量的差别你拿到的传感器模块如果是五路数字量输出那每路输出要么是高电平要么是低电平。但是注意不同模块在“检测到黑线”时输出的电平可能不一样关键是看模块上的指示灯。通常情况下模块检测到黑线时指示灯亮、输出低电平检测到白底时指示灯灭、输出高电平。但有的模块极性相反所以你需要实测。代码里初始化这些GPIO时可以用上拉输入或者浮空输入具体看传感器输出是什么电平。稳妥的用法是GPIO_Mode_IN_FLOATING或者GPIO_Mode_IPU如果出现读到的值都是0或都是1那就换个输入模式试试。5.2 经典二值逻辑五路传感器状态查表法最简单的循迹控制就是查表法。假设传感器检测到黑线时输出0白底输出1五路从右到左的值用二进制表示比如00100表示中间那路在黑线上小车应该直行00010表示黑线偏右小车需要往右转。代码逻辑大致是uint8_t s1, s2, s3, s4, s5; s1 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_3); s2 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4); s3 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5); s4 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_6); s5 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7); uint8_t pattern (s1 4) | (s2 3) | (s3 2) | (s4 1) | s5; switch(pattern) { case 0b00100: Set_Motor_Speed(SPEED, SPEED); break; case 0b01100: Set_Motor_Speed(SPEED, SPEED*0.7); break; case 0b11100: Set_Motor_Speed(SPEED, SPEED*0.4); break; // 其他情况... }查表法优点是直观、好调缺点是每个状态之间是跳变的小车转向时会有“一顿一顿”的感觉尤其速度稍高一点就明显。5.3 比例控制用误差值做平滑转向比查表法高级一点的是比例控制。把五路传感器位置映射成一个误差值比如最左路在线 → 误差为 -2左二路在线 → 误差为 -1中间 → 0右二路 → 1最右路 → 2。然后左右轮速度用误差乘以比例系数P来调整int error 0; switch(pattern) { case 0b00100: error 0; break; case 0b01100: error -1; break; case 0b11000: error -2; break; case 0b00110: error 1; break; case 0b00011: error 2; break; } int p 30; // 比例系数需要实测 int left_speed BASE_SPEED p * error; int right_speed BASE_SPEED - p * error; Set_Motor_Speed(left_speed, right_speed);这个逻辑的本质是黑线偏离中心越多转向越猛偏移小的时候只需轻微修正。这样小车会走得很顺滑不会左右甩头。P值怎么调我的经验是先设一个较小的P比如10看小车在直道上能不能稳定走如果来回摆动、走出S形说明P偏大要减小如果过弯太迟缓、冲出赛道说明P偏小要加大。修改代码后重新烧录反复测试直到小车能稳定跑完整圈。你还可以加一个D项微分控制用误差的变化率来抑制摆动但对C8T6这种裸机小车来说P控制已经能跑出很不错的轨迹了先别急着上PID。5.4 循迹速度控制与过十字路口逻辑循迹小车能跑多快取决于你的赛道有多宽、转弯半径有多大、传感器响应速度多快。刚调试的时候建议把基础速度设在占空比30%左右也就是300左右跑稳了再一点点加速。过十字路口是个经典难点。如果五路传感器全部都在黑线上也就是11111小车处于十字路口中央。这时应该保持直行不能转弯否则会在路口迷失方向。但如果你的赛道是那种带斜线交叉的复杂路线11111的状态还可以分情况讨论比如只有直行路线的十字路口保持直行如果是T字路口可能需要按既定方向转弯。这部分的逻辑完全取决于你的赛道规则必须自己根据实际场地定义。还有一个容易忽视的问题是“出线保护”。小车如果速度太快转弯时可能会让传感器全部变成00000全部在白底上此时代码如果没有任何处理小车就会持续冲出去。一般可以加一个简单策略上一时刻是左转状态现在全白那就继续左转并降低速度给小车一个“找线”的机会。5.5 传感器阈值校准为什么同一个代码换个场地就不行数字量循迹模块板上的电位器是用来调节阈值的。这个阈值调节很重要不同地面、不同光照下红外线的反射强度不一样如果你在深色桌面调好了拿去浅色地面就可能全部误判。调节方法很简单把传感器放在黑线中央旋转电位器直到对应输出指示灯的临界状态发生翻转再放到白底上确保输出是另一个状态。反复几次找到一个折中位置。如果代码里传感器读取用的是ADC模拟量那就不存在阈值电位器而是你在代码里设一个比较阈值。现场测试时可以先打印各通道ADC值看看黑线和白底的数值区间取中间值作为阈值。6. 实测阶段最常踩的坑完整排查思路记录这部分我按真实调试顺序把最容易遇到的问题列出来。如果你烧录完发现现象不对按这个清单一项项排查大概率能定位。6.1 蓝牙连不上不是模块坏了就是配置没对齐手机搜不到HC-05或者搜得到但连不上排查顺序是第一步确认模块是否为AT模式。如果上电时误触发了AT键模块会进入AT模式此时无法被手机连接。断电重新上电不要按键。第二步确认供电。HC-05工作电流并不大但供电电压不对会直接导致模块不启动或者频繁重启。用万用表量一下模块VCC和GND之间的电压正常应该在模块要求的范围内。第三步确认是否有串口工具能通信。把RX/TX交叉接USB转TTL发AT指令测一下。如果发送AT返回OK说明模块是好的问题出在手机端配对。第四步手机端确认配对密码。HC-05默认密码常为1234但如果你之前改过又忘了可以重新进AT模式发ATPSWD1234手动重置。还有一个不太容易想到的有些手机蓝牙协议栈对经典蓝牙SPP设备的兼容性不太好尤其是部分新机型可能搜不到老式HC-05模块。这种情况可以试试用另一台手机或者换一个符合BLE标准的模块。6.2 电机不转或方向不对先测引脚电平再谈驱动电机不转时先分清是哪个环节出了问题。用一个万用表或示波器测电机控制引脚的电平测GPIO输出引脚有没有PWM波形如果有说明单片机配置正常测L298N的ENA/ENB使能引脚是不是高电平如果没接或接错PWM波形进不来电机当然不转测电机两端电压如果驱动板有输出但电机不转可能是电机本身卡死或者抱死。电机方向不对一般是IN1~IN4接线顺序接反了。比如小车应该前进结果右轮前进、左轮后退车在原地打转。两个办法解决要么对调电机两根线要么在代码里把方向引脚电平反过来。代码改方向比较方便但要注意别把左右轮的逻辑搞混。6.3 循迹跑飞阈值、速度、传感器位置三连查循迹跑飞看起来像“代码不管用”实际原因通常是三个第一传感器阈值漂移。前面说过换场地必须重新校准。如果你发现小车在直道上都能跑飞先检查传感器指示灯在黑白线上的状态对不对。第二基础速度太快。循迹算法在低速下可能很稳定速度一上来一轮循环才几十微秒的响应时间根本来不及修正。把速度降下来确认算法没问题再逐步提速。第三传感器安装位置偏了。五路传感器要居中安装如果装歪了传感器读到的“误差值”本身就有偏置小车会沿着一条歪线走。还有一个经验循迹测试最好在光线稳定但不刺眼的环境下进行。强烈阳光下红外线传感器可能因为环境光干扰造成误判在特别暗的环境下虽然红外传感器不太依赖可见光但地面材质反光差异会变大也可能导致阈值变化。6.4 小车跑一会儿就死机复位十有八九是电源问题如果你遇到的现象是小车刚开始跑正常跑几秒或几十秒后突然重启甚至按键无响应这时候别怀疑代码先怀疑电源。电机在启动、加速、受阻瞬间的电流尖峰非常大如果用同一个电源给单片机和电机供电电压毛刺会直接灌进单片机的复位引脚或者稳压芯片导致系统复位。我的建议是电机电源和逻辑电源分开共地线尽量短粗在电机电源两端并联一个大容量电解电容470uF以上和一个小容量的瓷片电容104吸收尖峰在单片机电源入口加一个100uF左右的电容也有帮助。另外如果电池电压偏低比如两节18650放电到7V以下L298N输出5V给单片机供电时可能连5V都稳不住系统也会复位。用万用表监控单片机电源的电压变化是定位电源类问题的有效手段。6.5 烧录失败驱动、启动模式与BOOT引脚程序下载不进去也是新手常遇到的。stm32f103c8t6下载方式主要有两种用ST-Link通过SWD接口接SWDIO、SWCLK、GND这种方式一般不涉及BOOT引脚比较省心串口下载通过USART1需要把BOOT0拉高一般是跳线帽短接到1然后按复位进入下载模式下载完再扣回低电平复位运行。很多同学烧录失败是因为串口下载时忘了操作BOOT引脚或者CH340驱动没装好或者串口号选错。还有一种情况Keil工程配置里Debug选项选择的是ST-Link但你的实际下载器是串口芯片那当然下载失败。先确认下载器类型和工程配置一致。7. 拿到源代码之后怎么把它真正变成自己的东西最后一个话题也是最实用的一份源代码下载下来怎么才算真正吃透。我认为有三个层级。第一层会烧录能运行。按步骤把程序烧进去小车能跑蓝牙能连循迹能走。这个层面你只能算“会用”。第二层能改参调优。比如改PWM频率、改占空比、改循迹P系数、改蓝牙波特率知道改了之后会有什么影响。这个层面你已经理解代码的关键变量。第三层能重构重写。把代码里的每部分拆开换成你自己的实现方式。比如把查询模式改成中断模式、把查表循迹改成比例控制循迹、把单字节指令改成完整协议帧。这个层面这个项目才算真正属于你。我给一个具体的“改造路线图”先把代码里的引脚定义全部列出来核对你的接线并做一份你自己的接线表。把电机控制部分抽出来写一个通用的Set_Motor_Speed()然后基于它重构遥控和循迹分支。在循迹模式下加入比例控制把速度调出一个你满意的平滑效果。自己设计一个两字节或三字节的蓝牙协议包含帧头、命令、校验替代原来的单字符指令。如果还有精力尝试把系统从裸机轮询改成定时器时基调度把循迹、蓝牙、速度控制放在不同的时间片里执行。这个过程走下来你对STM32外设的理解一定会上一个台阶。源代码里的代码别只当它是答案去抄。最好的用法是先照着跑通再在它基础上做修剪和重构直到有一天你可以不打开源码自己从头写一个类似的工程。那时候这个项目的价值才算完全体现出来。最后再分享一个我自己的习惯每次调试小车之前先准备一个串口调试助手把MCU的串口输出打开。在关键逻辑分支里加一点打印信息比如当前模式、当前传感器状态、当前收到的指令。别小看这几行printf它能帮你把“玄学问题”变成“看得见的问题”排查效率能翻倍。嵌入式调试最重要的就是你得先知道真实世界发生了什么再让代码去匹配真实世界。小车这东西硬件和软件就像左右轮哪一边慢半拍车就跑不直。本文还有配套的精品资源点击获取