news 2026/9/24 12:24:23

STM8S103自制EM4100读卡器:125kHz射频卡解码从零到串口输出

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM8S103自制EM4100读卡器:125kHz射频卡解码从零到串口输出

每次把罐头盖拧开,第一个想到的永远是“里面到底写的是谁”。半年前我从柜底翻出几张积灰的EM4100门禁卡,第一反应就是找个读卡器把卡号拆出来看看。市面上一张125kHz低频读卡器要几十块,但如果用一颗STM8S103自己搭,成本能压到十元以内。这个项目我在面包板上调了整整一晚,最后从串口里看到稳定输出的卡号时,确实有一种“协议被我一层一层撕开”的快感。

整套方案不依赖RC522这类专用读卡芯片,从天线驱动、载波振荡、包络检波,到曼彻斯特解码和EM4100数据帧解析,全是分立元件加上STM8S103的定时器硬算。核心链路是先把125kHz载波上的负载调制信号变成干净的数字方波,再让单片机的输入捕获中断去测量边沿间隔,还原出64位数据帧,最后抽出40位ID。这个过程对理解非接触式卡的物理层和底层通信协议极其有帮助,适合只想验证射频卡原理的单片机爱好者,也适合想在低成本门禁、工位识别、宠物喂食器上快速上手的开发者。

1. 方案选型:为什么是STM8S103和125kHz

1.1 几块钱的小芯片到底能不能扛住解码

选单片机的第一原则永远是“够用就好”。125kHz RFID解调出来的曼彻斯特码,位周期约512us,数据率还不到2kbps,对主频几乎没有压力。但“没压力”不代表随便拿颗51就能做得舒服,核心难点在于要精准测量边沿间隔。STM8S103自带带捕获功能的定时器,一次捕获能精确到微秒级,中断处理后还有大量余量,完全可以实时解码。这颗芯片目前市场价不到两块,8KB Flash、1KB RAM、最高16MHz内部RC,这些资源用在低频RFID解码上非常合适。换STM32的话性能过剩还费电,换传统51又得在外围补捕获电路,麻烦又不准。

很多人喜欢拿它和市面上的专用读卡芯片比,比如RC522。RC522确实能直接读13.56MHz的卡,但它读不了125kHz的EM4100,而且它内部把调制解调都做完了,你只能通过SPI拿结果,中间过程全黑盒。而用STM8S103做低频读卡器,整个信号链露在外面,每一级波形都能用示波器测到,出了问题也知道去哪找,这对我这种喜欢把所有细节攥在手心的人特别重要。

1.2 125kHz和EM4100的底层到底长什么样

低频RFID的工作方式可以理解成一个微型变压器。读卡器天线线圈产生125kHz交变磁场,卡片靠近后内部谐振回路感应出电压,芯片再通过改变负载阻抗去影响天线端电压幅度,这个幅度变化被读卡器检波出来,就是数据信号。EM4100是低频段最常见的只读ID芯片,内部固化40位编码,前10位一般是厂商或版本标识,后32位是卡号。这种卡便宜、稳定,门禁、考勤、物流、动物耳标都在用。

和现在普及的13.56MHz NFC相比,低频的优点是穿透性稍好、抗金属干扰能力略强,缺点是通信速率慢、数据几乎明文、不支持复杂安全认证。但正因为协议公开、结构简单,EM4100成了学习RFID底层机制最合适的教材。低频还有一个特点:读卡距离只有几厘米,这对固定工位、门禁卡应用足够,也正好能让DIY方案把电路做得很紧凑。

2. 读卡器硬件设计:从天线到数字方波

2.1 先把硬件链路想清楚再动手

125kHz读卡器看起来元件很多,拆开就是四件事:产生载波、驱动天线、提取调制信号、整形成方波。我用的方案是NE555构成约125kHz自激振荡,经过三极管推挽放大后驱动LC谐振天线;天线端并联的二极管和电容组成包络检波器,把卡片负载调制引起的电压起伏提取出来;检波后的模拟信号再进LM393比较器,整形为标准数字方波;最后送入STM8S103的PB0输入捕获引脚。这里最关键的一点是:进入单片机的信号必须干净,不能有毛刺,否则定时器捕获到的边沿全部乱掉,软件再怎么写也白搭。所以比较器后面我会加一个小电容做滤波,压一压高频噪声。

