
简介这套STM32F103C8T6智能小车源码在Keil4环境下编写覆盖红外遥控、红外避障、红外跟随、红外循迹与电机调速五类实验功能经L293D驱动、TT直流减速电机的实车亲测可用适合嵌入式入门、电子竞赛或课程设计参考。资源含41个文件、约365KB涉及C/H源程序、Keil工程配置uvproj/uvopt、可烧录Hex、编译中间文件以及L293D电机驱动电路图和程序说明便于直接编译和二次修改。目前已有1639人学习下载对需要快速搭建同功能小车的开发者而言主逻辑与系统配置具备直接参考价值。代码按SYSTEM/USER等目录组织延时、系统初始化与主控分离红外遥控和多传感器切换逻辑清晰“程序说明必看.txt”可帮助理解外设配置与调速实现减少重复调试成本。1. 一篇代码看懂红外小车的五种工作模式怎么共存手头这份stm32f103c8t6智能小车红外遥控红外避障红外跟随红外循迹调速实验程序源代码把五种功能塞进了一个 KEIL4 工程里板子是蓝板最小系统驱动芯片是 L293D电机是 TT 直流减速电机。我拆完工程后最直接的感觉是这个项目的价值不只是“能跑”而是把红外信号在遥控、避障、跟随、循迹四种场景下的不同用法讲透了——同一路 PWM 输出怎么复用、同一对红外对管怎么靠阈值判断切换逻辑、NEC 协议解码在无 RTOS 环境下怎么写主循环。适合刚把 F103 的外设跑通、想做综合实验或者参加课设的开发者也适合想梳理电机驱动与传感器配合方式的嵌入式从业者。需要先说明白一个容易被忽略的事实红外避障和红外跟随硬件上用的是同一组对管区别只在判断逻辑——避障是“检测到物体就躲”跟随是“检测到物体就追”。这个工程把这层窗户纸捅破了下面逐层拆。2. 硬件架构与信号链路L293D 和红外模块怎么接才不打架2.1 最小系统板的引脚分配逻辑STM32F103C8T6 有 37 个 GPIO这个工程把资源分成了三块电机控制占用 4 个输出脚两路 PWM 加两个方向引脚实际上 L293D 需要 4 个输入引脚控制两个电机正反转再用两路 PWM 做使能调速、红外传感器占用 4 个输入脚两路避障/跟随对管加两路循迹对管、红外遥控接收头占用 1 个外部中断引脚。常见接法是 PB0/PB1 接 L293D 的 IN1~IN4PA0/PA1 输出 PWM 到 EN 使能端红外对管 OUT 接 PB4/PB5循迹模块接 PB6/PB7接收头接 PA8 并配置为外部中断输入。这个分配不是绝对的但思路是固定的PWM 输出必须挂在有 TIM 复用功能的引脚上红外输入引脚要避开 I2C 和 USART 的默认映射否则调试串口和传感器会抢引脚。2.2 L293D 驱动 TT 电机的接线与续流问题L293D 是双 H 桥驱动单芯片就能控制两个直流电机供电范围 4.5V~36V峰值输出电流 600mA 左右。对 TT 减速电机这种额定电流在 200mA~300mA 的空载电机来说余量够了。接线方式是电机 A 的两根线接 L293D 的 OUT1/OUT2电机 B 接 OUT3/OUT4IN1~IN4 接 STM32 的 GPIOEN1/EN2 接 PWM 输出脚。需要特别注意的是电机电源和逻辑电源要分开——L293D 的 VS 接电池正极例如 7.4V 锂电池VSS 接 5V两者共地但不要共用走线否则电机启动瞬间的压降会把单片机拉复位。这个工程里没有画 PCB用的是面包板或者杜邦线如果你自己搭电机引脚旁边必须并一个 104 瓷片电容和 100uF 电解电容否则红外接收头的解码成功率会明显下降。2.3 红外对管的“发射-接收”链路参数避障和跟随用的红外模块核心是一体化红外对管发射管持续发出 38kHz 载波接收管检测反射强度。模块上一般有电位器可调阈值比较器输出数字电平——检测到反射物体时输出低电平或高电平取决于模块设计没有物体时输出相反电平。这个工程里要注意的是避障和跟随共用两个对管左、右各一个所以程序里读的是同一个 GPIO只是判断逻辑不同。循迹模块则不同它用的是 TCRT5000 这种反射式光电传感器发射管发出的是非调制红外光接收管根据反射量输出模拟电压经过 LM393 比较器后输出黑白电平信号——白纸反光强输出一种电平黑线吸光输出另一种电平。这里有个容易搞混的点避障对管检测的是“有没有东西在前面”循迹对管检测的是“地面是黑还是白”两者虽然都叫红外但一个看水平方向、一个看垂直方向接线和阈值调节方向完全不同。3. 工程结构与核心代码五种模式的状态机轮询架构3.1 KEIL4 工程文件目录解读解压后看到的目录结构是这样的SYSTEM文件夹里是sys和delay这是正点原子风格的基础驱动代码sys.c里实现了中断分组配置和 GPIO 复用的底层封装delay.c提供delay_init()和delay_ms()/delay_us()。USER文件夹里是主程序stm32智能小车红外遥控红外避障红外跟随红外循迹调速.c和启动文件OBJ文件夹里是编译产物。工程文件后缀说明文件后缀作用是否可删除.uvprojKEIL 工程主文件否.uvopt工程选项配置调试器、Flash 算法否.hex编译生成的烧录文件可删.axf带调试信息的可执行文件可删.crf/.o/.d编译中间文件可删用 KEIL4 打开.uvproj后把Options for Target里的Debug设置为 ST-Link 或 J-LinkUtilities里的 Flash 算法选择STM32F10x Med-density 128K编译下载即可。3.2 主循环状态机框架不用 RTOS 怎么管理五个任务整个主程序的逻辑不复杂核心是一个while(1)循环加一个mode变量。直接看主循环的关键代码int main(void) { delay_init(); // 延时函数初始化 GPIO_Config(); // 所有 GPIO 方向配置 TIM2_PWM_Init(899, 0); // PWM 频率 72MHz / (8991) 80kHz NEC_Init(); // 红外接收头外部中断初始化 mode MODE_REMOTE; // 默认进入遥控模式 while(1) { if(remote_pressed) // 红外遥控有按键按下 { Parser_NEC_Data(); // 解码 NEC 协议数据 switch(remote_cmd) { case KEY_1: mode MODE_AVOID; break; // 避障模式 case KEY_2: mode MODE_FOLLOW; break; // 跟随模式 case KEY_3: mode MODE_TRACK; break; // 循迹模式 case KEY_4: mode MODE_SPEED; break; // 调速模式 case KEY_5: mode MODE_REMOTE; break; // 遥控模式 default: break; } remote_pressed 0; } switch(mode) { case MODE_AVOID: Avoid_Handle(); break; // 执行避障 case MODE_FOLLOW: Follow_Handle(); break; // 执行跟随 case MODE_TRACK: Track_Handle(); break; // 执行循迹 case MODE_SPEED: Speed_Handle(); break; // 执行调速 case MODE_REMOTE: Remote_Handle(); break; // 执行遥控 default: break; } delay_ms(10); // 主循环节拍 } }逻辑说明主循环先检查红外遥控是否有按键事件有则切换模式然后根据当前模式执行对应的处理函数。delay_ms(10)是整个循环的节拍它决定了红外对管的采样频率和电机控制指令的更新率——10ms 对循迹来说偏慢但够用对避障来说有点迟钝但很容易调实际项目里可以改成delay_ms(5)提升响应速度。参数说明TIM2_PWM_Init(899, 0)中的 899 是自动重装载值0 是预分频值算出来的 PWM 频率约 80kHz对直流电机来说其实偏高——TT 电机的最佳 PWM 频率在 10kHz~20kHz 之间高于 20kHz 后电枢电感感抗增大电流纹波变小但驱动芯片的开关损耗上升。工程里用这个参数说明作者没有刻意调优我建议改成TIM2_PWM_Init(7199, 0)得到 10kHz电机噪音和发热会更均衡。3.3 电机控制PWM 调速与正反转的四象限组合L293D 的控制逻辑是IN1/IN2 控制电机 A 的方向EN1 引脚输入的 PWM 控制速度。真值表如下IN1IN2EN1(PWM)电机状态00X刹车10PWM正转调速01PWM反转调速11X刹车工程里电机控制的代码封装成四个函数——前进、后退、左转、右转每个函数里设置 IN 引脚电平并写 PWM 占空比void Motor_Forward(u16 speed) { GPIO_SetBits(GPIOB, GPIO_Pin_0); // IN1 1 GPIO_ResetBits(GPIOB, GPIO_Pin_1); // IN2 0 TIM_SetCompare1(TIM2, speed); // EN1 输出 PWMspeed 范围 0~899 GPIO_SetBits(GPIOB, GPIO_Pin_2); // IN3 1 GPIO_ResetBits(GPIOB, GPIO_Pin_3); // IN4 0 TIM_SetCompare2(TIM2, speed); // EN2 输出 PWM }逻辑说明前进时两个电机同时正转左右轮速度一致。speed参数直接写入比较寄存器这个值在 0 到 899 之间变化——0 是停转899 是全速。注意这里占空比的计算方式占空比 speed / 900因为自动重装载值是 899计数范围是 0~899 共 900 个计数点。如果你把速度设为 450占空比就是 50%。参数说明TIM2 的通道 1 和通道 2 分别控制左右电机TIM_SetCompare1和TIM_SetCompare2是标准库函数函数名里的数字对应通道号不是电机编号。4. 五种模式的实现与切换逻辑从 NEC 解码到循迹纠偏4.1 红外遥控NEC 协议的软件解码实现这个工程用的遥控器是 NEC 协议载波 38kHz数据格式是9ms 引导码 4.5ms 间隔 8 位地址码 8 位地址反码 8 位命令码 8 位命令反码。软件解码最常见的方案是外部中断加定时器计时接收头输出引脚接 STM32 的外部中断每次下降沿触发中断在中断里读定时器的计数值算出相邻两个下降沿的时间间隔根据间隔判断是引导码还是数据位。void EXTI9_5_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line8) ! RESET) { u32 cur TIM_GetCounter(TIM3); // 读当前计数值 u32 diff cur - last_time; // 计算时间差 last_time cur; if(diff 13000) // 引导码约 13.5ms { nec_count 0; // 开始接收新帧 } else if(diff 2000) // 数据位 1 约 2.25ms { nec_data | (1 nec_count); // 写 1 nec_count; } else if(diff 1000) // 数据位 0 约 1.125ms { nec_count; // 写 0默认是 0 } if(nec_count 32) // 收满 32 位 { remote_pressed 1; // 置标志位 nec_count 0; } EXTI_ClearITPendingBit(EXTI_Line8); } }逻辑说明TIM3配置为自由计数模式计数频率 1MHz所以计数器的值直接对应微秒数。中断每次触发时读取当前计数值减去上次的值得到两次下降沿的间隔。NEC 协议的引导码是 9ms 低电平 4.5ms 高电平从下降沿看就是 13.5ms数据位 1 是 560us 低 1690us 高下降沿间隔 2250us数据位 0 是 560us 低 560us 高下降沿间隔 1120us。设置三个阈值区间就能区分三种情况。参数说明diff 13000这个阈值要留足余量因为发射管的载波频率误差和接收头的解调时间会导致实际测量值有 ±10% 的偏差。工程里没有使用定时器输入捕获而是用外部中断 读取计数器的方案这是比较取巧的做法因为输入捕获需要占用通道而外部中断只需要一个引脚。坏处是如果主循环里有长时间阻塞操作中断可能丢失——但这个工程的主循环只有一个delay_ms(10)不会丢数据。另外nec_data变量必须声明为volatile否则编译器优化可能导致标志位更新不及时。4.2 红外避障左右对管的差速转向策略避障逻辑的核心是读两个对管的输出电平判断障碍物在左边、右边还是正前方然后做出相应转向。实现代码如下void Avoid_Handle(void) { u8 left GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4); u8 right GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5); if(left 0 right 0) // 两个对管都没有检测到障碍 { Motor_Forward(600); // 全速前进 } else if(left 0 right 1) // 右边有障碍 { Motor_Right(500); // 左轮正转、右轮反转原地右转 delay_ms(80); // 转向持续时间 } else if(left 1 right 0) // 左边有障碍 { Motor_Left(500); // 原地左转 delay_ms(80); } else // 两边都有障碍 { Motor_Back(500); // 先后退 delay_ms(200); Motor_Left(500); // 再左转 delay_ms(150); } }逻辑说明这段代码的关键在于“同组对管、不同逻辑”的理解。对管输出低电平表示检测到反射物体所以在避障模式下读到 0 说明前方有障碍物需要躲避。Motor_Right实现的是原地右转——左轮正转的同时右轮反转这样车体绕中心旋转转弯半径为零在狭窄空间里比差速转弯更有效。参数说明delay_ms(80)是转向角度控制的核心在 600 占空比下80ms 大约能让车体转过 30~45 度这个值跟电池电压、地面摩擦系数强相关——电池满电时转向快电量低时同样时间转过的角度小。建议实测先用 100ms 试看转角是否合适再微调。另外这个避障逻辑有个漏洞如果障碍物只在对管的正下方太近导致接收管被物体完全遮挡对管可能读不到反射信号表现为“没检测到障碍”直接撞上去——这就是红外对管的近距离盲区一般 3cm 以内会失效后面讲调试时细说。4.3 红外跟随与避障完全相反的判断逻辑跟随模式和避障共用同一个对管但判断逻辑完全反转——读到障碍物信号就追上去丢失信号就停下来找。void Follow_Handle(void) { u8 left GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4); u8 right GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5); if(left 0 right 0) // 目标在正前方 { Motor_Forward(500); // 低速前进 } else if(left 1 right 0) // 目标偏左 { Motor_Left(400); // 向左修正 } else if(left 0 right 1) // 目标偏右 { Motor_Right(400); // 向右修正 } else // 目标丢失 { Motor_Stop(); // 停车等待 } }逻辑说明注意这里读到的电平含义和避障模式相反——避障模式里读到 0 要躲开跟随模式里读到 0 要靠近。这是因为对管输出电平只表示“反射强度是否超过阈值”而反射强度高意味着前方有物体至于这个物体是障碍还是目标完全由软件定义。这段代码还有一个细节跟随速度比避障低500 vs 600原因是跟随时要频繁修正方向速度太快会过冲——这是 PID 控制里“比例项过大会震荡”的朴素体现。参数说明Motor_Stop()的实现和Motor_Forward(0)不同前者是直接把 IN 引脚全部拉低让电机惯性滑行后者是给 PWM 占空比为 0——两者在 L293D 上的表现完全一样但代码可读性不一样。跟随模式最怕的是目标物体突然快速横移对管的两个探头会同时丢失信号这时候车原地停车是安全的策略。如果你想让跟随更顺滑可以把左右修正的转向从“原地转向”改成“差速转向”——一侧轮全速、另一侧轮半速这样车体是圆弧运动而不是原地旋转跟踪轨迹更自然。4.4 红外循迹双路对管的黑线纠偏与速度自适应循迹是这五个模式里对实时性要求最高的。双路循迹的逻辑是两个探头分别放在黑线两侧正常情况下一个探头压线一个探头离线车直线走两个都压到线说明车偏离需要大幅纠偏。void Track_Handle(void) { u8 left GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_6); u8 right GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7); u16 base_speed 450; // 基础速度 if(left 0 right 1) // 左探头压线车偏右 { Motor_Forward(200); // 左轮减速 Motor_Right(base_speed); // 右轮保持 } else if(left 1 right 0) // 右探头压线车偏左 { Motor_Left(base_speed); Motor_Forward(200); } else if(left 0 right 0) // 两个都压线十字或终点 { Motor_Forward(base_speed); // 直行通过 } else // 两个都离线丢线 { Motor_Stop(); } }逻辑说明这段代码的核心思想是“减速纠偏”——哪个探头压线就降低哪一侧的速度让车体向另一侧偏转。Motor_Forward(200)和Motor_Left(base_speed)同时调用实现的是左轮 200、右轮 450 的差速左转这种转向比原地转向更平滑适合循迹场景。循迹最容易翻车的地方是“双丢线”——两个探头都离开黑线时可能是到了终点也可能是速度太快冲出赛道工程里的处理是直接停车简单但安全。参数说明base_speed和纠偏速度 200 之间的差值是循迹性能的关键。差值过小比如 450 和 350纠偏力矩不够车会在黑线边缘来回振荡差值过大比如 450 和 100车会扭来扭去但无法稳定贴合黑线。常见做法是先设成 450/200 跑直线赛道如果蛇形明显就提高到 450/300如果偏离太远就降低到 450/150这个调参过程和 PID 的 P 项调法一模一样。4.5 调速模式动态改变 PWM 占空比的实现边界调速模式的实现本质上就是一个变量递增递减的问题void Speed_Handle(void) { static u16 speed 300; static u8 dir 0; if(dir 0) { speed 20; // 加速 if(speed 800) dir 1; // 到上限转减速 } else { speed - 20; // 减速 if(speed 200) dir 0; // 到下限转加速 } Motor_Forward(speed); delay_ms(100); // 每个速度档位保持 100ms }逻辑说明调速模式是一个开环的锯齿波速度扫描速度从 300 到 800 再到 200 循环变化每 100ms 改变一档每次步进 20。这展示了 PWM 调速最直接的实现方式——修改比较寄存器的值就能改变电机转速。实际项目里如果要做闭环调速需要额外加编码器和 PID 控制器但作为实验演示开环扫描已经能清楚看到车速随占空比变化的过程。参数说明static变量让speed和dir在多次调用之间保持值这是嵌入式 C 里很常用的状态保持技巧。步进 20 和延时 100ms 的组合决定了加速的平滑度步进太大电机有明显的顿挫感太小则整个周期太长。这里有个一般人不会注意的点PWM 占空比和电机转速不是线性关系。由于电机存在静摩擦和死区电压占空比低于 10% 时电机根本不转高于 90% 后转速几乎不再上升——所以调速范围取 200~800 而不是 0~899是避开了这个非线性区间。5. 方向修正逻辑对比四种模式的控制策略差异分析为了把五种模式的区别彻底讲透这里用表格对比它们的核心控制策略模式传感器输入控制策略速度设置转向方式红外遥控NEC 解码结果按键映射动作固定 600按键控制红外避障左右对管电平检测到障碍就转向600/500原地转向红外跟随左右对管电平检测到目标就靠近500/400差速修正红外循迹双路循迹电平压线侧减速纠偏450/200差速转向调速模式无开环锯齿波扫描200~800 循环直行从这张表能看出一个有意思的规律五种模式里只有遥控是“人直接控制”其他四种都是“传感器反馈控制”。反馈控制的共同点是都需要一个误差信号——避障的误差是“要不要转”、跟随的误差是“目标在哪个方向”、循迹的误差是“车偏了没有”——而它们的实现差异只在于误差的提取方式和执行器的响应策略。避障用硬转向原地转跟随用软修正差速转循迹用减速纠偏这是针对三种场景的不同动力学需求做的取舍。调速模式则是开环控制它不读任何传感器纯粹展示 PWM 的调速能力所以放在最后作为对比项。理解这个矩阵后你拿到任何带传感器的小车项目都能快速判断它的控制策略属于哪一类。6. 调试三板斧NEC 解码失败、循迹甩尾和 PWM 死区实测整个工程跑通容易调好难。我在实际调试中遇到最多的三个问题处理方式如下。NEC 解码不稳定现象是按遥控器十次只有六七次响应。第一步用逻辑分析仪抓接收头输出引脚的波形确认是否有完整的引导码和 32 位数据。如果波形完整但解码失败问题出在中断处理函数里的阈值区间——把diff 13000改成diff 12000把diff 2000改成diff 1800给测量误差留余量。如果波形本身不完整检查接收头的供电电压HS0038 的供电范围是 4.5V~5.5V用电池供电时电压降到 4V 以下接收灵敏度会急剧下降。还有个冷门坑接收头的输出引脚要加 10k 上拉电阻有些模块板上没集成这个电阻会导致下降沿抖动导致重复触发。循迹过弯甩尾现象是小车在弯道速度稍快就直接冲出赛道两个探头同时离线停车。这是因为循迹对管的响应带宽有限车速过快时探头扫过黑线的时间太短比较器来不及翻转。解决方法是降基础速度或者缩短主循环延时——把delay_ms(10)改成delay_ms(5)让控制周期减半纠偏响应更快。如果还不够把纠偏差值从 200 加大到 250让转向力矩更强。注意不要把这个延时改到 1ms 以下否则主循环会被传感器读取和电机控制完全占满红外遥控的中断优先级会被干扰。PWM 死区电压实测我用电位器把占空比从 0 慢慢加到 100%发现 TT 电机在占空比低于 8%约 72/900时纹丝不动到 12% 才开始转动而超过 85% 后转速几乎不再增加。这个死区范围由电机的静摩擦系数和电枢电阻决定。工程里调速模式的范围设成 200~800正好避开了这两个非线性区域。如果你自己写调速代码务必避开 0~72 的低占空比区间否则电机会发出嗡嗡声但不转时间长了还会发热——那不是程序问题是电机没有足够的启动转矩把静摩擦打破。实测方法是写一个主循环让速度从 0 每隔一秒加 10同时在电机轴上贴个小纸片观察它从哪一刻开始转动这个临界值就是你电机的死区下限。本文还有配套的精品资源点击获取