基于STM32的红外遥控学习系统:从NEC协议到万能遥控器实现
2026/9/9 13:26:27 网站建设 项目流程

简介:基于STM32的红外遥控学习系统是一份面向嵌入式初学者的实战源码包,内容涵盖STM32框架功能拆解、模块调用方式以及红外遥控应用开发要点。资源共包含986个文件,其中C源码与H头文件占大多数,辅以启动汇编文件、工程配置文件(如ewp/uvproj)、链接脚本及调试描述文件,便于直接打开工程查看代码与运行逻辑;压缩包整体约21.16MB,目录结构经过整理,适合快速定位到红外解码、定时器配置、USART等关键模块。已有46人学习下载,内容质量在小范围内获得认可。对于想要掌握STM32基础外设驱动和红外协议解析的开发者,这套经测试可运行的工程源码能提供完整的编写思路、功能模块注释和排错参考,有助于从理论学习平滑过渡到实际项目开发。 玩嵌入式这么多年,被各种奇奇怪怪的需求逼着做了不少项目,但最常被身边朋友问起的,反而是这个看起来不起眼的“基于STM32的红外遥控学习系统”。说直白点,这东西就是把别人家遥控器发出的红外按键码“学”过来,存在单片机里,按下按键再“发”出去,一台STM32就能做成一个万能遥控器。这个项目我前后做了三个版本,从最早只能学NEC协议,到后来把脉宽序列全采集,踩了不少坑,也积累了不少可以直接抄作业的经验。

这篇内容我尽量写全,从协议原理、硬件选型到代码实现、调试排错都过一遍。如果你是正在找毕业设计题目的学生,或者想给家里老空调做个智能联动的折腾党,又或者单纯想把红外遥控这块彻底搞明白,这篇都可以直接拿来当参考。

1. 项目概述:为什么做红外遥控学习系统

1.1 这个系统能做什么,适合谁

红外遥控学习系统的核心价值,就一句话:把任意红外遥控器的信号“复刻”一份。常见的应用场景有三个:

  • 家里的空调、电视、机顶盒遥控器丢了一个,想用另一台替代或者合并到一个遥控器上。
  • 想在单片机上做一个智能控制面板,用按键或者语音控制家里的红外设备。
  • 毕设需要体现“采集—存储—重发”的完整数据链路,红外遥控恰好是一个低成本、易验证的好方向。

实测下来,这套系统用最便宜的STM32F103C8T6就能跑起来,成本控制在20元以内,学习并复现市面上绝大多数NEC协议遥控器完全没问题。如果加上脉宽序列采集功能,兼容性还能扩展到更多杂牌遥控器。

1.2 方案对比:为什么选STM32做主控

做这个项目,主控可以是51、Arduino、ESP8266、STM32,我最终选STM32有三个硬理由:

  • 输入捕获是标配。红外学习最核心的操作是测量高低电平脉宽,STM32的定时器输入捕获可以精确到0.5微秒级别,51单片机靠主循环轮询根本达不到这个精度。
  • 发射时序可控。红外发射需要微秒级延时配合38kHz载波,STM32用PWM外设加定时器中断,不占用CPU主循环。
  • 存储量充裕。STM32的片上Flash有几十到几百KB,存上千条遥控指令都够用,不需要额外挂存储芯片。

当然,如果你只是玩一玩,Arduino的IRremote库写起来更快。但那个库封装得太狠,很多原理会被掩盖,出了问题不好排查。用STM32裸写一遍,协议栈和中断逻辑都会理解得透彻很多,以后再去做别的通信协议(比如433MHz射频、BLE)也更有底。

2. 原理先行:NEC协议与红外学习机制

2.1 NEC协议帧结构拆解