NE555为什么好用?因为125kHz这个频率正好落在555的舒适区,外部电阻电容就能调,成本不到五毛。当然也可以用STM8S103的定时器直接输出125kHz方波,但这种做法会让芯片既发载波又解数据,调试时两头都麻烦。我最终还是选择独立振荡器,让单片机专心做解码,排查问题也简单:先确认555有输出,再找后面一级的问题。

2.2 天线谐振网络不是随便绕个圈就行

天线线圈的电感和并联电容要谐振在125kHz附近,公式就是f=1/(2π√(LC))。DIY时可以先用0.2mm漆包线在5cm直径的圆柱土绕10匝,再用电感表测出实际电感,之后反推谐振电容。举个例子,实测L=200μH的话,谐振电容C=1/((2π×125000)^2×200μH),算下来约8.1nF,实际取两个4.7nF并联就行。如果L达到1mH,那电容只需要1.6nF左右。总之天线不是玄学,拿公式和工具算一遍,方向基本不会错。

这个LC回路的Q值会直接影响读卡距离。Q值高,选频特性好,距离能拉远,但频带变窄,对温度、金属物体的变化也敏感。所以建议用可调电容,在示波器上一边看天线两端波形一边调到最饱满。调好的标准很简单:天线端稳定出现125kHz正弦波,卡片靠近后包络检波点能看到明显的幅度起伏。如果没有起伏,就算后续电路再完美也读不到卡。

2.3 包络检波和比较器整形电路要点

包络检波我用的是1N4148和10nF电容,检波输出接到LM393的“+”端,“-”端接参考电压。参考电压由电位器调,不能太高也不能太低:太低会捡到噪声,太高会把有用信号削掉。实际操作时先把卡片拿远,把参考电压设成无卡时检波电压的80%左右,再靠近卡片微调,直到输出方波最稳定。LM393是开漏输出,输出端务必接10k上拉到3.3V或5V,再串个100Ω电阻、并个100pF电容进单片机PB0,能明显改善边沿质量。

这里有个很实际的坑:STM8S103的PB0作为输入捕获引脚时,如果外部信号悬空,会产生随机边沿中断,把状态机彻底打乱。所以引脚必须设成浮空输入,同时确保比较器输出端在无卡时也能保持稳定高或低电平,而不能处于三态漂移。

3. EM4100协议解析与曼彻斯特解码思路

3.1 64位帧结构:里面不全是卡号

很多刚接触EM4100的人会以为读出来的数字就是卡号,这是错的。卡片上存储的是一段64位的曼彻斯特编码帧,结构经常被误读。我按常用的公开协议整理成了这样:帧先发9个逻辑1作为同步头,接下来是10组“4位数据+1位偶校验”,共50位,这部分包含了10位版本/厂商码和32位卡号;最后再发4位列校验和1位停止位。所以完整一帧是9+50+4+1=64位。

40位有效ID被拆成10个4位nibble,每个nibble后面带一位偶校验,接收端可以提前发现单bit错误。换句话说,卡号不是“存了64位”,而是用64位帧保护了40位有效信息。理解这一点很重要,因为后面解析的时候不能简单把64位全部当卡号,而是要跳过头、丢掉每组校验位,再拼出ID。

每个数据位时长是64个载波周期,在125kHz下正好约512us。曼彻斯特编码的规则是:不管数据是0还是1,每个位周期中间必定有一次电平跳变;高到低表示1,低到高表示0。相邻两个bit如果相同,位边界处会多一次沿跳变;如果不同,位边界处就没有跳变。这一条是所有解码算法的基石。

3.2 边沿间隔法把数据流还原出来

既然每位一定有中央跳变,那就可以通过测量相邻两个边沿的时间差来还原原始bit序列。当一个bit与上一个不同时,两个中央跳变点之间隔了一个完整位周期,约512us;当一个bit与上一个相同时,中间还会插入一个边界跳变,所以相邻两次跳变间隔只有半个位周期,约256us。解码逻辑由此变得很直白:捕获到一次边沿,测量它与上一次边沿的时间差,差在256us附近,就重复输出上一个原始bit;差在512us附近,就把上一个原始bit取反后输出;超出这个范围,就认为是噪声或同步丢失,清零重来。

实际波形不会这么完美,所以我允许±80us的容差。在STM8S103里,TIM1分频到1MHz后,一个计数值就是1us,256us就是256个计数,512us就是512个计数,判断起来非常舒服。把计数器调准,再把容差放开,解码的稳定性比想象中好。

