ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于单片机的红外遥控电路系统:NEC协议解码与硬件设计实战

基于单片机的红外遥控电路系统:NEC协议解码与硬件设计实战 简介这是一套基于单片机的红外遥控电路系统完整项目资料面向电子/自动化类专业学生及单片机初学者可用于课程设计、毕业设计或电子产品开发参考。资源围绕红外遥控功能展开从单片机选型8051系列或AVR系列、红外接收模块TSOP系列到信号处理与NEC、RC5等编码协议解析均有系统论述设计报告覆盖硬件电路与软件编程开题及中期报告展现项目推进中的问题与解决思路中英文材料则帮助理解同类国际技术背景。压缩包整体约205KB文件总数不大主要包含设计报告、程序源码、bmp电路图、JPG截图和txt说明等报告与源码对应系统设计思路电路图与截图辅助理解硬件组成目录关系清晰便于分类查阅。目前已有80人学习下载对于希望动手实现红外遥控项目或快速掌握单片机系统设计的人来说是一份可直接参考的实践资料。1. 基于单片机的红外遥控电路系统zip包里真正要交付的是硬闭环拿到“基于单片机的红外遥控电路系统”这个压缩包名字后缀里挂着一长串报告类型但实质只有一个一套用单片机完成红外信号的发射、接收、解码与执行的电路系统。红外遥控是这个项目的外壳核心科目是“怎么用定时器和中断去测量一段微秒级的脉冲宽度”——NEC协议里一个位只有560微秒靠延时函数去轮询基本无解。这篇内容适合两类人一类要把这套系统作为课程设计或毕业设计交出去另一类是工作中第一次碰到红外遥控接收想快速弄清协议和电路边界。后面的内容按“协议→硬件→程序→验证”的顺序展开每一步都给出可以直接抄的参数和代码。2. 红外遥控协议与单片机选型要解NEC先把时间参数背下来2.1 NEC协议时间参数与常见误读绝大多数消费级红外遥控器采用NEC协议或与之兼容的变体项目报告里写“红外遥控电路系统功能实现”底层就是在处理NEC的时序。NEC的一帧数据由引导码、地址码、地址反码、命令码、命令反码组成其中引导码是9ms载波脉冲加4.5ms空闲间隔数据位用0.56ms载波脉冲表示一个位但0和1靠的是脉冲后面的间隔长度区分。信号段载波脉冲宽度间隔宽度判定要点引导码9ms±1ms4.5ms两个参数同时命中才算引导逻辑00.56ms0.56ms间隔约为0.56ms逻辑10.56ms1.69ms间隔约为1.69ms重复码9ms2.25ms与引导码间隔差2.25ms常见误读有两处。第一处是把载波和脉冲混为一谈认为接收头输出的低电平就是发射端的载波本身实际上接收头内部完成解调它输出的低电平持续时间等于发射端载波脉冲宽度单位是毫秒级别而载波频率是38kHz不可见。第二处是认为解码必须等一帧结束再处理其实引导码之后的每个边沿都是有效数据边沿之间的时间差才是要测的量。2.2 单片机选型对比51、STC、STM32在红外场景的差异做这套电路系统最常见的选择是STC89C52或STM32F103系列少数用STM32CubeMX生成工程后改成其它型号。选型时看三个资源定时器数量、输入捕获能力、中断优先级管理。单片机定时器资源红外解码友好度典型问题STC89C522个16位定时器一般需要外部中断配合主频低中断里处理时间长STC8系列5个定时器、可捕获好频率可到24MHz以上需要看具体型号手册STM32F1034个定时器多通道输入捕获很好硬件CCR暂存时间戳配置稍复杂但一劳永逸我一般会推荐STM32做接收端原因是输入捕获通道的CCR寄存器在边沿到来瞬间由硬件保存计数值即使中断响应延迟几个微秒时间戳仍是准确的。51单片机用外部中断同样能测脉宽但中断里要自己清标志、读定时器、切边沿任何一个环节多花几十微秒就会把1.69ms和0.56ms的区分空间压缩掉。2.3 用定时器而非delay做脉冲计时的最小验证代码在写完整程序之前先在单片机上验证一个问题接收头输出引脚的电平宽度能不能被测准。常见做法是用外部中断读取当前定时器计数值两次边沿之间做差。下面是一段用STM32标准外设库写的极简验证代码volatile uint32_t t_start, t_width; void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) RESET) return; if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_SET) { t_start TIM2-CNT; // 上升沿记录脉冲起始时刻 } else { t_width TIM2-CNT - t_start; // 下降沿得到脉冲宽度 } EXTI_ClearITPendingBit(EXTI_Line0); } // 初始化TIM2以1MHz计数CNT每微秒加1 void Tim2_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM2-PSC 72 - 1; // 72MHz / 72 1MHz TIM2-ARR 0xFFFF; // 16位自由计数溢出后回绕 TIM2-CR1 | TIM_CR1_CEN; }这段代码的逻辑是每微秒计数一次两次边沿的CNT差值就是脉冲宽度。参数说明PSC设为71对应72分频STM32F103主频72MHz时得到1MHz的计数频率ARR填0xFFFF让计数值在0到65535之间回绕9ms引导码有约9000个计数完全装得下。如果换成12MHz的51单片机这里要改用T0的16位计数模式定时器每1us或2us溢出重装一次初值效果相近但代码更长。解码不能用delay的另一个原因在中断里delay函数依赖SysTick而红外脉冲宽度本身只有数百微秒主循环里一个串口打印或按键去抖抖动都会导致采样丢失。这个最小验证代码是后面完整解码程序的骨架先让它跑通再往上搭状态机。3. 红外遥控电路系统的硬件部分红外LED驱动、接收头与电源去耦3.1 红外发射电路三极管开关与限流电阻计算红外遥控电路系统的发射端通常是一个按键矩阵加一个红外LED核心问题是让LED在载波时通过足够大的电流、在空闲时完全截止。经典电路用NPN三极管做开关单片机IO输出38kHz PWM信号接到基极集电极串红外LED和限流电阻。发射电路参数计算以配套的5mm红外LED为例正向压降典型值1.2V到1.5V峰值电流允许100mA取50mA作为设计值当电源为5V时限流电阻R(5V-1.2V)/50mA76Ω取标称值75Ω。实际项目中为了延长遥控距离常见做法是把两个红外LED串联或并联此时电阻要重新计算。基极电阻一般选1kΩ到4.7kΩSTM32的IO输出电流有限三极管选S8050或2N2222这种主流小功率管即可放大倍数在100以上时基极电流2mA到5mA就足够让管子进入饱和区。发射端载波用定时器PWM生成最省事以下是一段直接在寄存器层面配置38kHz载波的代码// 以STM32F103在72MHz下生成38kHz载波 // TIM3 CH1输出PWM频率 72MHz/(711)/(251) ≈ 38.5kHz RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM3-PSC 71; // 预分频72MHz→1MHz TIM3-ARR 25; // 周期26us对应38.46kHz TIM3-CCR1 8; // 高电平8us占空比约31% TIM3-CCMR1 TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1; // PWM模式1 TIM3-CCER | TIM_CCER_CC1E; // 使能CH1输出 TIM3-CR1 | TIM_CR1_CEN; // 启动定时器参数说明PSC71把72MHz分频成1MHzARR25得到26微秒周期频率1/26us约38.46kHz处于接收头带宽内CCR18使载波中高电平持续8us占空比约31%这类接收头对载波占空比要求通常在25%到50%之间。这套寄存器配置在发射和接收共用同一个单片机时尤其有意思接收端用TIM2发射端用TIM3互不干扰。还要注意红外LED的波长。峰值波长850nm到940nm都算红外但接收头内部的光电二极管有敏感区间850nm和940nm混用会明显缩短距离。购买元件时如果商家标了波长就按波长买没标就整批买同一家避免发射接收波长不对齐这种低级问题出现在报告里。3.2 红外接收头选型与最小电路HS0038B和1838的手册差异接收头是集成器件型号一般以HS0038、1838、VS1838B出现它们的引脚定义和电气参数高度兼容但细节差异会影响电路设计。共用特征3个引脚VCC接5V部分型号支持3.3VGND接地OUT输出解调后的脉冲信号。参数典型值说明载波频率38kHz可接受30-40kHz发射PWM频率必须落在接收头带宽内输出低电平约0.2V空闲时输出高电平收到载波输出低输出高电平接近VCC与单片机IO电平匹配供电电流0.5-1.5mA功耗很低但动态时纹波明显接收头输出引脚要接一个10kΩ上拉电阻到VCC吗多数内部已带推挽输出不必再上拉但加上也无害。VCC脚必须对地接一个0.1μF陶瓷电容并尽量靠近元件引脚同时在电源入口再加一个100μF电解电容。这是因为接收头内部有高增益放大器电源纹波会被解调电路当成信号放大导致输出脚出现不存在的脉冲。输出引脚到单片机的走线要短如果经过排线或长导线要在单片机端再并联一个100pF到1nF的电容滤除高频分量。有人会在接收头输出端串一个1kΩ电阻再接单片机我一般不建议这么做因为电阻和单片机IO的寄生电容组成RC低通会让上升沿变缓边沿位置偏移几十微秒后解码阈值就不好设置了。直接引脚对引脚连接最可靠。3.3 电源与布局电路系统噪声如何干扰解码整块“单片机的红外遥控电路系统”如果用一个5V电源同时给单片机、红外接收头、继电器或数码管供电解码成功率会随着负载切换明显波动。常见做法是接收头电源从单片机电源节点单独引出不要在数码管段驱动或继电器驱动走线上取电。电源线走线宽度不小于1mm地线用铺铜或短线连接接收头地线单独回到电源地避免与继电器地共享长走线。如果系统里有继电器、电机这类负载要额外加续流二极管和独立滤波否则每次关断瞬间的地弹会把接收头输出打出一串毛刺。验证方法很简单用手接近接收头观察输出有没有误动作或者用逻辑分析仪抓空闲时的输出电平正常的空闲波形是一条稳定高电平线有毛刺就说明电源或布局需要处理。4. 程序实现输入捕获解码、38kHz PWM发射与状态机联动4.1 定时器输入捕获配置与脉宽精度的关系红外遥控系统的接收程序分三层底层定时器负责测量脉冲宽度中层解析NEC帧结构上层把命令映射到外设动作。底层代码的关键是输入捕获极性切换TIM的输入捕获通道在捕获事件发生后CCR寄存器自动保存当前CNT的值中断里只要读CCR做差不需要在中断响应的瞬间读CNT这就是它能扛住高精度时序的根本原因。TIM_HandleTypeDef htim2; void MX_TIM2_Init(void) { htim2.Instance TIM2; htim2.Init.Prescaler 72 - 1; // 计数器频率 72MHz/72 1MHz htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0xFFFF; // 16位自由运行 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_IC_Init(htim2); TIM_IC_InitTypeDef ic; ic.ICPolarity TIM_INPUTCHANNELPOLARITY_RISING; // 先捕获上升沿 ic.ICSelection TIM_IC_SELECTION_DIRECTTI; ic.ICPrescaler TIM_ICPSC_DIV1; ic.ICFilter 0x0D; // 滤波去掉窄毛刺 HAL_TIM_IC_ConfigChannel(htim2, ic, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1); }这段配置里最值得调的两个参数是Prescaler和ICFilter。Prescaler决定每个计数代表多少微秒72-1时1MHz计数1个计数1微秒NEC协议的最小单位560μs换算成560个计数阈值设置直观。ICFilter取0x0D表示按系统时钟采样并做8次连续采样确认电平可以抑制接收头输出上的窄毛刺但滤波会带来约几十纳秒的延迟对三位数微秒的脉宽测量可忽略。如果系统时钟不是72MHzPrescaler要一起调整比如STM32G0跑64MHz就改成64-1。提示ICFilter不是越大越好滤波级数过高会让窄于570us的脉冲被滤掉0x0D附近是比较平衡的选择。对应的捕获中断回调里每来一个边沿就把脉宽存入一个环形数组不在中断里做协议解析这是后面所有逻辑能够稳定的前提volatile uint16_t pulse_buf[128]; volatile uint16_t pulse_cnt 0; volatile uint32_t last_edge; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance ! TIM2 || htim-Channel ! HAL_TIM_ACTIVE_CHANNEL_1) return; uint32_t now __HAL_TIM_GET_COUNTER(htim); uint16_t width (uint16_t)(now - last_edge); if (pulse_cnt 128) { pulse_buf[pulse_cnt] width; // 只存宽度不处理协议 } else { pulse_cnt 0; // 缓冲溢出重新开始一帧 } last_edge now; // 翻转捕获极性上升沿之后等待下降沿下降沿之后等待上升沿 if (__HAL_TIM_GET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1) TIM_INPUTCHANNELPOLARITY_RISING) { __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); } else { __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); } }注意捕捉的是相邻两次边沿的间隔即脉宽或间隔交替出现。接收头空闲为高、收到载波为低所以pulse_buf偶数下标是载波脉冲宽度奇数下标是空闲间隔。极性切换如果放在主循环里处理会丢下一次捕获必须在中断回调中立刻翻转。4.2 NEC解码逐位解析0/1阈值、帧校验与重复码有了脉宽数组解码就是查表判阈值。NEC一帧正常有66个宽度值引导码占2个四个数据字节共32位每位有脉宽和间隔两个宽度共64个。解析顺序是偶数下标当脉冲、奇数下标当间隔先验引导码再逐位拼接。// 接收头反相脉冲载波段间隔空闲段 // 0: 0.56ms脉冲0.56ms间隔共约1120计数 // 1: 0.56ms脉冲1.69ms间隔共约2250计数 // 判定位时只使用“间隔”长度脉冲宽度都应接近560us uint8_t nec_parse_bit(uint16_t gap) { if (gap 1400) return 1; // 1.69ms间隔阈值取(5601690)/2约1125取1400更稳 return 0; }这里有个工程取巧点0和1的间隔分别是560us和1690us中间阈值可以取1100us左右但考虑到接收头解调存在50us的抖动以及RC电路可能带来的边沿偏移我一般把阈值提高到1400us宁可把窄的1误判成宽的0也不把0拉成1。误判方向要跟实际功能测试的结果配合调整实测发现按键偶尔双击就说明阈值太靠近0.56ms一侧。解析函数先检查引导码再按位拼接数据最后校验三件事地址和地址反码取反一致、命令和命令反码取反一致、数据位个数够。这个校验逻辑对应报告里常写的“可靠性设计”部分。重复码处理要单独判断引导码后间隔是2.25ms而不是4.5ms此时不更新按键数据只把上一次命令的执行状态再触发一次对应遥控器上长按音量键的场景。4.3 红外发射编码用PWM输出38kHz载波并按NEC时序发码发射端和接收端是同一套时序的两个方向。发射端把逻辑0、逻辑1、引导码分别映射为“载波脉冲空闲间隔”载波由硬件PWM产生空闲时关闭PWM即可。用CCER的CC1E位控制输出置1时LED发载波清0时LED灭。#define PWM_ON() TIM3-CCER | TIM_CCER_CC1E #define PWM_OFF() TIM3-CCER ~TIM_CCER_CC1E void nec_send_byte(uint8_t data) { for (int i 7; i 0; i--) { PWM_ON(); delay_us(560); PWM_OFF(); if (data (1 i)) delay_us(1690); // 逻辑1的间隔 else delay_us(560); // 逻辑0的间隔 } } void nec_send_frame(uint8_t addr, uint8_t cmd) { PWM_ON(); delay_us(9000); PWM_OFF(); delay_us(4500); // 引导码 nec_send_byte(addr); nec_send_byte(~addr); nec_send_byte(cmd); nec_send_byte(~cmd); }delay_us必须用定时器或SysTick实现不能单纯靠空循环。以STM32F103的72MHz主频为例一个空循环周期大约6到12个机器周期不同编译优化级别下循环体耗时完全不同用空循环写出来的发射时序在-O2和-O0之间可能差出上百微秒。接收端本机自收自测时如果自己的发射信号自己解不出来优先检查的就是这个delay_us。发射端的PWM频率和占空比同样重要。载波频率偏移到40kHz以上时接收头增益下降遥控距离从10米掉到3米以下。STM32的定时器重装值只有整数72MHz下38kHz对应周期26.3us取ARR25得到38.46kHz偏差约1.2%在接收头带宽内。4.4 主循环状态机把遥控码映射成外设动作解码完成后主循环要做的是状态机转移而不是继续处理定时器数据。一个简单可靠的写法是空标志位法解码中断把结果放入全局变量置位ready标志主循环检测到标志后读取并清标志。volatile NEC_Frame g_frame; volatile uint8_t g_ready 0; volatile uint8_t g_repeat 0; while (1) { if (g_ready) { g_ready 0; if (g_repeat) { volume_step(g_frame.cmd); // 重复码继续执行上次命令 } else if (g_frame.addr 0x00 g_frame.addr_inv 0xFF) { switch (g_frame.cmd) { case 0x45: relay_on(); break; case 0x46: relay_off(); break; default: led_toggle(g_frame.cmd 0x0F); } } } // 按键扫描、数码管刷新等低优先级任务放这里 }这里要注意状态划分的颗粒度。不要在主循环里做脉宽解析也不要在中断里执行延时类的动作中层的解码函数和上层的执行动作通过全局变量耦合虽然简单但足够应对课程设计和多数产品原型。如果后续要扩展多通道遥控或加入工作模式切换把状态机改成显式的枚举状态枚举值对应空闲、接收中、帧校验、命令执行四个阶段逻辑会更清晰一点。5. 逻辑分析仪与串口回显红外遥控系统的验证和报告数据来源5.1 用逻辑分析仪抓接收头输出波形调试红外遥控电路系统时第一件事不是看解码结果而是看波形。普通示波器不方便长时采集微秒级脉冲序列一个8通道逻辑分析仪更顺手采样率设2MHz以上探针接接收头OUT引脚抓一次按键动作波形上应该看到一组9ms低电平开头、随后跟着几十个交替脉冲的序列。把这组波形在软件里用“解析NEC信号”功能展开能看到每个脉冲的宽度列表对比2.1的参数表就能定位问题出在发射端还是接收端。没有逻辑分析仪时也可以用示波器的单次触发模式但触发点设置较麻烦。5.2 串口打印脉宽把时间参数数字化逻辑分析仪查看形串口输出查看数值。在解码中断里把每个width值用串口DMA发送到上位机波特率115200足够承载每帧约70个宽度值。观察规律正常情况下的脉宽集中在560、1690、4500、9000附近标准差在几十微秒内。如果看到一串漂移明显的数值比如逐个增加或减少多是因为定时器计数频率设置错误前后边沿宽度呈线性漂移这时检查Prescaler和时钟树配置。// 伪代码捕获中断里打印时间戳和脉宽 printf(%u,%u\r\n, (unsigned)tick_ms, width); // 时间戳脉宽这段串口输出同时也是报告里“测试数据”章节的来源把50次按键的脉宽采样统计成表格列出最小、最大、平均值比写“系统运行正常”有说服力得多。5.3 报告中的数据表与波形图写作要点设计报告和开题中期报告需要的数据都能从上述调试过程获得。开题报告重点写方案选型对比把2.2的选型表和NEC时间参数表放上去中期报告写硬件调试进度放一张逻辑分析仪波形截图和串口打印截图最终报告写完整实现数据表必须包含每一类脉宽的设计值、实测值、误差率。中英文报告的摘要部分把“NEC协议”“输入捕获”“38kHz载波”作为关键词对应翻译注意“输入捕获”译作input capture而不是interrupt capture这是外审常见扣分点。程序里的注释保持英文缩写风格整个压缩包里的代码与报告在术语上就是同一套语言。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进