做红外遥控,绕不开NEC协议。市面上松下、索尼、飞利浦用得多的有各自协议,但国内最常见的还是NEC。NEC协议用38kHz载波调制,数据帧长这样:

  • 引导码:9ms高电平 + 4.5ms低电平,相当于一帧数据的“起始标志”。
  • 地址码:8位,一般是遥控器的设备地址,比如0x00是电视、0x04是空调之类,但具体看厂商定义。
  • 地址反码:地址码取反,用于校验。
  • 命令码:8位,对应按下的按键。
  • 命令反码:命令码取反,用于校验。

数据位的时间定义也很规整:

  • 逻辑0:560微秒高电平 + 560微秒低电平,总时长1.12ms。
  • 逻辑1:560微秒高电平 + 1690微秒低电平,总时长2.25ms。
  • 重复码:如果一直按住按键,遥控器会每隔110ms发一次9ms高电平 + 2.25ms低电平 + 560微秒高电平的重复帧。

学习系统要做的,就是把这些高低电平时间测出来,解析成地址和命令,存起来,发射时再按时序还原。

2.2 学习的核心:脉宽测量与信号还原

红外学习的本质,就是把接收头输出的TTL电平时间序列完整测下来。接收头(比如VS1838B)内部集成了光电二极管、放大器和解调电路,输出正好是反向的电平序列:没信号时为高,收到38kHz载波时为低。

所以用STM32的定时器输入捕获,在每次上升沿和下降沿都记录计数器的值,差值就是当前电平持续的时间。9ms引导码在72MHz主频下对应648000个计数,这个值已经超过了16位定时器的65535上限,所以要么用32位定时器(TIM2/TIM5),要么记录定时器溢出次数来扩展范围。我早期的版本在这块踩过坑,后面细说。

2.3 三种学习方案的设计取舍

做学习功能,设计上有三个层次,难易程度和兼容性成正比:

方案实现难度兼容性存储开销说明
仅NEC解码仅NEC协议每键约4字节直接解析出地址码和命令码,简单高效
脉宽序列采集所有脉冲式红外协议每键约100-200字节不解析协议,只存电平时间序列,通用性强
波形采样+频率识别所有协议含载波频率识别很大连38kHz以外的载波都能学,几乎万能

我建议新手先做方案一,把NEC协议跑通,理解整个流程。等这部分稳定了,再向方案二扩展。这个项目打包的内容也是按这个思路组织的——先有NEC解码的学习和发射模块,再留了脉宽采集的通用接口,方便二次开发。

3. 硬件搭建:接收电路、发射电路与存储选型

3.1 器件清单与选型建议

这个项目所需的硬件非常少,焊接难度也不高:

  • 主控:STM32F103C8T6最小系统板,淘宝十几块钱,Flash 64KB,SRAM 20KB,够用。
  • 红外接收头:VS1838B或者1838T,工作频率38kHz,三个引脚(VCC、GND、OUT),注意OUT输出的极性。
  • 红外发射管:5mm或者3mm的940nm红外LED,正向压降约1.2V到1.5V。
  • 三极管:S8050,用于驱动发射管,增加发射距离。
  • 电阻:1k、4.7k、10欧、33欧各准备几只。
  • 按键:四个轻触开关,用来模拟遥控器的四个键位。

供电方面,STM32用USB的5V经过板载稳压到3.3V即可。红外接收头可以直接用3.3V供电,发射管也建议用3.3V系统电压驱动,不要直接用5V,否则长时间工作会发热。

3.2 发射管驱动电路:为什么不能直接接GPIO

很多新手直接把发射管串个电阻接到GPIO上,结果发现遥控距离只有几十厘米,这其实是驱动电流不够。STM32的GPIO最大输出电流约20mA,而红外发射管想达到可靠控制距离,峰值电流最好到50mA甚至100mA。

我的做法是用S8050三极管做开关:

3.3V -> 红外LED正极 红外LED负极 -> 三极管集电极 三极管发射极 -> GND GPIO -> 1k电阻 -> 三极管基极

