51单片机红外遥控电机控制:NEC协议解码与PWM调速实现
2026/9/16 12:38:16 网站建设 项目流程

简介:围绕单片机电机远程控制的入门进阶实验,这套压缩包以红外遥控为核心,演示从信号解码到电机驱动控制的完整流程,适合电子、自动化等相关专业初学者与单片机爱好者动手实操。资源共10个文件,体积仅26KB,典型文件包含C语言源程序、头文件、Keil工程文件(uvproj/uvopt)、已编译的hex烧录文件、M51内存映射文件以及readme.txt实验说明,覆盖了程序编写、编译链接、下载调试与文档查阅等主要环节。已有178人学习/浏览过该资源,内容设计兼顾理论与实践:通过阅读源文件可掌握红外遥控编码/解码思路、PWM调速原理及电机驱动电路接口方法;配合hex文件可直接烧录验证,readme.txt则提供实验步骤与常见问题提示。整套资料结构紧凑,既能作为课堂教学的补充实例,也可作为课后自主研究的起步模板,帮助学习者快速打通单片机红外遥控控制电机的关键链路。

1. 红外遥控电机控制:从遥控器按键到PWM输出的完整链路

把红外遥控器对准实验板按一下,电机就转了——这个实验的真正难点不在电机,而在如何把红外信号里的时序精确还原成按键语义。压缩包里是一套完整的 Keil 工程:遥控器控制马达.c是主程序,lcd.c/lcd.h提供状态显示,另有编译好的遥控器控制马达.hex可以直接烧录,连M51映射文件和uvgui调试界面都留了。对刚学完定时器和中断的人来说,这是把三个基础模块串成完整系统的第一步;对有经验的开发者,这套代码里红外解码的状态判断和 PWM 的边界条件,仍值得对照自己项目的实现过一遍。下面按解码、驱动、控制逻辑、调试的链路拆开讲。

2. 红外遥控与 NEC 协议解码:为什么单片机要先“听”再“动”

2.1 NEC 协议与 38kHz 载波:接收头已经替你做了解调

红外遥控最常用的编码是 NEC 协议,家用影音设备里大量使用。发射端用 38kHz 载波承载数据:有载波表示一种电平状态,无载波表示另一种。直接看发射管的波形是高频调制过的,单片机没法直接处理,所以实验板上接的是带解调的接收头(常见如 VS1838B、HS0038B)。接收头内部完成放大、限幅、带通滤波和解调,输出端直接给出高低电平的基带信号。

这里有一个关键点:接收头输出的是反相逻辑。发射端发送载波时,接收头输出低电平;没有载波时输出高电平。于是 NEC 协议里 9ms 载波加 4.5ms 空闲的引导码,在接收头输出端表现为9ms 低电平加 4.5ms 高电平。很多初学者拿着示波器去对照协议文档,发现波形是反的,以为接收头坏了,其实这是正常现象。后续所有宽度判断,都要基于这个反相后的时序来写。

2.2 脉冲宽度测量:外部中断 0 加定时器 0 的配合

解码的核心思路是测量脉冲宽度。NEC 协议里逻辑 0 和逻辑 1 的区别不在低电平,而在随后的高电平持续时长:低电平都是 560us,逻辑 0 的高电平约 560us,逻辑 1 的高电平约 1.69ms。所以把接收头输出端接到单片机外部中断引脚(P3.2),用下降沿触发,在每次下降沿读定时器计数值,两次下降沿之间的间隔就等于“本次低电平 + 上一次高电平”的总宽度。

按照这个思路,定时器 0 采用方式 1(16 位),12MHz 晶振下机器周期 1us,最大可测 65.535ms,完全覆盖 NEC 协议里最宽的引导码 13.5ms。代码如下:

/* 外部中断0:P3.2 接红外接收头 OUT,下降沿触发 */ void isr_int0(void) interrupt 0 { unsigned int width; unsigned char ovf; TR0 = 0; /* 停定时器,准备读数 */ ovf = TF0; /* 保存溢出标志 */ TF0 = 0; width = (unsigned int)(TH0 << 8 | TL0); TH0 = 0; TL0 = 0; TR0 = 1; /* 清零后立即重启 */ if (ovf) { /* 定时器溢出:超过65ms无信号 */ ir_bit_index = 0; return; } if (width > 10000 && width < 16000) { /* 引导码:9ms低 + 4.5ms高,实测约13.5ms */ ir_bit_index = 0; return; } if (ir_bit_index < 32) { if (width > 1800 && width < 2800) { /* 逻辑1:560us低 + 1.69ms高,约2.25ms */ ir_code[ir_bit_index >> 3] |= (0x80 >> (ir_bit_index & 0x07)); } else if (width < 1600) { /* 逻辑0:560us低 + 560us高,约1.12ms,保持0 */ } else { ir_bit_index = 0; /* 宽度超界:重新同步 */ return; } ir_bit_index++; if (ir_bit_index >= 32) ir_frame_ready = 1; /* 4字节收满 */ } }

