ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32实现EM4100读卡器:曼彻斯特解码与定时器输入捕获详解

STM32实现EM4100读卡器:曼彻斯特解码与定时器输入捕获详解 手头刚好有一批EM4100兼容卡准备用STM32做一块125kHz的入门级读卡器。网上搜了一圈讲原理的文章不少但能把“从天线信号到卡号输出”这条完整链路讲清楚、并且直接给库函数版本代码的确实不多。HAL库的教程一抓一大把老标准外设库的反而成了稀有物种。这篇文章就用标准外设库函数版本把EM4100的曼彻斯特编码解码过程完整拆开先讲清楚64位帧里到底装了什么再讲曼彻斯特编码的波形规矩然后落到定时器输入捕获的库函数实现最后把我在实测里踩过的坑一并列出来帮你少走几趟弯路。1. EM4100与曼彻斯特编码先弄懂这64位里装了什么1.1 125kHz低频卡的数据链路长什么样EM4100是EM Microelectronic公司推出的一款125kHz RFID应答器芯片市面上大量门禁卡、考勤卡、动物耳标用的就是这颗芯片或者它的兼容替代品。读卡器这边的工作过程可以简单分成三步读写器天线持续发射125kHz的载波卡片靠近时通过线圈耦合获得能量然后以负载调制的方式把内部存储的64位数据“吐”给读卡器。读卡器天线接收到的信号本质上是一个被数据调制的载波必须先经过检波、整形还原成数字方波才能交给MCU去解码。很多人第一次做这个项目会误以为直接把天线两端接到STM32的GPIO上就能读到数据结果大概率是读到一堆乱七八糟的电平毛刺。原因很简单GPIO只能识别0和1而天线上的是高频模拟信号。所以真正的信号链是天线谐振回路 → 包络检波 → 比较器/施密特整形 → STM32引脚。硬件部分我在第3章会详细展开。1.2 EM4100的64位帧格式头、数据、校验、停止位EM4100的数据帧是固定64位结构如下表字段位数说明同步头9 bit全部为1用于接收端同步版本/厂商码4 bit厂商编号或版本号数据区40 bit10组每组4位数据列校验10 bit每组数据后跟1位偶校验停止位1 bit固定为0所以64位 9 4 40 10 1。那10位列校验位非常关键解码后必须逐组校验否则随便一组数据错位都能算出看似合理的卡号到真正门禁系统里才会发现读出来的是错卡。数据区40位通常就是卡的编号很多系统会把40位按BCD码拆成10位十进制数来显示。校验规则是偶校验每一组“4位数据 1位校验”一共5位里1的个数必须为偶数。比如某组数据是1010已经有2个1校验位就是0如果数据是1000只有1个1校验位就是1。解码程序里可以用异或来快速检查5位逐位异或结果必须是0。1.3 曼彻斯特编码的波形规矩EM4100的基带编码是曼彻斯特编码也叫双相编码。它的核心规则是每个比特周期内中间位置必有一次电平跳变用电平跳变的方向来表示逻辑值。具体到EM4100的实现逻辑“1”前半周期输出高电平后半周期输出低电平中间是下降沿。逻辑“0”前半周期输出低电平后半周期输出高电平中间是上升沿。比特率由125kHz载波分频而来EM4100是载波/64所以位周期约512us125000/64≈1953.125bps半位周期约256us。解码时所有时间判断都围绕256us和512us这两个基准值展开。这里有一个非常容易被忽略的细节当连续两个比特相同时比如连续发两个“1”前一个比特的后半是低电平后一个比特的前半也是低电平那么比特边界处不会发生跳变。反过来如果两个相邻比特不同边界处必然有一次跳变。这意味着接收端边沿间隔有时候是半个位周期256us有时候是一个完整位周期512us。解码算法的核心就是根据这两种不同的边沿间隔反推出原始半位序列再合成完整的比特流。2. 解码方案怎么选为什么最终落在定时器输入捕获上2.1 GPIO轮询和简单延时的痛点网上很多EM4100解码的入门代码用的是GPIO轮询一个while死循环里不断读引脚电平靠软件延时去判断高低电平持续时间。这种写法在理论学习阶段可以帮助理解曼彻斯特编码但放到实际项目里问题非常多。首先是CPU占用率近乎100%整个MCU别的事都干不了。其次是延时函数的精度受中断影响很大只要系统里开了串口、定时器或者任何中断软件延时的实际长度就会漂。最后轮询方式很难处理边沿抖动如果整形后信号有一点毛刺解码state machine就直接崩了。我见过有人用这种方案做成产品结果读卡成功率只有七成卡片稍微放偏一点就解不出来。2.2 定时器输入捕获的优势事件驱动精准记录边沿定时器输入捕获是解决上述问题的标准做法。它的本质是硬件级别的“边沿时间戳记录器”引脚上的上升沿或下降沿到来时定时器会把当前计数值锁存到捕获寄存器里同时触发中断。CPU不关心引脚平时是什么电平只在边沿发生的瞬间被通知处理完继续干别的事。STM32的定时器输入捕获有几个天然优势。第一捕获时间戳精度由计数时钟决定我们把定时器时钟配到1MHz就是us级分辨率测量256us的半位周期绰绰有余。第二捕获中断是硬件触发不受其他中断影响。第三标准外设库可以同时配置通道1捕获上升沿、通道2捕获下降沿两个通道都映射到同一个引脚一次跳变就能得到“发生时刻跳变方向”这个信息足以还原整条曼彻斯特码流。2.3 为什么用库函数版本而不是HAL库选择标准外设库SPL而不是HAL库主要有两个原因。第一标准外设库的函数调用层次少譬如TIM_GetCapture1()直接读寄存器而HAL库的HAL_TIM_IC_CaptureCallback()要走一堆回调注册流程中断响应路径更长。对于256us半位周期这种量级来说HAL其实也来得及但SPL的实时性确实更好。第二很多老工程师手头积累的代码都是SPL版本工程模板、串口驱动都是现成的换HAL反而要重新适配。当然SPL官方已经停止维护了新设计用HAL也完全没问题。这篇文章的代码逻辑是平台无关的你把初始化部分换成HAL写解码状态机部分直接复用照样能跑通。2.4 定时器预分频和时间基准的计算具体到时间基准以主流72MHz主频的STM32F103为例定时器挂在APB1上APB1分频为2时定时器时钟是72MHz。我们要把计数频率降到1MHz也就是1us计数一次预分频值取72-1。计数器用16位模式最大计数65535us约65ms而EM4100的位周期才512us所以完全不需要担心计数器溢出对解码造成影响。相关的阈值可以这样设计参数计算值说明计数频率1MHz72MHz主频预分频72-1半位周期256us解码基准位周期512us两个半位半位判定区间180~330us以256us为中心留出容差全位判定区间390~640us以512us为中心兼容边沿抖动帧间隔阈值1200us超过则认为是帧间空隙阈值区间不能设得太窄因为天线的Q值、检波电路的充放电时间都会造成边沿轻微前移或后移。实测下来180~330us这个区间在普通洞洞板上也能稳定工作。3. 解码前的硬件信号链从天线线圈到MCU引脚3.1 天线谐振回路和包络检波不可否认解码代码写得再好前端信号一塌糊涂也是白搭。EM4100返回的是125kHz载波上的负载调制信号幅度变化非常小必须经过谐振放大和检波。天线线圈可以用直径0.2~0.4mm的漆包线绕制七八圈、直径5cm左右配合一个并联电容构成LC并联谐振回路谐振频率用公式f1/(2π√(LC))计算。假设线圈电感是100uH要谐振在125kHz电容大约需要16nF。检波用肖特基二极管即可比如BAT54S或者1N5819正向压降小对小幅度的调制信号损失小。二极管半波整流后再并联一个滤波电容把125kHz载波纹波滤掉得到的就是一个包络信号。滤波电容不能取得太大否则会吃掉曼彻斯特编码的边沿信息一般100pF到1nF之间具体以示波器观察到的波形为准。3.2 比较器整形把模拟包络变成数字方波包络信号是模拟量不能直接进STM32。最稳妥的方案是用比较器LM393加滞回电路或者直接用施密特触发器芯片74HC14。比较器的参考电压要设置在包络幅度的中间位置这样高低电平宽度才对称。LM393输出是开漏结构记得接上拉电阻一般10kΩ到100kΩ都可以。施密特触发器74HC14的好处是自带滞回抗噪声能力强不需要额外计算参考电压。实际使用中可以先用二极管检波得到一个包络然后直接送进74HC14的两个串联反相器输出端就是比较干净的数字方波了。这种方法元件少焊接容易很适合手搓实验板。3.3 电平匹配和引脚选择74HC14在5V供电下输出高电平接近5VSTM32的GPIO是容忍5V输入的所以直接连接问题不大。如果比较器输出经过上拉到3.3V那就更简单了。GPIO建议选择带定时器输入捕获功能的引脚比如STM32F103C8T6的PA0可以映射到TIM2_CH1PA1映射到TIM2_CH2。这里用PA0一个引脚就够了通过定时器通道1直接输入、通道2间接输入实现双沿捕获。有一个实操细节GPIO模式不要配成复用推挽输入捕获引脚要配置成浮空输入或者上拉输入。浮空输入更省事但悬空时容易引入噪声如果整形的输出级是推挽型的浮空输入完全没问题。4. 库函数配置与核心代码实现4.1 GPIO、定时器和中断优先级初始化下面给出标准外设库的初始化代码以STM32F103系列72MHz主频为例。GPIO用PA0作为TIM2_CH1的输入捕获引脚配置为浮空输入。void EM4100_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); }注意RCC_APB1PeriphClockCmd里的外设是TIM2它挂在APB1总线上。如果工程里已经用了ADC或者DAC检查一下是否有引脚冲突PA0同时也是ADC123_IN0和WKUP引脚在外设丰富的板子上要小心。4.2 双通道输入捕获的配置定时器配置分成两部分时基单元和输入捕获单元。时基单元把计数频率配到1MHz自动重装载值设成最大值65535这样计数器每1us加1循环计数不产生更新中断。void EM4100_TIM_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; NVIC_InitTypeDef NVIC_InitStructure; TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // 72MHz / 72 1MHz TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // 通道1上升沿捕获 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 0x08; TIM_ICInit(TIM2, TIM_ICInitStructure); // 通道2下降沿捕获映射到同一个输入引脚 TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Falling; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_IndirectTI; TIM_ICInit(TIM2, TIM_ICInitStructure); TIM_ITConfig(TIM2, TIM_IT_CC1 | TIM_IT_CC2, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); }这里TIM_ICFilter0x08是输入滤波参数可以滤掉几微秒的毛刺。EM4100的边沿间隔是几百us几us的滤波延迟完全可以忽略但对消除比较器抖动带来的毛刺很有帮助。如果你的整形电路信号非常干净设成0也可以。4.3 中断服务函数边沿事件记录两个通道共用TIM2_IRQHandler中断进入中断后先判断是哪个通道触发的再读出捕获值和跳变方向交给解码函数处理。捕获值用16位无符号变量保存计算时间差时直接用当前值减上次值利用无符号数的自然回绕特性就能正确处理计数器溢出不需要额外判断。void TIM2_IRQHandler(void) { uint16_t capture; if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); capture TIM_GetCapture1(TIM2); EM4100_ProcessEdge(capture, 1); // 1表示跳变后为高电平 } if (TIM_GetITStatus(TIM2, TIM_IT_CC2) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC2); capture TIM_GetCapture2(TIM2); EM4100_ProcessEdge(capture, 0); // 0表示跳变后为低电平 } }这里有一个优先级问题同一个引脚发生上升沿和下降沿不可能同时触发所以先判断哪个通道都无所谓。但两个通道的中断标志必须都清除否则会一直在中断里打转。4.4 解码状态机从边沿序列反推曼彻斯特码流核心思路是相邻两次边沿之间的时间内电平保持不变。如果时间差约等于256us说明这段时间只覆盖了半个位周期如果约等于512us说明覆盖了一个完整位周期即跨过了一个比特边界且边界两侧电平相同。据此可以还原出一串“半位序列”每两个半位合成一个逻辑位。#define HALF_MIN 180 #define HALF_MAX 330 #define FULL_MIN 390 #define FULL_MAX 640 #define FRAME_GAP 1200 static uint8_t half_buf[160]; static uint16_t half_cnt 0; static uint16_t last_time 0; static uint8_t last_level 0; void EM4100_ProcessEdge(uint16_t cur_time, uint8_t new_level) { uint16_t dt cur_time - last_time; uint8_t n, i; // 帧间隙说明上一帧已经结束重新开始 if (dt FRAME_GAP) { half_cnt 0; half_buf[half_cnt] new_level; last_time cur_time; last_level new_level; return; } if (half_cnt 2) { // 起始阶段还没有建立足够的历史先把第一个电平存下来 if (half_cnt 0) { half_buf[half_cnt] last_level; } } if (dt HALF_MIN dt HALF_MAX) { n 1; } else if (dt FULL_MIN dt FULL_MAX) { n 2; } else { // 间隔异常判定为干扰重新同步 half_cnt 0; last_time cur_time; last_level new_level; return; } for (i 0; i n half_cnt sizeof(half_buf); i) { half_buf[half_cnt] last_level; } last_time cur_time; last_level new_level; if (half_cnt 128) { if (EM4100_ParseHalfBuf(half_buf, 128)) { half_cnt 0; } else { // 相位可能差半个位周期移位后再试一次 for (i 0; i 127; i) { half_buf[i] half_buf[i 1]; } half_cnt 127; } } }这段代码里的“相位偏移重试”是曼彻斯特解码最容易遗漏的细节。因为第一个边沿可能是上升沿也可能是下降沿如果直接把半位序列按固定两两分组可能整体错位一个半位。解决办法是用“9位全1同步头偶校验”去验证验证不通过就把半位序列左移一位重新分组这样无论是哪种起始相位都能正确解码。4.5 帧解析与卡号输出解析函数把半位序列按每两个半位合并成逻辑位然后检查同步头、偶校验和停止位。uint8_t EM4100_ParseHalfBuf(uint8_t *half, uint16_t len) { uint8_t bits[64]; uint8_t i, j, parity; uint64_t card_id 0; // 半位对转换为逻辑位高-低为1低-高为0 for (i 0; i 64 i * 2 1 len; i) { if (half[i * 2] 1 half[i * 2 1] 0) { bits[i] 1; } else if (half[i * 2] 0 half[i * 2 1] 1) { bits[i] 0; } else { return 0; } } if (i 64) return 0; // 同步头前9位必须全1 for (i 0; i 9; i) { if (bits[i] ! 1) return 0; } // 版本/厂商码可选 // uint8_t ver (bits[9] 3) | (bits[10] 2) | (bits[11] 1) | bits[12]; // 列校验从第13位开始每5位一组偶校验 for (i 0; i 10; i) { parity 0; for (j 0; j 5; j) { parity ^ bits[13 i * 5 j]; } if (parity ! 0) return 0; } // 停止位必须为0 if (bits[63] ! 0) return 0; // 提取40位数据组成卡号 for (i 0; i 40; i) { card_id (card_id 1) | bits[13 (i / 4) * 5 (i % 4)]; } printf(Card ID: %010llu\r\n, (unsigned long long)card_id); return 1; }需要注意数据位的索引要避开每组的校验位。公式13 (i/4)*5 (i%4)的意思是每4个数据位往后跳过一个校验位正好贴合帧格式。卡号用%010llu打印成10位十进制数与市面上大多数EM4100门禁系统的显示习惯一致。5. 实测调参与踩坑记录5.1 定时器溢出会不会影响解码理论上16位计数器在1MHz下每65.536ms溢出一次而EM4100相邻边沿间隔最大也就512us远小于溢出周期所以单次间隔测量不会受到溢出影响。真正需要注意的是帧间隙判断。EM4100每帧之间通常有一段“无调制”的间隔这个间隔如果超过1200us会被判定为新帧开始。如果某些读卡模块连续发帧不间隔则可以通过丢弃无效解析结果来不断重同步实际测试中只要卡片靠近天线几乎立即就能出卡号。5.2 边沿抖动导致偶发误码曼彻斯特编码最怕的是边沿抖动表现为某个边沿提前或延后几百us导致256us和512us的判定出错。我遇到过的原因是天线谐振频率偏了检波后的包络幅度变化慢比较器触发点不稳定。排查方法很简单把74HC14输出端接到示波器观察边沿是否干脆利落如果边沿有长时间爬坡先调整谐振电容值和比较器参考电压不要急着改程序。输入捕获滤波器也能帮上忙。TIM_ICFilter0x08在我的测试板上可以滤掉几us级的毛刺但如果信号本身质量太差滤波并不能解决根本问题。记住一个原则硬件上能做好的事不要丢给软件去擦屁股。5.3 解出的卡号偶尔会错位如果你发现大部分时候能解出卡号但卡号整体左移或右移一位几乎可以肯定是半位序列的相位对齐问题。我在代码里用“校验失败后左移一个半位再重试”的方式处理实测对起卡很有效。还有一种情况是数据组之间的列校验全过但同步头不是全1这时候多半是起始抓取的边沿不是真正的帧头而是前一帧的尾部数据解决方法是延长帧间隙判定阈值确保每次都是从完整头开始。5.4 卡片放偏之后读不出数据EM4100是低频耦合对天线和卡片的相对位置比较敏感。如果卡片距离天线太远检波出来的包络幅度过小比较器输出就会不稳定。可以试试把谐振电容换小一点让Q值升高天线两端电压会明显变大但Q值太高会压缩带宽编码波形的边沿可能变钝。这是一个折中的过程我通常用示波器同时看天线谐振电压和整形后的方波调到两者都满意为止。最后再说一个容易被忽略的习惯调试时把串口波特率设到115200通过串口打印卡号和校验结果比用调试器打断点方便得多。解出来的卡号最好和市售读卡器对比一下确认字节顺序一致因为不同系统的卡号排列方式可能不同有的显示10位十进制有的显示5字节十六进制代码里拼卡号的顺序做一次转换就够了。
返回列表