这样GPIO只需要提供约3mA的基极电流,三极管就能把集电极电流放大到100mA级别。实测用这个电路,发射距离可以做到8米以上,不加三极管直接接GPIO只有2米左右。需要注意的是,如果集电极电流过大,LED前要串联一个10到33欧的限流电阻,具体阻值根据发射管的额定电流算,一般取20欧左右比较安全。

3.3 存储介质选型:Flash与EEPROM到底用哪个

学习到的遥控码要断电不丢,存储介质是绕不开的一环。我试过三种方案:

  • 单片机内部Flash:STM32F103C8T6有64KB,存指令码绰绰有余,缺点是写之前要先擦除,按页(1KB)操作,不适合频繁改写。
  • 外部I2C EEPROM(比如AT24C02):按字节写,方便灵活,但需要多买一个芯片,布线也麻烦一点。
  • 外部SPI Flash(比如W25Q16):容量大速度快,但项目没必要搞这么复杂。

对大多数遥控学习场景,就用内部Flash。每条NEC指令整理成结构体也就16字节左右,一个按键一个地址,64KB足以存几千条。唯一要注意的是,STM32的Flash写入寿命大约1万次,正常使用完全够,但如果要反复调试写入代码,建议加一个“只在数据变化时才写入”的判断,减少不必要擦写。

外接晶振方面,常用8MHz无源晶振配两个20pF左右的负载电容,公式是C = 2 * CL - Cstray,CL是晶振规格书上标的负载电容,Cstray一般取3到5pF,算出来取整就近选标称容值。我试过省略这两个电容,板子也能跑,但串口波特率偶尔会偏,所以最好还是按规范来。

4. 代码实现:从输入捕获到红外发射

4.1 定时器输入捕获:测量脉宽的配置要点

我用的主控是STM32F103C8T6,标准外设库。第一步是配置定时器输入捕获。这里选了TIM2的通道1,对应PA0引脚,因为TIM2是32位定时器,不用担心大脉宽溢出。

void TIM2_Capture_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_ICInitTypeDef TIM_ICInitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0F; TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); }

这里有个关键细节:直接把上升沿捕获做完,两次相邻上升沿之间的差值,其实包含了“高电平+低电平”的完整周期。对NEC协议来说,相邻上升沿间隔是离散的几个值——逻辑0的完整周期1.12ms、逻辑1的完整周期2.25ms、引导码的低电平4.5ms。用这种方法虽然取巧,但已经能区分引导码、0和1,是初学者最容易上手的实现方式。

如果想更严谨地测量每个高电平和低电平的实际宽度,需要在中断里动态翻转捕获极性,代码复杂度会上升一个等级。建议先把“上升沿间隔法”调通,理解数据流之后再去改极性翻转。

中断服务函数这样写:

volatile uint32_t tim2_last_count = 0; volatile uint32_t tim2_capture_buf[256]; volatile uint8_t tim2_capture_index = 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); uint32_t now = TIM_GetCapture1(TIM2); uint32_t diff = now - tim2_last_count; tim2_capture_buf[tim2_capture_index++] = diff; tim2_last_count = now; } }

这段代码逻辑很朴素:每次捕获中断,把当前计数器值减去上次的值,就是这段时间间隔。唯一要提醒的是,如果连续捕获间隔超过定时器周期,需要额外处理溢出中断,好在32位定时器在72MHz下可以跑59秒才溢出一次,NEC一帧最长不过70ms,完全够用。

4.2 NEC帧解析与按键码存储

采集到脉宽序列之后,解析NEC帧其实是查表判断的过程。我把相邻上升沿间隔做归一化处理,用2000微秒(2ms)作为0和1的分界点,大于2ms的当作分隔符或者引导码:

