简介:RC6遥控解码程序是一份面向嵌入式开发者和家电自动化爱好者的C语言源码资源,用于解析飞利浦RC6红外遥控协议信号并生成对应控制指令,适用于智能家居控制、遥控器替代或逆向工程项目。压缩包内仅1个C文件,体积约1KB,便于直接阅读与移植到各类MCU工程中,文件虽小但涵盖初始化、中断捕获、脉冲分析、数据校验和指令解析等关键环节,结构清晰,适合中初级开发者研读。目前已有359人学习下载,说明该资源在简易红外解码场景中具备一定参考价值。通过研读此代码,可以掌握RC6协议起始码、地址码、命令码与校验码的解析思路,理解ASK调制下红外脉冲宽度测量的实现方法,同时为自行编写其他IR协议解码器提供可复用的框架。
1. RC6遥控解码程序到底是什么:一个红外遥控方案的价值与适用场景
“RC6遥控解码程序”这个标题看起来只是一个孤立的模块,但它背后是红外遥控协议里最容易被低估的一类:曼彻斯特编码。做智能家居控制面板时,我接过一台老电视的遥控器,按键值死活读不对,后来才发现那台遥控器用的不是常见的脉宽式遥控协议,而是 RC6。RC6 遥控解码程序就是专门把红外接收头输出的脉冲序列还原成地址和命令码的一段代码。它适合所有需要做红外学习、万能遥控、红外转发场景的嵌入式开发者。如果你只写过脉宽式解码,这篇正好帮你把协议解码的另一条分支补上。注意,RC6 的解码思路和 NEC 这类协议完全不同,照搬老代码一定会翻车。
2. RC6协议帧结构与曼彻斯特编码:解码前必须吃透的时序参数
很多开发者拿到红外信号第一反应是量高低电平宽度,然后按脉宽协议去解析。RC6 用这套思路会绕很大的弯路,因为它的信息不在单段电平宽度里,而在电平的翻转方向和翻转节奏里。所以解码前必须先把帧结构和曼彻斯特编码的时序参数吃透,否则后面状态机写得再漂亮,也只是一堆乱码的搬运工。
2.1 RC6帧的组成:引导脉冲、起始位、模式段与地址命令段
RC6 协议是消费电子领域一种常见的红外遥控协议,采用 38kHz 载波调制,逻辑位时间 T 的典型值是 444us。一次完整的按键帧不是从数据位开始的,而是由一个很醒目的引导脉冲开头。以最常见的模式 0 为例,一帧 RC6 数据按顺序由这几部分组成:
| 段名 | 长度/宽度 | 说明 |
|---|---|---|
| 引导脉冲高段 | 6T 高电平 | 红外发射管连续载波,用于让接收端确认“帧开始” |
| 引导脉冲低段 | 2T 低电平 | 引导结束的标志 |
| 起始位 | 2T(1T高 + 1T低) | 固定为曼彻斯特编码的 1,用于时间同步 |
| 模式段 | 3 位 | 常见模式 0 是 000 |
| 翻转位 | 1 位 | 每次按键按状态翻转,用于区分重复按键 |
| 地址段 | 8 位 | 设备地址 |
| 命令段 | 8 位 | 按键命令码 |
所以一帧完整的 RC6 模式 0 数据,从起始位开始算共有 1 + 3 + 1 + 8 + 8 = 21 个有效位。加上引导脉冲的 8T 时间,总时长约 8T + 21 × 2T = 50T,在 T=444us 时约 22.2ms。这个时长对后续的超时判断很有用:如果解码器等了一个帧周期还没等到下一个位边沿,大概率是遥控器松手了,可以复位状态机。
注意,有些资料会把引导脉冲也算成“起始位”的一部分,这容易造成状态机设计混乱。我的习惯是严格把引导脉冲当作前导码处理,起始位只指那 1 位曼彻斯特编码的 1。两者的判定阈值不同:引导脉冲是 6T 和 2T,起始位是 1T 和 1T。混在一起后,避坑章节里那条“偶发解出乱码”多半就是这么来的。
2.2 曼彻斯特编码与载波调制:解码盯边沿不盯电平
红外发射端先把数据按曼彻斯特规则变成基带波形,再用 38kHz 载波调制发射。接收头内部完成放大、滤波和解调,输出的是反相后的基带电平:有载波时输出低电平,无载波时输出高电平。因此我们拿到的信号已经去掉载波,剩下的是脉冲序列,但极性是反的。程序里如果直接按接收头输出电平去判断“高电平=有信号”,很容易把引导脉冲高低段整个弄反。
RC6 使用的曼彻斯特编码可以这样理解:每个数据位占 2T 时间,位周期中点在 1T 处一定会发生一次电平翻转。若前半位为高、后半位为低,表示逻辑 1;若前半位为低、后半位为高,表示逻辑 0。所以位值是由翻转方向决定的。从波形上看,逻辑 1 是“先高后低”,逻辑 0 是“先低后高”。每一个位周期内必然有一次中间翻转,而在位与位的边界处,如果相邻位相同就没有翻转,如果相邻位不同就多一次翻转。
也就是说,RC6 的信息不在电平持续多久,而在“边沿出现在什么时刻、朝哪个方向翻”。一次有效的曼彻斯特位,其前半段电平一定等于该位的值。利用这个特点,我们可以用固定周期 T 去采样电平,每个位周期采两次,取前半次采样的电平就能得到位值,再校验两次采样是否相反。这种做法比逐个测量脉冲宽度更抗抖动,也是后面代码里采用的核心策略。
2.3 用逻辑分析仪抓真实波形:动手测量位周期T
在写代码之前,我强烈建议先用逻辑分析仪抓一次完整按键波形。市面上常见的 8 通道入门逻辑分析仪完全够用,不需要示波器。操作步骤:把红外接收头输出引脚接到逻辑分析仪的通道 0,GND 接好;设置采样率不低于 1MHz;然后对着接收头按遥控器上一个按键,触发条件设为下降沿,抓一段约 50ms 的波形。
抓到的波形里,你会看到典型的引导脉冲和后续密集的翻转。用游标测量引导脉冲高段的时间,再除以 6,得到的值就是 T。例如高段实测 2.664ms,则 T ≈ 444us。不同厂商的 RC6 遥控器可能在 T 上有偏差,常见值有 444us、333us 甚至 300us 左右。绝不能只按手册写的 444us 去写死程序,至少要留 ±25% 的容差,或者做成自动测量。
参数表里最关键的三个值:
| 参数 | 典型值 | 作用 |
|---|---|---|
| T | 444us | 半位周期,决定采样定时器周期 |
| 引导高段 | 6T | 帧同步阈值 |
| 引导低段 | 2T | 确认引导结束 |
用逻辑分析仪确认这组参数后,再进入硬件和代码设计,基本可以一次通过。跳过这一步直接写程序,后续所有“玄学问题”都会归结到 T 不匹配上。
3. 从硬件到中断:搭建RC6解码的最小系统
解码程序要稳定运行,硬件和中断配置比解码逻辑本身更早决定成败。RC6 对时序要求高,但也没高到必须用专用 IR 解码芯片。普通单片机的一体化红外接收头加一个输入捕获定时器就够了。这章讲清楚接收头怎么接、捕获怎么配、边沿数据怎么缓冲。
3.1 红外接收头选型与接法:38kHz载波与反向输出
选一体化红外接收头,不要选光敏二极管加放大电路的分离方案。一体化接收头内部已经包含红外 PIN 二极管、自动增益控制、带通滤波和解调器,输出可以直接进单片机 GPIO。选择时注意载波频率要和遥控器匹配,RC6 普遍使用 38kHz 载波,所以选中心频率 38kHz 的接收头即可。
引脚通常有三个:电源、GND、信号输出。供电电压常见 5V 或 3.3V,购买时确认接收头是否支持你的系统电压。输出引脚是开集电极或类似结构,需要外接上拉电阻,典型值 4.7kΩ 到 10kΩ。如果不接上拉,输出低电位恢复高电位会变慢,直接导致后续 1T 间隔测量不准。电源引脚处建议并联一个 10uF 电解电容和一个 0.1uF 瓷片电容,靠近接收头放置。RC6 连续按帧时接收头功耗变化明显,电源纹波会让输出波形抖动,这个细节很多人忽略。
接收头输出电平是反相的:收到 38kHz 载波时输出低电平,空闲时输出高电平。所以解码程序里把“载波存在”记为低、“无载波”记为高,不要在这个地方做习惯性反补。引导脉冲高段在发射端是连续载波,对应到接收头引脚其实是持续约 6T 的低电平。后面所有逻辑都以“接收头引脚电平”为准,不做感性的正反转换。
3.2 定时器捕获模式:用输入捕获记录脉冲边沿时间戳
输入捕获非常适合解析像 RC6 这种“边沿驱动”的协议。把接收头输出接在定时器的捕获通道引脚上,配置定时器同时捕获上升沿和下降沿。每次捕获事件触发中断,中断里读取捕获寄存器,得到当前边沿的时间戳。之所以用捕获而不是 GPIO 轮询,是因为 RC6 最短边沿间隔只有 1T(约 444us),主循环里轮询很容易丢失边缘抖动,而捕获由硬件完成,时间精度有保障。
我一般会用一个 16 位或 32 位定时器,工作在某一个时基频率下。假如系统时钟 72MHz,定时器预分频到 1MHz,则每计数 1 代表 1us。这时 T=444 对应计数 444。这个数值要放进宏定义,后续所有阈值都以它为基准。需要特别处理定时器溢出:如果两个边沿间隔超过定时器最大计数值,时间戳相减会出错。常见做法是在溢出中断里维护一个溢出次数变量,计算时间差时把溢出部分补回去;或者选择具备更大位宽的定时器,并在配置时保证一个 T 的时间差远远小于溢出周期。
捕获极性要配置为双边沿触发,这样才能记录每个高低跳变。有些单片机输入捕获只支持单边沿,就需要用两个通道配合或使用外部中断加定时器读值。无论哪种方式,最终目标都是获得“边沿时刻”和“边沿方向(上升或下降)”两个信息。时间戳单位统一用定时器计数,不要中途换成微秒,避免除法误差。
3.3 边沿时间戳环形队列:给解码器提供原始素材
中断里做完整解码不是不行,但 RC6 的状态机包含多位解析,且可能涉及回调,放在中断里会让系统响应变差。更稳妥的做法是:中断只把边沿时间戳和边沿方向推进一个环形队列,解码状态机在主循环或低优先级任务里消费这个队列。这样中断服务程序保持极短,也方便后续扩展自定义学习协议。
一个简单实现如下:
#define EDGE_Q_SIZE 64 typedef struct { uint32_t time; // 定时器捕获值 uint8_t level; // 该边沿后的电平:0=低,1=高 } edge_t; static edge_t edge_q[EDGE_Q_SIZE]; static volatile uint8_t q_head; static volatile uint8_t q_tail; void edge_q_push(uint32_t time, uint8_t level) { uint8_t next = (q_head + 1) % EDGE_Q_SIZE; if (next == q_tail) { // 队列满,丢掉最旧的一条 q_tail = (q_tail + 1) % EDGE_Q_SIZE; } edge_q[q_head].time = time; edge_q[q_head].level = level; q_head = next; } uint8_t edge_q_pop(uint32_t *time, uint8_t *level) { if (q_head == q_tail) return 0; *time = edge_q[q_tail].time; *level = edge_q[q_tail].level; q_tail = (q_tail + 1) % EDGE_Q_SIZE; return 1; }这段代码里q_head是写指针,q_tail是读指针。队列满时直接丢弃最旧边沿,保证新边沿能写入。RC6 一帧约 50T,即使 T=444us,一帧也就几十个边沿,64 深度足够。注意q_head、q_tail在中断和主循环之间共享,需要声明为volatile;如果 MCU 平台对原子性有要求,还需要加临界区保护,一般单字节读写已经够用。
调用时机上,输入捕获中断里只做一次edge_q_push,解码器循环里不断edge_q_pop并喂给状态机。队列为空时主循环可以休眠或做其他任务。这种生产者/消费者模型让解码逻辑独立于硬件捕获,后面调试时可以直接往队列里灌模拟边沿,非常方便。
4. 状态机解码RC6:完整代码与逐段说明
有了边沿队列,接下来就是核心的解码程序。我采用“边沿状态机 + 半位定时采样”的混合方案:先用捕获到的边沿确认引导脉冲和起始位,然后在起始位结束时启动一个半位周期定时器,以 T 为周期连续采样电平,每两次采样合成一个数据位。这样的优势是,数据阶段不再依赖边沿中断,而是靠稳定时钟采样,对遥控器发出的轻微抖动有更高的容忍度。
4.1 主状态机:从搜台到锁定的五个状态
状态机负责识别帧头和起始位,它的输入是边沿事件。只有状态成功走到起始位结束,定时采样才会启动。下面是核心的状态定义与转移逻辑:
typedef enum { RC6_IDLE, RC6_LEADER_HIGH, RC6_LEADER_LOW, RC6_START_HIGH, RC6_START_LOW } rc6_sync_state_t; static rc6_sync_state_t sync_state; static uint32_t last_edge_time; static uint32_t edge_delta; // 判断 delta 是否在 target_ticks 的容忍范围内 static int in_tolerance(uint32_t delta, uint32_t target_ticks, uint32_t tolerance_percent) { uint32_t low = target_ticks * (100 - tolerance_percent) / 100; uint32_t high = target_ticks * (100 + tolerance_percent) / 100; return delta >= low && delta <= high; } void rc6_on_edge(uint32_t edge_time, uint8_t level_after_edge) { // level_after_edge: 0=低, 1=高 edge_delta = edge_time - last_edge_time; last_edge_time = edge_time; switch (sync_state) { case RC6_IDLE: if (level_after_edge == 1) { sync_state = RC6_LEADER_HIGH; } break; case RC6_LEADER_HIGH: // 期望引导脉冲高段结束:低电平下降沿,时长约 6T if (level_after_edge == 0 && in_tolerance(edge_delta, 6 * T_TICKS, 25)) { sync_state = RC6_LEADER_LOW; } else { sync_state = RC6_IDLE; } break; case RC6_LEADER_LOW: // 期望引导脉冲低段结束:高电平上升沿,时长约 2T if (level_after_edge == 1 && in_tolerance(edge_delta, 2 * T_TICKS, 25)) { sync_state = RC6_START_HIGH; } else { sync_state = RC6_IDLE; } break; case RC6_START_HIGH: // 起始位前半段高电平,结束后下降沿,时长约 T if (level_after_edge == 0 && in_tolerance(edge_delta, T_TICKS, 25)) { sync_state = RC6_START_LOW; } else { sync_state = RC6_IDLE; } break; case RC6_START_LOW: // 起始位后半段低电平,结束后上升沿,时长约 T if (level_after_edge == 1 && in_tolerance(edge_delta, T_TICKS, 25)) { // 起始位已锁定,启动半位采样定时器 rc6_start_half_timer(); sync_state = RC6_IDLE; // 采样阶段由定时器接管 } else { sync_state = RC6_IDLE; } break; } }这段代码里T_TICKS是前面测得的 T 对应的定时器计数。in_tolerance把容差写成函数,默认给 25%,实际项目中我用 20% 到 30% 都能工作,太严容易漏帧,太松容易被干扰误触发。注意RC6_IDLE状态下,输入捕获通道只对上升沿敏感,因为引导脉冲高段在接收头输出端是低电平,但我们要在边沿事件里找的是“电平变成高”的上升沿。实际接入时,通过逻辑分析仪确认极性后,保持代码里level_after_edge的判断一致即可。
状态机走到RC6_START_LOW成功,意味着起始位(1T 高 + 1T 低)已经收到,且当前时刻正好是数据位周期的起点。此时启动半位采样定时器是完美的同步点。如果中间任何一步不满足,状态机立即回到RC6_IDLE,不浪费 CPU 继续期待一个不完整的帧。
4.2 位周期判定与半位采样:曼彻斯特解码的落地实现
半位采样定时器要求第一次中断在启动后的 0.5T 时刻,之后每隔 T 中断一次。这个 0.5T 是故意错开的,使得每次中断正好落在曼彻斯特位窗口的前半段中间或后半段中间。中断服务程序里读取接收头输出电平,并和上次采样电平配对。
static uint8_t half_index; // 当前采的是半位组里的第几个:0=前半,1=后半 static uint8_t half_buffer[2]; static uint8_t bit_buffer[21]; static uint8_t bit_count; static volatile uint8_t sampling_active; void rc6_on_half_tick(void) { if (!sampling_active) return; uint8_t level = rc6_read_pin(); // 读取接收头输出引脚电平 half_buffer[half_index] = level; half_index ^= 1; if (half_index == 0) { // 说明刚填完一个位周期的两次采样,开始组合 if (half_buffer[0] == half_buffer[1]) { // 同一前半后半电平相同,不是合法曼彻斯特位,立即终止 rc6_stop_sampling(); return; } if (bit_count < 21) { bit_buffer[bit_count] = half_buffer[0]; // 前半段电平即该位的值 bit_count++; if (bit_count >= 21) { rc6_frame_done(bit_buffer); rc6_stop_sampling(); } } } }逻辑说明:half_index从 0 开始,第一次中断采样的是数据位 1 的前半段,存到half_buffer[0];第二次中断采样的是同一数据位的后半段,存到half_buffer[1]。填完两次后half_index回到 0,此时校验两次采样值是否相反。曼彻斯特编码中合法的一位必然前半与后半相反,如果相同,说明信号污染或者定时器同步丢失,立刻停止采样。
取half_buffer[0]作为位值,是因为我们定义了逻辑 1 = 前高后低、逻辑 0 = 前低后高;前半段电平直接等于逻辑值。这里有读者会问:如果接收头反相,前半段电平不也反了吗?确实,但rc6_read_pin()读取的是接收头输出的实际电平,我们定义曼彻斯特规则时就是按“实际读取电平”来定义的,因此代码里不需要额外反相。只要引导脉冲的状态机也是用同一个电平定义,前后就一致。
半位采样从bit_count为 1 开始:起始位已经在状态机处理时确认,并当作 bit0 记入。定时器第一次中断对应数据位 1 的前半段,所以代码里bit_count初始值要在rc6_start_half_timer()里设为 1。采样位数的上限是 21,也就是起始位加后续 20 位。到达 21 位后触发rc6_frame_done,完成一帧解码。如果中间出现非法半位对,停止采样并回到空闲状态。
4.3 解码结果回调与帧合法性校验
rc6_frame_done把原始 21 位传给上层前,一定要做基本合法性校验。RC6 模式 0 中,模式段应该是 000;不同协议变体可能用 001 表示扩展模式,但常见遥控器都是 000。程序里提取模式段、地址、命令,并把翻转位单独拿出来:
void rc6_frame_done(uint8_t bits[21]) { // bits[0] 起始位,固定为 1 // bits[1..3] 模式段 uint8_t mode = (bits[1] << 2) | (bits[2] << 1) | bits[3]; // bits[4] 翻转位 uint8_t toggle = bits[4]; // bits[5..12] 地址 uint8_t addr = 0; for (int i = 0; i < 8; i++) { addr = (addr << 1) | bits[5 + i]; } // bits[13..20] 命令 uint8_t cmd = 0; for (int i = 0; i < 8; i++) { cmd = (cmd << 1) | bits[13 + i]; } if (mode != 0) return; // 只接受模式 0 if (addr == 0x00 && cmd == 0x00) return; // 空值防抖 rc6_key_callback(addr, cmd, toggle); }这里命令和地址的高低位顺序没有统一标准,不同遥控器厂商可能把第一位作为最高位或最低位。我的做法是先从逻辑分析仪的波形上对照按键功能表确定顺序,再决定左移还是右移;如果方向反了,解码出来的 cmd 会和按键表呈镜像关系。rc6_key_callback是留给应用层的回调,比如点亮指示灯、执行联动动作。中断或定时器回调里尽量只置位标志,把真正的业务逻辑放到主循环。注意,如果地址和命令全 0,绝大多数真实遥控器不会产生这种值,可以用来丢弃空包。
5. RC6解码常见问题排查:五条实战避坑记录
状态机跑起来之后,真正的挑战才刚开始。RC6 解码在实木桌、白炽灯、空调遥控器旁边都可能出现各种稀奇古怪的问题。这里整理五条我在实际项目里踩过的坑,每一条都按“现象 -> 原因 -> 解决”描述,方便你对照排查。
5.1 现象:偶尔解出一帧,下一帧完全没反应
现象是按键十次,只有两三次能读到键值,其余时间逻辑分析仪看波形正常,程序就是不出来。最开始怀疑是中断优先级问题,查了一圈发现是容差阈值写死了 T_TICKS,而那块遥控器的实际 T 是 460us,差 3.6% 不算大,但引导脉冲 6T 差 96us,在 25% 容差内其实没问题。
真正原因是定时器和逻辑分析仪的地参考不同,测量时忘了把接收头的地和单片机共地。接收头输出波形在没有共地时看起来正常,但 ADC 比较器抖动导致边沿时间戳误差,时好时坏。解决手段很笨:把接收头 GND 和单片机 GND 用最短的线连接,并在接收头旁边补了一颗 0.1uF 电容。之后解码成功率恢复到近乎 100%。如果硬件没问题,再检查状态机 IDLE 状态是否在每次采样停止后正确复位,有时半位定时器停晚了,把下一帧引导脉冲采样进去,也会表现为无响应。
5.2 现象:按住按键时第一键正常,后续全是乱码
红外遥控器在按键持续期间会重复发送同一帧,间隔在几十毫秒到一百多毫秒不等。问题出在解码器收到第一帧后进入空闲,但没有等待静默期。第二帧的引导脉冲到来时,半位采样定时器可能还残留中断,或者状态机里last_edge_time还是上一帧的时间,导致引导高段宽度计算包含了帧间空白,等于超出了 6T 容差。
解决方法是每帧结束(成功或失败)后强制关断半位采样,并让状态机进入一个“帧间隔禁止窗口”。最简单的方式是记录帧结束时间,下一次边沿到来时先判断距离上次帧结束是否超过 10T,不超过就丢弃。这样能避免把重复帧的尾巴当作新帧头。同时要把last_edge_time清零或加上一个足够大的偏移,让第一个引导脉冲宽度从真正的高段开始计时。
5.3 现象:距离稍远就丢帧,靠近遥控器才能用
排除功率问题后,这是典型的接收头输出上升沿变缓。一体化接收头输出低电平时靠内部管子拉低,恢复高电平只能靠上拉电阻。如果上拉电阻选的 100kΩ,RC 常数太大,上升沿变成斜坡,输入捕获模块可能检测不到上升沿,或者检测时间明显滞后,导致 1T 间隔测出来偏大。
解决方法是把上拉电阻降到 4.7kΩ,并检查主控的 GPIO 是否启用了内部上拉。有时候内部上拉并联外部上拉会变成两者并联,等效阻值更小,不影响功能。另一个隐藏因素是信号线上并联了太大电容,比如软排线过长,分布电容会让边沿更缓。把接收头到主控的连线控制在 10cm 以内,不要用长杜邦线飞线。如果非要长距离,用屏蔽线或加一级施密特触发器整形。
5.4 现象:空调遥控器在附近时,解码器被反复唤醒
空调遥控器多为广角发射,且也会在按键时发送重复帧。RC6 状态机在 IDLE 状态下对任何上升沿都敏感,其他协议的遥控器只要载波也是 38kHz,就可能触发引导高段检测。假如空调遥控器的按键帧恰好有接近 6T 的高段,状态机就会进入 LEADER_LOW,随后因为 2T 不匹配回到 IDLE。虽然不会误报按键,但主循环被频繁唤醒,功耗指标难看。
解决思路是增加引导脉冲的验证条件:不仅要求 6T 高段,还要求紧随其后是 2T 低段,这两步已经很严格。更高阶的做法是在起始位之后检查模式段是否为预期值,例如模式 0 必须是三个 0。如果收到 3 位非 000 的模式,说明协议不匹配,直接丢弃。另外,可以在接收头输出上并联一个 100pF 到 470pF 的小电容,滤除高频毛刺,但电容太大会吃掉边沿,必须实测。
5.5 现象:接入RTOS后解码偶发卡死,复位后恢复
卡死表现为系统其他任务正常,但红外键值再也进不来。检查后发现半位采样中断和输入捕获中断共享同一个定时器外设,而 RTOS 的某个驱动也配置了定时器通道,抢占导致寄存器被覆盖。
解决方法是明确中断优先级和资源归属:输入捕获中断优先级高于半位采样定时器中断,因为边沿同步一旦丢失,整个帧都废;半位采样中断只访问静态数组,不加锁;解码回调里不做任何可能阻塞的操作,比如互斥锁、打印、延时,这些全部放到任务里。如果项目里只有一个定时器可用,就把它设计成交错模式,先捕获后定时,但要注意切换寄存器时关闭中断。之后我专门为红外解码保留了一个独占定时器,问题再没出现过。
6. 把RC6解码做成可复用的学习模块:自动T值测量与发送扩展
代码跑通后,下一步最关键的是不要写死 T。同一品牌不同批次的遥控器 T 值可能差 10%,更不用说还有 RC6 的扩展模式。我把解码器改成了支持自动学习的模块:每次进入 IDLE 后,用第一条引导脉冲高段时间除以 6,作为当前遥控器的 T 估计值。这样同一台设备可以适配多种 RC6 遥控器,不需要每次烧录新固件。
具体实现是在检测到引导高段结束时,记录高段宽度leader_high_ticks,然后计算T = leader_high_ticks / 6。把这个新 T 用于后续 2T、1T 的判定,同时更新半位采样定时器的周期。第一次接收第一帧时不急着送按键值,先学习参数;第二帧再用新参数解码。这里要注意,如果高段本身被干扰,除出来的 T 可能离谱,所以要限制 T 在 300us 到 600us 之间,超出范围就丢弃。
发送功能扩展也不难:红外发射电路用 38kHz 载波,按 RC6 的帧结构反向生成波形。发送前先输出 6T 载波,再停 2T,然后按曼彻斯特规则逐位发送。用 PWM 产生载波,普通 GPIO 控制载波通断,就可以实现。难点在于精确的 1T 定时,和接收一样建议用定时器,不要用软件延时循环。我最早用delay_us写发送,结果每发一次,系统时间基准就漂一点,后来换成 DMA 加定时器波形输出才稳定。
我给自己的硬性要求是:接收模块必须在没有逻辑分析仪的情况下也能自适应参数;发送模块必须能回读自己发射的波形。如果你做万能遥控器,这两条是底线。最深的教训是,RC6 协议看似小众,一旦环境里有同频红外干扰,任何“看起来对”的脉冲序列都可能是伪帧,所以合法帧校验永远不能省。希望帮到你。
本文还有配套的精品资源,点击获取