拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32定时器中断解码433M无线遥控:从脉宽测量到状态机

简介一份基于STM32定时器中断实现433M无线遥控解码的完整源程序包适合正在学习STM32无线通信、智能家居遥控接入或需要快速实现RF433解码的嵌入式开发者。程序利用定时器中断精确捕获外部信号边沿进而测量脉冲宽度完成对433M遥控信号的软件解码方案经实际测试可用可直接烧录运行。压缩包整体仅5KB包含1个头文件和1个C源文件函数封装清晰核心解码逻辑集中在rf433模块内方便用户按需修改引脚或定时器配置也能轻松移植到其他STM32系列工程中。目前已有584人学习下载。通过这份源码读者既能理解定时器中断配合状态机解析无线协议的思路也能直接调用现成接口获取遥控键值省去从零编写和调试信号解码的繁琐过程适合作为毕业设计、课设或量产项目的前期参考。1. STM32定时器中断解码433M无线遥控先搞懂“时间戳”遥控器的接收模块DATA脚在按键时吐出的不是规整的串口数据而是一串宽度不同的方波。信息不在电平高低里而在“这个电平维持了多久”。所谓STM32定时器中断解码433M无线遥控不是用定时器产生载波也不是靠死循环等跳变而是让定时器给每个边沿做一次精准计时把脉宽序列喂给状态机最终还原出芯片ID和按键值。这个标题看着像老工程实际上解决的是低成本、不阻塞主循环的通用解码问题。接下来我会按“原理→最小工程→参数调校→产品化”的顺序把它讲透配套代码可以直接抄进STM32项目里改着用。2. 解码原理433M无线遥控的0/1由脉宽决定不是由电平决定2.1 接收模块输出的是“有载波/无载波”的时间序列很多第一次接触433M无线遥控的人会误以为解码要处理高频信号。实际上市面上常见的超外差/超再生接收模块已经把射频部分做完了DATA引脚输出的就是解调后的数字脉冲串。遥控器发射时模块输出高电平不发射时模块输出低电平。载波频率433M只是运输工具数据本身通过“载波有无持续了多久”来表达也就是ASK/OOK调制。所以解码程序只需要做一件事把DATA脚上每个高电平、低电平的持续时间准确测出来再按协议把这些时间还原成0/1和一帧数据。这个“测时间”的动作如果放在主循环里用循环等待会挡住其他任务如果放在外部中断里中断服务函数又不能花太多时间做协议解析。于是行业里最常见的做法就是把定时器当成一块高精度秒表在GPIO边沿中断里读一下当前计数值差值就是脉宽。整段程序可以完全面向事件驱动。2.2 EV1527和PT2262的时序特征先看最常见的EV1527学习码遥控器。它一帧包含24bit其中前20bit是遥控器ID后4bit是按键值。EV1527的码元定义很清晰一个码元由固定宽度的高电平和跟随的可变低电平组成。以很多模块实测值来看单位脉宽T在320~350us附近0码是“高约1T低约1T”1码是“高约1T低约3T”同步码是“高约1T低几毫秒到十几毫秒”。PT2262则是一类更老的编码芯片它的地址由芯片引脚电平配置脉宽由外接振荡电阻决定不同板子的实际宽度差异比EV1527大得多。参数可以用表格粗看但具体到自己的接收模块一定要实测。协议一帧组成0/1特征同步特征EV152724bit20bit ID4bit键值高短低 / 高长低高超长低PT2262系列由芯片型号决定高低电平宽度比例不同超长低电平这些协议都没有标准串口那样的起始位和停止位也没有时钟线唯一的同步依据就是对“时间”的度量。因此解码程序的骨架就是“测量脉宽→判断脉宽→组装bit→组装帧”。2.3 三种“定时器中断解码”的方案对比标题里强调“定时器中断”实际工程里有三种常见实现很多人下到的源程序可能是其中任一种容易看得一头雾水。方案实现方式优点缺点EXTI定时器取时间戳每个边沿进EXTI中断读自由运行定时器的CNT算差值精度高到1us主循环不阻塞代码通用有两个中断源状态机要自己写定时器输入捕获定时器通道自动记录边沿计数值进CAPTURE中断中断次数少硬件自动打时间戳双边沿捕获配置绕换引脚麻烦定时器固定间隔采样每10us进一次定时器中断读GPIO再叠状态机只用一个定时器逻辑直观10us中断频率高433M这类宽脉冲尚可短脉冲吃力我一般选用第一种。只要把定时器预分频配成1us计数一次一个32位定时器可以连续跑71分钟不回绕完全够一次按键产生的几百个边沿用。方案B在STM32上能做但双边沿捕获要两个通道配合反而把简单问题复杂化方案C适合给新手理解原理占用中断资源太多不如A干净。2.4 为什么解码逻辑要放进状态机而不是临时拼代码从时间戳到按键值中间要经过同步码识别、bit组装、帧长度判断、出错恢复。写成一个状态机之后每次EXTI中断只需执行几十条指令既不会拖长中断时间也方便后续扩展别的433M协议。状态机在源程序里通常对应一个 rf_decoder_feed(dur, level) 函数中断只负责喂数据主循环只负责收结果。还要说明一点定时器中断在这里是“提供时间基准”不是“所有解码代码都在定时器中断里跑”。固定间隔采样的方案才会把状态机放进定时器中断里而EXTI定时器方案的中断里只有差分计算和一次状态机调用耗时比定时器采样方案小得多。3. 用STM32定时器中断解码433M无线遥控的最小工程3.1 引脚与定时器分配接收模块DATA脚接PA0原因是PA0对应EXTI0中断入口独立且开销小。TIM2配置成32位自由运行定时器计数器每1us增加1。CubeMX里不需要给TIM2开中断只要它默默计数就行因为解码需要的是随时能读到精确计数值而不是等定时器溢出。外设配置作用PA0GPIO_MODE_IT_RISING_FALLING下拉双边沿外部中断TIM2PSC71ARR0xFFFFFFFF向上计数1us自由运行计数器EXTI0优先级设为1高于普通外设保证边沿不丢下拉还是上拉取决于接收模块空闲电平。多数超再生模块空闲时DATA为低按键时输出高脉冲所以用下拉如果模块空闲为高要用上拉。这一点错了程序会不断进中断或者完全收不到脉冲。3.2 GPIO与定时器初始化代码下面是一段不依赖具体主程序的HAL库初始化适用于STM32F103这类72MHz主频的芯片。计时公式是 timer_tick APB定时器时钟 / (PSC1)这里得到1MHz即1格1us。/* rf433_drv.c */ static TIM_HandleTypeDef htim2; void rf433_gpio_init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); gpio.Pin GPIO_PIN_0; gpio.Mode GPIO_MODE_IT_RISING_FALLING; gpio.Pull GPIO_PULLDOWN; /* 根据模块空闲电平选择上下拉 */ HAL_GPIO_Init(GPIOA, gpio); HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0); HAL_NVIC_EnableIRQ(EXTI0_IRQn); } void rf433_timer_init(void) { __HAL_RCC_TIM2_CLK_ENABLE(); htim2.Instance TIM2; htim2.Init.Prescaler 72 - 1; /* 72MHz / 72 1MHz计数器11us */ htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0xFFFFFFFF; /* 32位自由运行长时间不回绕 */ htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(htim2); HAL_TIM_Base_Start(htim2); }PSC71是把72MHz时钟分频成1MHz如果芯片跑在48MHzPSC改成47如果APB1定时器时钟不是72MHz先确认RCC配置。ARR填0xFFFFFFFF后TIM2永远不会触发更新中断计数器从0数到0xFFFFFFFF再回绕配合uint32_t减法的无符号特性时间差依然正确。3.3 边沿中断里做时间差分每次边沿到达就读一次TIM2计数值和上一次的差值就是上一段电平持续了多少微秒。这段代码要短不能放延时、打印或Flash写入。static volatile uint32_t last_tick; static volatile uint8_t first_tick 1; void EXTI0_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); } void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin ! GPIO_PIN_0) return; uint32_t now __HAL_TIM_GET_COUNTER(htim2); if (first_tick) { first_tick 0; last_tick now; return; /* 第一次边沿没有上一段丢弃 */ } uint32_t dur now - last_tick; last_tick now; uint8_t level HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0); rf_decoder_feed(dur, level); }当检测到下降沿时读到的level为低说明dur是刚结束的高电平宽度当检测到上升沿时读到的level为高说明dur是刚结束的低电平宽度。用“当前电平”反推“刚结束的电平”是这段代码的关键也是看源程序时最容易绕晕的地方。注意如果工程里已经由CubeMX生成了 EXTI0_IRQHandler就不要再在别处重复定义否则会链接冲突。标准外设库的老工程写法不太一样但核心逻辑同样是“差分计数状态机”。3.4 解码状态机把脉宽变成bit再把bit变成帧状态机只需要两个状态等待同步和正在收数据。收到同步低电平后进入ST_DATA之后每来一个合法的“高低”组合就解出一个bit攒够FRAME_BITS就置完成标志。/* rf433_decode.h */ #define FRAME_BITS 24 #define HIGH_MIN_US 220 #define HIGH_MAX_US 500 #define LOW0_MIN_US 180 #define LOW0_MAX_US 600 #define LOW1_MIN_US 750 #define LOW1_MAX_US 2200 #define SYNC_MIN_US 3500/* rf433_decode.c */ typedef enum { RF_WAIT_SYNC, RF_ST_DATA } rf_state_t; static rf_state_t state RF_WAIT_SYNC; static volatile uint32_t rx_buf 0; static volatile uint8_t rx_bits 0; static volatile uint8_t high_ok 0; volatile uint8_t rf_rx_done 0; volatile uint32_t rf_rx_frame 0; static void rf_add_bit(uint8_t bit) { rx_buf (rx_buf 1) | bit; if (rx_bits FRAME_BITS) { rf_rx_frame rx_buf; rf_rx_done 1; state RF_WAIT_SYNC; rx_bits 0; } } void rf_decoder_feed(uint32_t dur_us, uint8_t level) { if (dur_us 50000) { /* 键抬起后的长低电平或噪声 */ state RF_WAIT_SYNC; rx_bits 0; return; } if (level) { /* 刚结束的是低电平 */ if (dur_us SYNC_MIN_US) { state RF_ST_DATA; rx_bits 0; high_ok 0; return; } if (state RF_ST_DATA high_ok) { if (dur_us LOW1_MIN_US dur_us LOW1_MAX_US) { rf_add_bit(1); } else if (dur_us LOW0_MIN_US dur_us LOW0_MAX_US) { rf_add_bit(0); } else { state RF_WAIT_SYNC; rx_bits 0; } } else if (state RF_ST_DATA) { state RF_WAIT_SYNC; rx_bits 0; } } else { /* 刚结束的是高电平所有数据位的高都应落在同一窗口 */ high_ok (dur_us HIGH_MIN_US dur_us HIGH_MAX_US); if (state RF_ST_DATA !high_ok) { state RF_WAIT_SYNC; rx_bits 0; } } }逻辑说明分三块。第一低电平分支里先判断同步原因是同步码的低电平远长于数据位一旦识别到同步说明后面跟着一帧数据此刻最安全的行为是清空前面残留的bit。第二数据位的判断必须依赖high_ok因为EV1527一个bit的高电平宽度基本固定如果高电平不在窗口内后面低电平再像0或1也不能信。第三rx_buf左移填入bit先收到的在高位这样rf_rx_frame直接就是协议里的原始帧不需要再按bit反转。主循环里这样取结果if (rf_rx_done) { rf_rx_done 0; uint32_t frame rf_rx_frame; uint32_t dev_id frame 4; uint8_t key frame 0x0F; /* 业务逻辑先比对dev_id再分发key */ }FRAME_BITS24是针对EV1527的。PT2262或其他芯片机型改这个宏即可但要注意有些协议一帧不是24bit盲目按24去截断会在最后一个bit之前误报完成。3.5 换芯片和换主频时怎么改这三个地方最容易忘预分频器按实际时钟改成1MHz计数GPIO上下拉按模块空闲电平改HAL_GPIO_EXTI_Callback在旧标准库里对应的是GPIO_EXTI_Callback或直接在中断函数里写判断现在用HAL库则都要经过HAL_GPIO_EXTI_IRQHandler转发。4. 433M解码参数标定先数清楚脉宽再定阈值4.1 用示波器或逻辑分析仪量一组真实波形拿到源程序后第一件事不是烧录而是把接收模块DATA脚接上逻辑分析仪按一次按键抓完整一帧。操作步骤接收模块DATA接逻辑分析仪通道GND共地采样率至少2MS/s。触发方式选下降沿单次触发然后按遥控器任意键。测量连续几个高电平和低电平宽度记录最大、最小、典型值。如果模块输出电平反相波形看起来是低多高少就把逻辑取反后再量。我见过最省事的工具是几十元的24MHz采样逻辑分析仪配PulseView看433M这类几十kHz的脉冲绰绰有余。测出来的数据像这样信号段实测范围初始参数判定数据位高电平340~355usHIGH_MIN220, HIGH_MAX500通过0码低电平330~360usLOW0_MIN180, LOW0_MAX600判01码低电平990~1080usLOW1_MIN750, LOW1_MAX2200判1同步低电平8~9msSYNC_MIN3500判同步这个表是EV1527类模块的常见值不能代表所有模块。不同批次接收模块的输出整形电阻会有差异尤其是PT2262这类靠振荡电阻定脉宽的协议差两三倍都很正常。4.2 容差窗口怎么定解码程序最忌讳用固定值比较比如“如果低电平等于350us就是0”。实际波形受遥控器电池电压、温度、接收模块批次影响脉宽会有20%左右的漂移。正确做法是把判定写成区间LOW0的区间要覆盖实测0码的分布LOW1的区间覆盖实测1码的分布两个区间之间留出至少300us的空隙。EV1527的0码约350us、1码约1050us所以把0码上限放6001码下限放750是安全的。如果测出来0码最大500、1码最小800空隙只有300应把区间收窄到450~700和750~1100。区间大了抗漂移区间小了抗误判要按实际模块折中。4.3 三个必调参数与三张典型“失败脸”第一个必调参数是SYNC_MIN_US。它的作用是识别一帧的开始设太大容易漏同步设太小容易把某个1码长低电平误判成同步。通常放在“实际同步低电平的一半”附近。EV1527同步低电平常见8ms以上初始值3500us很安全。第二个必调参数是LOW1_MIN_US。它决定了1码的最低门槛和LOW0_MAX之间的差值就是容错带宽。差得太窄遥控器离得远、波形变钝时就会误判。第三个必调参数是FRAME_BITS。很多人解出来只有16bit或者总是多一位问题往往不是算法而是把24bit的帧硬按别人的协议挑。调试时先在串口打印rx_bits看到真实位数再固定宏。现象优先检查处理解出来全是0或全是1接收模块电平是否反相读GPIO后取反再喂状态机帧解出来少几位FRAME_BITS/LOW1_MAX过小打印rx_bits按实际长度调整靠近开发板时乱码上下拉和电源纹波换GPIO_PULLUP接收模块远离开关节点注意EXTI回调里不要调用printf、HAL_Delay、Flash写入。这些操作在中断里会拖长沿到沿的时间窗口导致下一个边沿中断被延迟解码成功率直线下降。4.4 超时重置和Keil调试技巧遥控器按键松开后模块输出会落回空闲电平此时两个边沿间隔会超过几十毫秒。状态机里已经加了dur_us50000的强制复位保证下一次按键能从干净的WAIT_SYNC开始。没有这个保护中间一个噪声位会让状态机一直停在ST_DATA里等低电平后边的帧全部废掉。Keil里调试这类中断驱动的代码不要直接在EXTI回调里打断点每按一次键会断几百次时序完全失真。可以设条件断点rf_rx_done1或者把rf_rx_frame加到Watch窗口按下遥控器后看变量变化。如果怀疑某个脉宽判断不对临时在rf_decoder_feed入口存最近的8个dur到一个数组里break后查看数组比printf好用得多。5. 进阶从“解出一帧”到“做成一个可用的遥控器”5.1 把dev_id和key分开处理EV1527一帧的24bit里前20bit是设备ID后4bit是按键值。如果只取keyframe0x0F就分发业务会遇到一个很实际的问题手里两把不同遥控器如果是同型号同批次ID可能相同按键冲突更危险的是某把遥控器的地址位和另一把重合按下A遥控器的键1会把B遥控器的键1一起触发。常见的做法是业务层用一个结构体同时保存dev_id和keytypedef struct { uint32_t dev_id; uint8_t key; } rf_event_t;每次收到完整帧先比较dev_id是否在已学习的列表里再按key分发动作。这样换遥控器时不会出现“明明没按却动了”的问题。5.2 长按时重复帧的处理433M遥控器在长按时会反复发射相同帧间隔通常在几十毫秒到一百毫秒。如果主循环每发现rf_rx_done就去触发继电器、蜂鸣器会被连续触发很多次。常见做法是做一次300ms去抖void rf_handle_event(uint32_t frame) { uint32_t dev_id frame 4; uint8_t key frame 0x0F; static uint32_t last_id; static uint8_t last_key; static uint32_t last_ms; if (dev_id last_id key last_key HAL_GetTick() - last_ms 300) { return; } last_id dev_id; last_key key; last_ms HAL_GetTick(); /* 真正的按键业务写在这里 */ }这个去抖放在解码状态机外面而不是中断里因为它依赖HAL_GetTick()的1ms节拍放中断里反而没必要。5.3 学习码存储和地址冲突的最后一关如果做的是遥控插座、车库门这类设备最好把dev_id存进片内Flash而不是每次上电重新学习。存的时候要连校验一起存否则Flash某个字节磨损后可能出现一把从没配过的遥控器被放行。最简做法是存dev_idkeyCRC16校验通过才启用。5.4 量产前必做的自检步骤最后给一个能直接放到工厂测试程序里的验证技巧定义一个计数器让测试员把同一把遥控器的同一个键连续按50次统计rf_rx_done触发次数并检查50次里有没有出现不同的dev_id只要dev_id不一致或成功率低于98%就说明解码参数或者接收模块位置有问题。把这段统计留在产品里还有一个额外好处用户上门投诉“经常失灵”时可以让售后按住按键同时读统计值就能立刻区分是射频链路差还是测试环境差。这套“定时器打时间戳状态机”的解码模板换到38K红外遥控解码也一样能用把时间窗口换掉就是另一个协议的解码器。如果换遥控器型号后解出的帧总是错位先回到第4章量一遍真实波形再动代码。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门