#define NEC_LONG_DELAY 4500 // 引导码低电平时间,实际测约4.5ms #define NEC_LOGIC_0 1120 // 逻辑0完整周期约1.12ms #define NEC_LOGIC_1 2250 // 逻辑1完整周期约2.25ms uint8_t NEC_Decode(uint32_t *buf, uint8_t len, uint8_t *addr, uint8_t *cmd) { // 首先判断引导码,第一个间隔应该约13.5ms(9ms高 + 4.5ms低) if (buf[0] < 10000 || buf[0] > 16000) return 0; // 连续4组,每组8位,共32位数据 uint32_t data = 0; for (int i = 1; i < 33; i++) { if (buf[i] > 2000) data = (data << 1) | 1; else if (buf[i] > 900) data = (data << 1) | 0; else return 0; } *addr = (data >> 24) & 0xFF; *cmd = (data >> 8) & 0xFF; // 校验反码 if ((*addr ^ ((data >> 16) & 0xFF)) != 0xFF) return 0; if ((*cmd ^ (data & 0xFF)) != 0xFF) return 0; return 1; }

由于NEC协议加了地址反码和命令反码校验,解析相当可靠。我开发时遇到过一个问题:有些新出的空调遥控器虽然号称NEC协议,但地址码是16位的,占用了命令反码的位置,这类就需要特殊判断,不能一棍子打死判为无效。

存储方面,我把结构体定义成固定长度16字节:协议标志、地址码、命令码、按键索引、时间戳、校验值,然后写到Flash的最后一页,避免和程序代码冲突。写Flash前先备份本页数据,擦除,再写回,这步逻辑虽然繁琐但必须做,否则容易把程序区擦掉。

4.3 发射环节:PWM载波与精确延时的配合

发射比接收好理解,就是把采集到的电平宽度重新还原出来。38kHz载波我用TIM3的PWM输出,占空比设成1/3,这样兼顾发射功率和接收头的解调灵敏度。

