简介:面向需要实现433MHz无线遥控功能开发的单片机工程师,这份EV1527解码程序提供了一套可直接移植的C语言源码,无论使用AVR、ARM Cortex-M、PIC还是STM32等常见平台,都能通过中断方式完成信号接收与解码,不阻塞主流程,适合遥控系统、无线传感器等低功耗短距离场景。压缩包共2个文件,包含EV1527.c源文件与EV1527.h头文件,前者实现解码算法与信号处理流程,后者提供函数声明与常量定义,便于快速集成。已有922人学习下载,可见其通用性受到开发者认可。压缩包虽仅2KB,但代码结构清晰,开发者还提供了移植教程入口,按教程适配引脚与定时器后即可获得完整的433MHz数据解析能力,用于扩展无线控制功能。 做单片机项目时,遇到需要接收433MHz遥控器信号的场景并不少。最开始我图省事,直接买了带解码输出的接收模块,结果发现要么学习按键数量有限,要么拿到手的模块解码逻辑跟遥控器对不上,而且价格还不便宜。后来换成“射频接收头+自己写解码程序”的路线,一个几块钱的RXB6再加几十行C代码,就把EV1527遥控器完整解了出来,放在51、STM32、ESP32上都能跑。这篇就聊聊我在写433-EV1527解码程序时积累的经验,包括协议细节、状态机设计、移植方法和实际踩过的坑。
1. 为什么放着现成的解码模块不用,非要自己写
1.1 成品解码模块的“省事”代价
市面上的433接收模块大体分两类。一类是纯接收头,输出原始波形,比如RXB6、MX-05V,价格一两块钱,拿回来不能直接用,得自己写解码。另一类是自带解码的模块,比如带学习码输出的SRX882,模块上直接有四个IO口可以接单片机,遥控器按一下,对应IO就输出高低电平,看起来确实省事。
但这类成品模块的问题在于扩展性差。它内置的了解码逻辑是写死的,支持的学习按键数量有限,而且换一个牌子的遥控器,可能学习方式都不一样。更麻烦的是,很多模块的IO输出是瞬态的,你得自己判断按键按下的事件,无法获取遥控器的唯一识别码。如果你的项目需要区分多个遥控器,或者需要把遥控器特征值保存到EEPROM里做离线匹配,成品模块基本帮不上忙。
1.2 接收头加自研解码的真实成本
自研解码方案用到的硬件极其简单:
- 一个433MHz超外差接收头(RXB6或者国产兼容型号),约1到3元。
- 一个单片机GPIO,支持外部中断最好。
- 一个定时器,能产生微秒级时间戳。
就这么点东西,解码出来的数据是完整的20位原始码,包含ID、按键码和序列号。你可以自己做任意复杂的配对逻辑,也可以把接收到的码直接通过串口打印出来,排查问题非常直观。
1.3 这篇文章适用的场景和读者
如果你在做智能家居遥控开关、无线门铃识别、遥控插座改造、车库门控制、毕业设计里用到433M遥控器,或者单纯想搞懂EV1527这种学习码芯片到底是怎么编码的,那这篇文章应该对你有用。我会把解码协议、通用状态机、完整代码和移植方式都写清楚,尽量做到你拿到代码后换个单片机也能跑起来。
2. EV1527协议里的关键细节:三态编码、同步码和振荡电阻
2.1 EV1527是什么,和PT2262有什么区别
EV1527是台湾远翔科技推出的一颗编码发射芯片,常见于各种433MHz/315MHz遥控器。华矽的HS1527、瑞泰的RT1527、飞凌的FP1527这些芯片和EV1527基本引脚兼容、编码协议一致,所以行业里统一叫“1527编码”。
提到1527,很多老工程师会想到PT2262。两者最大的区别是:
- PT2262是固定码芯片,靠外部拨码开关或者跳线组合设置地址和数据,发出去的码永远不变。
- EV1527是学习码芯片,它的编码结果里包含12位随机序列号,每次上电重新生成。接收端必须先“学习”一次,把序列号记下来,之后才能识别这个遥控器。
这也就是为什么市面上很多遥控插座、遥控门铃第一次使用时,需要“对码”才能正常工作。
2.2 一帧数据到底长什么样
EV1527发射的一帧数据由同步码加20位数据码组成。以常见的2.7MΩ振荡电阻为例,基本时间单位T大约在300到350微秒左右:
- 同步码:1个T的低电平加31个T的高电平,总宽度约32T。
- 数据位0:1个T的高电平加3个T的低电平,总宽度约4T。
- 数据位1:3个T的高电平加1个T的低电平,总宽度约4T。
- 悬空位f:高、低电平各1T再加一个1T宽度的特殊脉冲,普通遥控器里很少用到。
20位数据按“高位先发”的顺序输出,依次是4位ID、4位按键码、12位序列号。具体来说:
- 高4位是芯片ID,很多遥控器固定为0b1000或0b0101。
- 中间4位是按键码,比如遥控器上A键是0b0000,B键是0b0001等。
- 低12位是序列号,上电随机生成,范围0到4095。
按下遥控器按键时,接收端会持续收到多帧相同的数据,直到松开按键。
2.3 T值为什么重要,振荡电阻如何影响解码
EV1527发射模块上有一颗振荡电阻,决定了T的大小。不同厂家为了省成本或者匹配天线,用的电阻值不一定一样。比如2.7MΩ时T约300多微秒,1.2MΩ时T可能只有150微秒,4.7MΩ时T能到700微秒。
写解码程序时,如果按照绝对时间宽度去判断数据位,遇到不同振荡电阻的遥控器就容易误判。更稳妥的做法是:只判断相邻两个电平的时长比例,而不是判断绝对宽度。因为不管T怎么变,数据位内部的长短段比例始终接近3比1,同步码内部长段和数据位短段的比例接近31比3,这个比例关系是非常稳定的。我的解码程序正是基于这个思路设计的。
3. 兼容所有单片机的解码方案:一个IO加一个定时器
3.1 为什么不用输入捕获外设
很多STM32的教程一上来就教你用定时器输入捕获来测脉宽。输入捕获确实好用,但问题在于不是所有单片机都有这个外设。比如经典的STC89C52就没有输入捕获通道,你要移植解码程序就得重新写一套。为了让代码在51、STM32、ESP32、AVR上都能跑,我选择最基础的资源:一个GPIO的外部中断加一个自由运行的定时器。外部中断用来记录边沿时刻,定时器只负责提供一个微秒级的计数时间戳。这套方案几乎没有平台依赖性。
3.2 核心原理:边沿中断加时间戳
解码的核心原理很简单:每次GPIO电平跳变(上升沿或下降沿)都会触发外部中断,我们在中断里读取定时器的当前计数值,减去上次记录的时刻,就得到一段电平的持续时间。连续的电平持续段构成了完整的编码流。
为了识别同步码,我利用的是一个关键特征:同步码里有一段极长的电平,宽度约31T,远大于普通数据位的任何一段电平。程序里判断:如果某一段电平持续时间超过5毫秒,就认为收到了同步码。这个5毫秒的阈值,对常见的T值范围(150到700微秒)都适用,足够可靠。
3.3 状态机的三个关键阶段
解码过程是一个典型的状态机:
- 空闲态:等待第一个边沿,准备检测同步码。
- 同步态:逐段测量电平宽度,寻找超过5毫秒的长电平。
- 数据态:找到同步码后,跳过同步码后面的短电平,开始把后续电平按“两段一组”解析成数据位。
这里有一个容易出错的点:数据位是连续的,位和位之间没有停顿。如果分组错位,后面所有位都会错。所以我的做法是:检测到同步码长电平之后,用一个专门的“跳过短电平”状态,把同步码后半段的1T短电平忽略掉,然后再开始数据位的两段分组。
3.4 数据位判定里的容差设计
在数据态中,我把每个数据位对应的两段电平分别记为seg1和seg2。判定逻辑为:
- 如果max(seg1, seg2) 大于 min(seg1, seg2)的2倍,认为是有效数据位。
- 如果seg1大于seg2,该位判为1。
- 如果seg2大于seg1,该位判为0。
- 如果两段宽度接近,说明是干扰或者悬空位,直接丢弃并重新等待同步码。
这里用2倍作为判定阈值而不是严格的3倍,是为了容忍波形上升下降沿带来的测量误差。实测下来,这个阈值对大多数EV1527遥控器都有不错的兼容性。
4. 完整解码程序剖析:从边沿中断到帧校验
4.1 移植接口定义
为了做到“兼容所有单片机”,程序把平台相关的部分封装成两个接口:
extern void ev1527_timer_init(void); /* 初始化1MHz自由运行定时器 */ extern uint32_t ev1527_timer_get(void); /* 读取当前1MHz计数值 */解码逻辑本身完全不依赖具体MCU的寄存器,只要这两个接口实现正确,代码就可以在任意单片机上运行。定时器建议配置成1MHz计数频率,也就是每1微秒计一个数。如果单片机主频不够高,无法做到1MHz精确计数,也可以适当降低频率,只要保证分辨率在20微秒以内即可。
4.2 核心代码
下面是我整理后的完整解码程序,去掉了具体的平台相关部分,只保留核心状态机逻辑:
#include <stdint.h> /* 移植接口:由外部实现 */ extern void ev1527_timer_init(void); extern uint32_t ev1527_timer_get(void); /* 解码状态 */ typedef enum { DECODE_IDLE = 0, DECODE_SYNC, DECODE_AFTER_SYNC, DECODE_SEG1, DECODE_SEG2 } decode_state_t; /* 解码结果 */ typedef struct { uint16_t raw; /* 原始20位数据 */ uint8_t id; /* 4位ID */ uint8_t key; /* 4位按键码 */ uint16_t serial; /* 12位序列号 */ } ev1527_frame_t; /* 解码成功回调,在中断上下文执行,建议只做标志位处理 */ extern void ev1527_frame_received(ev1527_frame_t *frame); /* 同步码长电平最小宽度:5ms,覆盖常见振荡电阻对应的T值 */ #define EV1527_SYNC_MIN_US 5000u /* 数据位两段长度比例阈值:max / min >= 2 判定为有效 */ #define EV1527_RATIO_THRESHOLD 2u static volatile decode_state_t g_state; static volatile uint32_t g_last_tick; static volatile uint16_t g_raw; static volatile uint8_t g_bit_count; static volatile uint32_t s_seg1; void ev1527_gpio_isr(uint8_t level) { uint32_t now = ev1527_timer_get(); uint32_t dur = now - g_last_tick; g_last_tick = now; switch (g_state) { case DECODE_IDLE: g_state = DECODE_SYNC; break; case DECODE_SYNC: /* 检测到超过5ms的长电平,认为是同步码的长段 */ if (dur > EV1527_SYNC_MIN_US) { g_raw = 0; g_bit_count = 0; g_state = DECODE_AFTER_SYNC; } break; case DECODE_AFTER_SYNC: /* 同步码后面的短电平,直接跳过,准备对齐数据位 */ g_state = DECODE_SEG1; break; case DECODE_SEG1: /* 记录数据位第一段 */ s_seg1 = dur; g_state = DECODE_SEG2; break; case DECODE_SEG2: { uint32_t seg2 = dur; uint32_t max_dur = (s_seg1 > seg2) ? s_seg1 : seg2; uint32_t min_dur = (s_seg1 < seg2) ? s_seg1 : seg2; if ((min_dur > 0) && (max_dur > (min_dur * EV1527_RATIO_THRESHOLD))) { /* 有效数据位:seg1大于seg2判为1,反之判为0 */ uint8_t bit = (s_seg1 > seg2) ? 1u : 0u; g_raw = (uint16_t)((g_raw << 1) | bit); g_bit_count++; if (g_bit_count >= 20) { ev1527_frame_t frame; frame.raw = g_raw; frame.id = (uint8_t)((g_raw >> 16) & 0x0F); frame.key = (uint8_t)((g_raw >> 12) & 0x0F); frame.serial = (uint16_t)(g_raw & 0x0FFF); ev1527_frame_received(&frame); /* 接收完成,复位等待下一帧 */ g_state = DECODE_IDLE; g_bit_count = 0; g_raw = 0; break; } g_state = DECODE_SEG1; } else { /* 两段宽度接近,判为干扰或悬空位,重新同步 */ g_state = DECODE_IDLE; g_bit_count = 0; g_raw = 0; } break; } default: g_state = DECODE_IDLE; break; } }注意代码里有一个关键的位序约定:g_raw = (g_raw << 1) | bit,意味着先收到的数据位放在高位,这与EV1527高位先发的时序一致。如果你的遥控器和这个顺序相反,把移位方向改一下即可。
4.3 51单片机和STM32的接入示例
在STM32上,接入代码很简单。用TIM2产生1MHz计数,PA0作为外部中断输入:
void ev1527_timer_init(void) { /* 使能TIM2时钟,配置为1MHz向上计数 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseInitTypeDef t; TIM_TimeBaseStructInit(&t); t.TIM_Prescaler = 72 - 1; /* 72MHz分频到1MHz */ t.TIM_Period = 0xFFFF; TIM_TimeBaseInit(TIM2, &t); TIM_Cmd(TIM2, ENABLE); } uint32_t ev1527_timer_get(void) { return TIM_GetCounter(TIM2); } void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { EXTI_ClearITPendingBit(EXTI_Line0); uint8_t level = (GPIOA->IDR & GPIO_Pin_0) ? 1 <p> <a href="https://download.csdn.net/download/qq_37749995/89471309" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>