这段代码每次中断只做一件事:读宽度、判断类型、存入缓冲区,占用的时间极短。注意TH0 << 8 | TL0必须把 TH0 强制转成 16 位再移位,否则在 Keil C51 里容易因整型提升规则得到错误结果。ir_code数组按字节存储 32 个位,先收到的位落在字节高位,这对应的是发射端先发 LSB 的原始顺序,和第 4 章映射表的对比逻辑直接相关。宽度判断的阈值用的是区间而不是单点,因为遥控器本身有制造公差,电池电压变化也会让脉宽漂移几十微秒,区间判断比==判断可靠得多。

2.3 时序参数表与阈值选择

把 NEC 协议关键参数和实测宽度整理在一起,方便写代码时查:

信号低电平高电平接收头输出(下降沿间隔)判断阈值
引导码9ms4.5ms约 13.5ms10000~16000us
逻辑 0560us560us约 1.12ms< 1600us
逻辑 1560us1.69ms约 2.25ms1800~2800us
重复码9ms2.25ms+560us约 2.8ms单独处理

阈值选在 1600us 和 2800us 之间留了缓冲。如果发现遥控器按键偶尔失灵,优先查电池电压,低于 2.4V 时脉宽会明显偏离标称值,区间判断还有可能救回来,但阈值上下边缘就开始吃紧了。另外注意重复码:长按按键时遥控器每 110ms 发一次重复码,它的下降沿间隔约 2.8ms,落在逻辑 1 的上边缘之外,按上面的代码会走width > 2800的分支被丢弃。这个行为本身不会让程序出错,但会导致一个现象——长按按键时只响应第一帧,后面的重复操作全部丢失,第 6 章会讲怎么处理。

2.4 为什么不用查询方式解码

有些入门教程用 while 循环读引脚电平来测脉宽,写起来直观,但有个致命问题:解码期间 CPU 被占死,无法响应电机控制、LCD 刷新等其他任务。本实验里电机靠 PWM 持续输出,如果 PWM 由定时器中断产生,查询解码会拉长中断响应时间,导致 PWM 波形周期性抖动,电机声音变得刺耳。外部中断加定时器的方案把解码过程拆成一个个测量事件,ISR 只做轻量判断,主循环无阻塞地跑状态机,这才是工程上可扩展的写法。

提示:接收头输出端最好加一个 10kΩ 上拉电阻到 VCC。虽然多数接收头内置上拉,但加外部上拉可以提升抗干扰能力,尤其是电机启动瞬间电流变化大的场景。

3. 电机驱动与 PWM 调速:数字信号如何变成机械转速

3.1 驱动电路选型:单片机 IO 直接驱动不了直流电机

单片机 IO 最大输出电流通常只有 20mA 左右,而直流电机启动瞬间电流可能到几百毫安甚至更高,直接用 IO 接电机会把单片机拉死,或者导致复位。实验板上的电机驱动方案常见有三种:L298N 双 H 桥、ULN2003 达林顿管阵列、以及 DRV8833 这类集成芯片。压缩包的工程名里没有标明驱动型号,但源码逻辑上遵循的是“方向引脚 + 使能引脚”的标准结构,这正好对应 L298N 的使用方式。

L298N 的控制逻辑很简单:ENA 接 PWM 用来调速,IN1、IN2 决定方向。表格里列一下:

ENAIN1IN2电机行为
PWM01正转,速度由 PWM 占空比决定
PWM10反转,速度由 PWM 占空比决定
111刹车(两端同时接高)
0xx自由停止,电机滑行

这里容易踩坑的是“自由停止”和“刹车”的区别。IN1=IN2=0 时电机两端没有电压差,转子靠惯性继续转;IN1=IN2=1 时电机会被短接制动,立即停下。遥控器上的“停止”按键如果接的是刹车模式,电机会瞬间停住,机械上会有冲击;如果接的是自由停止,电机还要转一两圈才停。工程上要根据实际设备选,实验里一般用自由停止更安全。

3.2 定时器 1 产生 PWM:100us 中断周期下的占空比实现

