
简介一套基于单片机控制的遥控心形灯项目资料面向电子爱好者、单片机初学者以及希望通过真实项目理解LED显示与遥控解码的开发者。压缩包共18个文件主要涵盖C语言源代码、hex烧录文件、电路原理图PDF、遥控使用说明与焊接注意事项图片、元件清单Excel以及Keil工程配置文件整体仅628KB目录结构清晰便于按需查找目前已有323人学习。内容围绕心形灯从硬件到软件的实现流程展开主程序、中断服务函数、遥控解码模块、LED驱动与PWM亮度控制等关键代码一应俱全同时附有原理图与实物图说明能够辅助完成电路连接、程序烧录和整机调试。除了核心源码与硬件设计文件外还保留了编译调试生成的lst、m51、obj等中间文件方便对照理解编译链接过程。对想动手做智能小制作或入门单片机开发的读者来说这是一套可直接参考的完整案例。1. 遥控心形灯不是“点亮LED”那么简单做遥控心形灯整个流程里最不值钱的是买材料最值钱的是把单片机程序和遥控信号接起来。很多资料给一张原理图、一段能跑的流水灯就宣称“完整”结果你焊好板子、下载完程序灯确实亮了遥控器按了却没反应。问题几乎都出在单片机程序里的红外解码段。这篇文章用 STC89C52RC 这颗最常见的 51 单片机把心形灯从硬件驱动、NEC 红外解码到多模式灯效的完整链路讲清楚。适合正在做电子课程设计、或者想在一个具体项目里把 51 单片机程序源码逐段读明白的读者。文中参数表按 12MHz 晶振给出可以直接套用。2. 心形灯硬件搭建LED矩阵与驱动电路选型2.1 心形LED阵列的矩阵化布局心形灯不能每颗 LED 单独占一个 IO一颗 LED 正极接高、负极接低才能点亮独立控制 40 颗以上 LED 就要 80 多个引脚51 单片机根本没有这个资源。所以第一步就是把灯排成矩阵。最容易采购的现成板子是 8×8 点阵共阳或共阴都有焊成心形后每 8 个 LED 共享一行或一列。以共阳阵列为例子行引脚接 LED 正极列引脚接负极反过来共阴也一样。扫描时一次只让一行导通把这一行对应的 8 列数据送到 IO 口快速扫完 8 行利用视觉暂留效果形成完整画面。定义一个心形图案用 8 个字节就能表示每一字节代表一行置 1 的位表示该位置的点亮code unsigned char heart_shape[8] { 0x18, 0x3C, 0x7E, 0xFF, 0xFF, 0x7E, 0x3C, 0x18 }; // 0x18 00011000表示这一行中间两颗晶亮这套数组是心形灯所有灯效的基础。流水灯、闪烁、呼吸灯最终都是去改这 8 个字节里的内容而不是直接控制 60 多颗 LED。2.2 驱动电路ULN2003、74HC595还是三极管矩阵本身不解决电流问题。51 单片机每个 IO 只能提供大约 20mA8 颗 LED 同时点亮就接近上限而且整片 LED 亮度会塌下去。常见方案有三种。方案接线复杂度单路电流适用场景74HC595 串转并中需 3 根控制线20mA引脚紧缺、灯点多的场合ULN2003 达林顿管低直接接行脚500mA共阳阵列行驱动首选S8050 三极管阵列中每行一个基极电阻200mA发光管功率较大的自制板我一般会用 ULN2003 做行驱动、P0 口直接驱动列。ULN2003 内部自带续流二极管驱动感性负载反而要注意LED 矩阵是纯阻性更不用担心。74HC595 的优点是只用 3 根线就能扩展 8 路输出适合引脚已经被占满的 STC15 系列但它的刷新需要移位时钟代码量会多一段对新手不算友好。选择原则一句话行列扫描的刷新率要大于 50Hz否则人眼能看到闪烁驱动方案只要保证单行 8 颗灯同时点亮时不掉压什么方案都成立。2.3 引脚分配与点亮第一颗LED的最小代码整套系统的最小引脚分配如下这张表建议贴在调试电脑上模块信号名称单片机引脚说明心形灯行驱动R1~R8P2.0~P2.7接 ULN2003 输入低电平选中心形灯列驱动C1~C8P0.0~P0.7接 LED 阴极高电平点亮红外接收头IR_OUTP3.2接 HS0038 OUT 脚必须使用 INT0工作指示灯LED_SYSP1.0上电自检闪烁用先写一个最小点亮函数确认硬件没有虚焊。共阳阵列里行线由 ULN2003 反相驱动给 P2 写 0xFE 表示选通第一行P0 写图案字节这一行的 8 颗灯就会亮起来void display_line(unsigned char line, unsigned char col_data) { P2 ~(0x01 line); // 选中第 line 行低电平有效 P0 col_data; // 该行 8 列的电平状态 delay_us(100); // 短延时保持亮度 } void test_heart(void) { unsigned char i; for (i 0; i 8; i) { display_line(i, heart_shape[i]); } }delay_us(100)是扫描的关键参数8 行全部扫一遍要用 800us刷新率约 1.2kHz远超 50Hz 闪烁下限还留有余量。如果把这个延时加大到 2ms整体刷新率会掉到 62Hz 左右眼睛敏感的人会看到轻微闪烁。实际调参时优先调这个延时不要动主循环频率。3. 单片机程序核心NEC红外解码与按键映射3.1 NEC协议帧结构与时间阈值市售遥控器绝大多数走 NEC 协议载波 38kHz经 HS0038 接收后从输出引脚得到的是解调后的高低电平。一个完整的 NEC 帧由这几段组成9ms 引导码4.5ms 空闲随后是 32 位数据最后还有 8ms 左右的停止位。数据位靠高低电平宽度区分。逻辑 0 是 560us 低电平加 560us 高电平总共约 1.12ms逻辑 1 是 560us 低电平加 1.69ms 高电平总共约 2.25ms。遥控器按住按键不放时发送的是重复码只有 9ms 引导码和 2.25ms 空闲不携带数据。单片机程序解码的关键就是测量相邻下降沿之间的时间差。HS0038 输出在无信号时是高电平收到载波时低电平。32 位数据的每一位都以一个下降沿开始所以定时器只要不停记录下降沿间隔大于阈值判定为 1小于阈值判定为 0整个 NEC 帧就拆解完成了。3.2 用INT0下降沿捕获做逐位解码让 P3.2 同时作为 INT0 输入下降沿触发中断再把定时器 0 配成 16 位模式读取每次中断时的定时器计数值。两次计数的差值就是位宽的微秒数12MHz 晶振下机器周期 1us定时器计数值直接对应微秒省去换算。下面是一段可直接编译的 51 单片机程序完成引导码检测和 32 位逐位移入// NEC遥控解码INT0下降沿 定时器0计时 #include reg52.h sbit IR_IN P3^2; bit ir_ok 0; // 一帧数据接收完成 unsigned char ir_key; // 解码出来的键码 unsigned char ir_addr; // 用户码 unsigned long ir_buffer; // 32位原始数据 static unsigned char ir_bit_cnt; static unsigned char ir_state 0; static unsigned long ir_tmp; void int0_isr(void) interrupt 0 { unsigned int cnt (TH0 8) | TL0; // 读取当前计数值 TH0 0; TL0 0; if (cnt 11000) { // 超过11ms判定是引导码准备收32位数据 ir_state 1; ir_bit_cnt 0; ir_tmp 0; return; } if (ir_state 1) { ir_tmp 1; if (cnt 1700) { ir_tmp | 1; // 2.25ms左右为逻辑1 } // cnt在1.1ms左右时什么都不做ir_tmp按0处理 ir_bit_cnt; if (ir_bit_cnt 32) { ir_state 0; ir_buffer ir_tmp; ir_key (unsigned char)(ir_tmp 16); ir_addr (unsigned char)(ir_tmp 24); ir_ok 1; } } } void timer0_init(void) { TMOD 0x01; // 定时器016位模式 TH0 0; TL0 0; TR0 1; } void main(void) { timer0_init(); EX0 1; // 开INT0中断 IT0 1; // 下降沿触发 EA 1; while (1) { if (ir_ok) { ir_ok 0; // 灯效切换逻辑写在这里 } } }cnt 11000是引导码判定阈值。NEC 引导码加空闲总长约 13.5ms但定时器从下降沿才开始计时第一个边沿到数据位第一个边沿之间正好经历 9ms 载波加 4.5ms 空闲。考虑到中断晚到、时钟偏差11ms 作为阈值可以稳妥地区分引导码和普通数据位。逻辑判定阈值取 1700us因为逻辑 1 的完整周期 2.25ms、逻辑 0 只有 1.12ms中间有 1ms 左右的裕量不会误判。还有一个值得注意的点这段代码不处理重复码。按住遥控器不放时重复码的间隔远超过 16 位定时器溢出周期需要额外计数器扩展。多数灯效场景不需要连续动作不处理重复码反而更可控。3.3 反码校验与真实遥控器按键映射NEC 协议为了避免误码32 位数据里包含了用户码、用户码反码、键码、键码反码。正常做法是校验反码再响应否则两个数据位出错时灯效会乱跳。继续用 3.2 节的解码结果校验和映射放在主循环里void process_ir(void) { unsigned char key; if (!ir_ok) return; // 检查反码用户码和键码都应互为反码 if (((unsigned char)(ir_buffer 0xFF)) ! (unsigned char)(~(ir_buffer 8))) return; key (unsigned char)((ir_buffer 16) 0xFF); if (key ! (unsigned char)(~(ir_buffer 24))) return; switch (key) { case 0x45: mode 1; break; // 遥控器数字键1 case 0x46: mode 2; break; // 数字键2 case 0x47: mode 3; break; // 数字键3 case 0x44: mode 4; break; // 数字键4 case 0x40: mode 5; break; // 数字键5 default: break; } }0x45、0x46这类键值是最常见的一款 NEC 遥控器的编码但不同遥控器键码不一样。拿到新遥控器时先写一个把ir_key原样送到 PWM 灯或串口打印的测试程序把每个按键的实际码值记录下来再倒推映射表。这步看着琐碎却能省掉后面数个小时的无效调试。4. 心形灯灯效程序流水灯、呼吸灯与模式切换4.1 一个显示函数驱动整片心形阵列灯效代码不应该直接操作 P0 和 P2那样每换一个图案就要重复写一遍扫描逻辑。把扫描函数固定下来主循环只负责改显示缓冲区这是单片机程序源码里最常见的结构。定义二维数组frame当作显存显示循环不停地把frame里的 8 字节刷到 LED 上unsigned char frame[8] {0,0,0,0, 0,0,0,0}; void display_refresh(void) { unsigned char i; for (i 0; i 8; i) { P2 ~(0x01 i); P0 frame[i]; delay_us(120); } P0 0xFF; // 消隐关闭所有列防止拖影 }末尾的P0 0xFF是很多人会漏的一步。如果不消隐下一行切换瞬间上一行的残余数据会串到当前行高亮图案边缘会出现淡淡的拖影。显示刷新被主循环循环调用即可不要放在延时函数里递归调用。4.2 流水灯效果帧动画与位移动流水灯单片机程序最直观的做法不是写死 8 帧而是用一个左移循环生成动态帧。心形灯沿轮廓流动的效果可以把frame里的位按顺序循环移动每移动一次刷新一定次数再移位这样速度可控。void led_flow_run(unsigned char speed) { unsigned char i, j; for (j 0; j 32; j) { // 连续移动32次覆盖一轮 for (i 0; i 8; i) { frame[i] (heart_shape[i] j) | (heart_shape[i] (8 - j)); } // 用计数方式实现速度调节 for (i 0; i speed; i) { display_refresh(); } } }speed参数控制每帧保持的时间数值越大流动越慢。这里用循环嵌套调用display_refresh()而不是在display_refresh里加长延时原因很实在显示函数要保持固定刷新频率灯效速度是另一层时间控制混在一起时不同灯效之间的速度感会完全失控。4.3 呼吸灯效果与软件PWM实现呼吸灯要用 PWM 调亮度51 单片机硬件 PWM 资源少网上流传的软件方案大多是延时调占空比。精度要求不高时用计时器定时 2ms 周期在中断里翻转 IO 电平简单且不阻塞主循环。一个可行的呼吸实现是让定时器中断周期为 2ms中断里每次增减一个步长值步长跑完一个周期后改变方向unsigned char pwm_cnt 0; unsigned char pwm_step 5; bit pwm_dir 0; // 0表示渐亮1表示渐暗 void timer1_isr(void) interrupt 3 { pwm_cnt pwm_step; if (pwm_cnt 100) { pwm_direction 1; } else if (pwm_cnt 5) { pwm_direction 0; } if (pwm_direction 1) { P0 0x00; // 暗 } else { P0 heart_shape[0]; // 亮 } } void breath_start(unsigned char step) { pwm_step step; TMOD 0x0F; TMOD | 0x10; // 定时器116位模式 TH1 (65536 - 2000) 8; // 12MHz下2ms TL1 (65536 - 2000); ET1 1; TR1 1; }这段代码把整片 LED 当单灯来呼吸如果要心形轮廓内逐点呼吸需要把P0的赋值换成按灰度级查表刷新。pwm_step决定单次亮度增量取值 5 时一个呼吸周期约 200 个中断周期、0.4s取 10 时约 0.2s。建议最小 3、最大 20超出这个区间肉眼会明显感觉到跳变或拖沓。4.4 模式切换主循环与参数说明所有灯效共用一套状态机模式由变量mode控制遥控键直接写mode。主循环结构固定为刷新显示、检查遥控标志、按模式执行对应的效果函数。模式遥控按键效果推荐参数0任意键待机全灭无11心形常亮亮度上限 80%22流水灯speed 8033呼吸灯pwm_step 544闪烁1Hz亮 400ms / 灭 400ms55全部灯效循环每 5 秒切换实现时注意一个细节灯效函数不能有死循环阻塞。led_flow_run内部的 32 次循环要设计成可以被mode修改打断的状态机否则按遥控器换模式时必须等当前 32 帧跑完才响应观感会很差。做法是在每次循环开头检查一个mode_changed标志被遥控器触发后立即return。5. 调试技巧与几个必踩的坑5.1 先看波形再改代码遥控解码调不通八成不是代码逻辑错而是接收端电路或时序阈值问题。先拿逻辑分析仪夹在 P3.2 和 GND 上按遥控器按键观察波形里有没有完整的 NEC 格式脉冲串。如果连 9ms 引导码都看不到检查 HS0038 的供电是否 5V、OUT 脚是否接反如果波形正常但解码无效重点检查中断触发方式和定时器初值。逻辑分析仪采样率 500kS/s 以上足够不需要买高档设备。5.2 STC下载与电源细节STC89C52RC 下载程序需要冷启动点“下载”后在软件提示的 5 秒内断电再上电没反应时把 USB 转串口的 DTR/RTS 信号线反过来插一次。硬件上 P0 口必须接 10k 排阻上拉否则点亮 LED 后电平被拉低单片机程序运行会时好时坏。矩阵扫描时瞬时电流能达到 300mA 以上用 USB 线直供容易复位至少保证 1A 电源或在电源两端并一个 100uF 电解电容。5.3 移植到其他单片机时怎么改这套代码结构换到 STC15 或 STM32 时只需要把INT0_ISR改成对应外部中断回调把pwm_cnt换成硬件定时器更新事件。需要注意的只有阈值3.2 节的两个阈值基于 12MHz 机器周期计算STC15 的 1T 模式或 11.0592MHz 晶振下cnt 11000和cnt 1700必须按实际时钟缩放否则引导码和逻辑位全部误判。改完时钟先跑一遍“按一次键亮一个灯”的最简实验再上整套灯效问题定位会快很多。本文还有配套的精品资源点击获取