简介:围绕EV1527与SC2260两种常用编码器的信号解析需求,这套单片机解码程序面向具备一定C语言与STC单片机基础的电子爱好者、嵌入式学习者及遥控产品开发者,重点解决多地址脉宽编码的识别与处理问题,可用于无线门锁、报警器、遥控接收等场景的原型验证。压缩包共30个文件,以13个.h头文件和11个.c源文件为主体,配合Keil工程文件与说明文本,整体约47KB,代码结构覆盖主程序、串口、定时器、EEPROM及中断处理等模块。主程序已实现对两类编码信号的解码并经过实测,另附中断版本,便于对照理解查询与中断两种处理方式;其中PCA扩展外部中断部分仍有调试空间,可作为进一步研究捕获与实时响应的切入点。已有1550人学习,适合用作解码原理学习、工程移植与项目改造的参考。
1. 拆开一个卷帘门遥控器:EV1527 发、SC2260 收,软件解码到底解的是什么
卷帘门遥控器按下去没反应,拆开看到发射板上丝印 EV1527,接收盒里是 SC2260 模块,这种组合在低成本无线遥控里太常见了。很多人拿到“ev1527 sc2260 解码程序”以为插上就能读出键值,实际拿到的是接收模块 DATA 脚上的一串原始电平:同步头、24 个宽度编码的位、重复帧,外加电源和天线带来的抖动。这套程序做的事,是把这些电平还原成 20 位地址加 4 位按键,再做去重和白名单比对。适合正在做门禁、卷帘门、智能家居改造的嵌入式开发者,也适合手里有一堆 315/433MHz 遥控器想先读懂波形再决定要不要换方案的人。先别急着写中断,先把一帧的时序账算清楚。
2. EV1527 的 24 位帧与 SC2260 输出电平:先把时序账算到微秒
2.1 同步头、0 码、1 码的宽度关系
EV1527 的编码不复杂,复杂的是不同遥控器、不同电池电压下 T 会漂。它一帧由同步头和 24 个数据位组成,每个数据位占 4T,用高电平宽度区分 0 和 1:0 码是高 1T、低 3T,1 码是高 3T、低 1T。同步头通常是高 4T、低 124T,这个超长低电平就是软件对齐帧起点的锚点。T 常见在 200μs 到 400μs 之间,手里没有示波器时,先抓最窄的高电平,大概率就是 1T。
| 元素 | 高电平 | 低电平 | 总长 | 用途 |
|---|---|---|---|---|
| 同步头 | 4T | 124T | 128T | 帧起点 |
| 数据 0 | 1T | 3T | 4T | 位值 0 |
| 数据 1 | 3T | 1T | 4T | 位值 1 |
| 24 位数据 | 24×4T | 96T | 20 位地址 + 4 位按键 |
注意:上表是发射端编码的理想值。接收模块输出可能反相,也可能因为 AGC 把窄脉冲展宽,所以判据不能写死成“高电平大于 2T 就是 1”,要先确认极性。我一般会先抓一帧,量出最窄高电平和最宽高电平,再决定阈值。
24 位里前 20 位是地址,后 4 位是按键数据。很多廉价遥控器地址固定,按键只变低 4 位;也有遥控器把地址和按键打散,不能只看最后 4 位就下结论。程序输出时最好保留完整 24 位,再在应用层拆地址和键值,否则换一个遥控器就要改解码层。
2.2 SC2260 模块输出的是“原始波形”还是“四路电平”
SC2260 在不同板子上角色不一样。有的板子把它当硬件解码器,配对学习后直接输出四路电平,MCU 读到的是“哪一路被按下”,根本不需要软件解 24 位;有的模块只做射频接收和解调,DATA 脚输出的是上面那种宽度编码波形,软件解码程序面向的是后者。买模块时如果看到 DATA 脚随按键输出一串不等宽脉冲,就是原始波形;如果四根输出脚按一下只有一根变高,那就是硬件解码输出。
这套程序默认接原始波形。接硬件解码板也能用,但要把输入层从“边沿计时”改成“读四路 GPIO”,解码层和白名单层可以保留。别把两种板子混在一起调:一边量 DATA 脚波形,一边又去看四路电平,最后会怀疑代码,其实是接错了脚。
硬件解码板还有一个坑:输出可能是锁存或脉冲。锁存型按一下变高,再按一下变低;脉冲型只在高电平维持几十毫秒。程序里要做边沿触发还是电平采样,取决于板子手册,不能照抄原始波形的中断代码。
2.3 用逻辑分析仪抓一帧:通道、采样率与标注方法
抓波形是后面所有参数的地基。逻辑分析仪采样率至少 1MS/s,EV1527 的 T 在 250μs 左右时,1MS/s 已经够用;如果 T 小到 150μs,建议 2MS/s 以上。通道接接收模块 DATA 脚,地线必须共地,触发设成下降沿,按下遥控器抓 200ms 窗口,正常能看到一帧同步头加 24 位,再隔几十毫秒重复。
抓下来的 CSV 不要直接肉眼看,用一个小脚本把边沿间隔转成高电平宽度列表,先聚类。下面这段 Python 只做一件事:读两列时间戳和电平,输出每个高电平的持续时间和所属帧的大致位置。参数min_gap_us用来切帧,sync_low_us是同步头低电平下限,按实测改。
import csv def parse_edges(path, sync_low_us=20000, min_gap_us=5000): edges = [] with open(path, newline='') as f: for row in csv.reader(f): if len(row) < 2: continue try: t = float(row[0]) * 1_000_000 # 秒转微秒 level = int(float(row[1])) except ValueError: continue edges.append((t, level)) highs = [] last_rise = None last_fall = None for t, level in edges: if level == 1: last_rise = t elif level == 0 and last_rise is not None: highs.append((last_rise, t - last_rise)) last_fall = t # 用长低电平切帧:两个下降沿之间超过 min_gap_us 视为帧间隔 frames = [] current = [] prev_fall = None for rise, width in highs: if prev_fall is not None and rise - prev_fall > sync_low_us: if current: frames.append(current) current = [] current.append((rise, width)) prev_fall = rise + width if current: frames.append(current) return frames if __name__ == "__main__": frames = parse_edges("capture.csv") for i, frame in enumerate(frames[:5]): print("frame", i, "bits", len(frame)) print([round(w) for _, w in frame[:30]])代码里sync_low_us不是死参数,先看抓包文件里最长低电平是多少,再取它的 80% 左右。min_gap_us用来过滤毛刺,太小会把噪声当帧,太大可能把正常重复帧合并。跑完先看每帧是不是 24 个高电平脉冲;如果是 25 个,多半把同步头高电平也算进去了,后面状态机要注意剔除。
3. 解码程序骨架:外部中断抓边沿,定时器算宽度
3.1 中断服务函数只做三件事:记时间、算宽度、喂状态机
解码程序最怕在中断里做长耗时操作。我的习惯是外部中断只做三件事:读定时器计数、算与上一次边沿的差值、把“高电平宽度”交给状态机。串口打印、浮点运算、Flash 写入全部放到主循环。下面这段 C 代码是骨架,定时器按 1MHz 计数,所以时间单位直接是微秒;EV1527_T_US先填 250,后面标定再改。
#include <stdint.h> #include <stdbool.h> #define EV1527_T_US 250u #define SYNC_HIGH_MIN_US (3u * EV1527_T_US) #define SYNC_LOW_MIN_US (100u * EV1527_T_US) #define BIT_HIGH_MIN_US (1u * EV1527_T_US) #define BIT_HIGH_MAX_US (5u * EV1527_T_US) typedef enum { EV_WAIT_SYNC = 0, EV_RECV_BITS } ev_state_t; typedef struct { ev_state_t state; uint32_t last_edge_us; uint32_t last_fall_us; uint32_t high_us; uint32_t code; uint8_t bit_cnt; uint8_t frame_ready; } ev1527_t; static ev1527_t ev; /* 边沿回调:level=1 上升沿,level=0 下降沿 */ void ev1527_on_edge(uint8_t level, uint32_t now_us) { uint32_t delta = now_us - ev.last_edge_us; ev.last_edge_us = now_us; if (level == 0) { /* 下降沿:刚刚结束一段高电平 */ ev.high_us = delta; ev.last_fall_us = now_us; if (ev.state == EV_WAIT_SYNC) { if (ev.high_us >= SYNC_HIGH_MIN_US) { ev.state = EV_RECV_BITS; ev.bit_cnt = 0; ev.code = 0; } } else { if (ev.high_us < BIT_HIGH_MIN_US || ev.high_us > BIT_HIGH_MAX_US) { ev.state = EV_WAIT_SYNC; /* 宽度离谱,丢帧重来 */ return; } /* 高电平接近 1T 判 0,接近 3T 判 1,阈值取 2T */ uint32_t bit = (ev.high_us > (2u * EV1527_T_US)) ? 1u : 0u; ev.code = (ev.code << 1) | bit; ev.bit_cnt++; if (ev.bit_cnt >= 24) { ev.frame_ready = 1; ev.state = EV_WAIT_SYNC; } } } else { /* 上升沿:记录低电平长度 */ if (ev.state == EV_RECV_BITS && ev.last_fall_us != 0) { uint32_t low_us = now_us - ev.last_fall_us; /* 帧中间出现超长低电平,说明上一帧断了 */ if (low_us > SYNC_LOW_MIN_US) { ev.state = EV_WAIT_SYNC; } } } }这段代码的判据只用高电平宽度,低电平只在上升沿做“断帧”保护。SYNC_HIGH_MIN_US取 3T 是为了容忍同步头高电平从 4T 缩到 3T 多;SYNC_LOW_MIN_US取 100T 是为了不和数据位的 3T 低电平混淆。BIT_HIGH_MAX_US放到 5T 是给接收模块展宽留余量,超过就认为不是合法位。主循环看到frame_ready后取走code,再清标志,别在中断里直接调用业务函数。
如果你的 MCU 没有输入捕获,只能用 GPIO 中断加定时器,注意中断延迟。1MHz 计数下 1 个 tick 是 1μs,中断延迟几十微秒会直接吃掉 0 码的 1T 高电平,所以定时器频率别低于 1MHz,GPIO 中断优先级要高于串口和 SysTick。
3.2 状态机对齐同步头与 24 位数据
状态机只有两个状态,但顺序要对:先等同步头高电平,再进入收位;收满 24 位后立刻回到等待同步头,不能停在收位状态等下一帧。因为下一帧开头是 4T 高电平,如果还在收位状态,会被当成一个 1 码收进去,帧就错位了。上面代码在bit_cnt >= 24时直接切回EV_WAIT_SYNC,就是为了避开这个坑。
同步头的低电平没有在下降沿里判断,而是在下一个上升沿用low_us做断帧保护。这样做的好处是中断里少一个分支,坏处是如果同步头低电平被噪声截断,可能漏掉断帧。实际调试时可以在下降沿后加一个定时器超时:超过 100T 没有上升沿,就强制回等待状态。这个超时不要用软件延时,用硬件定时器单次触发。
收位过程中如果high_us落在 1T 和 3T 之间,比如 2T,说明波形已经变形。代码里用 2T 做阈值,等于把中间值判成 0 或 1,具体判成哪个取决于>还是>=。我一般用>,让 2T 落到 0,再靠后续连续帧投票。单帧解码本来就不该被当成绝对正确,尤其是电池电压低的时候。
3.3 输出层做去重、白名单和按键映射
解码层输出 24 位原始码,输出层要做三件事:去重、白名单、按键映射。去重不是简单比较上一次码值,因为同一帧会重复发很多次,中间还可能夹着噪声帧。常见做法是连续 2 到 3 帧相同才认为有效;白名单则是把学习到的地址存起来,只放行已知遥控器。
#define EV_ADDR_MASK 0xFFFFF0u /* 高 20 位地址 */ #define EV_KEY_MASK 0x00000Fu /* 低 4 位按键 */ typedef struct { uint32_t last_code; uint8_t same_cnt; uint32_t learned_addr[8]; uint8_t learned_num; } ev_output_t; static ev_output_t out; uint8_t ev1527_poll(uint32_t *code_out) { if (!ev.frame_ready) { return 0; } uint32_t code = ev.code; ev.frame_ready = 0; if (code == out.last_code) { if (out.same_cnt < 3) { out.same_cnt++; } } else { out.last_code = code; out.same_cnt = 1; } if (out.same_cnt < 2) { return 0; /* 至少连续两帧相同才放行 */ } uint32_t addr = code & EV_ADDR_MASK; uint8_t key = code & EV_KEY_MASK; for (uint8_t i = 0; i < out.learned_num; i++) { if (out.learned_addr[i] == addr) { *code_out = key; return 1; } } return 0; }EV_ADDR_MASK和EV_KEY_MASK按你实际遥控器的位定义改,别默认所有 EV1527 都是高 20 位地址。有的遥控器按键位在中间,有的地址位有交叉,最稳妥的办法是抓两个不同按键的码做异或,变的那几位就是按键位。same_cnt阈值 2 是折中:太低容易把噪声放进来,太高会感觉按键迟钝。门禁场景可以取 2,卷帘门这种要及时停下的场景取 1 到 2 之间要实测。
4. 阈值标定与容差:把 0/1 判据从玄学变成可复现
4.1 实测 T 的分布:不同遥控器、不同电池电压
同样标着 EV1527,不同遥控器的 T 能差出 30%。新电池和旧电池的宽度也不一样,旧电池往往把高电平拉窄,0 码的 1T 可能掉到 0.7T。标定时不要只抓一只遥控器,至少抓三只,每只抓 10 帧,把高电平宽度排序,看是不是明显分成两簇:一簇在 1T 附近,一簇在 3T 附近。如果两簇黏在一起,说明接收模块带宽不够或者天线失配,先别改代码,改硬件。
| 观测量 | 正常表现 | 异常表现 | 处理 |
|---|---|---|---|
| 0 码高电平 | 0.8T~1.3T | 小于 0.5T | 检查电池、接收模块供电 |
| 1 码高电平 | 2.6T~3.4T | 大于 4T | 降低接收模块增益或拉远距离 |
| 同步头低电平 | 110T~130T | 小于 80T | 检查是否被噪声截断 |
| 帧间隔 | 20ms~60ms | 忽长忽短 | 电源纹波或同频干扰 |
表格里的范围不是标准值,是现场能用的容差带。T 的绝对值用中位数估:把所有 0 码高电平排序取中值,再把所有 1 码高电平排序取中值,两者平均除以 2 就是 T 的估计。别用最小值当 T,最小值容易被噪声拉低。
4.2 用中值阈值和滞回窗口抵抗占空比漂移
固定阈值2T在 T 漂移时会翻车。更稳的做法是动态阈值:每次上电先收若干帧,统计 0 簇和 1 簇的中值,阈值取两簇中点;运行中如果连续多帧判据落在阈值附近,就重新标定。滞回窗口是给阈值加一个死区,比如阈值 ±0.2T 内不判位,等下一帧再试,避免在边界上抖动。
def estimate_threshold(high_widths): xs = sorted(high_widths) # 假定 0 码和 1 码各占一部分,用 1/3 和 2/3 分位粗略分簇 n = len(xs) low_cluster = xs[:max(1, n // 3)] high_cluster = xs[2 * n // 3:] t0 = low_cluster[len(low_cluster) // 2] t1 = high_cluster[len(high_cluster) // 2] t_est = (t0 + t1) / 4.0 # (1T + 3T) / 4 = T threshold = (t0 + t1) / 2.0 # 2T return t_est, threshold这段脚本假设 0 码和 1 码数量大致均衡。如果遥控器地址里 1 特别多,分位数会偏,最好先用已知码型标定一次。t_est用来更新EV1527_T_US,threshold用来替换代码里的2u * EV1527_T_US。滞回窗口可以写成threshold ± 0.2 * t_est,落在这个区间就丢弃当前帧,不要硬判。
4.3 接收模块 AGC 与天线位置带来的延迟
SC2260 接收模块内部有 AGC,强信号时会把窄脉冲展宽,弱信号时又可能把宽脉冲压窄。天线位置也会影响:天线靠近金属门框,波形上升沿变缓,高电平宽度看起来变长;天线卷成一团,距离缩短但方向性变差。调试时先把天线拉直,离金属 5cm 以上,再抓波形,别在装好外壳之后才怀疑代码。
AGC 延迟还表现为“第一帧总是错的”。接收模块从休眠到稳定需要几毫秒,遥控器按下的第一帧可能只有半个同步头。程序里可以丢弃上电后的前 2 帧,或者要求连续 3 帧一致才输出。这个策略在门禁上会增加一点延迟,但比误开一次强。
5. 避坑与排查:解码成功率从 60% 到稳定复现
5.1 中断里做浮点运算和串口打印
现象:一按遥控器就死机,或者串口输出乱码,解码成功率极低。原因:在 GPIO 中断里调用printf或浮点除法,中断执行时间超过几十微秒,下一个边沿被漏掉,0 码的 1T 高电平直接丢失。解决:中断里只做整数减法、移位和状态机,把frame_ready置位后立刻退出;串口打印放主循环,且一次只打一行。浮点阈值提前算成整数,别在中断里算0.2 * T。
我见过最隐蔽的翻车是中断里调用了HAL_GetTick(),这个函数本身可能关中断,和外部中断嵌套后时间戳倒退。时间戳统一用硬件定时器计数,不要混用 SysTick。
5.2 同步头误判与重复帧
现象:串口不停输出同一个码,或者偶尔输出一个地址全 0 的码。原因:同步头低电平判断太松,把数据位之间的 3T 低电平当成帧间隔;或者收满 24 位后没有及时切回等待状态,下一帧的同步头被当成数据位。解决:同步头低电平下限设到 100T 以上,收满 24 位立即切状态;输出层加连续帧计数,同一码连续 2 到 3 帧才放行。地址全 0 的码多半是噪声帧,白名单直接过滤。
还有一种重复帧是遥控器本身发的:按住按键时 EV1527 会连续发帧,帧间隔可能只有 20ms。去重窗口不要设成 500ms,否则第二次按键会被吞掉。用“码值变化 + 连续帧数”去重,不要用固定时间窗。
5.3 电源噪声、天线和地线
现象:靠近开关电源时解码率下降,手摸天线就好,放手就断。原因:接收模块供电纹波大,AGC 频繁调整;地线太长,形成天线效应。解决:接收模块 VCC 就近加 100nF 和 10μF 电容,地线单点接到 MCU 地;DATA 线远离电机和继电器线;天线拉直,不要绕在金属件上。卷帘门电机启动时干扰最大,程序里可以在电机动作期间暂停解码,或者加磁珠和屏蔽。
如果必须和电机共电源,至少给接收模块单独加 LDO,别直接从电机 12V 降压后共用。血泪经验是:波形看着干净,一接电机就乱,最后查出来是地线环流,不是代码。
5.4 把 SC2260 硬件解码输出当软件解码输入
现象:程序一直等不到边沿,或者读到的四路电平完全对不上按键。原因:板子上的 SC2260 是硬件解码输出,DATA 脚不是原始波形,而是四路电平或锁存输出。解决:先确认板子类型。量 DATA 脚:按键时是一串脉冲还是单根线变高。如果是四路电平,把输入层改成读 GPIO,解码层旁路,直接拿四路状态做按键映射。别在原始波形状态机里等同步头,等不到的。
反过来,如果板子只做射频接收,却按四路电平去读,也会失败。买模块时看清楚是“接收模块”还是“接收解码模块”,两者脚位和输出都不一样。
6. 进阶:把 24 位码拆成地址与按键,再做自检统计
地址白名单是这套程序落地时最该加的一层。把学习到的 20 位地址存进 Flash,按键只输出低 4 位,换遥控器时进学习模式长按保存。下面这段代码只做地址比对,learn标志由外部按键触发,写入前先擦除对应页。参数EV_ADDR_MASK要和你的位定义一致,别直接抄。
#include <string.h> #define ADDR_SLOT_MAX 8 typedef struct { uint32_t addr[ADDR_SLOT_MAX]; uint8_t count; } addr_book_t; static addr_book_t book; int addr_book_add(uint32_t code) { uint32_t addr = code & 0xFFFFF0u; for (uint8_t i = 0; i < book.count; i++) { if (book.addr[i] == addr) { return 0; /* 已存在 */ } } if (book.count >= ADDR_SLOT_MAX) { return -1; } book.addr[book.count++] = addr; return 1; } int addr_book_match(uint32_t code) { uint32_t addr = code & 0xFFFFF0u; for (uint8_t i = 0; i < book.count; i++) { if (book.addr[i] == addr) { return 1; } } return 0; }验证稳定性不要靠“按十下看亮不亮”,用连续帧统计:让遥控器连续发 100 帧,记录解码成功、校验失败、白名单拒绝的次数,再改变距离和电池电压重复。下面这张表是我一般会记录的字段,跑三组就能看出阈值是否偏。
| 测试项 | 距离 1m | 距离 5m | 旧电池 |
|---|---|---|---|
| 收到帧数 | 100 | 100 | 100 |
| 24 位完整帧 | 98 | 95 | 90 |
| 连续 2 帧一致 | 96 | 92 | 84 |
| 白名单放行 | 96 | 92 | 84 |
如果“24 位完整帧”很高但“连续 2 帧一致”掉得厉害,说明单帧里有偶发位错,优先查电源和天线;如果完整帧本身就低,查同步头阈值和中断延迟。调完阈值再跑一遍,别只跑一次就下结论。
从那以后我每次拿到新遥控器,都先抓 10 帧、算一次 T 的中值、再写死阈值,最后用 100 帧统计验收,不再凭“按着有反应”判断程序稳不稳。这套解码程序的源码和抓包脚本放在同一份资源里,引脚改一下就能接你的板子;拿到后先跑抓包脚本对阈值,再上白名单。希望帮到你。
本文还有配套的精品资源,点击获取