void IR_Init_Send(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 1894; // 72MHz / (1894+1) ≈ 38kHz TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 630; // 占空比约1/3 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_CtrlPWMOutputs(TIM3, ENABLE); TIM_Cmd(TIM3, DISABLE); // 先关,发射时才打开 }

发射的时候,载波开启、延时、关闭、延时,反复循环就还原出了红外波形。有一个坑必须提醒:延时函数不能在发射过程中被中断打断,否则时间会偏差,接收端解调失败。我在发射时直接关掉全局中断,发射完再开,实测这样最稳妥。

void IR_SendBit(uint8_t bit) { if (bit) { TIM_Cmd(TIM3, ENABLE); Delay_Us(560); TIM_Cmd(TIM3, DISABLE); Delay_Us(1690); } else { TIM_Cmd(TIM3, ENABLE); Delay_Us(560); TIM_Cmd(TIM3, DISABLE); Delay_Us(560); } }

4.4 完整逻辑流程串讲

整体工作流程是:

  1. 上电初始化,从Flash读取已学过的遥控码表。
  2. 通过按键选择“学习模式”或者“发射模式”。
  3. 学习模式下,等待接收头捕获一帧数据,解析出地址和命令,存入内存并写Flash,用LED闪烁提示成功。
  4. 发射模式下,根据按键索引从码表取出地址和命令,按NEC时序驱动PWM载波发射。
  5. 发射完成,LED熄灭,等待下一次按键。

这套流程我用一个状态机实现,把按键扫描、红外接收、发射逻辑拆成独立模块,方便以后扩展成串口控制或者其他触发方式。

5. 调试实录:常见问题与排查思路

5.1 连接不上目标板与下载失败

这个项目调试中第一道坎,往往不是代码,而是下载器连不上芯片。很多人在Keil里点下载,弹出一堆红字,最常见的是:

error: no stm32 target found!

遇到这个提示,先别急着怀疑代码,从硬件排查:

  • 确认STM32的BOOT0引脚状态。BOOT0拉高会进入ISP模式,这时候SWD接口是不可用的。正常调试要接GND。
  • 确认VDD和GND有没有接反,STM32最小的供电回路都通不过,SWD自然连不上。
  • 如果用的是ST-Link,检查ST-Link上SWDIO、SWCLK、GND三根线是不是接到了芯片对应引脚,RST线不是必须的,但接到NRST能提高连接成功率。
  • 如果之前的程序把SWD引脚复用成了普通GPIO(比如用了PA13、PA14),会导致下载器连接失败。解决办法是按住板子的复位键,在Keil里点下载,同时松开复位,利用复位瞬间短暂的空窗期擦除Flash。STM32的Flash loader或者ST-Link Utility也可以做整片擦除。

我用ST-Link Utility擦过很多次“锁死”的板子,可以说这是嵌入式调试的基本操作。

5.2 接收不到信号、学习失败、发射无效三大高频问题

学习模式下接收不到信号是出现频率最高的问题。接收头的三个引脚是VCC、GND、OUT,但不同厂家脚位定义不完全一样,我见过最混乱的一次是买来的接收头丝印和文档对不上,换了方向才好。排查顺序是:

  • 用示波器或者万用表测接收头OUT引脚,环境安静时应该稳定在3.3V高电平,按下遥控器时能测到波形翻转。
  • 确认接收头工作电压,VS1838B支持2.7V到5.5V,但有些国产接收头高电压下解调灵敏度反而变差,3.3V下表现更好。
  • 遥控器对着接收头按,距离不要超过30厘米。接收头有角度限制,最好正对。我调试时经常把遥控器和接收头放在同一平面,这样接收效果最好。
  • 如果接收头OUT引脚的波形正常,但定时器捕获不到中断,检查GPIO配置和定时器时钟有没有打开,这是寄存器级问题,需要对着参考手册核对。

发射无效的排查思路反过来:先确认PWM波形有没有出来,最简单的办法是用接收头对着发射管,在发射时看有没有解调后的波形。如果PWM有波形但发射距离近,大概率是三极管驱动电路问题,检查基极电阻、集电极限流电阻的阻值是否计算正确。还有一个容易忽略的细节:发射管的正负极方向,红外LED管脚长的那端是阳极,短的接集电极,接反了完全不亮。

学习失败还有一种情况是信号格式本身不标准。部分遥控器会在引导码后面加一段空闲时间,或者命令码不是标准的NEC排列,导致解码校验失败。应对方案是在解析代码里放宽容错,把严格校验改成“地址码匹配优先、命令码取反不强制校验”,至少能学个七七八八。

5.3 还有几个值得注意的细节

红外项目有个容易被人忽略的问题:环境光干扰。阳光强烈的室内、靠近白炽灯的地方,接收头可能会被环境光中的红外成分干扰,导致误触发。解决思路有三层:

  • 接收头选择带内置滤波的型号(比如VS1838B本身对可见光不敏感),大部分情况下够用。
  • 软件上增加信号有效性判断,比如判断引导码宽度是否符合预期,不符合就丢弃,不要进入解析流程。
  • 在接收头前面加一层深色滤光片,或者把整个接收部分封闭在遮光外壳里,只留一个透红外的小窗口。这个对工业环境特别有效。

还有一个和开发环境有关的建议:如果你用的是Keil MDK,记得安装对应芯片的器件支持包,否则新建工程时根本找不到STM32F103C8T6这个型号。网上有些人把C51和MDK装在同一台电脑里,安装顺序错了可能导致编译环境冲突,我踩过这个坑。另外,老版本的MDK对Win10以上系统有兼容问题,安装最新版ARM Compiler 5的兼容包能避免一堆莫名其妙的编译报错。

最后说一个实用的扩展方向:学习系统采集到的脉宽数据,不仅可以直接发射,还可以通过串口上传到上位机显示波形,用来排查协议问题。我在调试时就是靠这个功能,把一个杂牌遥控器的非标时序成功反推出来,后来还顺手做了一个手机蓝牙控制的版本,在STM32上挂一个蓝牙模块,用手机App点按钮就能控制红外设备。如果你把基础版做完还不尽兴,往这个方向扩展,可玩性会高很多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询