ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于38kHz载波与NEC协议的红外收发系统设计与解码

2026/9/17 19:48:46 拓冰建站 浏览量
基于38kHz载波与NEC协议的红外收发系统设计与解码 简介这份PDF围绕红外通信收发系统的设计与实现展开面向电子信息、通信工程等专业的课程设计或实验实践者帮助读者理解以红外线为载体的无线信息传输原理并完成从方案设计到装配调试的完整训练。文档共1个PDF文件压缩包约483KB内容以实验指导书形式组织涵盖实验目的、设计要求、工作原理、发射与接收电路、功能实现与数据测量等模块。已有135人学习或下载。读者可据此掌握正弦波振荡器、KD-9300音乐芯片、LM386放大电路等关键环节了解Protel仿真、PCB设计、静态工作点调试、增益测量及失真排查思路并参考具体元件参数与测试数据完成基础要求和音乐传输提高要求。其价值在于把理论分析、电路参数与调试记录放在同一份资料中适合作为实验报告撰写、课程设计参考或红外通信入门实操指南。1. 为什么还在用红外一个 38kHz 载波撑起的低成本链路空调遥控器按下去 20 毫秒之内房间里那台机器就动了。整条链路没有握手、没有配对、没有地址冲突协商成本不到两块钱。这就是红外通信到今天还没被取代的原因它把点对点、单向、短指令这个场景压榨到了极限。标题里的收发系统拆开就是两件事——发射端用一颗红外 LED 把电信号变成 940nm 的近红外光接收端用一颗一体化接收头把光还原成电平中间靠 38kHz 载波和一套固定时序做约定。它适合谁适合要做遥控器、红外抄表、简易设备间短距数据回传的嵌入式开发者也适合想搞明白为什么我的解码老是校验不过的调试者。往下走我们从器件选型一路做到能跑通的编解码代码。2. 红外收发系统的器件选型与 38kHz 载波调制原理2.1 发射端三件套IR LED、限流电阻与驱动管发射端的核心矛盾是LED 要打得远就得瞬间灌大电流但单片机 IO 口的拉电流通常只有 20mA 上下而且长期大电流会把 LED 烧掉。行业里的通行做法是用脉冲驱动——不是持续点亮而是以 38kHz 的频率通断占空比压到三分之一左右。这样平均电流不大峰值电流却能做到几百毫安。典型电路是 MCU 的 PWM 引脚经一颗 NPN 三极管比如 S8050或小功率 MOSFET 去拉 IR LED 的阴极LED 阳极串一颗限流电阻接 5V。电阻取值按R (VCC - VF - VCEsat) / IF_peak算940nm 的 IR LED 正向压降约 1.2V三极管饱和压降按 0.2V电源 5V想要 200mA 峰值R (5 - 1.2 - 0.2) / 0.2 ≈ 18Ω。实际选 15~22Ω 之间配合软件上的占空比控制把平均电流压在 LED 的连续额定值以内。需要提醒的是发射管和接收头必须同频——发射端载波跑偏到 35kHz 或者 42kHz接收头内部的带通滤波器就会把信号衰减掉表现就是近距离能用、两三米外直接失灵。所以下面这段用于反推定时器参数的代码值得先跑一遍把误差控制在 1% 以内。def timer_params(f_clk_hz, f_pwm_hz, duty1/3): 根据定时器时钟和期望载波频率反推 ARR / CCR 寄存器值。 f_clk_hz: 定时器输入时钟如 72e6 f_pwm_hz: 目标载波频率NEC 一般取 38000 duty: 占空比接收头推荐 1/3比 1/2 更省电也更抗饱和 arr round(f_clk_hz / f_pwm_hz) - 1 ccr round((arr 1) * duty) real f_clk_hz / (arr 1) return arr, ccr, real arr, ccr, real timer_params(72_000_000, 38_000) print(fARR{arr} CCR{ccr} 实际载波{real:.1f}Hz 误差{abs(real-38000)/38000*100:.3f}%) # 输出ARR1894 CCR632 实际载波37994.7Hz 误差0.014%参数说明ARR决定周期CCR决定高电平持续时间duty1/3是接收头数据手册里常见的推荐值。误差 0.014% 已经远好于接收头 ±3% 的带宽容限可以放心用。2.2 接收端为什么必须选一体化模块VS1838B 的带通与 AGC接收端不要自己搭光电二极管加运放。环境里到处都是白炽灯、日光灯、显示器的红外成分裸管加放大电路会被干扰光直接淹没。一体化接收头VS1838B、HS0038、TSOP 系列内部集成了光电二极管、前置放大器、自动增益控制AGC和中心频率 38kHz 的带通滤波器输出端直接给出解调后的数字电平。它的输出极性要记住静态时输出高电平检测到 38kHz 载波时输出拉低。也就是说发射端亮灯的那段时间接收端读到的是低电平。这个反相关系是新手最容易搞反的地方解码时把 mark 和 space 对调结果就是引导码永远匹配不上。2.3 波长与载波频率的匹配表应用场景发射波长典型接收头中心频率选型备注家电遥控、通用遥控940nmVS1838B38kHz成本最低货源最广通用首选需要更远距离850nmTSOP483838kHz850nm 发光效率高但会有微弱红爆可见强环境光户外、工业940nm带窄带通的接收头38kHz依赖接收头内部滤光片外壳要避光低功耗电池设备940nm低功耗型接收头38kHz注意接收头静态电流通常几十微安选型顺序是先定频率38kHz 最通用再定波长室内用 940nm最后看距离需求决定要不要加一级驱动管或换 850nm。3. NEC 协议的时序拆解与发送端编码实现3.1 引导码、地址码与位定义NEC 的四个时间常数NEC 协议把所有信息压缩成四个时间常数理解了这四个数协议就通透了一半名称载波段mark空闲段space总时长引导码9.0ms4.5ms13.5ms逻辑 00.56ms0.56ms1.12ms逻辑 10.56ms1.69ms2.25ms重复码9.0ms2.25ms 0.56ms 载波11.81ms数据部分固定 32 位顺序是8 位地址、8 位地址反码、8 位命令、8 位命令反码。全部按位低有效LSB first发送。反码的存在是为了让接收端做一次自校验——地址反码不等于地址取反就说明这帧被干扰了直接丢弃。3.2 用 TIM PWM 输出 38kHz 载波的最小配置发送端的核心思路是让定时器在 PWM 输出和关闭之间切换用开一段、关一段的方式拼出上面的时间常数。以 STM32 的 TIM3 为例第 2 章算出来的 ARR1894、CCR632 直接写进去即可。/* 38kHz 载波初始化72MHz / (01) / (18941) ≈ 37994.7Hz */ static void ir_carrier_init(void) { __HAL_TIM_SET_PRESCALER(htim3, 0); /* 不分频 */ __HAL_TIM_SET_AUTORELOAD(htim3, 1894); /* 周期 */ __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 632); /* 1/3 占空比 */ HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Stop(htim3, TIM_CHANNEL_1); /* 先停住输出保持低 */ } /* 输出一段 38kHz 载波对应接收端的低电平 */ static void ir_mark(uint32_t us) { HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); delay_us(us); HAL_TIM_PWM_Stop(htim3, TIM_CHANNEL_1); } /* 输出一段无载波空闲对应接收端的高电平 */ static void ir_space(uint32_t us) { delay_us(us); /* PWM 已停LED 熄灭线路自然空闲 */ }delay_us建议用 DWT 周期计数器实现普通空循环在开了中断之后抖动会很大容易把 560us 拖成 700us 以上直接掉出解码容差窗口static inline void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks) { } } /* 使用前需使能CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; */3.3 发送一帧的完整 C 代码与逐行说明/** * 发送一帧标准 NEC 码 * param addr 8 位地址 * param cmd 8 位命令 */ void ir_send_nec(uint8_t addr, uint8_t cmd) { uint8_t frame[4] { addr, (uint8_t)~addr, cmd, (uint8_t)~cmd }; ir_mark(9000); /* 引导码9ms 载波 */ ir_space(4500); /* 4.5ms 空闲 */ for (int i 0; i 4; i) { for (int b 0; b 8; b) { ir_mark(560); /* 每位都以 560us 载波开头 */ if (frame[i] (1u b)) /* 先发低位 */ ir_space(1690); /* 逻辑 1 */ else ir_space(560); /* 逻辑 0 */ } } ir_mark(560); /* 结束位让接收端确认帧尾 */ }逻辑说明外层循环依次吐出地址、地址反码、命令、命令反码四个字节内层循环按位从低到高发送这是 NEC 的硬性规定反过来发会得到看似正确但命令错乱的诡异现象。参数说明所有时间单位都是微秒直接对应第 3.1 节表格里的数值不要随意调整——接收头内部的解码逻辑对这些时间是有容差窗口的一般允许 ±15% 到 ±25%。3.4 载波占空比与峰值电流的取舍占空比调到 1/2 时LED 的峰值电流虽然一样但平均电流翻倍长时间连发比如按住按键重复发送会让发射管温升明显。1/3 是接收头数据手册的推荐值既能保证接收头内部的 AGC 正常工作又能把平均功耗压下来。如果非要拉距离与其加占空比不如加大峰值电流——把限流电阻从 22Ω 降到 15Ω效果比改占空比更直接。提示连续发送多帧时帧与帧之间至少留 40ms 以上的间隔否则接收头的 AGC 会来不及恢复增益后面的帧会被削顶。4. 接收端解码边沿捕获、抗抖动与帧校验4.1 从接收模块波形到微秒级时间戳接收端最省资源的做法是外部中断加时间戳。把接收头输出脚接到任意带 EXTI 的 GPIO双边沿触发每次中断记录距离上一次边沿过了多久。这样得到的是一串电平持续时长交给主循环去解析。#define IR_MAX_EDGE 72 static volatile uint16_t ir_t[IR_MAX_EDGE]; /* 各段时长单位 us */ static volatile uint8_t ir_n; /* 已记录段数 */ static volatile uint32_t ir_t0; /* 上次边沿时间戳 */ static volatile uint8_t ir_ready; /* 一帧采集完成标志 */ void HAL_GPIO_EXTI_Callback(uint16_t pin) { if (pin ! IR_RX_Pin) return; uint32_t now micros(); uint32_t dt now - ir_t0; ir_t0 now; if (dt 10000) { /* 空闲超过 10ms判定为新的一帧 */ ir_n 0; ir_ready 0; return; /* 这段空闲本身不作为数据段 */ } if (ir_n IR_MAX_EDGE) { ir_t[ir_n] (uint16_t)dt; } if (ir_n 68) ir_ready 1; }关键点在于空闲超过 10ms 就重置这个判断。一帧 NEC 数据里最长的时间段是引导码的 9ms留 1ms 余量刚好。不做这个重置上一帧的尾部数据会污染下一帧的头部表现就是偶尔解码成功、偶尔校验失败。4.2 解码器的状态机与容差窗口解码不用状态机那么重直接扫描引导码即可。下面的实现把全部时间常数都做了区间判断容差按 ±30% 放宽——实测遥控器一致性差异、晶振精度差异、中断延迟都吃在这段余量里。/** * 解析 NEC 帧 * param t 时长序列 * param n 段数 * param out 解析出的 32 位原始码地址|地址反|命令|命令反 * return 0 成功负数表示失败原因 */ int nec_decode(const uint16_t *t, uint8_t n, uint32_t *out) { int i 0; /* 扫描引导码9ms 低 4.5ms 高 */ for (; i 1 n; i) { if (t[i] 8000 t[i] 10000 t[i1] 3800 t[i1] 5200) break; } if (i 1 n) return -1; /* 未找到引导码 */ i 2; uint32_t code 0; for (int b 0; b 32; b) { if (i 1 n) return -2; /* 数据段不足 */ uint16_t mark t[i]; uint16_t space t[i1]; if (mark 350 || mark 800) return -3; /* 载波段应为 560us */ if (space 350 space 900) code | (0u b); /* 逻辑 0 */ else if (space 1300 space 2000) code | (1u b); /* 逻辑 1 */ else return -4; /* 该位无法判定 */ i 2; } *out code; return 0; }参数说明mark是载波段理论 560us区间取 350~800usspace是空闲段逻辑 0 理论 560us逻辑 1 理论 1690us两段区间刻意留了空隙避免临界值误判。返回负数的四个错误码分别对应没有引导码数据不完整载波段异常位无法判定调试时把它们打到串口上一眼就能看出问题在哪一环。4.3 校验地址反码与命令反码拿到 32 位原始码之后必须做反码校验这是 NEC 协议自带的唯一一道防错机制int nec_verify(uint32_t raw, uint8_t *addr, uint8_t *cmd) { uint8_t a raw 0xFF; uint8_t na (raw 8) 0xFF; uint8_t c (raw 16) 0xFF; uint8_t nc (raw 24) 0xFF; if ((uint8_t)~a ! na) return -1; /* 地址反码不匹配 */ if ((uint8_t)~c ! nc) return -2; /* 命令反码不匹配 */ *addr a; *cmd c; return 0; }很多现成遥控器的反码是严格按位取反的但也有少数厂商在扩展 NEC 里直接把地址扩成 16 位这时第二个字节不再是反码而是地址高字节。遇到反码校验总失败的情况先把原始 32 位码打到串口看一遍别急着怀疑代码。4.4 用逻辑分析仪验证时序解码调不通的时候最快的定位方式是抓波形。把逻辑分析仪的通道接到接收头输出脚采样率设到 1MHz 以上触发条件设成下降沿抓一帧之后量四个数引导码低电平是不是 9ms、引导码后的高电平是不是 4.5ms、短间隔是不是 560us、长间隔是不是 1690us。哪一个对不上就往发射端找问题全都对得上但解码还是失败那就是软件里的容差窗口或者边沿计数逻辑写错了。5. 联调排错与实用技巧距离、干扰与重复码5.1 常见故障对照表现象大概率原因排查动作近距离能用1 米外失效载波频率跑偏或峰值电流不足用示波器量 PWM 引脚实际频率反推定时器参数引导码匹配不上接收头极性与代码假设相反确认空闲是高电平载波段是低电平偶尔解码成功、多数失败中断里做了耗时操作导致时间戳抖动中断只记录时长解码放主循环反码校验总失败非标准 NEC地址为 16 位打印原始 32 位码人工比对日光灯下误触发环境光干扰或接收头无滤光片接收头加黑色遮光套管软件里加连续两帧一致性判断5.2 提高通信距离的三个办法第一是加峰值电流把限流电阻降到 15Ω 左右配合 1/3 占空比平均电流仍在安全区。第二是给发射管加聚焦结构很多成品遥控器外壳前端那个小凸透镜不是装饰它能把发散角从 120° 收到 30° 以内轴向能量密度提升明显。第三是给接收头加遮光套管减少侧向环境光进入这一条在室内灯光复杂的环境里效果往往比前两条加起来都明显。注意降限流电阻之后一定要复测连发时的 LED 温升脉冲电流超规格短时间内看不出问题连续工作半小时之后才会暴露。5.3 重复码与长按处理NEC 协议里长按按键不会重复发送完整帧而是发送重复码9ms 载波 2.25ms 空闲 560us 载波。它的长度和正常帧完全不同用第 4 章的解码器会直接返回未找到引导码。正确做法是单独识别——在扫描引导码之后如果紧接着的空闲段只有 2.25ms 左右就判定为重复码把上一条命令再触发一次。识别逻辑很轻/* 在 nec_decode 的引导码匹配之后插入 */ if (t[i] 1800 t[i] 2700) { /* 2.25ms 空闲 → 重复码 */ *out 0xFFFFFFFFu; /* 约定值表示重复 */ return 0; }参数说明2.25ms 留的是 1800~2700us 区间重复码后面那个 560us 载波可以不判断因为它和下一帧之间必然隔着一段长空闲会在 4.1 节的重置逻辑里被丢掉。5.4 用现成遥控器标定你的接收端接收端写完之后别急着和自己写的发射端联调——两端同时错会互相掩盖问题。更快的办法是找一个现成的空调或电视遥控器对准你的接收头按一下把原始 32 位码和引导码时长打出来。如果现成遥控器的数据能稳定解析说明接收链路和时序窗口都是对的剩下的问题一定在发射端如果连现成遥控器都解不出来就老老实实回去量波形从接收头极性和供电电压开始查起。这一步能把两端都有 bug的排查时间砍掉一大半。本文还有配套的精品资源点击获取