
几年前我第一次用RC522做读卡器网上找一段例程改吧改吧卡号就出来了。后来换了一个带独立NFC控制器的板子要自己跑协议栈才发现当初那些被封装掉的“读卡流程”才是真正的分水岭。ISO14443A读卡流程说白了就是从射频场建立、请求应答、防碰撞、选卡到应用层交互的完整状态机。这篇文章我准备把这条链路从头拆到尾把REQA、ATQA、防碰撞、SELECT、SAK、ATS这些环节背后的机制和工程细节一次说清楚。无论你是刚接触NFC的嵌入式新手还是被奇怪的读卡问题纠缠过一阵子的老手这篇文章应该能帮你补齐那些平时容易被忽略的底层逻辑。1. 从RF场到UIDISO14443A读卡流程到底在做什么1.1 五层台阶ISO14443A读卡流程不是一个“函数调用”而是一组按固定顺序发生的协议交互。标准里PCD读卡器和PICC卡片之间要通过多次消息往返才能从一张处于“未知状态”的卡变成可以交换应用数据的通信对象。这整个过程可以拆成五个阶段。第一阶段是射频场激活。PCD产生13.56MHz的载波场卡片进入场中后获得能量完成上电复位等待PCD发请求。第二阶段是请求阶段PCD发出REQA卡片回ATQA双方建立最基本的联系。第三阶段是防碰撞阶段场上同时有多张卡时PCD需要通过位级仲裁选出一张卡拿到完整UID。第四阶段是选择阶段PCD发送SELECT命令卡片返回SAK表明自己是否支持ISO14443-4、是否还有下一级防碰撞等关键信息。第五阶段才是应用层交互根据SAK选择Mifare原生命令还是ISO14443-4的APDU传输完成认证、读写、钱包扣款之类的具体业务。很多人在第一步就栽跟头是因为把这些阶段当成“发一条命令读一个数据”来写忽略了每个阶段都有独立的超时、重试和状态迁移逻辑。比如同样一张Mifare Classic卡在IDLE态和HALT态对同一个命令的反应完全不同。不理解状态机就会出现“这次能读下次读不了”的玄学问题。1.2 PICC的五个状态决定了你得按状态机来写程序ISO14443-3里给PICC定义了若干个状态工程上常用的有五个POWER-OFF、IDLE、READY、ACTIVE、HALT。每个状态下卡片对命令的响应能力不一样。POWER-OFF是卡片没有获得足够能量时的状态读卡器把场关掉卡就回到这里。卡片进场后上电进入IDLE态此时它只对REQA和WAKEUP有反应。收到REQA并且PCD通过防碰撞和SELECT流程选中它之后进入ACTIVE态这时才可以执行应用层命令。如果PCD发出HLTA卡片进入HALT态HALT态下它不再响应REQA只能被WAKEUP重新唤醒回IDLE态。这个状态模型解释了为什么读卡程序必须是状态机而不是顺序调用。你发REQA之前可能要先发一个WAKEUP把所有卡从HALT态拉回来否则上一次被HALT的卡不会理你。你做完防碰撞之后如果没走SELECT卡不会进入ACTIVE后面发任何读写命令都会被无视。所谓“读卡流程”本质上就是在驱动这张卡经历“IDLE→READY→ACTIVE”的状态迁移。2. REQA与ATQA请求阶段的短帧、应答与状态判断2.1 短帧里的REQA/WAKEUP差异REQA命令的值是0x26WAKEUP是0x52这两个命令都使用短帧格式发送短帧一共只有7bit有效数据。别看它们参数简单背后的分工很明确REQA只对IDLE态的卡片有效WAKEUP除了对IDLE态有效还能把HALT态的卡重新拉回IDLE态。实际工程里轮询代码通常会在每一轮最开始发一次WAKEUP再发REQA。原因是上一轮如果读过某张卡并发了HLTA这张卡正处于HALT态直接发REQA它根本不回应先用WAKEUP唤醒它这张卡才会回到IDLE态参与新一轮竞争。有些驱动偷懒只发REQA结果就是单张卡正常、多张卡轮换着读的时候经常丢卡。这里还要提一个容易被误解的点REQA/WAKEUP虽然是短帧但PICC回应ATQA使用的是标准帧而且ATQA需要用CRC_A校验。也就是说请求阶段看似简单其实已经涉及到了编码格式切换。调试时如果逻辑分析仪上看到ATQA波形乱七八糟先检查是不是PCD发送REQA时使用的是修正Miller编码、而解码ATQA时切换到了Manchester副载波解码很多自定义协议的读卡器就是在这两种编码切换上出问题的。2.2 ATQA中的UID长度和防碰撞能力ATQA是2字节数据不同厂商、不同型号的卡片差异很大。做底层驱动时不能把ATQA当成一个“无关紧要的应答”它里面包含UID长度和防碰撞能力的早期声明。常见经验值如下表所示注意这些只是“常见值”不同批次、不同封装的卡片可能存在差异最终以芯片数据手册为准。卡片类型常见ATQA常见SAK典型说明Mifare Classic 1K0x04000x084字节UID走Mifare原生命令Mifare Classic 4K0x04000x184字节UID块数更多Mifare Ultralight / NTAG0x44000x004字节UID走UL/NTAG命令DESFire EV10x03440x207字节UID支持ISO14443-4ATQA的低位部分一般会体现UID长度和防碰撞是否完整但厂商自定义位也很多所以不要写“ATQA等于某个固定值就认为是什么卡”的强判断代码而是把它作为流程分支的一个参考。实际项目中更稳妥的做法是先用ATQA判断基本类型范围再结合SAK一起做最终派发。2.3 轮询时最容易犯的固定超时错误请求阶段还有一个很容易踩的坑REQA发出后ATQA并不是“立刻”回来。ISO14443A定义了PICC响应与PCD帧之间的时间关系叫FDT帧延迟时间同时还规定了卡片最大响应时间FWT。对于Type A卡片的响应存在一个最小间隔量级通常在几十微秒到上百微秒而最大响应时间默认大概在5毫秒量级并且卡片后续可以通过ATS协商修改。很多自研读卡器在轮询阶段用一个很短固定超时比如200微秒结果在低温、弱场或者卡片离天线较远时频繁读不到卡。因为卡片收到REQA后内部需要完成场整流、上电稳定、协议状态机切换最后再回ATQA这个时间不是一个严格定值。工程上我一般把轮询阶段的ATQA等待窗口设置为520毫秒同时配合多次重试而进入ISO14443-4块传输后再根据ATS协商的FWT动态配置块超时。这里的原则是请求阶段宁长勿短块传输阶段按卡片的FWT走。3. 防碰撞的级联机制为什么一张卡会来回“冒泡”3.1 多卡同时进场数据为什么会撞车当多张卡同时处于IDLE态PCD发出REQA后它们可能都会回复ATQA。更麻烦的是在防碰撞阶段PCD要求所有IDLE/READY态的卡发送自己的UID如果两张卡UID的前缀相同它们会在同一时刻上拉或下拉负载PCD解码时就会看到一位同时出现0和1的边沿这就是“碰撞”。ISO14443A的防碰撞机制采用“位级仲裁”PCD先让所有卡发送完整UID片段通过Manchester编码检测第一位碰撞的位置然后PCD只发送碰撞之前已经正确收到的位要求卡在剩余位继续竞争。每轮都能筛掉一批卡最终只留下一张。这个过程很像一群人同时喊自己的编号你只听清了前几位就让大家从听得清的位置继续往下喊直到只剩一个人。3.2 NVB参数的正确计算方式防碰撞命令的关键参数是NVB表示“本次命令中PCD实际发送了多少有效位”。这个数字包含SEL和NVB这两个字节而不只是后面的UID数据。NVB的高半字节表示完整发送的字节数低半字节表示最后一个不完整字节中发送的有效bit数。例如0x20表示只发送了2个完整字节SEL、NVB后面的UID数据全部不发送这是典型的“请求完整UID”命令0x50表示发送了5个完整字节也就是SEL、NVB以及3字节UID前缀0x53表示发送了5个完整字节外加第6个字节的前3bit。计算NVB的代码非常简单但错就错在很多人把UID数据的长度直接当成字节数去拼NVB忽略SEL和NVB本身也要计入。一个稳妥的写法是这样static uint8_t build_nvb(int complete_bytes, int valid_bits) { return (uint8_t)((complete_bytes 4) | (valid_bits 0x0F)); } // 只发SELNVB请求完整UID片段 uint8_t nvb_request build_nvb(2, 0); // 0x20 // 选择命令SELNVB5字节数据4字节UIDBCC uint8_t nvb_select build_nvb(7, 0); // 0x703.3 CT标记与多级UID拆分ISO14443A的UID有三种长度4字节、7字节、10字节。4字节UID只需要一级防碰撞就可以完成7字节UID需要两级10字节UID需要三级。这里有个容易懵的点级联防碰撞里每级固定携带4字节“UID片段”但第一位可能是级联标记CTCT的值是0x88。如果PCD在第一级防碰撞收到的前4字节中首字节是0x88说明后面跟着的只有3个真正的UID字节当前级并不能凑出完整UID必须继续下一级。以7字节UID为例第一级拿到的数据结构是CT0x88、UID0、UID1、UID2、BCC1。PCD先用SEL0x93完成这一级的选择然后切换到SEL0x95进行第二级防碰撞第二级才拿到UID3、UID4、UID5、UID6、BCC2。10字节UID进一步使用SEL0x97。每一级都要做完整的防碰撞和SELECT不能跨级跳。判定当前级是否结束的方法是看收到片段的首字节是不是0x88以及最后SAK中代表“UID是否完整”的bit。两个信号配合使用才能确认级联是否继续。3.4 BCC校验一个字节保护一串卡号每一级UID片段包含5字节数据最后一个字节是BCC它是前四个字节的异或结果。BCC的作用是防止防碰撞阶段因误码选出错误UID。static uint8_t compute_bcc(const uint8_t uid_cln[4]) { return uid_cln[0] ^ uid_cln[1] ^ uid_cln[2] ^ uid_cln[3]; }举个例子某一级收到的4字节是0x04、0x3A、0x5B、0x01那么BCC就是0x04 ^ 0x3A ^ 0x5B ^ 0x01 0x64。如果收到的第5字节不是0x64说明这一级数据已经被干扰应当丢弃并重新发起防碰撞而不是继续往下走。工程实现中防碰撞循环要有次数上限不能无限重试。常见做法是单级最多重试35次达到上限就放弃本轮轮询回到REQA阶段重新开始避免因为一张损坏卡卡死整个读卡流程。4. SELECT与SAK选卡之后的协议分叉点4.1 SELECT命令的NVB为什么是0x70防碰撞成功后PCD会发送SELECT命令明确告诉场上的卡“我要选择你”。SELECT命令由SEL、NVB、完整UID片段4字节加BCC、CRC_A组成。之前我说过NVB计算要把SEL和NVB计入。完整数据时SEL占1字节、NVB占1字节、UID片段加BCC占5字节一共7字节所以NVB自然就是0x70。正因为如此很多人误以为0x70是“发7字节数据”其实它指的是整个命令里SELNVB数据字段一共7个完整字节CRC_A不算在内。收到SELECT后PICC会回传SAK卡片这时才真正进入ACTIVE态。所以判断“选卡是否成功”不能只看SELECT发出去没、有没有回包而要解析SAK内容。4.2 SAK里的三个关键bitSAK是一个字节不同厂商的卡对这个字节的自定义位非常多但标准和工程上最常用的三个信息点如下。第一个是UID是否完整。SAK中bit6为1说明当前级联还没有完成后面还要继续防碰撞bit6为0说明UID已经完整选卡成功。第二个是是否支持ISO14443-4。SAK中bit5为1代表卡片支持ISO14443-4的块传输协议流程要切换到RATS/ATS分支bit5为0代表卡片走Mifare原生命令或者其他私有命令集。第三个是厂商/型号信息。这部分没有统一标准比如Mifare Classic 1K常见SAK0x08Mifare Classic 4K常见SAK0x18NTAG常见SAK0x00。建议把这些值做成白名单而不是用bit位去猜型号。工程上最稳妥的派发逻辑是先看bit6判断级联是否结束再看bit5判断是否走ISO14443-4最后用完整SAK查白名单确定具体驱动。SAK返回0x08的卡直接走Mifare Classic认证读写SAK返回0x20的卡进入ATS流程再通过ATS内容确认具体协议能力。4.3 分叉后的两条路Mifare原生命令与ISO14443-4SAK一旦返回读卡流程就分叉了。Mifare Classic这类卡不走ISO14443-4而是使用NXP私有的认证和读写命令。ISO14443-4是标准块传输协议适用于DESFire、Java Card、部分银行卡等需要先走RATS/ATS协商参数。这个分叉点是底层驱动设计中最容易写乱的部分。很多通用NFC库会在SAK返回后统一尝试ATS结果遇到Mifare Classic时卡片不会响应RATS白白耗费超时反过来一些只写过Mifare Classic驱动的工程师看到DESFire的SAK也直接走Crypto-1认证结果卡在认证命令上报错。正确做法是把SAK作为主状态机的一个分支条件两个协议栈并行实现由SAK决定启用哪一套。5. 应用层交互流程认证、块传输与HALT收尾5.1 Mifare Classic的扇区认证与读写Mifare Classic 1K一共16个扇区每个扇区4个块每个块16字节。认证的单位是扇区而不是块。也就是说你要读写某个块必须先对所在扇区做认证。认证命令的关键字是0x60Key A或0x61Key B后面跟块号和密钥。卡片收到后会返回一个随机数之后所有数据交互都通过Crypto-1流密码加密。读写单个块时读命令是0x30加块号写命令是0xA0加块号和16字节数据。注意这里的“读写”是整块读写驱动层需要做好块号与扇区号的换算。这个流程有几个常见问题。一是认证失败后不能立刻重试同一个扇区卡片内部有错误计数器频繁重试可能让卡片在一段时间内拒绝认证。二是认证状态只对当前选中的扇区有效切换到另一个扇区必须先重新认证。三是数据写入后建议立即读回校验因为近距离无接触传输对场强波动很敏感单纯“写成功”并不等于数据落对了。5.2 ISO14443-4的RATS/ATS与块传输支持ISO14443-4的卡片在收到SAK之后再收到RATS命令0xE0加参数才会进入标准块传输模式。RATS里的参数可以指定PCD的帧尺寸FSD和逻辑通道CID卡片返回ATS作为应答。ATS里包含TL、T0、TA、TB、TC和历史字节核心作用是告诉PCD卡片能支持多大的帧尺寸、要不要CID、要不要NAD、以及默认FWT大概是多少。拿到ATS之后后续数据交换使用I-block信息块、R-block确认块和S-block控制块。I-block承载真正的APDU数据R-block负责ACK或NAK确认S-block用于WTX等待时间扩展和DESELECT。实际开发中很多自定义驱动在这里栽跟头要么把RATS参数里的FSD设置得比卡片FSC还大导致卡片直接不回应要么忽略ATS里的FWI把块超时设得比卡片FWT短导致大流量传输频繁超时。我的经验是先把ATS原始字节完整解析出来再根据FWI计算WTX参数最后才进入APDU交互。5.3 HLTA的正确时机HLTA命令值0x50加0x00和CRC_A发送给卡片能让处于ACTIVE态的卡片进入HALT态。HALT态的好处是让卡片退出后续防碰撞竞争减少场上多卡时的干扰。这张卡读完不想要了应该立即发HLTA如果接下来还要读其他卡HLTA能让当前卡暂时“退下”下一轮轮询再通过WAKEUP把它叫回来。如果一直不发送HLTA这张卡虽然已经读完但它仍然处于ACTIVE态场上其他卡片在防碰撞时可能会收到来自它的干扰信号尤其在同时读取多张卡片的场景里会出现“串卡”。每次轮询开始时先发WAKEUP再发REQA的原因也是因为上一轮可能有些卡进入了HALT态。HALT态设计是ISO14443A协议里很基础但很容易被忽略的一环时序上一定要留给它足够的位置。6. 实测调优波形验证、时序边界与故障排查6.1 逻辑分析仪观察ISO14443A需要关注的信号调试底层读卡流程时一把采样率足够的逻辑分析仪比什么都管用。ISO14443A中PCD到PICC方向使用的是100% ASK调制、修正Miller编码PICC到PCD方向是847kHz副载波负载调制、Manchester编码。想同时观察两个方向的信号最好通过天线差分信号或者芯片内部解调输出点接探头。采样率建议不低于10MS/s有条件直接上50MS/s。ISO14443A的副载波是847kHzManchester编码的位速率是106kbps每个数据位对应约8个副载波周期太低采样率根本看不出碰撞时那种代表性边沿错乱。逻辑分析仪主要看三件事一是REQA发出去之后ATQA有没有按标准时序回来二是防碰撞阶段多个卡同时回应时Manchester波形是否出现碰撞特征三是SELECT之后SAK是否完整、CRC是否通过。把这四个关键帧抓下来保存成模板后面每次调天线、改驱动都拿新波形和模板对比效率会高很多。6.2 从波形反推代码问题的一次排查记录有一次我调试一款自研读卡器现象是读Mifare Classic 1K成功率只有六成失败时没有任何错误码直接收不到SAK。一开始怀疑天线功率不够加了功放反而更差。用逻辑分析仪抓波形后发现REQA、ATQA、防碰撞都正常SELECT命令也发出去了但卡片回SAK的起始位置比驱动等待窗口晚了大概60微秒。顺着波形继续看发现驱动在SELECT之后的接收窗口设成了固定300微秒而卡片因为内部Crypto-1状态初始化实际回包超过了这个值。修正方法是把SELECT后的响应等待改成动态参数先用一个较宽的初始窗口后续再根据卡片类型和实测调整。最终把超时窗口调到1毫秒成功率直接回到99%以上。这个案例说明读卡流程调试不能只盯着代码里的循环和返回值卡片的物理层响应时间与驱动超时窗口的匹配往往才是“灵异故障”的根源。6.3 读卡失败自查表下面这些排查点是我在实际项目中反复用到的建议直接打印出来贴工位。故障现象可能原因排查方向完全读不到卡天线失谐、场未建立、卡片不在场示波器看天线谐振波形确认卡片是Type A偶尔能读偶尔不能REQA后ATQA等待窗口过短延长请求阶段超时到520ms防碰撞循环卡死NVB计算错误、CRC校验失败抓波形确认NVB是否为正确的完整字节数多卡时选错卡未正确执行级联或忘记HLTA按SEL0x93/0x95/0x97逐步走完整状态机SAK后协议分支错误只判断SAK的bit位没做白名单先看bit6级联再看bit5支持14443-4最后查白名单7. 工程落地读卡芯片选型与低功耗轮询代码骨架7.1 几类读卡方案的适用边界做读卡器选型时很多人只盯着“支持ISO14443A”这一句话其实不同芯片对ISO14443A的覆盖深度差异巨大。方案特点适合场景RC522/RC522成本低、资料多但对ISO14443-4支持较弱Mifare Classic门禁、简易读卡器PN532内嵌完整NFC协议栈支持14443A/B、Felica原型验证、通用NFC模块CLRC663射频功率强、支持多种协议适合专业读写器工业读卡器、多协议设备ST25R3916接收灵敏度高、低功耗轮询能力强低功耗手持设备、高要求嵌入式选型时除了看协议支持还要看芯片是否自带完整防碰撞状态机。自带协议栈的PN532让你不用关心NVB和SAK但代价是功耗和灵活性都受限用RC522这类射频前端自己跑协议则必须把ISO14443A的状态机吃透这也是我为什么建议每位嵌入式工程师至少手动实现一遍读卡流程的原因。7.2 低功耗轮询的设计要点电池供电的读卡器对功耗非常敏感低功耗轮询的核心思路是“短时间打场快速判断有没有卡没有立刻掉场”。具体做法是周期性唤醒MCU打开射频场发送WAKEUP和REQA等待ATQA的时间窗口控制在几个毫秒以内。如果收到ATQA继续执行完整读卡流程如果没有收到马上掉电关场进入睡眠等待下一个周期。打场时间越短平均功耗越低但太短会导致卡片上电不稳、响应概率下降所以这个窗口需要实测权衡。部分芯片比如ST25R3916有自动低功耗轮询模式可以配置脉冲场参数、轮询周期和协议类型MCU可以全程睡眠检测到卡片再被中断唤醒。使用这类芯片时仍然需要理解ISO14443A读卡流程因为自动轮询只解决“发现卡”发现之后的防碰撞、选卡和应用层流程还是要由芯片输出的中断事件驱动来完成。7.3 一个可直接上手的轮询状态机伪代码下面给出一个典型轮询状态机的骨架它把流程拆成“请求、防碰撞、选卡、派发”四段方便移植到不同芯片平台int iso14443a_poll(uint8_t *uid, uint8_t *uid_len, uint8_t *sak) { uint8_t uid_cl1[5]; uint8_t uid_cl2[5]; uint8_t uid_cl3[5]; // 1. 先用WAKEUP把HALT态的卡拉回IDLE再发REQA send_short_frame(WAKEUP); send_short_frame(REQA); if (wait_atqa(20) 0) { return -1; } // 2. 第一级防碰撞 if (anticollision(SEL_CL1, uid_cl1) 0) { return -2; } // 3. 如果遇到级联标记逐级向上 if (uid_cl1[0] CT) { if (select_card(SEL_CL1, uid_cl1) 0) { return -3; } if (anticollision(SEL_CL2, uid_cl2) 0) { return -4; } if (uid_cl2[0] CT) { if (select_card(SEL_CL2, uid_cl2) 0) { return -5; } if (anticollision(SEL_CL3, uid_cl3) 0) { return -6; } // 组装10字节UID memcpy(uid, uid_cl1[1], 3); memcpy(uid 3, uid_cl2[1], 4); memcpy(uid 7, uid_cl3, 4); *uid_len 10; } else { // 组装7字节UID memcpy(uid, uid_cl1[1], 3); memcpy(uid 3, uid_cl2, 4); *uid_len 7; } } else { // 4字节UID memcpy(uid, uid_cl1, 4); *uid_len 4; } // 4. 发送SELECT接收SAK进入协议派发 if (select_card_for_sak(uid, *uid_len, sak) 0) { return -7; } // 5. 这里根据SAK走Mifare原生分支或ISO14443-4分支 if ((*sak 0x20) ! 0) { enter_iso14443_4(); } else { enter_mifare_classic(); } return 0; }这段代码省略了超时重发、CRC校验和错误恢复实际工程里每步都要加次数限制和状态清理。尤其是anticollision函数内部必须根据碰撞位置动态调整NVB不能只是一次性请求完整UID。这段骨架的价值是让你对整个读卡流程的“体积”有个直观认识真正能用的轮询逻辑远比一段示例代码复杂。我在好几个项目里实践下来的体会是ISO14443A读卡流程的难点从来不在某个单一命令而在阶段之间的状态衔接。每次拿到新的读卡芯片或者新的卡片类型我第一件事不是写业务逻辑而是用逻辑分析仪把REQA到SAK的完整波形跑一遍把标准时序存成模板。之后所有问题包括天线调参、超时配置、协议派发都以这份模板作为对照基准。这个习惯帮我省掉了大量“今天能读明天不能读”的排查时间建议你也试试。