尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

STM32定时器输入捕获实现433MHz遥控器解码实战

发布时间:2026/9/29 2:35:18

资讯中心
01
ARTICLE

STM32定时器输入捕获实现433MHz遥控器解码实战

STM32定时器输入捕获实现433MHz遥控器解码实战
平时搞嵌入式最常遇到的一类需求就是跟无线遥控器打交道。车库门、卷帘门、灯光遥控、报警器、智能家居里面大量使用的433MHz遥控器说贵不贵但想把它跟自己的单片机项目对接起来就会发现市面上现成的解码模块要么太贵、要么不灵活。网上搜433MHz解码能出来一堆帖子但大多是让你拿示波器去看波形或者给一段看不明白的汇编看完还是一头雾水。其实STM32自带的定时器输入捕获功能就能干干净净地把这件事搞定而且代码量不大逻辑盘清楚之后半小时就能跑通。这篇文章把我自己的完整做法、代码和踩过的坑都写出来给正在做相关项目或者准备拿这个做毕设的朋友一个可以直接抄作业的参考。先说结论这套方案用STM32F103的定时器输入捕获通道把433MHz接收模块输出的脉冲宽度逐边沿测量出来再按编码规则还原成地址位和数据位。实测不管是EV1527还是PT2262编码的遥控器都能稳定解析。整个过程不依赖外部中断、不用示波器盲调逻辑清晰、CPU占用也很低非常适合刚学完定时器想找个实战项目练手的朋友。1. 项目概述与方案选型1.1 这个项目到底在解决什么问题先说清楚需求。433MHz遥控器其实就是一个无线发射器按下按键之后它内部的编码芯片会生成一串数字信号再通过射频电路调制到433MHz载波上发出去。接收端用一块超外差接收模块把载波解调输出端得到的就是这串数字信号的高低电平变化。我们要做的就是把这串电平变化还原成这个遥控器的地址是什么、按下的是哪个按键。为什么这件事值得自己做一是市面上成品的解码模块通常只输出固定格式的串口数据灵活性差换了遥控器方案可能就废了二是自己解码能直接拿到最底层的数据方便和三极管、继电器、无线门锁、LED联动这些硬件无缝对接。而且距离也不愁接收模块加根导线天线开阔地测个三五十米很正常。我自己当时做这个项目起因是车库门口装的无线遥控开关原装的遥控器丢了新的又不好配。后来一查控制器就是用433MHz接收模块加单片机的方案那索性自己写一套解码逻辑找两个旧遥控器直接做一个万能遥控中继。这个项目做完之后同样的代码改改还能用来做无线门禁、智能灯控、远程报警器一次投入多次复用。1.2 为什么选定时器输入捕获而不是外部中断新手拿到这个需求第一反应往往是我用外部中断检测到电平跳变的时候去读一下定时器的值不也能算出脉宽吗理论上确实可以但实际操作中问题不少。外部中断方案里每次跳变都要进中断服务函数然后自己去读CNT寄存器、算差值、清零这些操作本身就要消耗十几个甚至几十个CPU周期。如果这时候恰好有别的更高优先级中断在执行或者你正好在主循环里跑一段耗时操作边沿就可能被漏掉脉宽就算错了而且这种错误非常难查。定时器输入捕获的思路完全不同。它是在硬件层面做的自动记录只要把定时器的某个通道配置成输入捕获模式信号边沿一到来硬件就会自动把当前CNT计数器的值锁存到CCR寄存器里然后才触发中断。也就是说即使CPU正在忙别的这个时刻的计数时间戳也不会丢。你进中断的时候只需要做减法算脉宽、判断是0还是1、然后把数据组装起来干净利落。用生活里的例子比喻一下外部中断方案就像前台管理员每次有人进门都得亲自跑出去登记输入捕获方案则是在门口装了个自动闸机人来的时候闸机自动记录通行时间管理员只需要隔一会儿去翻一下记录本就行。效率完全不是一个级别。另外还有一个隐藏好处输入捕获模式天然就是一个脉宽测量仪。你不但能用它解码433MHz信号平时想测个频率、测个PWM占空比、或者分析电机编码器输出的脉冲只要改几行代码就能复用。这也是为什么我强烈建议刚入门的朋友把输入捕获学透它属于那种一次学会处处能用的基础技能。2. 433MHz遥控信号格式与捕获原理2.1 遥控器编码的底层格式想要解码第一步是知道对方发过来的数据长什么样。市面上最常见的433MHz遥控器超九成用的是EV1527或者PT2262这类编码芯片。这类芯片虽然品牌不一样编码思路却高度相似发送的一帧数据由同步头SYNC、数据位、结束间隔三部分组成。以EV1527为例它发送的一帧数据是20位其中前8位是地址位后12位是数据位。地址位的作用是区分不同的遥控器同一个品牌同一批生产的遥控器地址一般相同数据位则代表按键信息哪个按键按下对应的数据位组合就不一样。PT2262略有区别它是24位数据4位是地址位/数据位组合具体要用的时候可以查芯片手册但解析思路完全一样。关键是怎么表示0和1。这类芯片用的不是普通的高电平表示1低电平表示0而是脉冲宽度编码。在EV1527里短脉冲表示一个逻辑长脉冲表示另一个逻辑具体规则我直接放到下面的对比表里编码芯片同步头特征数据0表示数据1表示一帧完整数据EV1527低电平约9.5ms高电平约4ms短高脉冲约320us 短低脉冲约320us长高脉冲约640us 短低脉冲约320us8位地址 12位数据 结束间隔PT2262低电平约10ms高电平约2ms短脉冲振荡周期长脉冲振荡周期对应占空比变化4位地址 6位数据 结束间隔从接收模块的输出端看正常情况下没有信号时输出是高电平有的模块是低电平需看具体型号一旦有信号引脚上就会出现一连串宽度各异的高低电平翻转。我们的目标就变成了把这串宽度各异的脉冲测量出来宽度对应什么逻辑再把逻辑拼成完整的地址和数据。这里有个特别容易忽略的点同步头是整个解码流程的关键锚点。因为一帧数据可能有多个0或者多个1你不可能从中间开始辨认必须先把同步头找出来从同步头之后开始一位一位数。所以代码里的状态机一定要以检测到同步头作为接收数据的起始条件。2.2 定时器输入捕获的工作机制STM32定时器输入捕获的原理说白了就一句话计数器在后台不停递增你的输入信号出现上升沿或下降沿时硬件会把当前计数值咔一下抓拍保存到CCR寄存器里同时触发一次中断。这个抓拍是硬件完成的所以精确度和实时性都非常好。我们需要做的是在中断服务函数里用这次抓拍到的值减去上一次抓拍到的值得到一个两者之间隔了多少个计数周期。再乘以计数器的时钟周期就是脉冲宽度。比如定时器经过预分频后是1MHz那计数器每递增一次就是1微秒两次抓拍值相差1000意味着这个脉冲持续了1000微秒也就是1毫秒。具体到ST32的定时器通道配置有几种常见做法。一种是用两个通道一个配成上升沿捕获一个配成下降沿捕获同时测量高电平和低电平的宽度另一种是一个通道内部不断切换捕获边沿每次中断后翻转一下触发边沿用同一个CCR寄存器连续测量相邻沿的间隔。我用的是第二种代码量更小而且对433MHz这种归零码来说完全够用。支持输入捕获的定时器很多TIM2、TIM3、TIM4、TIM5这些通用定时器都可以。实际选择时主要看两点一是通道对应的GPIO引脚是否和你接收模块连接的位置冲突二是定时器的位数F103上TIM2和TIM5是32位计数器TIM3和TIM4是16位。对测脉宽来说16位完全足够除非你想测长达几十秒的超长脉冲。2.3 关键参数计算定时器输入捕获的参数配置核心就两个预分频值PSC和自动重装值ARR。这两个值决定了计数器的分辨率和最大可测量范围。先看时钟源。F103在默认配置下定时器挂在APB1上一般是72MHz。如果我们把预分频设成71也就是72分频那么计数器的计数频率就是72MHz / 72 1MHz每计数一次就是1微秒。这个分辨率对433MHz信号来说足够精细了因为遥控器脉冲宽度基本在几百微秒到几毫秒之间微秒级分辨率能够很清晰地区分短脉冲和长脉冲。再看最大测量范围。16位计数器最多计到65535在1MHz计数频率下就是65535微秒约65.5毫秒。EV1527的同步头总共约十几毫秒数据位脉宽最大的也只有几百微秒所以65毫秒的测量范围绰绰有余。这也是我为什么把这两个参数固化成72MHz时钟下PSC71、ARR0xFFFF的原因简单可靠覆盖绝大多数场景。如果你用的板子不是72MHz主频或者你接了分频后定时器时钟不是这个值只需要记住换算关系计数频率 定时器时钟 / (PSC 1)脉冲实际时间 两次捕获差值 / 计数频率。拿一个已知宽度的方波信号先测一遍就知道你的配置对不对了。3. 完整代码实现与解析3.1 捕获初始化与引脚配置整个代码我基于STM32标准库来写使用的是F103系列芯片GPIO和定时器的配置思路在所有型号上通用。如果你用的是HAL库逻辑完全一样只是函数名不同。接线方面433MHz接收模块的数据输出引脚直接接到单片机PA6也就是TIM3_CH1对应的引脚VCC接3.3VGND共地。注意有些接收模块在3.3V下灵敏度会略低如果距离不远可以直接用3.3V想追求更远的接收距离可以单独给模块供5V但数据引脚最好串一个1k电阻再接单片机避免电平不匹配。初始化代码其实不长主要是四步开时钟、配GPIO、配定时器、开中断。直接看代码void TIM3_CH1_Capture_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPD; // 下拉输入默认低电平 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 71; // 72MHz / (711) 1MHz即1us计数一次 TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); 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 0x03; // 滤波滤掉窄毛刺 TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM3, ENABLE); }几个关键点说一下。GPIO的模式我配的是下拉输入这是因为我的接收模块在无信号时输出高电平有信号时输出低电平的脉冲下拉输入比较配合我的电平逻辑。如果你的模块无信号时输出低电平可以改成上拉或者干脆用浮空输入然后靠接收模块自身的输出去驱动前提是模块输出能力足够。这里推荐给引脚加一个下拉/上拉而不是完全浮空是为了避免模块输出端悬空时引脚电压不确定引入随机干扰。预分频设置成71定时器计数频率就是1MHz代码里所有脉冲宽度数值都是以微秒为单位。自动重装值设成0xFFFF也就是65535意味着计数到65535之后会回绕重新开始。正常情况下433MHz信号里最长的低电平也就10毫秒左右也就是10000个计数不会超过最大值所以不用担心溢出。TIM_ICFilter这一项是很多人忽略的。它本质上是硬件层面的输入滤波器只有持续稳定超过一定时间的电平变化才会被识别为有效边沿。对射频接收模块来说输出端经常会有一些微秒级的毛刺如果不加滤波这些毛刺会被当成有效边沿捕获导致解码全乱。我实测下来滤波系数设成0x03就能滤掉绝大多数杂波又不影响正常的几百微秒级脉冲。3.2 中断服务函数里如何还原脉冲宽度初始化配置完成之后核心工作就在中断服务函数里了。它是整个解码任务的心脏每出现一个信号边沿都会进来一次。我在这里用一个静态变量记录上一次捕获的时间戳然后计算两次捕获之间的差值也就是当前脉冲的宽度。关键代码如下volatile uint16_t last_cap 0; volatile uint8_t recv_step 0; volatile uint32_t recv_data 0; volatile uint8_t recv_bit_cnt 0; volatile uint8_t recv_done 0; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_CC1) ! RESET) { uint16_t now TIM_GetCapture1(TIM3); uint16_t diff now - last_cap; last_cap now; // 根据当前捕获的边沿类型做处理 if (TIM_GetCapturePolarity(TIM3, TIM_Channel_1) TIM_ICPolarity_Rising) { // 这次是上升沿捕获说明前面是一个低电平脉冲 // 判断这个低电平是不是同步头 if (diff 6000) // 大于6ms认为是同步头 { recv_step 1; // 进入数据接收阶段 recv_bit_cnt 0; recv_data 0; TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Falling); // 下一次捕获下降沿 TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); return; } } else { // 这次是下降沿捕获说明前面是一个高电平脉冲 // 同步头之后的高电平是数据位宽值区分0或1 if (recv_step 1) { if (diff 400) // 高电平宽度大于400us认为是1 { recv_data (recv_data 1) | 1; } else { recv_data (recv_data 1); } recv_bit_cnt; if (recv_bit_cnt 20) // EV1527一共20位 { recv_step 2; // 接收完成 recv_done 1; } } TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Rising); // 下一次捕获上升沿 } TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); } }这段代码的核心逻辑是在同一个通道上不断切换捕获边沿。上升沿到来时说明刚结束了一个低电平我们把这个低电平的宽度和同步头的特征比对下降沿到来时说明刚结束了一个高电平我们根据这个高电平的宽度判断数据位是0还是1。有两点需要特别说明。第一捕获值的差我用now - last_cap直接做无符号减法这里其实利用了16位无符号数的溢出特性即使计数器回绕差值依然正确不用额外处理溢出问题。第二同步头的判断我用的是低电平宽度大于6毫秒这个阈值需要根据遥控器实测调整。EV1527的同步头低电平大概9毫秒左右PT2262的大概10毫秒所以6毫秒能很好地区分同步头和普通数据位因为数据位低电平最长也就几百微秒。3.3 脉宽解析与数据位组装接收完成的标志recv_done置1之后主循环就可以拿数据了。但20位二进制数据直接读出来比较难用更友好的做法是把地址位和数据位拆分分别存到不同的变量里。EV1527的前8位是地址后12位是数据。我在程序里加了一个解析函数uint16_t get_ev1527_address(void) { return (uint16_t)((recv_data 12) 0xFF); } uint16_t get_ev1527_code(void) { return (uint16_t)(recv_data 0xFFF); }你也可以根据自己需求调整位数。比如你用的遥控器数据位只有4位那就把掩码改成0x0F。写完代码之后最好先用一个已知的遥控器按键做测试把收到的地址打印出来如果每次按同一个键结果都一样说明解码正确如果每次结果都不一样多半是数据位没对齐需要检查同步头判断是否太严格或太宽松。另外接收模块输出信号的极性在不同模块上可能相反。有的模块无信号时输出低电平有信号时输出高电平这种情况下代码里上升沿和下降沿的判定逻辑恰好相反所以你在调试时如果发现捕获到的脉宽值全部异常先看看模块输出的电平极性是否正确。3.4 主函数与接收状态机最后是主函数和整个接收状态机的串联。我的习惯是主循环只做检查标志位、处理数据、打印结果这些轻量操作所有实时性要求高的部分全部放在中断里。这样程序结构清晰也方便以后加其他功能。int main(void) { uart_init(115200); TIM3_CH1_Capture_Init(); while (1) { if (recv_done) { recv_done 0; printf(addr0x%02X, data0x%03X\r\n, get_ev1527_address(), get_ev1527_code()); // 在这里可以添加业务逻辑比如根据data值控制继电器、LED等 } } }接收状态机在中断里一共有三个状态空闲等待、接收数据、接收完成。用recv_step这个变量表示。初始状态是0也就是空闲等待此时不断检测同步头一旦检测到低电平宽度超过6毫秒就跳转到状态1开始拼接数据位拼接满20位之后跳到状态2通知主循环可以读取结果。这个状态机的好处是逻辑清晰不容易出现半截数据被当成一帧的情况。不过20位的判断条件只适用于EV1527。如果你用PT2262一帧数据是24位那判断条件要改成recv_bit_cnt 24。这也是我建议你第一步先确认遥控器型号的原因代码里编码参数对不对直接影响结果。4. 常见问题与排查技巧4.1 杂波干扰导致误码解码类项目最容易遇到的头号问题就是杂波。接收模块在没有收到遥控器信号的时候输出端也可能出现随机跳变这些跳变一旦被定时器捕获到就会干扰状态机的判断。轻微的情况是偶尔收到一帧假数据严重的时候状态机可能一直停在等待同步头或者数据接收阶段导致正常的遥控器数据也收不进来。解决这个问题的三板斧一是GPIO配置下拉/上拉让默认电平确定二是开启定时器输入滤波三是在软件上做帧校验。前两个在初始化代码里已经做了第三个我再说具体一点。我的做法是主循环里不直接信任recv_done而是连续等待两帧数据都一样才认定接收成功。遥控器按键按下的瞬间通常会在几十毫秒内重复发送多帧相同数据我们利用这个特性只有连续两帧的地址和数据完全一致才置位有效数据标志。uint16_t last_addr 0, last_code 0; uint8_t valid_count 0; if (recv_done) { recv_done 0; uint16_t addr get_ev1527_address(); uint16_t code get_ev1527_code(); if (addr last_addr code last_code) { valid_count; if (valid_count 2) { printf(valid: addr0x%02X, data0x%03X\r\n, addr, code); valid_count 0; } } else { last_addr addr; last_code code; valid_count 0; } }如果遥控器是那种按下只发一帧的这个方法就不适用了那就得用另一种思路解析完一帧数据之后校验地址位是否在你的已知地址列表里不在就丢弃。4.2 长按遥控器重复帧与自动复位长按遥控器的时候发射芯片会以固定的时间间隔反复发送同一帧数据。解码出来后就会频繁触发控制逻辑比如窗户开一半突然又收到一次数据又执行一次翻转结果卡在中间。这个问题在解码类项目里几乎必现。处理办法是增加一个冷却时间收到一帧有效数据之后在几百毫秒内忽略后续相同的数据。这样才能保证一次按键只触发一次动作。uint32_t last_recv_time 0; if (valid_data (HAL_GetTick() - last_recv_time 500)) { // 执行动作 last_recv_time HAL_GetTick(); }500毫秒这个值可以根据实际遥控器的重发间隔调整。EV1527典型的重发间隔是几十毫秒到一百多毫秒之间所以500毫秒能很好地屏蔽掉同一帧的重复触发又不至于把用户的连续按键误判成一次。如果你用的是HAL库就用HAL_GetTick标准库项目一般自己维护一个毫秒计数方法都一样。4.3 不同遥控器不兼容很多朋友在项目里会遇到这种情况用A遥控器调试得好好的换个B遥控器就解码失败了。原因多半是B遥控器的编码芯片跟A不同或者即使芯片型号一样生产批次不同脉宽参数也有细微差异。以EV1527为例同样是1的高电平宽度有的芯片发640us有的发580us都算正常。如果你的代码里把判断阈值卡死在400us理论上应该都能识别因为不管是580还是640都大于400。但遇到那种脉宽特别短的遥控器比如1只有350us而你的判断阈值是400us那就有可能把1误判成0。解决办法是不要用固定的绝对值而是用动态阈值。具体做法是检测到同步头之后把随后的高电平宽度记录下来取前1-2个数据位作为基准后续数据位跟基准比较比基准宽判为1比基准窄判为0。这个思路有点像自适应解码对不同品牌的遥控器鲁棒性更好。代价是代码稍微复杂一点我建议先把固定阈值版本跑通确认整个链路没问题之后再考虑优化成自适应。4.4 调试时常用的几个手段调试解码项目最痛苦的就是看不到信号。我自己的调试习惯是分三步走。第一步用逻辑分析仪或者示波器去抓接收模块的数据输出引脚确认模块真的解调出了脉冲波形。这一步能排除接线、供电、天线这些低级问题。第二步在中断服务函数里加一个调试计数器每捕获到一个边沿就加1然后在主循环里打印出来。如果计数器在按下遥控器后疯狂增长说明捕获链路是通的如果纹丝不动那问题出在模块输出或者GPIO配置上。第三步把每次捕获到的脉宽值打印出来同步头、数据0、数据1的宽度一目了然这时候再回头调整阈值就非常轻松。没有示波器怎么办也简单。你可以用另一个定时器比如TIM2配置成定时中断每100微秒记录一次当前接收引脚的电平状态连续记录一段时间然后在串口里把高低电平变化打印成一段简易波形。虽然效率低但偶尔用来排查问题完全够用。另外提醒一句调试阶段最好先把接收模块跟单片机的连接线尽量缩短杜邦线太长容易引入干扰导致捕获到的脉宽忽大忽小明明代码没问题却总以为拍错了。我自己就被这根线坑过一晚上。5. 实操心得与扩展方向这套定时器输入捕获的解码方案我后来在好几个项目里都复用上了。有一次是给朋友做了一个车窗升降器的无线控制器有一次是做一个两路继电器无线开关都是同样的代码改成不同业务逻辑就上了。真正吃透这段代码之后你会发现433MHz解码只是输入捕获能力的一个缩影。说几个我个人的体会。第一不要在中断服务函数里做太多事情尽量只做测量脉宽、拼接数据这些核心操作跟业务相关的判断全部丢给主循环。中断函数里一旦加了打印、延时或者其他耗时操作捕获时间戳就会出问题尤其在高频边沿的时候容易出现盖不住中断的情况。第二善用状态机来管理接收过程不要用一堆散落的标志位去判断当前该干什么状态机让整个流程的边界清晰很多排查问题的时候也容易定位。第三代码里的阈值尽量不要硬编码写死定义成宏或者放到配置结构体里后面换遥控器方案的时候只改一处就行。再分享一个小技巧。如果你想让这套系统支持学习功能也就是按下遥控器按键自动记忆地址和数据其实只需要在解码成功之后把地址和数据存到EEPROM或者Flash里下次再收到相同地址的数据就认为是合法的。毕业设计里做智能家居系统的时候这个功能非常加分评委问起来也很有得聊。后续我还打算把解码逻辑往前再推一步做一个频率计模式和占空比测量模式顺便把这几个功能封装成一个独立的驱动模块以后项目里要用直接调API就行。你如果对这部分感兴趣欢迎在评论区留言人多的话我下一篇就把频率计和占空比测量也写了。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。