十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

51单片机红外遥控电机控制:NEC协议解码与PWM调速实现

51单片机红外遥控电机控制:NEC协议解码与PWM调速实现 简介围绕单片机电机远程控制的入门进阶实验这套压缩包以红外遥控为核心演示从信号解码到电机驱动控制的完整流程适合电子、自动化等相关专业初学者与单片机爱好者动手实操。资源共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 采用方式 116 位12MHz 晶振下机器周期 1us最大可测 65.535ms完全覆盖 NEC 协议里最宽的引导码 13.5ms。代码如下/* 外部中断0P3.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) { /* 逻辑1560us低 1.69ms高约2.25ms */ ir_code[ir_bit_index 3] | (0x80 (ir_bit_index 0x07)); } else if (width 1600) { /* 逻辑0560us低 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.25ms560us约 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自由停止电机滑行这里容易踩坑的是“自由停止”和“刹车”的区别。IN1IN20 时电机两端没有电压差转子靠惯性继续转IN1IN21 时电机会被短接制动立即停下。遥控器上的“停止”按键如果接的是刹车模式电机会瞬间停住机械上会有冲击如果接的是自由停止电机还要转一两圈才停。工程上要根据实际设备选实验里一般用自由停止更安全。3.2 定时器 1 产生 PWM100us 中断周期下的占空比实现PWM 产生的常见做法是让定时器以固定周期中断在中断里根据占空比变量决定输出引脚的电平。以 12MHz 晶振、机器周期 1us 为例要产生 10kHz 的 PWM 频率定时周期就是 100us定时器 1 的初值为 65536 - 100 65436即 TH10xFFTL10x9C。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 方式116位手动重装 */ 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. 按键映射与电机控制逻辑状态机替代散装 if4.1 先读码再建立映射表红外解码做出来后第一步不是写控制逻辑而是把遥控器每个按键的编码读出来。把lcd.c里的显示函数接上在ir_frame_ready置位后用 LCD 显示ir_code[2]命令码按一遍所有按键把值记录下来。不同遥控器按键定义不一样压缩包配套的遥控器具体码值以 readme.txt 为准这里给出映射关系的组织方式按键功能命令码示例控制动作CH-0x45急停刹车CH0x46正转启动CH0x47反转启动VOL-0x44减速VOL0x40加速数字 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()末尾加两行第一行显示KEY0x45这样的码值第二行显示FWD PWM30或者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.8ms2.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()里检查宽度判断分支的顺序重复码检测必须放在“宽度超界复位”之前否则永远进不了这个分支。本文还有配套的精品资源点击获取
返回列表