PWM 产生的常见做法是让定时器以固定周期中断,在中断里根据占空比变量决定输出引脚的电平。以 12MHz 晶振、机器周期 1us 为例,要产生 10kHz 的 PWM 频率,定时周期就是 100us,定时器 1 的初值为 65536 - 100 = 65436,即 TH1=0xFF,TL1=0x9C。

unsigned char pwm_duty = 0; /* 0~100,对应0%~100%占空比 */ sbit MOTOR_PWM = P1^0; /* 接 L298N 的 ENA */ void timer1_pwm_init(void) { TMOD = (TMOD & 0x0F) | 0x10; /* T1 方式1:16位手动重装 */ TH1 = 0xFF; TL1 = 0x9C; /* 100us 中断周期 */ ET1 = 1; TR1 = 1; } void isr_timer1(void) interrupt 3 { static unsigned char pwm_cnt = 0; TH1 = 0xFF; /* 重装初值 */ TL1 = 0x9C; if (++pwm_cnt >= 100) pwm_cnt = 0; MOTOR_PWM = (pwm_cnt >= pwm_duty) ? 0 : 1; }

写这段代码时有一个细节值得注意:pwm_cnt >= pwm_duty用大于等于而不是小于。当pwm_duty = 100时,pwm_cnt永远走不到 100,条件恒为假,输出保持高电平,得到 100% 占空比;当pwm_duty = 0时条件恒为真,输出保持低电平。这样设计的好处是占空比可以达到满量程,不需要在应用层做“99% 封顶”的特殊处理。如果把判断写成pwm_cnt < pwm_duty,则pwm_duty最大只能设 99,语义上限缺失,代码阅读者很容易踩坑。

3.3 参数计算与占空比边界

PWM 频率选 10kHz 是经过考虑的。低于 1kHz 时电机转动会有明显的周期性顿挫感,伴随可听见的啸叫;高于 20kHz 后人耳基本听不到,但开关损耗增加,51 单片机在中断里做这么高频的翻转也吃力。10kHz 是直流电机驱动电路里常用的折中值。

改变pwm_duty的值就能改变电机转速,但要注意一个边界:占空比低于 10% 时,电机的启动转矩可能不足以克服静摩擦,表现为电机“嗡嗡”响但不转。所以遥控器按键里的“低速挡”建议在代码里做下限保护,比如if (pwm_duty < 10) pwm_duty = 10;,而不是让用户直接调到 0。同样,方向切换时最好先把占空比降为 0,等电机停稳再反向,直接反向冲击电流容易烧驱动芯片。

提示:L298N 的逻辑电源和电机电源需要共地,否则 IN1/IN2 的高电平参考点和电机驱动桥不一致,会导致电机随机启停。这是接线最容易忽略的一步。

4. 按键映射与电机控制逻辑:状态机替代散装 if

4.1 先读码,再建立映射表

红外解码做出来后,第一步不是写控制逻辑,而是把遥控器每个按键的编码读出来。把lcd.c里的显示函数接上,在ir_frame_ready置位后用 LCD 显示ir_code[2](命令码),按一遍所有按键,把值记录下来。不同遥控器按键定义不一样,压缩包配套的遥控器具体码值以 readme.txt 为准,这里给出映射关系的组织方式:

按键功能命令码(示例)控制动作
CH-0x45急停(刹车)
CH0x46正转启动
CH+0x47反转启动
VOL-0x44减速
VOL+0x40加速
数字 0~90x16~0x0A直接设定占空比 10%~100%

这里有一个位序问题容易导致码值对不上:NEC 协议在发射时先发 LSB,而第 2 章代码把先收到的位存进了字节高位,所以ir_code[2]里的值跟遥控器说明书上的编码表是位序反转的关系。标准做法是解码后做一次位反转再查表:

unsigned char reverse_bits(unsigned char b) { b = ((b & 0xAA) >> 1) | ((b & 0x55) << 1); b = ((b & 0xCC) >> 2) | ((b & 0x33) << 2); return (b >> 4) | (b << 4); }

这个函数用移位加或运算把字节的 bit7 和 bit0 互换、bit6 和 bit1 互换,依次类推,执行完得到按 LSB 先行的值。若在调试中发现按“CH-”显示的是 0xA2 而不是 0x45,就是位序没转,加一行key_code = reverse_bits(key_code);即可。

4.2 状态机组织运行逻辑

电机控制不能写成散装 if 堆在中断里。红外解码完成了,主循环只需要处理“收到完整帧”这一个事件,但电机当前处于什么状态、按键动作是否合法,需要用状态机来约束。定义三态枚举:

