资讯动态

51单片机红外遥控小车:红外解码与PWM电机驱动全解析

发布时间:2026/10/15 18:26:01 来源:尧图企业网站定制
简介一套51单片机红外遥控小车的项目资料包适合电子爱好者与嵌入式初学者借助红外遥控器控制小车实现前进、后退、左转和右转。项目以51单片机为控制核心通过红外接收头获取遥控编码利用中断与定时器完成脉宽解析再经I/O口驱动电机电路改变车轮转向与速度。压缩包共包含十七个文件体积约四十KB涵盖Keil工程文件、C语言主程序、汇编启动代码、编译生成的hex固件、列表文件以及备份文件等可直接导入工程编译并烧录至单片机。目前已有一千一百零六人学习查看。从该资源中可以获得完整代码框架、固件输出与工程配置思路对照硬件连接图便可快速理解红外解码和PWM调速原理为后续扩展蓝牙或WiFi控制提供良好基础。1. 红外遥控小车51入门最容易做“能跑但没说清原理”的项目很多人第一次接触单片机做的第一个带执行机构的项目就是它51单片机红外遥控小车。网上一搜一大把的图纸、代码、视频照着接线烧录几分钟就能让小车动起来。但真把车放到地上跑几分钟问题就来了遥控距离忽远忽近、按一下车跑两下、电机一转单片机就复位、烧录失败要断电重插——这些不是运气问题是没把红外解码时序和电机驱动的电气逻辑打通。这篇文章不聊“怎么抄一套能跑的程序”而是把红外遥控小车拆成“红外接收与解码”和“电机驱动与调速”两条主线顺带把供电、调试、排错这些新手最容易翻车的环节讲透。适合已经点亮过LED、写过流水灯的读者也适合手头有套件但跑不顺、想真正搞懂每根线和每段代码意义的从业者。读完你能做到的底线是不靠运气、不看脸地把车调好并且知道出问题时该量哪里、改什么参数。2. 硬件选型与最小系统先把“用谁收红外、用谁驱动电机”定下来2.1 红外接收头选型一体化接收头的引脚与电平逻辑红外遥控小车里的“眼睛”是一体化红外接收头常见的是HS0038B或类似封装的三脚器件。元件封装看起来都差不多但引脚定义不是统一的——不同厂家的接收头信号脚朝向可能不同。我一般拿到器件先看规格书里的引脚图再对着PCB上丝印确认而不是默认“字面朝自己左地右信号”。接收头的工作逻辑要注意空闲时OUT引脚输出高电平收到38kHz载波时输出低电平。也就是说它输出的是解调后的波形不是原始的红外光信号。这意味着单片机只需要检测电平跳变的时间长短就能还原出遥控器发送的数据。我的习惯是找逻辑分析仪先量一下接收头输出确认空闲电平是高否则后续解码代码里的边沿判断全部会反。接收头供电要就近放一个0.1uF去耦电容而且地线必须和单片机共地。不要在面包板上用长飞线把接收头的地接到单片机那排地线的另一端——电机一转地线上压差变化会让接收头输出抖动遥控距离直接缩到半米以内。先接好电源和地再去谈解码。2.2 电机驱动方案对比L298N和TB6612的取舍驱动两个直流减速电机市面上最常见的是L298N模块和TB6612模块。L298N便宜、耐操、网上教程最全但它内部是达林顿管饱和压降大在5V电机供电下实际输出到电机的电压会打折车速会比预期慢。TB6612是MOSFET方案压降小PWM调速线性度更好但接线更细、模块更容易烧。对比项L298N模块TB6612模块供电范围电机6-12V逻辑5V电机4.5-13.2V逻辑3.3-5V导通压降约2V发热明显约0.3V发热小PWM调速可用但线性一般线性好死区小新手友好度高螺丝端子好接线中引脚密容易插错常见损坏很少烧发热多电机短路或反接容易烧我的建议是手里有L298N就先用L298N别为了“高级”去换TB6612。L298N的坑在于发热和压降但你可以用提高电机电压来补偿而且它的接线螺纹端子对新手非常友好。如果是从零买零件直接选TB6612用起来更干净。2.3 供电系统两个电池盒和三种接线方式这个项目的供电通常不是一组电池而是两组一组给单片机5V或3.3V逻辑一组给电机6-12V。如果只用一组电池升压/降压同时供两边电机的启动电流会拉低电压单片机瞬间复位——这是最常见的翻车现场。我常用的接法有三种。第一种单片机用USB线从电脑取电电机单独用电池两者只共地不共电源。这是调试阶段最稳的方案因为电脑USB供电很干净下载调试互不干扰。第二种两组独立电池盒单片机那组用3节AA电池4.5V左右或一个5V稳压模块电机那组用6V或7.4V。第三种单组7.4V锂电先用DC-DC降压出5V给单片机再直接接电机驱动前提是DC-DC的输入输出共地且输出能力足够。无论哪种接法地线都必须连通。很多人的小车“能跑但不受控”查到最后就是电机电源的地和单片机的地没接到一起接收头的信号参考点悬空了。共地问题怎么说都不为过每个模块的GND都要汇聚到同一点然后从这个点引出到电池负极。3. 红外解码先把NEC协议读透再写51的中断代码3.1 NEC协议时序引导码、地址码、数据码与重复码的判断依据市面大多数红外遥控器走的是NEC协议——准确说是它的变体。标准NEC协议里每帧数据以9ms引导高电平加4.5ms低电平开头接着是8位地址码、8位地址反码、8位数据码和8位数据反码。每个数据位的判断依据是低电平持续时间560us低电平后接560us高电平是“0”560us低电平后接1.69ms高电平是“1”。但实际买到的遥控器未必严格走这个时序。很多便宜遥控器已经把厂商码做成固定值甚至引导码的时长都有偏差。所以写解码代码的核心不是“背诵NEC时序表”而是用定时器测量两次电平跳变之间的时间再按阈值归类。我的经验是先用遥控器对着接收头按一个键用逻辑分析仪抓一段波形读出实际的高电平和低电平时长再回来调阈值。不要直接抄网上代码里的560us、1690us那些数字在你的遥控器上不一定成立。重复码也要处理。按住按键不放遥控器发送的不是整帧而是9ms高电平2.25ms低电平560us高电平的重复码也可能没有最后的560us。如果你不做重复码判断按住按键时小车会一抖一抖地动——每来一次重复码就执行一次动作。解决方法是把重复码当成“继续执行上一次动作”的信号而不是当成新按键。3.2 用定时器外部中断实现解码一份能直接跑通的C51代码我用的是STC89C52系列外部中断0接接收头OUT引脚定时器0工作在方式1做16位计时。每次外部中断触发时读取当前定时器值减去上次中断时的定时器值得到两次跳变的时间间隔再根据电平状态判断是引导码、0、1还是重复码。#include reg52.h #include intrins.h #define uchar unsigned char #define uint unsigned int sbit IR_IN P3^2; // 外部中断0引脚接接收头OUT sbit LED P1^0; // 调试用LED解码成功亮一下 uchar ircode[4]; // 地址码、地址反码、数据码、数据反码 uchar irbuf[4]; // 解码过程暂存 uchar bitcnt; // 当前正在接收第几个数据位 uchar recv_flag; // 完整帧接收完成标志 uint last_time; // 上次中断时的定时器值 bit start_flag; // 是否已经收到引导码 // 定时器0初始化方式116位定时每10us计数一次 void timer0_init(void) { TMOD 0xF0; // 保留定时器1原有模式 TMOD | 0x01; // 定时器0工作在方式1 TH0 0; TL0 0; ET0 1; TR0 1; EA 1; } // 外部中断0初始化下降沿触发 void int0_init(void) { IT0 1; // 设置为下降沿触发 EX0 1; EA 1; } // 根据两次跳变的时间间隔判断码型 uchar check_bit(uint gap_us) { if (gap_us 3000 gap_us 6000) { return 2; // 引导码特征约4.5ms低电平 } if (gap_us 1800 gap_us 2600) { return 1; // 逻辑1特征约1.69ms高电平 } if (gap_us 350 gap_us 950) { return 0; // 逻辑0特征约560us高电平 } return 3; // 无法识别的时间间隔 } // 外部中断0服务函数每次电平跳变都会进入 void ir_int0(void) interrupt 0 { uint now_time, gap; uchar result; bit cur_level; now_time (TH0 8) | TL0; // 读取当前计数器值 TH0 0; // 计数器清零重新计时 TL0 0; gap now_time - last_time; // 两次跳变的时间差 last_time 0; // 统一以本次中断为新的时间起点 cur_level IR_IN; // 读取当前引脚电平 if (!start_flag) { // 还没有收到引导码先判断本次间隔是不是引导码特征 if (gap 3800 gap 5600) { start_flag 1; // 引导码确认 bitcnt 0; for (uchar i 0; i 4; i) irbuf[i] 0; } return; } // 已经确认引导码开始收数据位 // 因为外部中断是下降沿触发每次进入对应的是电平从高跳低 // 此时要测的是“上一次高电平持续了多久”即gap if (bitcnt 32) { result check_bit(gap); if (result 0) { irbuf[bitcnt / 8] 1; // 逻辑0补0 } else if (result 1) { irbuf[bitcnt / 8] 1; irbuf[bitcnt / 8] | 0x01; // 逻辑1补1 } else { start_flag 0; // 时序不对重新等引导码 bitcnt 0; return; } bitcnt; if (bitcnt 32) { // 收完32位复制到结果缓冲并置标志 for (uchar j 0; j 4; j) ircode[j] irbuf[j]; recv_flag 1; start_flag 0; bitcnt 0; } } } void main(void) { timer0_init(); int0_init(); while (1) { if (recv_flag) { recv_flag 0; LED ~LED; // 这里可以按键值分发处理 } } }注意这段代码把“读取计数器值”放在中断入口处而不是读IR_IN电平。因为外部中断是下降沿触发进入中断时引脚已经是低电平此时读到的电平不能代表跳变前的高电平持续时间。我们要算的是上一次跳变到本次跳变之间的时间差所以应该在进入中断后立刻锁存计数器值再判断电平分类。start_flag置1之后的第一次中断gap实际上是引导码的高电平时间这个值通常在9ms附近不会进入数据位的任何分类分支所以会走else把start_flag清零重来——但引导码已经有了起始标记这样设计其实是让“引导码低电平”之后的第一个数据位从第二次中断才开始计数。这段代码的check_bit函数里给引导码判断的是3000到6000us范围。实际NEC引导码高电平是4.5ms但不同遥控器会有浮动所以放宽到3-6ms。如果你发现根本收不到任何数据先把check_bit里的阈值打印出来对比实际波形而不是盲目改代码。参数说明定时器0的初值每次中断都会清零所以计数器溢出的情况不用额外处理——只要两次跳变间隔小于65.535ms16位计数器就不会循环覆盖。NEC协议里最长的间隔是9ms引导码高电平加4.5ms低电平总共13.5ms也远小于65.535ms所以不用担心溢出问题。3.3 解码结果怎么验证串口打印与逻辑分析仪的配合写解码代码最容易陷入“灯不亮就是代码错”的误区。实际上你不开串口打印就不可能知道解码收到的是多少。我建议先加一段串口发送函数把ircode[4]的内容用十六进制发到电脑串口助手。这样你能确认按不同的键数据码是否变化按住不放重复码是否被正确忽略近距离和远距离的接收成功率差异。// 串口初始化9600bps8N1 void uart_init(void) { SCON 0x50; TMOD 0x0F; TMOD | 0x20; TH1 0xFD; TL1 0xFD; TR1 1; } void uart_send_byte(uchar dat) { SBUF dat; while (!TI); TI 0; } void uart_send_ir(void) { uart_send_byte(ircode[0]); uart_send_byte(ircode[1]); uart_send_byte(ircode[2]); uart_send_byte(ircode[3]); }这段代码中TH10xFD对应11.0592MHz晶振下的9600波特率。要注意的是如果你的板子用的不是11.0592MHz晶振这个值必须重算。我遇到过有人板子上是12MHz晶振还照抄0xFD结果串口乱码折腾半天还以为是解码问题。4. 电机控制与PWM调速让小车从“转起来”到“走得稳”4.1 L298N控制引脚与逻辑电平ENA/ENB不是摆设L298N模块上的输入引脚IN1-IN4控制电机方向ENA和ENB控制使能。很多教程会把ENA、ENB直接接5V让电机满速转这样确实能动但你没法调速了。正确做法是ENA和ENB接单片机的PWM输出引脚通过占空比控制速度。IN1/IN2控制的是A电机正反转IN1高、IN2低正转反过来反转两个同高或同低就是刹车。这里有个新手必踩的坑L298N模块上的5V输出端子是用来给单片机供电的不是。它内部有稳压芯片可以从电机电源稳压出5V但输出能力有限。如果你同时给单片机、接收头、舵机都从这个5V取电一旦电机启动5V会被拉垮单片机复位。我见过最典型的翻车就是12V电池进L298N5V输出给单片机电机一启动单片机重启。我的习惯是单片机单独供电L298N的5V输出只接逻辑端不接单片机。逻辑电压匹配也要注意。L298N的逻辑高电平阈值在不同供电下不一样如果单片机是5V系统IN1-IN4直接接P1口没问题。如果用的是3.3V单片机逻辑高电平可能不够驱动模块不会可靠动作。51单片机基本是5V系统这个坑暂时不用操心但如果你后面换STM32或ESP32就要加电平转换。4.2 软件PWM的实现与占空比设定定时器中断是唯一靠谱的方式51单片机没有硬件PWM输出STC的一些型号有但基础51没有所以要用定时器中断模拟。常见做法是定时器每100us中断一次在中断里计数计数小于某个值时输出高大于该值时输出低一个周期结束重新计数。这样占空比就等于“高电平计数值/周期计数值”。// PWM通道参数 uchar pwm_left 0; // 左侧电机占空比 0-100 uchar pwm_right 0; // 右侧电机占空比 0-100 uchar pwm_cnt 0; // 当前周期计数值 // 定时器0中断每100us进一次 void timer0_isr(void) interrupt 1 { TH0 0x9E; // 100us定时初值11.0592MHz TL0 0x58; pwm_cnt; if (pwm_cnt 100) { pwm_cnt 0; } // 左电机PWM输出 if (pwm_cnt pwm_left) { ENA_PIN 1; } else { ENA_PIN 0; } // 右电机PWM输出 if (pwm_cnt pwm_right) { ENB_PIN 1; } else { ENB_PIN 0; } }这个方案的周期是100us乘以100等于10ms也就是PWM频率100Hz。对直流电机来说100Hz是能听的到轻微啸叫但动作正常的频率。如果你觉得电机有尖锐噪音可以把周期缩短到5ms即每次中断50us、计数到100PWM频率200Hz。我实测200Hz比100Hz噪音小很多但51单片机中断负载更高如果同时在做红外解码可能互相干扰。我的建议是先跑100Hz保证系统稳定噪音大点无所谓调通了再优化。占空比设定不一定要0-100线性映射。我调试车的时候发现占空比低于30时电机根本转不起来——克服静摩擦所需的最小占空比取决于电机减速比和车身重量。所以代码里要有一个死区补偿pwm值小于30就当0处理否则设成30。否则遥控器微调速度时车会一顿一顿地动像“咳嗽”。5. 串口调试与常见问题排查把“玄学失灵”变成可定位的现象5.1 现象遥控距离突然从8米缩到1米换了新遥控器也一样原因接收头供电不稳。我排查过一台车接收头电源线从单片机5V引脚飞了根15cm长的线经过面包板插孔再回到接收头。电机一转长线上的寄生电感加去耦不足38kHz载波信号直接被电源噪声淹没。解决接收头VCC和GND之间就近焊接一个0.1uF陶瓷电容电源线缩短到5cm以内。如果用的是面包板把接收头挪到单片机电源引脚旁边不要跨过电机驱动模块上方走线。5.2 现象上电瞬间小车自己往前冲一下然后才停住原因L298N的ENA/ENB引脚在单片机还没初始化时是悬空的悬空电平不确定。此时电机驱动部分处于“使能但不转”的随机状态一旦输入引脚上有残余电压电机就会转一下。解决单片机上电后第一件事就是把ENA和ENB拉低再初始化PWM和定时器。代码里加个引脚初始化函数在main函数开头执行比在while循环里反复设要可靠。另外可以用下拉电阻10k把ENA和ENB拉到地硬件层面保证默认不使能。5.3 现象解码时好时坏同一个键按十次有三次没反应原因中断优先级和定时器重装值打架。红外解码的定时器0中断和外部中断0如果同时触发51的中断优先级是由IP寄存器决定的。如果定时器中断优先级高外部中断来了却被延时响应电平跳变时刻就测不准了。解决把外部中断0设为高优先级定时器0设为低优先级。代码里写PX01。同时检查定时器0的重装值是否每次都正确赋值有些简化代码只装了TH0忘装TL0时间长了计数就漂了时间间隔测量变得不稳定。5.4 现象电机堵转或急停时单片机直接复位原因电机堵转电流瞬间很大如果电机电源和单片机电源共地但物理路径很差地线电位会被抬升单片机的复位引脚检测到高电平触发复位。另一个可能是电源电压被拉到低于单片机工作电压。解决电机电源地线和单片机地线在电池负极处单点连接。如果已经共地还复位给单片机电源加一个大电容——1000uF电解电容并联0.1uF陶瓷电容储能缓冲瞬态。还有一招是L298N的输出端并联续流二极管模块自带的话就不用管防止电机反电动势灌回电源。5.5 现象STC下载程序老是失败要多试几次才行原因STC系列下载用的是串口冷启动也就是先点“下载”再给单片机断电重新上电。如果板子上的电源开关设计不合理或者下载线和电机共用了电源电机负载会导致上电时序乱掉。解决下载时把电机电池断开只给单片机供电。这是最简单粗暴但最有效的办法。也可以检查串口线是否过长我用超过30cm的杜邦线下载就容易失败换短线就好了。6. 进阶验证与调试技巧把红外小车当平台而不是终点6.1 用逻辑分析仪量NEC时序的三个步骤不要只靠串口打印判断解码是否正确要直接看波形。用逻辑分析仪的三个通道CH1接接收头OUTCH2接单片机P3^2引脚CH3接ENA引脚。按一次遥控器按键观察三个通道的相对关系接收头OUT出现完整帧P3^2引脚同一个波形ENA引脚在解码成功标志置位后才开始输出PWM。这样能区分“遥控器没发码”“解码失败”“解码成功但电机没动”三种情况。逻辑分析仪采样率不用太高2MHz足够看NEC协议。触发方式设成下降沿触发通道阈值设成1.5V左右TTL电平。如果波形里看到引导码后面跟的不是32个数据位而是34个说明你的时间阈值边界卡得太紧某个位被判错了。6.2 三组环境光干扰测试判断系统有没有余量把车放在三种环境下各跑一分钟白炽灯下、LED灯下、阳光直射窗户边。白炽灯和阳光含有大量红外成分接收头可能误触发。观察串口打印的ircode是否出现随机变换的数据。如果白炽灯下有干扰说明接收头的AGC电路被强光压制解决办法是给接收头加一个不透光的塑料套管只露出正前面的窗口减少杂散光进入。这个套管用黑色热缩管就行。6.3 从遥控小车到循迹小车的升级红外解码复用同一套中断逻辑当你把遥控功能调稳后这辆车的价值不在于“能遥控”而在于它是你验证“传感器输入控制输出”的通用平台。循迹小车只是把红外接收头换成几个红外对管中断和PWM逻辑完全复用。再往后加蓝牙模块也只是把串口收数据换成收蓝牙数据包分发逻辑一样。我后来做某图像处理Demo的时候还专门回来翻过当年这套红外小车的PWM代码——软件PWM的思路在任何没有硬件PWM的平台都一样。一个调试习惯保留到现在所有外设的初始化函数都集中放在main函数最前面按“电源稳定→串口→中断→PWM→主循环”的顺序排。这套顺序救了我很多次每次上电复位后先能说话再动轮子。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