
简介基于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位对应按下的按键。命令反码命令码取反用于校验。数据位的时间定义也很规整逻辑0560微秒高电平 560微秒低电平总时长1.12ms。逻辑1560微秒高电平 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 64KBSRAM 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到底用哪个学习到的遥控码要断电不丢存储介质是绕不开的一环。我试过三种方案单片机内部FlashSTM32F103C8T6有64KB存指令码绰绰有余缺点是写之前要先擦除按页1KB操作不适合频繁改写。外部I2C EEPROM比如AT24C02按字节写方便灵活但需要多买一个芯片布线也麻烦一点。外部SPI Flash比如W25Q16容量大速度快但项目没必要搞这么复杂。对大多数遥控学习场景就用内部Flash。每条NEC指令整理成结构体也就16字节左右一个按键一个地址64KB足以存几千条。唯一要注意的是STM32的Flash写入寿命大约1万次正常使用完全够但如果要反复调试写入代码建议加一个“只在数据变化时才写入”的判断减少不必要擦写。外接晶振方面常用8MHz无源晶振配两个20pF左右的负载电容公式是C 2 * CL - CstrayCL是晶振规格书上标的负载电容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.5ms9ms高 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 / (18941) ≈ 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 完整逻辑流程串讲整体工作流程是上电初始化从Flash读取已学过的遥控码表。通过按键选择“学习模式”或者“发射模式”。学习模式下等待接收头捕获一帧数据解析出地址和命令存入内存并写Flash用LED闪烁提示成功。发射模式下根据按键索引从码表取出地址和命令按NEC时序驱动PWM载波发射。发射完成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点按钮就能控制红外设备。如果你把基础版做完还不尽兴往这个方向扩展可玩性会高很多。本文还有配套的精品资源点击获取