typedef enum { MOTOR_STOP, MOTOR_FORWARD, MOTOR_REVERSE } motor_state_t; motor_state_t motor_state = MOTOR_STOP; unsigned char pwm_duty = 0; unsigned char last_key = 0;

主循环里的处理逻辑可以这样组织:

void motor_control_loop(void) { unsigned char key; if (!ir_frame_ready) return; ir_frame_ready = 0; key = reverse_bits(ir_code[2]); /* 转为协议码值 */ switch (key) { case 0x45: /* 急停 */ motor_state = MOTOR_STOP; pwm_duty = 0; break; case 0x46: /* 正转 */ motor_state = MOTOR_FORWARD; if (pwm_duty == 0) pwm_duty = 30; break; case 0x47: /* 反转 */ motor_state = MOTOR_REVERSE; if (pwm_duty == 0) pwm_duty = 30; break; case 0x44: /* 减速 */ if (pwm_duty > 10) pwm_duty -= 10; break; case 0x40: /* 加速 */ if (pwm_duty < 100) pwm_duty += 10; break; default: break; } /* 按状态机输出到驱动引脚 */ switch (motor_state) { case MOTOR_STOP: MOTOR_IN1 = 0; MOTOR_IN2 = 0; /* 自由停止 */ break; case MOTOR_FORWARD: MOTOR_IN1 = 0; MOTOR_IN2 = 1; break; case MOTOR_REVERSE: MOTOR_IN1 = 1; MOTOR_IN2 = 0; break; } }

这段逻辑把“按键码”和“电机状态”分成两层:第一层 switch 负责按键语义映射,第二层 switch 负责把状态翻译成引脚电平。好处是如果换了遥控器,只需要改第一层映射表的码值;如果换了驱动芯片,只需要改第二层的引脚逻辑,互不干扰。正转启动时把占空比默认设为 30%,避免从 0 直接跳变造成电流冲击。

4.3 误触发与无效帧过滤

红外遥控在环境中容易被阳光、节能灯干扰,接收头偶尔会输出杂散信号。第 2 章的宽度区间判断已经过滤掉大部分无效脉冲,但在主循环处理帧数据前还可以加一道校验:NEC 协议的第 3 字节和第 4 字节是命令码及其反码,校验规则是(unsigned char)(ir_code[2] ^ ir_code[3]) == 0xFF。如果反码不匹配,整帧丢弃。这一步对防止电机误动作很关键,尤其是调试时遥控器对着墙壁反射导致信号畸变的场景。

另一个容易被忽略的场景是按键弹跳:很多万用遥控器按键按下时会产生一次完整帧,松开时又不小心触发一次。如果按“正转”后紧接着收到一个“停止”码,电机马上又停了,体验很差。简单处理是记录上一次按键码,如果相同就在 200ms 内忽略重复帧;但注意这不能和长按重复码混为一谈,第 6 章会展开讲。

5. 实验调试与波形验证:用逻辑分析仪和示波器抠时序细节

5.1 从 hex 到开发板:烧录与最小验证

压缩包里的遥控器控制马达.hex是编译好的固件,直接用 STC-ISP 或出厂配套烧录工具写入即可。烧录时注意选择正确的单片机型号和串口号,波特率建议先选 9600 或 2400,避免高速下载时因 USB 转串口芯片不稳定而反复失败。烧录完成后按照明灯能点亮、LCD 能显示初始画面作为最小启动验证。

首次上电不要接电机,先用手持遥控器按几个按键,观察 LCD 上是否出现对应的码值。如果 LCD 没有任何反应,优先检查红外接收头的 VCC 和 GND,以及 OUT 是否接到了 P3.2;其次检查晶振频率是否真的 12MHz,有些实验板用 11.0592MHz,定时器初值需要重新计算。

5.2 用逻辑分析仪看 NEC 时序判断解码是否准确

没有示波器时逻辑分析仪够用。把探头夹在红外接收头 OUT 引脚和 GND 之间,采样率设置为 2MHz 以上,按下遥控器按键抓取一段波形。图上应该能看到明显的“宽脉冲 + 一串窄脉冲”结构:最宽的一段是 13.5ms 的引导码,随后是 32 个 1.12ms 或 2.25ms 的脉冲。如果抓到的波形里引导码缺失或者宽度明显偏短,说明接收头供电不足或者遥控器电池快没电了。

对比解码软件的采样值:逻辑分析仪上测到逻辑 1 的高电平宽度如果是 1.5ms 左右,低于协议标准的 1.69ms,但仍然大于 560us,说明发射端存在较大偏差。这时候要看解码代码的阈值是否还能覆盖,如果总是把逻辑 1 误判成逻辑 0,说明接收头灵敏度下降,换一个接收头比调阈值更有效。

