首页 / 资讯中心 / 文章详情

红外遥控码库实战:从raw码采集到回放验证的完整链路

红外遥控码库实战:从raw码采集到回放验证的完整链路 ★ FEATURED ARTICLE
简介红外遥控码库是一份面向电子爱好者、硬件开发者及智能家居集成商的实用技术资源可据此实现电视、空调、投影仪等家用电器的无线遥控与自动化控制。RAR压缩包体积仅27KB内部按设备型号组织码库内容涵盖相机、空调、IPTV等约20种常见设备的遥控码便于按需提取对应指令。码库基于红外线通信原理包含发射端编码与接收端解码所需的码值数据部分文件以二进制或ASCII码形式存储指令开发者可借助Python或C编写自定义遥控程序也可利用资源附带的网址链接进一步获取约3万种设备的开源码库满足更广泛的兼容需求既可用于简单家电控制也可支撑复杂智能家居联动。目前已有3353人学习下载适合从入门到进阶的智能家居开发者作为参考素材库。1. 红外遥控码库采回来的码发不出去才是第一个坎做红外遥控码库做到后面你会发现最卡进度的不是采集而是回放一条码采回来、解析完、存进库按下发送键设备却没反应这时候你连问题出在协议还是载波都说不清。红外遥控码库解决的就是这条链路上从数据格式到发射验证的一整段问题——把遥控器每个按键变成可复用、可匹配、可回放的数据记录而不是一堆躺在文件夹里的 CSV。我见过不少项目负责人一上来就研究怎么把波形解析得漂漂亮亮结果卡在「存 raw 还是存协议码」这种选择题上。做码库的核心目标只有一个让任意一台设备能从库里找到一条能用的码并稳定发出去。这套思路适合正在做智能家居网关、万能遥控器、电视和机顶盒联动的人。下面从协议基础拆到采集、存储、匹配和发射验证最后给一套能直接落地的验收手段。2. 红外协议三兄弟与码的两种形态NEC、RC-5、SIRC 和 raw 码2.1 38kHz 载波与脉冲宽度先看懂波形再谈码红外遥控的本质是「载波 通断」。发送端用 38kHz 左右的方波驱动红外 LED无信号时 LED 灭有信号时 LED 以 38kHz 点亮接收端的一体化接收头把这段载波解调成普通的高低电平脉冲。所以码库里要记录的不是 38kHz 的每一个周期而是解调后的「脉冲持续多久、间隔持续多久」这个时间序列业内一般叫 raw 码。raw 码的典型长相是一组微秒数组9000, 4500, 560, 560, 560, 1690, 560, 560……偶数下标是脉冲时长红外 LED 亮的时间奇数下标是间隔时长灭的时间。第一对 9000/4500 是引导头后面每对对应一个数据位。为什么选 38kHz 而不是更高或更低因为一体化接收头内部有带通滤波器38kHz 能避开大多数环境红外干扰而且同频段的接收头型号最普遍采购和替换都容易。一体化接收头输出是反极性的这一步坑过不少人。绝大多数接收头空闲时输出高电平收到红外脉冲时输出低电平所以逻辑分析仪上看到的「脉冲段」其实是低电平。如果采集脚本按正极性存后续所有 raw 码的脉冲和间隔就全反了。我习惯在解析脚本开头加一个极性参数默认按低有效处理采集完对比原遥控的已知键不对再翻过来。注意38kHz 是载波频率和数据位率是两回事。载波频率决定信号能不能被接收头识别数据位长度决定这个位是 0 还是 1。改载波不会改变码的语义但可能让接收端完全不响应。2.2 NEC、RC-5、SIRC 三种协议的位格式差异NEC 协议是电视、机顶盒里最常见的。它的一帧是9ms 引导脉冲 4.5ms 间隔 8 位地址 8 位地址反码 8 位命令 8 位命令反码总时长约 67ms。逻辑位用脉冲距离区分逻辑 1 是 560us 脉冲 1690us 间隔逻辑 0 是 560us 脉冲 560us 间隔LSB 先发。地址和命令都带反码是为了校验这也是码库解析时判断「有没有抓错帧」的天然依据。NEC 的反码不是摆设。解析时把地址和命令各自跟反码做异或结果不是 0xFF 就说明这一帧抓坏了。采集环境里这很常见——遥控器按键按下的瞬间接触不良、电池电压不够、接收头前有遮挡都会导致某一位翻转。我一般把校验失败的帧单独统计而不是直接丢弃因为连续失败的键多半是按键弹跳或协议判断错了。RC-5 是另一种常见协议。它没有长引导头而是用曼彻斯特编码位时间内电平翻转一次表示 0 或 1。一帧固定 14 位包含起始位、控制位、toggle 位、5 位地址和 6 位命令。toggle 位每次按键都翻转用来区分「按了一下」和「一直按住」。SIRC 则是脉宽编码逻辑 0 大约 600us 脉冲加 600us 间隔逻辑 1 大约 1200us 脉冲加 600us 间隔帧头约 2.4ms而且同一品牌下还有 12 位、15 位、20 位三种变体。下面这张表是我在码库字段里固定要带的协议参数方便快速判断一段 raw 码属于谁协议常见载波单帧长度引导头逻辑位区分常见场景NEC38kHz约 67ms约 9ms 4.5ms脉冲后间隔 560/1690us电视、盒子、风扇RC-536kHz约 25ms无长引导头曼彻斯特翻转部分影音设备SIRC40kHz约 25ms 或更长约 2.4ms脉冲宽度 600/1200us部分日系影音设备表里数值是典型值同一协议在不同厂商手上会有微调。我一般只拿这些数值当第一轮过滤真正判定协议靠采集到的 raw 码做统计而不是拿着表硬套。2.3 raw 码与协议码码库里存的到底是谁抓完波形之后手上有两种「码」一种是微秒数组叫 raw 码一种是解析出来的地址和命令比如 NEC 的 address0x00、command0x45叫协议码。协议码短、可读、方便搜索但代价是丢失了所有时序细节。遇到一个没见过的协议时你根本不知道地址从哪一位开始强行解析出来的命令码很可能完全对不上按键。所以我在这类项目里的做法很固定码库的权威数据永远是 raw 码协议码只作为辅助索引。匹配时先用品牌和机型缩小范围再比对 raw 码的相似度展示给用户调试时才用协议码做可读化。这样就算某天发现某个协议名归错了类也不影响回放因为发射模块根本不需要理解协议它只需要按时序把载波通断做出来。有些现成的码库文件干脆只给 raw 码连协议名都不写。这种库用起来笨但可靠。我做过一个某智能家居项目前期只有 300 多条 raw 码没有一条带协议字段一样能把电视和空调的常用键跑通。原因是回放只依赖时序协议解析只是给人看的。如果你的数据源很脏别为了「标准化」把 raw 丢掉。raw 码也有版本问题。同一个键不同批次遥控器发出的引导头可能差几百微秒命令位间隔也有轻微漂移。所以入库时不要只存一条 raw 码可以为同一个键存多条候选后面匹配和回退都用得上这个策略到第 4 章展开。3. 把一枚遥控器按键送进码库采集、清洗与入库全流程3.1 采集链路的最小硬件组合采集红外码不需要专业仪器最常见的一套组合是 38kHz 一体化接收头加逻辑分析仪或者接收头直连单片机做边沿捕获。接收头负责把红外信号转成 TTL 电平逻辑分析仪或单片机负责记录每次电平跳变的时间戳。只要能拿到「什么时刻电平变了」后面解析 raw 码就是纯软件的事。选接收头时注意两点。第一是载波频率买 38kHz 通用型号覆盖 NEC 和 SIRC 都没问题第二是输出极性多数接收头是低有效但个别型号是高有效接上以后先用已知遥控器按一下键看波形方向对不对。供电电压按接收头规格来常见 3.3V 到 5V 都能工作但逻辑分析仪的采样电平要和接收头供电匹配否则采到的边沿会乱。如果不想碰逻辑分析仪单片机直接抓边沿也够用。把接收头输出脚接到支持外部中断的 GPIO 上在中断里读定时器计数值相邻两次中断的时间差就是一段脉冲或间隔的时长。注意中断服务函数里不要做耗时操作只做「读计数值、存数组、清零」三件事解析放到主循环。3.2 用 GPIO 边沿事件采集脉冲序列一段可跑的采集脚本我常用一个省事的办法逻辑分析仪采样一整段按键波形导出一份 CSV每行是时间和电平再用脚本把边沿时间换算成 raw 码。好处是波形能反复看按键抖动、尾部噪声都在原始数据里不丢信息。下面是解析 CSV 的核心脚本import sys, csv def parse_edges(csv_path, sample_ns25, invertTrue): 把逻辑分析仪导出的 (时间ns, 电平) 文本转成脉冲/间隔微秒数组。 偶数下标为脉冲时长, 奇数下标为间隔时长。 values [] last_time None last_level None def norm_level(text): # CSV 里电平可能是 1/0, High/Low, On/Off, 统一成 1/0 t text.strip().lower() if t in (high, on, 1, true): return 1 return 0 with open(csv_path, r, encodingutf-8) as f: reader csv.reader(f) for row in reader: if len(row) 2: continue try: t_ns int(row[0]) level norm_level(row[1]) except ValueError: continue if last_time is not None and level ! last_level: dur_ns t_ns - last_time if dur_ns 0: values.append(dur_ns // 1000) # ns - us last_time t_ns last_level level # 接收头多数是低有效: 空闲高电平, 收到脉冲时拉低 seen_level None result [] for v in values: if seen_level is None: seen_level 0 if invert else 1 result.append(v) seen_level 1 - seen_level return result if __name__ __main__: raw parse_edges(sys.argv[1]) print(首 32 段:, raw[:32]) print(总段数:, len(raw))这个脚本的逻辑是CSV 里只有发生电平跳变的位置才是有效信息所以只在相邻两行电平不同时记录时长时间基准统一转成微秒。invert 参数对应 3.1 说的接收头极性默认低有效如果采集结果的开头不是 9ms 量级的大脉冲把 invert 改成 False 再跑一次。sample_ns 是逻辑分析仪的实际采样周期对 38kHz 解调后的边沿采样率建议不低于 4MHz否则容易丢边沿。如果你更习惯单片机方案核心逻辑是一样的只是把 CSV 换成了中断里的边沿数组// 边沿捕获: 配置定时器自由计数, 在 GPIO 中断里记录相邻两次跳变的时间差 #define EDGE_BUF_SIZE 256 volatile uint32_t edge_buf[EDGE_BUF_SIZE]; volatile uint8_t edge_cnt 0; volatile uint32_t last_tick 0; void gpio_edge_isr(void) { uint32_t now TIMER-CNT; if (edge_cnt EDGE_BUF_SIZE) { edge_buf[edge_cnt] now - last_tick; } last_tick now; }这段代码的关键是 TIME_BASE_US 的换算关系。比如主频 72MHz、预分频 71计数频率就是 1MHz每个计数值正好 1us。缓冲区 256 个边沿足够装下一条 NEC 帧一帧约 68 个边沿加部分重复帧如果 edge_cnt 溢出说明连续收到了多帧或环境噪声需要清空后重新采集。3.3 清洗与入库去抖动、去重复帧、生成码库记录拿到原始边沿数组后不能直接入库先做三步清洗。第一步去抖动把小于 80us 的窄脉冲或窄间隔合并到相邻段里这些短段来自按键机械弹跳或接收头边缘噪声留着会让后续相似度匹配变差。第二步去尾部噪声NEC 帧在最后一个 stop bit 之后还有一小段残留电平通常小于 200us直接截掉。第三步去重复帧按住遥控器按键不放接收到的不是一帧而是每隔 110ms 左右一帧波形首尾都是引导头。简单判断数组前两段都是 9ms 加 4.5ms 量级的引导头且整个数组长度相近就是重复帧只保留第一帧。如果两次按同一个键的波形在中段差几十微秒这是正常抖动不要当不同码处理。清洗后就能生成码库记录。我在某智能家居项目里的最小记录字段是品牌、机型、按键名、协议、载波、raw 数组、重复间隔、采集来源、是否已验证。协议和载波可以先用 2.2 的参数表粗判后面匹配失败再人工复核。注意按键名一定要规范化比如 power 不要存成「開機」「power_on」「main_power」三种写法否则后面匹配逻辑会被命名坑死。4. 码库存储与匹配JSON 存 raw、SQLite 建索引、多码回退兜底4.1 给一条码设计一组不会后悔的 JSON 字段码库的物理存储我建议两层一层是给人读的 JSON 文件存码的完整内容和来源信息另一层是 SQLite 数据库存索引和匹配状态。JSON 是码库的真身SQLite 是查找入口。为什么不用纯数据库存 raw因为 raw 码动辄几百个数字塞进关系表里又长又难维护而且调试时随手打开 JSON 看到完整波形比查库方便太多。{ brand: 某品牌, model: 某机型, protocol: nec, carrier_hz: 38000, repeat_gap_ms: 110, keys: { power: { address: 0, command: 69, raw: [9000, 4500, 560, 560, 560, 1690], candidates: [ [9000, 4500, 560, 560, 560, 1690] ], verified: 1 } } }字段含义protocol 和 carrier_hz 是回放前要读的参数keys 下面每个键至少要有 raw 数组address/command 是解析结果允许为空candidates 里放同一个键的多条候选 rawverified 标记是否通过第 6 章的回放验证。这个结构里 protocol 只是优化匹配用的发射模块不依赖它所以未来发现协议判断错了也能安全纠正不用改 raw。raw 数组的存储单位统一用微秒数值不做任何缩放。有些外部数据源会给你 50us 步进、100us 步进或者别的基准入库时一定要先换算成微秒再落盘同时在 JSON 里保留一个 source_unit 字段记录原始基准方便后面对账。4.2 SQLite 索引表与品牌-机型匹配SQLite 只存索引和匹配状态不存 raw 大字段。两张表足够devices 存设备维度信息keys 存按键维度的索引。建表语句我一般这样写CREATE TABLE devices ( id INTEGER PRIMARY KEY, brand TEXT NOT NULL, model TEXT, protocol TEXT, carrier_hz INTEGER DEFAULT 38000, source TEXT, created_at TEXT DEFAULT (datetime(now)) ); CREATE TABLE keys ( id INTEGER PRIMARY KEY, device_id INTEGER REFERENCES devices(id), key_name TEXT NOT NULL, address INTEGER, command INTEGER, verified INTEGER DEFAULT 0 ); CREATE INDEX idx_keys_device ON keys(device_id); CREATE INDEX idx_keys_name ON keys(key_name);raw 数组不落 SQLitekeys 表里通过 device_id 回表读 JSON 文件这样 SQLite 库体积小、查询快。匹配查找的顺序一般是先用 brand 加 model 精确查能查到就直接用查不到就降级成 brand 匹配把该品牌所有机型下同名按键的码收集起来再不行就全库按 key_name 查用协议过滤后排序。这个三级降级逻辑要写在查询层不要让接入方自己拼 SQL。机型字段经常不准遥控器上印的型号和机身上的型号可能不一样。我见过用户拿 A 型号的遥控器控制 B 型号电视的情况所以匹配表里可以给同一设备存多个已知型号匹配时用 IN 查询全部算进去。4.3 多码回退同一个键存多条候选按优先级依次试码库里同一个按键有多条候选是常态来源可能是不同批次遥控器、不同年份机型、甚至不同协议。匹配时不能只取第一条而要按可信度排好序第一条失效自动切第二条。我在某项目里定义了一套优先级优先级匹配条件使用场景1brand model 完全命中protocol 一致同款机型直接用2brand model 命中protocol 不一致同机型换了协议试发3brand 命中key_name 命中同品牌跨机型候选排序4全库 key_name 命中协议过滤未知品牌兜底回退策略要配套发送层按下一次发 3 遍间隔用 repeat_gap_ms用户 800ms 内没有确认设备有反应自动切换下一条候选。注意「用户确认」在红外场景里最好做成实体反馈比如屏幕上让用户选「有反应 / 没反应」不要自作聪明用环境声音判断。排序时 verified 字段优先没验证过的码永远排在验证过的码后面。5. 回放发射阶段的四个高频踩坑载波、脉宽、重复帧与协议误判5.1 发射链路先做好38kHz 载波不是玄学发射链路比采集更容易出问题。最小方案是单片机定时器输出 38kHz PWM用三极管或 MOS 管驱动红外 LEDLED 峰值电流做到几百毫安级这样 8 到 10 米距离才稳。如果直接拿 GPIO 高电平驱动 LED电流只有几毫安2 米以外就会开始玄学时灵时不灵。// 用定时器输出比较生成载波, 按 raw 数组控制通断 // seq 偶数下标为脉冲时长, 奇数下标为间隔时长, 单位 us void ir_send_raw(uint16_t *seq, uint16_t len, uint32_t carrier_hz) { uint32_t half (TIMER_CLOCK / carrier_hz) / 2; // 半个载波周期 for (uint16_t i 0; i len; i) { if (i % 2 0) { TIMER-CCR half; // 占空比 50% TIMER-CR | TIMER_EN; // 开 PWM, LED 发载波 } else { TIMER-CR ~TIMER_EN; // 关 PWM, LED 灭 } delay_us(seq[i]); } TIMER-CR ~TIMER_EN; // 结束必须关载波 }这段代码直接从 raw 数组生成载波通断。half 的计算依赖 TIMER_CLOCK也就是定时器实际计数频率比如分频后是 1MHz载波 38kHz 对应 half 约 13。发送完记得把定时器关闭不然 LED 一直发载波既费电又可能干扰同房间其他红外设备。有些协议对帧与帧之间的间隔很敏感尤其是空调。发送时不要在每帧后面拼自己的延时直接用 raw 里最后一段间隔作为帧间距或者用 repeat_gap_ms 字段统一控制不要让上层调用方随手填一个延时。5.2 坑一raw 码脉宽单位与时间基准错位现象库里看波形完全正常但一发送设备就没反应或者同一个码时好时坏。原因raw 码的单位没统一。我见过三种数据来源直接给微秒、给 50us 步进数值 18 代表 900us、还有 Pronto hex 那种按 0.241246us 基准的格式。如果加载时默认按微秒发50us 步进的码会被压缩成实际时长的约 1/50设备当然不认。解决入库时强制统一转成微秒并在 JSON 里保留 source_unit 字段。加载外部码库时先用一小段已知引导头做单位自检比如 NEC 的 9000/4500如果读到的数值接近 180/90说明数据是 50us 步进需要先乘以 50 再入库。5.3 坑二重复帧不足空调和机顶盒不响应现象电视按一次就响应空调按十次只响应两三回。原因很多空调遥控器每次按键连续发 2 到 5 帧接收端按帧数做确认有些机顶盒要求按住时每 110ms 补一帧否则认为信号中断。只发一帧相当于话只说了半句。解决发送层默认连续发 3 帧帧间隔用 repeat_gap_ms如果库里存的是「一条完整包含重复帧的 raw」发送时不要再去重直接整条发。这个参数可以在发送前打印出来核对NEC 设备常见 110ms 间隔但个别机型会用 40ms 或 80ms以采集到的重复帧实测为准。5.4 坑三把协议误判当码值错误越修越乱现象用 NEC 解析某按键得到 command0x1F和码库对不上但把原始 raw 发出去设备就正常。原因协议判断错了。RC-5 的 toggle 位会被当成数据位SIRC 的 12 位帧被按 15 位解析NEC 的地址反码被当成命令。协议层的错误会污染 address/command 字段但不会影响 raw 回放因为发射模块按 raw 原样发。解决排查时直接对比 raw 码的前两段。NEC 引导头约 9000us 加 4500usSIRC 约 2400us 加 600usRC-5 没有明显引导头光看前两段就能排除大多数误判。入库时始终保留 raw 码协议字段只是辅助索引别让解析结果反过来覆盖 raw。5.5 坑四载波偏差导致遥控距离骤降现象波形回放看着没错设备也能响应但距离超过 3 米就失灵角度偏一点更差。原因定时常数算错实际载波只有 34kHz 或者到了 42kHz。一体化接收头内部带通滤波器对频率偏差很敏感偏差超过约 ±15% 接收灵敏度明显下降。解决用逻辑分析仪在接收头输出端量回放波形注意解调输出看不到 38kHz要量发射端 LED 两端或者用一个不带解调的光敏管配合示波器看。我一般会在代码里留一个载波自检模式连续输出 10ms 载波用频率计或示波器读了实际频率再回头校准定时器半周期值。6. 码库建完后的进阶玩法自学习、自动识别与回放验证闭环6.1 从「抄码库」到「自学习入库」如果设备本身带了红外接收和发射就可以做自学习用户把原遥控对着设备按一次键设备自动完成采集、清洗、入库三件事。这样码库会随着设备落地越用越全而不是永远依赖人工录码。自学习入库的字段和人工录入完全一致只是 source 标成 user_learnedverified 默认 0因为还没经过回放验证。6.2 用自动协议识别减少标错协议识别的核心是看引导头不需要上复杂算法def guess_protocol(raw): if len(raw) 4: return unknown pulse, gap raw[0], raw[1] if 7000 pulse 11000 and 3500 gap 5500: return nec if 2000 pulse 3000 and 400 gap 900: return sirc if pulse 100 and 600 gap 2500: return rc5 return unknown这段代码是典型的经验阈值逻辑阈值来自历史码库的统计换一批设备可能要调整。注意 RC-5 判断里引导头可能小于 100us说明信号从低电平开始这是曼彻斯特编码的特点。协议识别只用来填字段不参与回放决策所以判断错了也不会让码库不可用。6.3 回放验证让每条入库码都过一遍「发出去再收回来」最后一件事也最重要验证码库里的每一条码真的能回放。做法是发送模块把 raw 码发出去同一套红外接收头在旁边把回放波形采集下来再和原始 raw 做逐边沿比较容差 ±80us误差比例小于 5% 算通过def verify_raw(ref, captured, tol_us80): if abs(len(ref) - len(captured)) max(2, len(ref) // 10): return False n min(len(ref), len(captured)) bad sum(1 for i in range(n) if abs(ref[i] - captured[i]) tol_us) return bad / n 0.05这条验证脚本我会放在每次入库和每次更新数据源之后跑跑不过的码自动标记 unverified不进入正式匹配。之前跳过这步的码后来在现场挨个翻车我才养成了先验证再入库的习惯。每一条能稳定回放的码比一百条标注漂亮的死数据有用得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站