3.3 同步、收帧与校验的软件状态机

解码第一步是搜索9个连续逻辑1同步头。我维护了一个9位移位寄存器,每解出一个bit就移入一位,然后检查寄存器值是不是0x1FF。一旦命中,说明同步头对齐了,紧接着开始缓存之后的55位,也就是10组×5位加上4位列校验和1位停止位。收集满55位后,先检查每一组4位数据的偶校验,有任何一组不一致就直接丢掉这帧。校验通过后,把40位有效数据提取出来,高10位作为版本/厂商码,低32位作为卡号,最后通过UART打印。

这个状态机的好处在于不需要额外硬件同步时钟,软件自己就能“咬住”帧边界。实现上,我用一个add_raw_bit函数统一收数据:没同步时只更新移位寄存器;同步后则把bit存进数组,存满一帧再交给解析函数。整个流程不复杂,但状态切换的逻辑要写清楚,尤其是同步成功后要立刻锁存,不能继续去搜索头部,不然容易收错起始位置。

4. STM8S103核心代码实现

4.1 开发环境和引脚规划

我用的是SDCC编译器,也可以用IAR for STM8。芯片选STM8S103F3P6,用SWIM接口烧录。引脚分配上,PB0配置为TIM1_CH1输入捕获,接收比较器输出的解调方波;PD4接一个LED,解码成功后点亮;UART1的TX默认在PD5,串口输出卡号。系统时钟用内部16MHz HSI,不用外部晶振。TIM1分频到1MHz,这样定时器每计数一次对应1us,后面处理256us、512us的边沿间隔时直接比数值就行。

烧录和调试建议用ST-Link,成本很低。SDCC环境下需要把标准外设库一起放进工程,ST官方提供的库文件可以直接从官网下载,注意芯片型号选STM8S103F3P6。如果只用代码里的寄存器操作,不依赖库也能跑,但GPIO和UART初始化还是用标准库写更省心,下面的代码我都基于标准外设库。

4.2 定时器输入捕获初始化

初始化代码做了三件事:把PB0设成浮空输入,保证外部信号能干净进来;配置TIM1工作在输入捕获模式,捕获通道1映射到TI1;使能CC1捕获中断,并让计数器以1MHz频率计数。初始边沿设成上升沿,每次中断完成边沿间隔测量后,再切换CC1P比特,让它变成下降沿或上升沿,这样能连续跟住每一个边沿。具体代码如下。

void gpio_init(void) { /* PB0 input capture, floating input, no external interrupt */ GPIO_Init(GPIOB, GPIO_PIN_0, GPIO_MODE_IN_FL_NO_IT); /* PD4 LED output push-pull */ GPIO_Init(GPIOD, GPIO_PIN_4, GPIO_MODE_OUT_PP_HIGH_FAST); } void timer1_init(void) { /* 16MHz / 16 = 1MHz, 一个计数值 = 1us */ TIM1_TimeBaseInit(15, TIM1_COUNTERMODE_UP, 0xFFFF, 0); /* CH1 mapped to TI1, rising edge initially */ TIM1_ICInit(TIM1_CHANNEL_1, TIM1_ICPOLARITY_RISING, TIM1_ICSELECTION_DIRECTTI, TIM1_ICPSC_DIV1, 0); /* enable capture compare interrupt */ TIM1_ITConfig(TIM1_IT_CC1, ENABLE); TIM1_Cmd(ENABLE); }

这里TimeBaseInit的第一个参数是预分频值15,实际分频比是16,所以定时器时钟从16MHz变成1MHz。有人会问为什么不用更大的分频,让计数器直接按512us溢出?因为在32.768ms内会频繁溢出,处理回绕反而麻烦。1MHz下16位计数器溢出时间是65.535ms,完全够用。

4.3 中断服务函数与曼彻斯特解码

中断函数是整个解码的核心。每次捕获到边沿,先读取CCR1寄存器,算出与上一次边沿的差值,再切换下一次捕获极性。接着根据差值判断当前bit是重复还是翻转:差在256us附近,说明当前bit和上次相同;差在512us附近,说明当前bit和上次相反。其他情况一律视为噪声,重置状态机。完整的代码如下。