5.3 常见故障排查表

故障现象可能原因排查方向
按键无反应,LCD 无码值接收头 OUT 未接 P3.2,或供电异常先测 OUT 静态电平,正常应为高
LCD 有码值,电机不转驱动芯片使能脚未接 PWM确认 ENA 是否连到 P1.0,而非固定高电平
电机只能正转不能反转IN1/IN2 其中一脚虚焊手动给 IN2 一个高电平,看电机是否反转
转速跳变不连续占空比更新和中断写入冲突pwm_duty改为在主循环统一更新,避免半个 PWM 周期内多次写入
一按遥控器单片机就复位电机电源和单片机电源共用且电压跌落L298N 电机供电独立,并共地

其中“一按遥控器就复位”最隐蔽。电机启动瞬间电流可达 300mA,如果和单片机共用同一个 5V 电源,电压跌落会让单片机进入掉电复位状态。把一个 1000uF 电解电容并在电机电源两端能缓解,根治方法还是电机单独供电,逻辑部分只要共地即可。

5.4 用 LCD 显示码值和转速状态

压缩包里的lcd.c提供了基本的屏幕驱动,调试时可以把关键变量显示出来。比如在motor_control_loop()末尾加两行:第一行显示KEY=0x45这样的码值,第二行显示FWD PWM=30或者STOP。这样做的好处是能把“红外解码对不对”和“电机控制逻辑对不对”分成两个独立问题去排查,而不是等整条链路都调通了才去猜是哪一环出了问题。

6. 定时器溢出与重复码恢复:让电机在长按时不“打嗝”

6.1 定时器溢出检测:空闲帧间隔的隐藏陷阱

第 2 章的解码代码在每次中断里检查了TF0溢出标志,但里面有一个容易被忽略的场景:遥控器两帧之间的间隔约 40ms 甚至接近 110ms,而定时器 0 最大只能测 65.5ms。如果上一次下降沿发生在 40ms 前,下一次下降沿到来时定时器已经溢出并回绕过一次,读出的宽度是一个很小的值,会被误判成逻辑 0 或逻辑 1,产生一帧错乱的数据。

处理思路是:在中断开头同时检查溢出标志,只要溢出就说明这是一个超长间隔,不能用来解码,直接把位索引清零,等下一帧开始。第 2 章代码里的ovf分支已经做了这件事,但没有解释为什么——现在需要明确一点:这个检查的优先级必须高于宽度判断,否则一个回绕后的宽度会带着错误数据进入ir_code

6.2 重复码恢复:长按保持动作连续

按住遥控器按键不放时,NEC 协议会在第一帧之后每隔约 110ms 发送一个重复码,结构是 9ms 低电平、2.25ms 高电平、0.56ms 低电平、然后一直保持高电平到下一个重复码。在接收头输出端看,重复码产生的下降沿间隔约 2.8ms(2.25ms 加 0.56ms),既不是逻辑 0 也不是逻辑 1,会走“宽度超界”分支复位位索引。

这个行为有一个实际后果:用户按住“正转”键想连续调整电机转向,或者按住“加速”键连续加速时,只有第一帧被处理,后续重复码全部被丢弃。电机不会持续加速,只会停在第一次设置的占空比上。修复方式是在位索引复位分支里增加一个重复码检测:

if (width > 2800 && width < 3400) { /* 重复码:9ms低 + 2.25ms高 + 560us低,约2.8ms */ ir_frame_ready = 1; /* 重新执行上一次的操作 */ return; }

这样长按“加速”时,每 110ms 收到一次重复码,main循环里key仍然是上一帧的按键码,case 0x40会持续生效,占空比逐级递增直到 100%。同理,长按“正转”能让电机一直保持在正转状态,而不是按一下转一下、不按就停。这个处理对“遥控器 + 电机”这种需要持续操作的场景几乎是必须的,很多教程例程不写这一条,导致用户以为电机控制电路有问题。

6.3 零成本验证方法

验证重复码恢复是否生效,不需要示波器,只用眼睛看:按住“加速”键不放,听电机声音是否每隔约 0.1 秒出现一次转速跳变,稳定到达最高速后声音保持恒定;松开按键,转速应当停在当前档位不再变化。如果按住后转速只变一次,说明重复码还是被丢掉了,回到isr_int0()里检查宽度判断分支的顺序:重复码检测必须放在“宽度超界复位”之前,否则永远进不了这个分支。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询