基于PIC10F206单片机的通用红外遥控发射器设计与实现
1. 项目概述与核心价值
最近在整理工作室的旧设备,发现一堆不同品牌、不同型号的电视机、机顶盒和音响,遥控器多得能开个“遥控器博物馆”。每次想开个投影仪,都得在抽屉里翻半天,体验极差。这让我想起了红外遥控这个看似古老却无处不在的技术,它依然是控制家电最直接、最可靠的物理方式之一。于是,我萌生了一个想法:能不能自己动手,做一个通用的、可编程的红外遥控发射器?它要足够小巧,成本要低,还得能兼容市面上主流的红外协议,比如飞利浦的RC5和索尼的SIRC。这样,一个硬件就能“通吃”大部分设备,再配合简单的编程,就能实现自定义的宏命令,比如一键开启“观影模式”(打开投影、降下幕布、关闭主灯)。
这个项目的核心,就是围绕一颗非常经典的8位单片机——PIC10F206来展开。你可能觉得在ARM Cortex-M满天飞的今天,还用这种老古董是不是有点“复古”?但恰恰相反,对于红外遥控这种对时序要求极其苛刻、功能单一且对成本极度敏感的应用,PIC10F206这类极简架构的MCU有着不可替代的优势:它功耗极低,外围电路简单,价格便宜,而且经过几十年的市场检验,稳定可靠。通过这个项目,我们不仅能深入理解RC5和SIRC这两种主流红外协议的精髓,更能掌握在资源极度受限的MCU上,如何用软件精准模拟复杂时序的硬核技巧。这不仅仅是做一个遥控器,更是一次对嵌入式系统底层编程和时序控制的深度实战。
2. 红外遥控协议核心:RC5与SIRC深度解析
要发射红外信号,首先得搞清楚我们要“说”哪种“语言”。红外遥控协议就是设备间的通信语言,不同的厂商定义了不同的语法。我们这个项目重点实现两种最经典、应用最广泛的协议:RC5和SIRC。
2.1 飞利浦RC5协议:曼彻斯特编码的典范
RC5协议由飞利浦公司制定,其最大特点是采用了曼彻斯特编码。这种编码方式的好处是它自带时钟信息,抗干扰能力强,接收端更容易同步。
一个完整的RC5帧长度为14位,其结构如下:
- 两位起始位(Start Bits):总是为逻辑‘1’,用于标志帧的开始。
- 一位控制位(Toggle Bit):这是RC5协议的一个巧妙设计。每次按下同一个键,这一位就会翻转(0变1或1变0)。接收端通过判断这一位是否变化,可以区分是“长按”还是“重复按下”。如果键被一直按住,接收芯片会周期性地重复发送上一帧,但其中的Toggle Bit保持不变,这样设备就知道这是“长按”动作,而不是多次独立的“短按”。
- 五位系统地址(System Address):用于区分不同类型的设备,比如电视机、音响、机顶盒等。标准RC5定义了0-31共32个系统地址。
- 六位命令码(Command):代表具体的按键功能,如音量加、频道减、电源开关等,共64个命令。
RC5的载波频率固定为36kHz,这是红外接收头最常用的中心频率之一。每一位数据的传输时间(位周期)是固定的1.778ms。曼彻斯特编码规定,每一位数据中间必须有一次电平跳变:从高到低的跳变代表逻辑‘0’,从低到高的跳变代表逻辑‘1’。因此,每一位都会被这个跳变点分成两个等长的半周期(各889μs),在半周期内,载波信号(36kHz方波)要么持续发射(代表高电平),要么静默(代表低电平)。
注意:理解曼彻斯特编码是理解RC5的关键。它不是简单地用“有载波”和“无载波”来代表1和0,而是用“跳变方向”来代表。这要求我们的发射程序必须非常精确地控制载波开关的时机,确保跳变发生在每一位的正中间。
2.2 索尼SIRC协议:脉冲宽度编码的代表
索尼的SIRC协议采用了另一种思路:脉冲宽度编码。它用不同宽度的脉冲来代表逻辑‘1’和‘0’,结构上通常比RC5更简单,但不同版本的SIRC帧长不同。
我们以实现最普遍的12位SIRC为例,其结构如下:
- 一个起始脉冲(Start Pulse):一个长达2.4ms的高电平脉冲(期间发射载波),后跟一个0.6ms的低电平间隙。这个长脉冲非常显眼,用于唤醒接收端并同步时钟。
- 7位设备地址(Device):用于区分索尼旗下的不同产品线。
- 5位命令码(Command):代表具体的按键。
SIRC的载波频率通常为40kHz(部分早期设备使用38kHz或其它频率,但40kHz最为通用)。它的编码规则是:
- 逻辑‘0’:一个0.6ms的高电平脉冲(载波),后跟一个0.6ms的低电平间隙。总时长1.2ms。
- 逻辑‘1’:一个1.2ms的高电平脉冲(载波),后跟一个0.6ms的低电平间隙。总时长1.8ms。
可以看到,逻辑‘1’的脉冲宽度是逻辑‘0’的两倍,接收端通过测量高电平脉冲的宽度来区分数据。这种编码方式对发射端的时序精度要求同样很高,但解码逻辑相对曼彻斯特编码更直观一些。
2.3 协议对比与选型思考
为什么选择同时实现这两种协议?因为它们代表了两种主流的编码流派,覆盖了绝大多数消费电子品牌。飞利浦、汤姆逊、诺基亚等众多品牌使用或兼容RC5;而索尼及其关联品牌则使用SIRC。实现这两个协议,基本上就能控制工作室里八九成的老设备。
从实现难度上看,SIRC协议因为其简单的脉冲宽度编码,在编程上稍微容易一些。RC5的曼彻斯特编码需要更精细的时序控制,特别是在位中间进行电平翻转,对中断响应和延时函数的精度挑战更大。但正因为有挑战,实现后才更能体现我们对MCU的掌控力。
3. 硬件核心:PIC10F206最小系统与电路设计
硬件部分的目标是极简、低成本、高可靠性。PIC10F206是一款仅有6个引脚的8位MCU,但“麻雀虽小,五脏俱全”,它内部集成了振荡器、定时器、比较器等外设,非常适合本项目。
3.1 PIC10F206资源盘点与引脚分配
我们先看看这颗MCU的家底:
- 程序存储器:仅有768个指令字。这意味着我们的代码必须极其精简,不能有任何冗余。
- RAM:64字节。所有的变量、数组、堆栈都在这64字节里,必须精打细算。
- 时钟:内部4MHz RC振荡器。这是我们所有时序的基准。虽然精度不如外部晶振,但对于红外遥控(误差通常在±5%以内可接受)来说,通过软件校准完全够用。
- 外设:一个8位定时器(TMR0),一个模拟比较器,一个可编程的弱上拉GPIO。
基于以上资源,我们的引脚分配方案如下:
- GP0:配置为输出,连接红外发射二极管(IR LED)的驱动三极管基极。这是我们的“信号发射引脚”。
- GP1:配置为输入,可连接一个轻触按键,用于切换协议或发送测试信号。在实际产品中,这里可以接一个编码器或多个按键来扩展功能。
- GP2:这个引脚比较特殊,可以复用为T0CKI(定时器0外部时钟)或比较器输出。在本项目中,我们暂时不用,配置为输出,可以接一个状态指示灯LED,用于调试指示。
- GP3:仅能作为输入,且是主复位引脚(MCLR)。我们必须将其使能为GPIO输入,并连接上拉电阻,用于模式选择(例如,通过短路到地选择RC5或SIRC模式)。
- VDD/VSS:电源和地。工作电压范围很宽(2.0V-5.5V),我们可以用两节AAA电池(3V)或一个CR2032纽扣电池(3V)供电,追求极致小巧。
3.2 红外发射电路设计要点
红外发射部分的核心是将MCU输出的数字信号(0/1)转换为被38kHz(或40kHz)载波调制的红外光信号。电路并不复杂,但有几个关键点需要注意:
- 驱动电路:GP0引脚的驱动能力有限(通常几个mA),无法直接驱动IR LED发出足够强的光(需要100mA左右的峰值电流)。因此必须使用三极管(如常见的8050 NPN三极管)进行电流放大。GP0连接三极管基极,通过一个限流电阻(如1kΩ)。IR LED和一个小阻值的限流电阻(如10Ω)串联后,接在电源和三极管的集电极之间。发射管导通时,电流流经IR LED,使其发光。
- 载波生成:38kHz或40kHz的载波必须由软件生成。我们的策略是,在需要发射“高电平”(即发送载波)时,让GP0引脚以载波频率快速翻转(输出方波);在需要“低电平”时,则让GP0保持低电平。这就要求我们的延时函数必须能产生非常精确的载波半周期延时(例如,对于38kHz,周期约26.3μs,半周期约13.15μs)。
- 电源去耦:在MCU的VDD和VSS引脚之间,尽可能靠近引脚放置一个0.1μF的陶瓷电容,用于滤除电源噪声,确保MCU在快速切换输出时工作稳定。
- LED限流电阻计算:假设我们使用3V电源,IR LED的正向压降(Vf)约为1.2V,三极管饱和压降(Vce_sat)约为0.2V。期望的LED峰值电流If为100mA。那么限流电阻R = (Vcc - Vf - Vce_sat) / If = (3 - 1.2 - 0.2) / 0.1 = 16Ω。我们可以选择一个15Ω或18Ω的电阻。务必注意:这个电流是脉冲式的,平均电流很小,所以普通1/8W的电阻足够了,但IR LED本身要选择能承受100mA峰值电流的型号。
3.3 低功耗设计考量
既然是遥控器,低功耗是必须的。PIC10F206在这方面有天然优势。
- 睡眠模式:在待机时,MCU可以进入SLEEP模式,此时功耗可降至1μA以下。我们可以通过GP1或GP3引脚上的按键中断将MCU唤醒。
- 关闭无用外设:在初始化时,关闭模拟比较器、弱上拉等不需要的功能模块。
- 优化软件:发射信号时全速运行,发射完毕立即进入睡眠。整个发射过程在几十毫秒内完成,平均功耗极低,一对电池用上一年毫无压力。
4. 软件架构与核心代码实现
在768个指令字的限制下写代码,每一行都必须物尽其用。我们不能使用标准库,甚至要谨慎使用函数调用(因为涉及堆栈操作)。我们将采用“轮询+精确定时”的主循环架构。
4.1 系统初始化与时钟校准
首先进行最精简的初始化:
// PIC10F206 配置位设置 (根据编译器不同,写法可能不同) #pragma config MCLRE = OFF // 将GP3作为输入,而非复位引脚 #pragma config CP = OFF // 关闭代码保护 #pragma config WDT = OFF // 关闭看门狗,需要低功耗时可开启 void main() { // 1. 设置振荡器为内部4MHz OSCCAL = 0xFF; // 校准到最高频率,实际项目中可根据需要调整 // 2. 配置GPIO TRIS = 0b111010; // GP0输出,GP1输入,GP2输出,GP3输入 GPIO = 0x00; // 初始输出全低 // 3. 关闭模拟功能,所有引脚为数字IO CMCON0 = 0x07; // 关闭比较器 // ... 其他初始化 }时钟校准是关键。内部RC振荡器有误差,我们可以通过测量一个已知时间(比如用定时器产生一个1秒的闪烁)与真实时间的偏差,来微调OSCCAL寄存器的值,或者更实际一点,在软件延时函数中进行补偿。例如,如果我们发现实际延时比理论值慢2%,就在延时循环中减少相应的循环次数。
4.2 精确定时与载波生成函数
我们没有硬件PWM,所以38kHz载波必须用软件“捏”出来。我们将使用汇编语言或高度优化的C语言内联汇编来编写最核心的延时函数。
// 假设系统时钟为4MHz,指令周期为1μs。 // 生成13.15μs延时(38kHz载波的半周期) void delay_13us() { // 这是一个高度简化的示例,实际需要精确计算指令周期 _asm NOP // 1 cycle NOP // 1 cycle // ... 更多NOP或循环 RETURN // 2 cycles _endasm } // 发射一个载波脉冲(高电平) void send_carrier_pulse(unsigned int duration_us) { unsigned int count = duration_us / 13; // 计算需要多少个半周期 for(unsigned int i=0; i<count; i++) { GPIObits.GP0 = 1; // 输出高 delay_13us(); GPIObits.GP0 = 0; // 输出低 delay_13us(); } }对于SIRC协议,send_carrier_pulse(600)就是发送0.6ms的载波(逻辑‘0’的脉冲部分),send_carrier_pulse(1200)就是发送1.2ms的载波(逻辑‘1’的脉冲部分)。发送完毕后,再调用一个delay_600us()函数产生低电平间隙。
对于RC5协议,我们需要在一个位周期(1.778ms)内,根据数据位是0还是1,在中间点(889μs)进行一次电平翻转。这需要更精细的状态控制。
4.3 RC5协议发送函数实现
RC5的发送逻辑是状态机驱动的。我们需要先构建要发送的14位数据帧(包含起始位、控制位、地址、命令),然后逐位发送。
void send_rc5(unsigned char address, unsigned char command, unsigned char toggle) { unsigned int frame = 0; // 构建帧:起始位(2b) + 控制位(1b) + 地址(5b) + 命令(6b) frame = (0x03 << 12) | ((toggle & 0x01) << 11) | ((address & 0x1F) << 6) | (command & 0x3F); // 发送14位数据,从最高位开始 for(char i=13; i>=0; i--) { char bit = (frame >> i) & 0x01; send_rc5_bit(bit); } } void send_rc5_bit(char bit_value) { // RC5曼彻斯特编码:位中间跳变 // 如果bit_value==1,则前半周期低,后半周期高 // 如果bit_value==0,则前半周期高,后半周期低 if(bit_value) { // 前半周期:低电平(无载波) delay_889us(); // 后半周期:高电平(发送载波) send_carrier_for_duration(889); } else { // 前半周期:高电平(发送载波) send_carrier_for_duration(889); // 后半周期:低电平(无载波) delay_889us(); } }这里的delay_889us()和send_carrier_for_duration(889)都需要用之前提到的精确定时方法来实现。关键在于,send_carrier_for_duration函数内部不是简单地让GP0持续高电平889μs,而是要以38kHz的频率不断开关GP0,持续889μs。
4.4 SIRC协议发送函数实现
SIRC的实现相对直白,就是按照时序发送起始脉冲和后续的数据位。
void send_sirc12(unsigned char device, unsigned char command) { // 1. 发送起始脉冲:2.4ms高 + 0.6ms低 send_carrier_pulse(2400); delay_us(600); // 低电平间隙 // 2. 发送7位设备地址(LSB first) for(char i=0; i<7; i++) { if((device >> i) & 0x01) { send_sirc_bit(1); // 发送逻辑‘1’ } else { send_sirc_bit(0); // 发送逻辑‘0’ } } // 3. 发送5位命令码(LSB first) for(char i=0; i<5; i++) { if((command >> i) & 0x01) { send_sirc_bit(1); } else { send_sirc_bit(0); } } } void send_sirc_bit(char bit_value) { if(bit_value) { send_carrier_pulse(1200); // 逻辑‘1’脉冲 } else { send_carrier_pulse(600); // 逻辑‘0’脉冲 } delay_us(600); // 每位后的固定低电平间隙 }4.5 主循环与协议选择
主程序非常简单,大部分时间MCU都在睡眠。
void main() { sys_init(); while(1) { if(按键被按下) { // 检测GP1或GP3 // 防抖延时 delay_ms(20); if(按键确认按下) { // 根据GP3的电平选择协议 if(GPIObits.GP3 == 1) { // 假设高电平选择RC5 send_rc5(0x00, 0x0C, toggle_bit); // 例如,发送电视电源命令 toggle_bit ^= 1; // 翻转控制位 } else { // 低电平选择SIRC send_sirc12(0x01, 0x15); // 例如,发送索尼电视电源命令 } // 等待按键释放 while(按键仍按下); delay_ms(20); // 释放防抖 } } SLEEP(); // 进入睡眠模式,等待中断唤醒 } }5. 调试、优化与常见问题排查
软件写好了,电路焊完了,第一次按下按键,对面的设备很可能毫无反应。别急,调试阶段才是学习的精华。
5.1 硬件调试:眼见为实
没有示波器?没关系,我们可以用智能手机摄像头做一个简易的“红外探测器”。打开手机的相机APP,将IR LED对准摄像头,按下发射键。在手机屏幕上,你应该能看到IR LED发出微弱的紫色或白色光点(因为手机摄像头的CMOS对红外光敏感)。如果看不到闪烁,说明驱动电路或MCU根本没有控制IR LED发光,检查三极管是否接反、限流电阻是否过大、GP0引脚是否配置为输出。
如果有条件,强烈建议使用示波器或逻辑分析仪探头连接到GP0引脚。你可以清晰地看到发送的波形:对于SIRC,应该能看到2.4ms的密集方波(载波)起始脉冲,以及后续宽度不同的数据脉冲。对于RC5,应该能看到周期为1.778ms,中间有跳变的曼彻斯特编码波形。通过测量脉冲宽度和间隔,可以精确判断我们的时序是否准确。
5.2 软件时序校准
这是最可能出问题的地方。内部RC振荡器的频率偏差、函数调用开销、循环指令的周期数,都会影响最终延时。
校准方法:
- 粗调:写一个测试程序,让GP0引脚每1秒翻转一次,控制一个普通LED闪烁。用秒表或手机计时器测量实际闪烁周期。如果偏差较大,调整
OSCCAL寄存器值(如果MCU支持)或修改延时函数的基准循环次数。 - 细调:针对红外协议。可以找一个已知能正常工作的商业遥控器,用逻辑分析仪捕获其波形,测量其位周期、脉冲宽度。然后调整我们的
delay_us函数,使我们发出的波形参数尽可能接近商业遥控器。通常误差在±5%以内,接收端都能正确解码。
5.3 发射距离与角度问题
如果设备只在很近的距离或特定角度才有反应,问题通常出在发射功率上。
- 检查驱动电流:用万用表测量发射时流过IR LED的电流是否达到设计值(如100mA)。如果太小,减小LED的串联限流电阻。
- 检查IR LED型号:确保使用的是大功率、窄角度的红外发射二极管,而不是普通的小功率LED。窄角度的LED能量更集中,射程更远。
- 注意发射方向:红外光基本是直线传播,且容易被障碍物遮挡。确保发射管直接对准设备的红外接收窗口。接收窗口通常是一个深色(通常是黑色)的半透明塑料片。
5.4 协议兼容性问题
设备没反应,也可能是协议或地址/命令码不对。
- 确认协议:查阅设备说明书或通过网络搜索,确认你的设备到底支持RC5还是SIRC,或者是NEC、松下等其他协议。
- 确认地址和命令码:同一个协议下,不同品牌的设备可能使用不同的系统地址。命令码也未必通用。例如,“电源”键在RC5协议中,电视的地址可能是0x00,命令是0x0C,而音响的地址可能是0x10。你需要找到对应设备的正确码值。可以尝试用手机红外APP(如果手机带红外发射)学习原装遥控器的码值,或者在网上搜索红外码库。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| LED完全不亮(手机摄像头看不到) | 1. 电源未接通 2. MCU未工作/程序未运行 3. 三极管驱动电路错误 4. IR LED或电阻损坏 | 1. 检查电源电压,测量VDD。 2. 检查GP2指示灯程序是否运行。 3. 用万用表测GP0电压,按键时应有变化。测三极管各极电压。 4. 更换元件。 |
| LED亮但设备无反应 | 1. 载波频率偏差太大 2. 协议、地址或命令错误 3. 发射功率不足(距离近才有效) 4. 波形时序严重错误 | 1. 用示波器测量载波频率,调整延时函数。 2. 确认设备支持的协议和码值。 3. 测量LED电流,换用大功率窄角度LED。 4. 用逻辑分析仪对比商业遥控器波形。 |
| 反应不灵敏,时好时坏 | 1. 电源电压不足或波动大 2. 软件防抖处理不好,误触发 3. 环境有强红外干扰(如日光灯、太阳光) | 1. 更换新电池,在VDD加滤波电容。 2. 增加按键防抖延时和释放判断。 3. 避开强光,或尝试重复发送几次信号。 |
| 只能控制一种协议设备 | 1. 协议选择电路(GP3)故障 2. 软件中协议判断逻辑错误 | 1. 检查GP3的上拉电阻和接地开关。 2. 调试时,分别固化RC5和SIRC代码测试。 |
6. 项目扩展与进阶玩法
实现基础功能只是开始,基于这个框架,我们可以玩出很多花样:
变身“万能学习型遥控器”:这是最有价值的升级。增加一个红外接收头(如VS1838B)连接到MCU的另一个引脚。先让MCU进入“学习模式”,用原装遥控器对准接收头发射信号,MCU记录下波形的时间序列,并压缩存储到有限的RAM或外置EEPROM中。需要时,再将这些波形原样发射出去。这需要更复杂的代码来处理不同协议的自适应学习,但对PIC10F206的编程能力是极大的锻炼。
增加OLED/LCD显示:虽然PIC10F206引脚和资源有限,但通过软件模拟I2C,驱动一个128x32的OLED小屏幕是可能的。可以显示当前模式、设备名称、自定义宏命令名称等,让遥控器更具可玩性。这需要将项目迁移到资源稍多的MCU,如PIC12F1840。
接入智能家居平台:增加一个蓝牙模块(如HC-05)或Wi-Fi模块(如ESP-01S),让这个红外发射器具备网络连接能力。你可以通过手机APP、语音助手(集成到Home Assistant等平台)来控制传统红外设备,实现“老设备智能化”。此时,PIC10F206可以作为网络模块的协处理器,专门负责高精度的红外信号生成。
宏命令与场景联动:利用MCU的存储空间,预存多组红外码序列。按一个键,依次发送“开电视->切换HDMI1->开音响->音量调至20%”等一系列命令,实现一键场景切换。
这个基于PIC10F206的红外遥控发射器项目,从硬件到软件,充满了“螺蛳壳里做道场”的乐趣。它强迫你去思考每一个字节、每一个时钟周期的价值,去理解最底层的时序逻辑。当你最终按下自己制作的遥控器,成功点亮对面那台老电视时,那种成就感是直接用现成模块无法比拟的。它不仅仅是一个工具,更是一个理解嵌入式系统如何与物理世界对话的绝佳范例。