INTERRUPT_HANDLER(TIM1_CAP_COM_IRQHandler, 12) { uint16_t now; uint16_t diff; if (!(TIM1_SR1 & TIM1_SR1_CC1IF)) return; now = TIM1_GetCapture1(); diff = (uint16_t)(now - last_edge); last_edge = now; /* 切换捕获边沿,下一次捕获相反沿 */ if (TIM1_CCER1 & TIM1_CCER1_CC1P) TIM1_CCER1 &= (uint8_t)~TIM1_CCER1_CC1P; else TIM1_CCER1 |= TIM1_CCER1_CC1P; /* 半位周期:数据位不变 */ if ((diff >= BIT_HALF_MIN) && (diff <= BIT_HALF_MAX)) { add_raw_bit(last_data_bit); } /* 全位周期:数据位翻转 */ else if ((diff >= BIT_FULL_MIN) && (diff <= BIT_FULL_MAX)) { last_data_bit ^= 1; add_raw_bit(last_data_bit); } /* 其他都视为噪声 */ else { sync_found = 0; raw_bit_pos = 0; sync_shift = 0; } TIM1_ClearITPendingBit(TIM1_IT_CC1); }

这里需要不断更新last_edge,否则回绕差值会错。STM8的16位减法在无符号下自然处理了回绕,只要每次中断都及时读走,就不会出错。add_raw_bit函数的实现前面说过,关键是同步前用移位寄存器找9个1,同步后存满55位再置标志。

4.4 数据校验和卡号提取

一帧55位收满后,先逐组校验偶校验位。第0、1、2、3位是第一个nibble,第4位是这一组的偶校验位;第5~8位是第二个nibble,第9位是校验位,以此类推。如果所有10组校验都通过,就把40位有效数据通过移位拼成一个uint64_t临时变量,高10位取版本/厂商码,低32位取卡号。实现如下。

static void parse_frame(void) { uint64_t raw = 0; uint8_t i; for (i = 0; i < 50; i++) { if ((i % 5) == 4) { uint8_t p = frame_bits[i]; uint8_t n = 0; uint8_t j; uint8_t calc; for (j = 0; j < 4; j++) { n = (n << 1) | frame_bits[i - 4 + j]; } calc = (n & 1) ^ ((n >> 1) & 1) ^ ((n >> 2) & 1) ^ ((n >> 3) & 1); if (calc != p) { frame_ready = 0; /* 校验失败,丢弃 */ return; } } else { raw = (raw << 1) | frame_bits[i]; } } card_version = (uint16_t)((raw >> 30) & 0x3FF); card_id = (uint32_t)(raw & 0xFFFFFFFF); frame_ready = 0; card_ok = 1; }

这种四位数nibble拆开再拼的方式,比一次性连续取32位要安全。因为EM4100的帧结构天然把数据切成4位一组,跟着帧结构走,校验逻辑也清晰。串口打印的时候,我会输出Vendor: %03X Card: %08X,把版本和卡号分开,方便对卡。比如打印Vendor: 1F0 Card: 01234567,说明这张卡的40位原始ID是0x1F001234567

4.5 主函数和串口输出

主函数思路很线性:初始化外设后开全局中断,然后死循环里轮询frame_ready标志。一旦发现一帧收完,就调用解析函数;解析成功后点亮LED,并通过串口输出卡号。串口波特率我用9600,不管调试还是接上位机都够用。UART初始化用标准库一行搞定,发送直接重定向putchar,这样可以用printf输出。

void uart1_init(void) { UART1_Init(9600, UART1_WORDLENGTH_8D, UART1_STOPBITS_1, UART1_PARITY_NO, UART1_SYNCMODE_CLOCK_DISABLE, UART1_MODE_TX_RX); } int putchar(int ch) { while (!(UART1_SR & UART1_SR_TXE)); UART1_DR = (uint8_t)ch; return ch; } void main(void) { CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1); uart1_init(); gpio_init(); timer1_init(); enableInterrupts(); while (1) { if (frame_ready) { parse_frame(); if (card_ok) { GPIO_WriteHigh(GPIOD, GPIO_PIN_4); printf("Vendor: %03X Card: %08X\r\n", card_version, card_id); card_ok = 0; } } } }

在STM8S103上使用printf会占不少Flash,如果觉得空间紧,也可以把格式化拆成十六进制移位发送。不过8KB Flash对于这个工程还是够的,只要不引入浮点格式化,就没问题。

5. 调试过程、实测记录和常见问题

5.1 按波形一级一级查,比盲调代码高效得多

