简介:这份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, duty=1/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(f"ARR={arr} CCR={ccr} 实际载波={real:.1f}Hz 误差={abs(real-38000)/38000*100:.3f}%") # 输出:ARR=1894 CCR=632 实际载波=37994.7Hz 误差=0.014%参数说明:ARR决定周期,CCR决定高电平持续时间,duty=1/3是接收头数据手册里常见的推荐值。误差 0.014% 已经远好于接收头 ±3% 的带宽容限,可以放心用。
2.2 接收端为什么必须选一体化模块:VS1838B 的带通与 AGC
接收端不要自己搭光电二极管加运放。环境里到处都是白炽灯、日光灯、显示器的红外成分,裸管加放大电路会被干扰光直接淹没。一体化接收头(VS1838B、HS0038、TSOP 系列)内部集成了光电二极管、前置放大器、自动增益控制(AGC)和中心频率 38kHz 的带通滤波器,输出端直接给出解调后的数字电平。
它的输出极性要记住:静态时输出高电平,检测到 38kHz 载波时输出拉低。也就是说,发射端"亮灯"的那段时间,接收端读到的是低电平。这个反相关系是新手最容易搞反的地方,解码时把 mark 和 space 对调,结果就是引导码永远匹配不上。
2.3 波长与载波频率的匹配表
| 应用场景 | 发射波长 | 典型接收头 | 中心频率 | 选型备注 |
|---|---|---|---|---|
| 家电遥控、通用遥控 | 940nm | VS1838B | 38kHz | 成本最低,货源最广,通用首选 |
| 需要更远距离 | 850nm | TSOP4838 | 38kHz | 850nm 发光效率高,但会有微弱红爆可见 |
| 强环境光(户外、工业) | 940nm | 带窄带通的接收头 | 38kHz | 依赖接收头内部滤光片,外壳要避光 |
| 低功耗电池设备 | 940nm | 低功耗型接收头 | 38kHz | 注意接收头静态电流,通常几十微安 |
选型顺序是先定频率(38kHz 最通用),再定波长(室内用 940nm),最后看距离需求决定要不要加一级驱动管或换 850nm。
3. NEC 协议的时序拆解与发送端编码实现
3.1 引导码、地址码与位定义:NEC 的四个时间常数
NEC 协议把所有信息压缩成四个时间常数,理解了这四个数,协议就通透了一半:
| 名称 | 载波段(mark) | 空闲段(space) | 总时长 |
|---|---|---|---|
| 引导码 | 9.0ms | 4.5ms | 13.5ms |
| 逻辑 0 | 0.56ms | 0.56ms | 1.12ms |
| 逻辑 1 | 0.56ms | 1.69ms | 2.25ms |
| 重复码 | 9.0ms | 2.25ms + 0.56ms 载波 | 11.81ms |
数据部分固定 32 位,顺序是:8 位地址、8 位地址反码、8 位命令、8 位命令反码。全部按位低有效(LSB first)发送。反码的存在是为了让接收端做一次自校验——地址反码不等于地址取反,就说明这帧被干扰了,直接丢弃。
3.2 用 TIM PWM 输出 38kHz 载波的最小配置
发送端的核心思路是:让定时器在 PWM 输出和关闭之间切换,用"开一段、关一段"的方式拼出上面的时间常数。以 STM32 的 TIM3 为例,第 2 章算出来的 ARR=1894、CCR=632 直接写进去即可。
/* 38kHz 载波初始化:72MHz / (0+1) / (1894+1) ≈ 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[i+1] >= 3800 && t[i+1] <= 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[i+1]; 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~800us;space是空闲段,逻辑 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"的排查时间砍掉一大半。
本文还有配套的精品资源,点击获取