调试这套读卡器,示波器是必需品。第一次上电,先看NE555输出,应该是一个干净的125kHz方波;再看天线两端,应该出现明显谐振正弦波,峰峰电压能到几十伏;然后看检波电容正极,靠近卡片后能看到包络线被调制成曼彻斯特码形状;最后看LM393输出,必须是一个边沿很陡的数字方波。每一级波形正常了,才轮到单片机解码。我实际调试下来,最常见的问题出在天线匹配上。很多人问“代码没问题为什么读不到卡”,十有八九是天线没谐振,载波根本没发射出去,自然也就没有回程调制信号。

如果手里只有万用表没有示波器,也有个土办法:找一台收音机靠近天线,如果天线正常谐振,收音机里能听到125kHz附近的干扰声。这个办法虽然不精确,至少能判断驱动级有没有在工作。

5.2 常见问题排查速查表

现象可能原因解决方向
完全读不到卡125kHz振荡频率偏差大用频率计测NE555输出,调RC
读卡距离只有几毫米天线没谐振或Q值太低重新调LC电容,加大驱动电流
串口输出乱码波特率不对或UART初始化有误确认16MHz内部时钟和9600分频
偶发误码掉帧比较器参考电压不对用示波器看检波波形,微调阈值
中断不触发引脚没配置成输入捕获功能检查GPIO复用和TIM1初始化
重复输出同一张卡状态机没正常复位检查frame_readycard_ok清除逻辑

这些坑我在一晚上内基本都踩了一遍。尤其“比较器阈值”这个坑最隐蔽:阈值调太高,低速的曼彻斯特码直接被削得只剩一半;阈值调太低,底噪又会让比较器乱跳,解码状态机频繁重置。所以调试时一定要把示波器探头放在检波输出端,慢慢转电位器,看到方波占空比稳定了再去动软件。

5.3 几个让项目更稳定的经验

第一是天线Q值。Q值调高确实能增加读卡距离,但过高会有频偏风险,手一靠近天线甚至都能改变谐振点。所以我最后会在天线两端并一个几十k的电阻,人为把Q值降一点,换稳定性。第二是电源隔离。NE555和STM8共用一个5V电源时,数字部分的地噪声容易串进模拟检波,建议在LM393输出到PB0之间加1k串联电阻,同时STM8的电源引脚放一个100nF去耦电容,效果立竿见影。第三是软件滤波。边沿间隔判断允许一定容差后,如果还是偶发误码,可以在串口打印前增加一帧“连续两次读到相同卡号才上报”的判断,这个在门禁项目里尤其有用。

6. 一点个人体会和后续还能怎么玩

整个项目做完,我最深的体会是:不要把RFID读卡当成一个黑盒,尤其是低频125kHz这种“老”技术,只要愿意一层一层剥开,它真的能变成看得见摸得着的通信过程。用一颗STM8S103就足够应付,根本不需要上专用芯片,这种“刚好够用”的满足感,比直接接一块RC522强太多。

后面想继续折腾的话,还有几个方向很有意思:一是把天线驱动从NE555换成更稳定的晶体振荡器,读卡距离还能再提升;二是把串口输出改成RS485,就可以做成一个简单的门禁读头,接上位机管理多张卡;三是把读到的卡号存进片内EEPROM,做成“白名单”模式,不依赖上位机就能控制继电器开门。这套方案的可玩性很高,硬件再化简一点,甚至能塞进一个小型ABS外壳里,做成一个随身读卡工具。如果你也想试试,建议从555振荡电路开始,一级一级调波形,最后再看代码——把物理链路搞明白了,程序只是顺理成章的事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 12:23:13

汽车电子视角下的电机控制进阶路径:从波形调试到车规量产

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 12:19:28

STM32为何是语音交互系统的物理世界守门员

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 12:19:13

ISO/IEC 33002过程评估执行要求:从22页标准到可落地的评估体系

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 12:07:31

Play Framework 2.4 迁移指南:Anorm 独立化与新版本特性全解析

后端Web框架 【免费下载链接】playframework The Community Maintained High Velocity Web Framework For Java and Scala. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/pl/playframework 点击查看 免费下载 本指南基于 Play Framework 2.4 迁移文档中关于 Anorm 的…

作者头像 李华
网站建设 2026/9/24 12:05:06

入侵检测系统设计与实现:从架构选型到落地避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华