最近在搞一个13.56MHz的RFID读卡项目核心用了复旦微的FM17550芯片来读写S50卡也就是大家常说的M1卡。从硬件接线到代码全部跑通之后发现网上的资料虽然不少但大多零散着讲寄存器或者只讲模块很少有把“硬件连接、原理分析、完整代码”串起来说清楚的。这篇文章就把我实际调试的经验整理出来从FM17550的选型原因、和单片机的连接方式到ISO14443A的通信流程、完整的读写代码最后再附上我踩过的几个典型坑给准备做门禁读卡器、智能储物柜终端、校园卡读写设备或者单纯想学RFID开发的同行一个可复现的参考。1. 整体设计思路为什么选FM17550来读写S50卡1.1 项目需求与芯片选型先说需求背景。要做的是一个嵌入式读卡终端功能很简单靠近卡片读取出UID然后对卡片指定扇区进行数据的读和写。卡片选定为S50卡这是市场上非常普及的Mifare Classic 1K 卡片容量1KB很多门禁、考勤、会员卡用的就是它。读卡芯片方面市面上常见的选择无外乎NXP的RC522、RC522复旦微的FM17550或者更高级的PN532。FM17550之所以被我选中核心原因有三个第一FM17550和RC522在引脚上基本兼容这意味着大量RC522的成熟模块、天线设计方案可以直接复用生态资料非常丰富。第二FM17550本身支持SPI、I2C、UART三种主机接口像STM32、Arduino、ESP32这类常见单片机都能很方便地接进去灵活性很高。第三复旦微是国产芯片供货和成本在项目量产阶段更有优势。不过这里有一个关键提醒FM17550虽然和RC522寄存器大体一致但并非100%完全相同个别细节寄存器有差异我后面会专门讲这个问题。从整个技术方案来看读卡终端的链路其实不复杂单片机通过SPI总线操作FM17550的寄存器FM17550通过天线线圈与S50卡进行13.56MHz的无线通信完成寻卡、防碰撞、认证、读写块等操作。真正让新手头疼的往往不是芯片本身而是对M1卡存储结构和ISO14443A通信流程的理解不到位。1.2 适合谁参考以及最终效果这篇内容适合以下几类人第一次接触RFID读卡开发、手里有RC522/FM17550模块但不知道怎么写出完整读写功能的嵌入式开发者想把读卡功能集成到自己的STM32或者单片机项目里的朋友以及想搞清楚S50卡扇区、块、密钥这些概念背后的实际用法的初学者。整个项目跑通后最终能达到的效果是单片机启动初始化FM17550串口打印等待刷卡提示刷卡时能正确读到并打印出卡片的4字节UID然后对指定扇区的一个数据块执行默认密钥认证读出16字节数据再写入一组新的16字节数据。整个过程稳定卡片没有卡死或认证失败的情况。2. 硬件连接FM17550与单片机的连线细节2.1 接口模式选择与引脚说明FM17550支持三种主机接口通过芯片上的接口选择引脚来配置。我实际使用的是SPI模式速度够快接线也最简洁。SPI模式下主要引脚分配如下VCC接3.3V电源注意FM17550不能直接接5V否则长时间运行容易损坏。GND公共地。SDA/NSSSPI片选信号低电平有效接单片机的一个普通GPIO。SCKSPI时钟。MOSI主机发送、从机接收。MISO主机接收、从机发送。RST复位引脚低电平有效可以接GPIO控制硬复位。IRQ中断请求输出一般调试时可以悬空不接省一个IO。这里必须强调一个高频踩坑点很多RC522模块上的SDA引脚其实就是SPI片选不是I2C的数据线。如果你拿RC522模块但按FM17550裸芯片的引脚定义去接线很容易搞混。更稳妥的做法是拿到模块后先确认丝印SDA接片选SCK接时钟MOSI和MISO按SPI接线对应。2.2 以STM32F103为例的接线表我用的主控是STM32F103C8T6也就是常见的Blue Pill开发板。FM17550模块和STM32的最简接线如下FM17550引脚STM32F103引脚说明VCC3.3V电源GNDGND地SDA/NSSPA4SPI片选软件控制SCKPA5SPI时钟MOSIPA6主机输出MISOPA7主机输入RSTPA3复位控制IRQ不接悬空即可如果你用的是Arduino对应关系也很简单VCC接3.3VGND接GNDSDA接D10SCK接D13MOSI接D11MISO接D12RST接D9。如果单片机是5V逻辑电平比如传统的51单片机建议加电平转换芯片或者至少在FM17550的SPI引脚上串电阻分压否则长期运行有风险。2.3 天线设计和读卡距离的注意事项很多新人以为FM17550读卡距离只和功率有关系其实天线的匹配影响巨大。FM17550需要外接一个27.12MHz的晶振天线部分通常由TX1、TX2、RX引脚连接LC匹配电路再接线圈。成品模块的天线已经调好读卡距离一般能到3到5厘米满足门禁类应用。如果你是自己画板子做天线务必注意天线线圈的尺寸和匝数常见的推荐值是5x5厘米到6.5x6.5厘米的矩形线圈3到4匝配合匹配电容谐振在13.56MHz。我调试时遇到过一个情况用某宝买的裸模块第一天读卡距离正常第二天换了一个卡片后距离明显缩短。排查后发现不是天线问题而是卡片本身的天线线圈尺寸和品质差异。不同厂家生产的S50卡天线参数会有区别这是正常的不必过分纠结。3. S50卡的存储结构与ISO14443A通信流程3.1 搞清楚S50卡内部到底是怎么组织的S50卡容量是1024字节但不是一整块连续内存而是分成16个扇区每个扇区4个块每个块16字节。这个结构非常重要因为所有读写操作都是基于“块地址”进行的。扇区块地址范围用途说明扇区0块0~块3块0是厂商块存放UID等出厂只读块1~块2是数据块块3是尾部块扇区1块4~块7数据块块4~块6尾部块块7扇区2块8~块11数据块块8~块10尾部块块11.........扇区15块60~块63数据块块60~块62尾部块块63每个扇区的块3是尾部块结构固定前6字节是密钥A中间4字节是访问位控制字节后6字节是密钥B。默认出厂情况下密钥A和密钥B都是FF FF FF FF FF FF访问位允许用密钥A或密钥B读取数据块和写入数据块。这就是为什么很多教程里用默认密钥就能直接读写S50卡。扇区0的块0尤其特殊里面存放的是UID4字节、BCC校验字节、SAK字节、ATQA字节以及厂商数据。这块数据在普通S50卡上是只读的如果尝试往块0写数据卡片会返回错误。这也是很多新手“写卡失败”的原因——他们拿着默认密钥认证成功后直接往块0写数据结果发现怎么也写不进去。3.2 读卡必须走完的六步流程S50卡遵循ISO14443A Type A协议整个读卡过程不是“发一个读命令就能读”而是一个严谨的状态机。完整流程分六步第一步寻卡。单片机发送REQA请求指令0x26卡片应答返回2字节的ATQA对于S50卡来说通常是0x04 0x00。第二步防碰撞。发送防碰撞命令0x93 0x20卡片返回4字节UID加1字节BCC校验码。第三步选卡。发送选卡命令把UID和BCC发回去卡片返回1字节SAKS50卡通常返回0x08。第四步认证。发送认证命令0x60使用密钥A或0x61使用密钥B加上目标块地址、6字节密钥和4字节UID。认证通过后该扇区内的数据块才可以读写。第五步读操作。发送0x30加上块地址卡片返回16字节数据。第六步写操作。发送0xA0加块地址卡片返回确认信号然后再发送16字节数据加CRC卡片再次确认写成功。之所以有防碰撞这一步是因为13.56MHz射频场里可能同时有多张卡。防碰撞算法通过UID逐位比较确保同一时间只锁定一张卡进行通信。如果你在开发过程中发现读卡偶尔会读到随机UID多半是没做防碰撞或者防碰撞实现有缺陷。4. 完整代码实现从底层寄存器到应用层读写4.1 底层SPI通信与寄存器读写封装我强烈建议在项目初期就用软件模拟SPI来跑通逻辑确认功能正常后再切换硬件SPI。软件GPIO模拟的好处是方便移植到任意单片机而且调试排错时可以直接用逻辑分析仪看时序。下面这段代码就是基础软件SPI读写#define SPI_CLK_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_5) #define SPI_CLK_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_5) #define SPI_MOSI_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_6) #define SPI_MOSI_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_6) #define SPI_MISO_READ() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7) #define SPI_CS_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4) #define SPI_CS_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4) static void SPI_WriteByte(uint8_t byte) { for (uint8_t i 0; i 8; i) { SPI_CLK_LOW(); if (byte 0x80) { SPI_MOSI_HIGH(); } else { SPI_MOSI_LOW(); } byte 1; SPI_CLK_HIGH(); } } static uint8_t SPI_ReadByte(void) { uint8_t value 0; for (uint8_t i 0; i 8; i) { SPI_CLK_LOW(); value 1; if (SPI_MISO_READ()) { value | 0x01; } SPI_CLK_HIGH(); } return value; }有了SPI读写字节的函数就可以封装FM17550寄存器操作了。FM17550在SPI模式下的寄存器地址格式是地址左移1位最低位用来区分读写方向。写寄存器时最低位为0读寄存器时最低位为1。代码如下void FM17550_WriteReg(uint8_t reg, uint8_t value) { SPI_CS_LOW(); SPI_WriteByte((reg 1) 0x7E); SPI_WriteByte(value); SPI_CS_HIGH(); } uint8_t FM17550_ReadReg(uint8_t reg) { uint8_t value; SPI_CS_LOW(); SPI_WriteByte(((reg 1) 0x7E) | 0x80); value SPI_ReadByte(); SPI_CS_HIGH(); return value; }有了寄存器读写再额外封装两个常用的位操作函数置位和清位。这在后续配置中断、控制FIFO时非常常用。void FM17550_SetBitMask(uint8_t reg, uint8_t mask) { uint8_t tmp FM17550_ReadReg(reg); FM17550_WriteReg(reg, tmp | mask); } void FM17550_ClearBitMask(uint8_t reg, uint8_t mask) { uint8_t tmp FM17550_ReadReg(reg); FM17550_WriteReg(reg, tmp (~mask)); }4.2 芯片初始化不要随便抄RC522的初始化FM17550的初始化主要工作是复位芯片、关闭内部CRC计算、配置定时器、开启天线。网上大量RC522的初始化代码可以直接参考但FM17550个别配置项有差异尤其是一些时序相关的寄存器。下面的初始化代码在我测试的多颗FM17550上都能稳定工作void FM17550_Init(void) { // 硬复位 GPIO_ResetBits(GPIOA, GPIO_Pin_3); delay_ms(10); GPIO_SetBits(GPIOA, GPIO_Pin_3); delay_ms(10); // 软复位 FM17550_WriteReg(0x01, 0x0F); delay_ms(2); FM17550_WriteReg(0x01, 0x00); // 配置定时器设置超时时间 FM17550_WriteReg(0x2A, 0x8D); // TModeReg FM17550_WriteReg(0x2B, 0x3E); // TPrescalerReg FM17550_WriteReg(0x2C, 0x03); // TReloadRegH FM17550_WriteReg(0x2D, 0xE8); // TReloadRegL // 配置发送和接收 FM17550_WriteReg(0x11, 0x3D); // ModeReg FM17550_WriteReg(0x14, 0x83); // TxControlReg开启天线 FM17550_WriteReg(0x15, 0x40); // TxASKReg // 清FIFO FM17550_SetBitMask(0x0A, 0x80); }初始化完成后可以读一下版本寄存器0x37验证芯片是否工作。FM17550读回的值不同批次会有差异但通常不会是0x00或0xFF否则基本可以断定SPI通信有问题或者芯片没正常工作。4.3 核心通信函数向卡片发送数据并接收应答所有和S50卡的命令交互最终都是通过FM17550的FIFO寄存器发送数据、然后等待接收应答的。下面这段是通用的收发函数也是整个驱动里最核心、最容易出问题的地方uint8_t FM17550_ToCard(uint8_t command, uint8_t *sendData, uint8_t sendLen, uint8_t *backData, uint16_t *backLen) { uint8_t irqEn 0x00; uint8_t waitIRq 0x00; uint8_t lastBits; uint8_t n; uint16_t i; if (command 0x0E) // PCD_AUTHENT 认证命令 { irqEn 0x12; waitIRq 0x10; } else if (command 0x0C) // PCD_TRANSCEIVE 收发命令 { irqEn 0x77; waitIRq 0x30; } FM17550_WriteReg(0x02, irqEn | 0x80); FM17550_ClearBitMask(0x04, 0x80); FM17550_SetBitMask(0x0A, 0x80); FM17550_WriteReg(0x01, 0x00); for (i 0; i sendLen; i) { FM17550_WriteReg(0x09, sendData[i]); } FM17550_WriteReg(0x0D, 0x00); FM17550_WriteReg(0x01, command); if (command 0x0C) { FM17550_SetBitMask(0x0D, 0x80); } i 1000; do { n FM17550_ReadReg(0x04); // ComIrqReg i--; } while ((i ! 0) !(n 0x01) !(n waitIRq)); FM17550_ClearBitMask(0x0D, 0x80); if (i 0) { return 1; // 超时 } if ((FM17550_ReadReg(0x06) 0x1B) ! 0x00) { return 2; // 通信错误 } if (command 0x0C) { n FM17550_ReadReg(0x0A); // FIFOLevelReg lastBits FM17550_ReadReg(0x0C) 0x07; *backLen (n - 1) * 8 lastBits; if (n 64) { n 64; } for (i 0; i n; i) { backData[i] FM17550_ReadReg(0x09); } } return 0; }这段代码看起来长但逻辑很清晰先根据命令类型配置中断然后往FIFO里写数据启动对应命令轮询等待完成中断最后从FIFO中读出应答数据。状态返回值0表示成功1表示超时2表示通信错误方便上层函数判断。4.4 寻卡、防碰撞、选卡拿到UID的完整过程拿到UID是后续所有操作的前提。S50卡的寻卡、防碰撞、选卡三个操作需要依次完成。寻卡发送0x26只等待2字节的ATQA应答uint8_t FM17550_Request(uint8_t reqMode, uint8_t *tagType) { uint8_t status; uint16_t backBits; FM17550_WriteReg(0x0D, 0x07); status FM17550_ToCard(0x0C, reqMode, 1, tagType, backBits); if ((status 0) (backBits 0x10)) { return 0; } return 1; }防碰撞命令发送0x93 0x20返回4字节UID和1字节BCC校验码。BCC是前4字节的异或值代码里要校验否则可能拿到错误的UIDuint8_t FM17550_Anticoll(uint8_t *serial) { uint8_t status; uint8_t i; uint8_t serNumCheck 0; uint8_t buffer[2] {0x93, 0x20}; uint16_t backBits; status FM17550_ToCard(0x0C, buffer, 2, buffer, backBits); if (status 0) { for (i 0; i 4; i) { serNumCheck ^ buffer[i]; } if (serNumCheck ! buffer[4]) { return 1; // 校验失败 } for (i 0; i 4; i) { serial[i] buffer[i]; } return 0; } return 1; }选卡指令0x93 0x70后面要带上UID和BCC并且整个帧需要计算CRC附加在末尾。很多新手会忘记选卡时的CRC导致选卡一直不成功。计算CRC可以利用FM17550内部的硬件CRC协处理器uint8_t FM17550_CalculateCRC(uint8_t *data, uint8_t len, uint8_t *result) { uint8_t i; uint8_t n; FM17550_ClearBitMask(0x05, 0x04); FM17550_SetBitMask(0x0A, 0x80); for (i 0; i len; i) { FM17550_WriteReg(0x09, data[i]); } FM17550_WriteReg(0x01, 0x03); // PCD_CALCCRC i 0xFF; do { n FM17550_ReadReg(0x05); i--; } while ((i ! 0) !(n 0x04)); result[0] FM17550_ReadReg(0x22); result[1] FM17550_ReadReg(0x21); return 0; } uint8_t FM17550_Select(uint8_t *serial) { uint8_t buffer[9] {0x93, 0x70}; uint8_t i; uint8_t checksum 0; uint16_t backBits; for (i 0; i 4; i) { buffer[2 i] serial[i]; checksum ^ serial[i]; } buffer[6] checksum; FM17550_CalculateCRC(buffer, 7, buffer[7]); uint8_t status FM17550_ToCard(0x0C, buffer, 9, buffer, backBits); if ((status 0) (backBits 0x18)) { return buffer[0]; // 返回SAKS50通常为0x08 } return 0xFF; }4.5 认证、读块、写块真正操作卡片数据认证是读写数据的前提。S50卡的认证命令中需要指定使用密钥A还是密钥B、目标块地址、6字节密钥和4字节UID。认证成功后FM17550内部会自动维护该扇区的加密通信状态后续读写不再需要重复传入UIDuint8_t FM17550_Auth(uint8_t authMode, uint8_t blockAddr, const uint8_t *key, uint8_t *uid) { uint8_t buffer[12]; uint16_t backBits; uint8_t status; buffer[0] authMode; // 0x60使用密钥A0x61使用密钥B buffer[1] blockAddr; memcpy(buffer[2], key, 6); memcpy(buffer[8], uid, 4); status FM17550_ToCard(0x0E, buffer, 12, buffer, backBits); if ((status 0) !(FM17550_ReadReg(0x08) 0x08)) { return 0; } return 2; }读块操作很简单向卡片发送0x30加块地址卡片返回16字节数据uint8_t FM17550_Read(uint8_t blockAddr, uint8_t *data) { uint8_t buffer[2] {0x30, blockAddr}; uint16_t backBits; uint8_t status FM17550_ToCard(0x0C, buffer, 2, buffer, backBits); if ((status 0) (backBits 0x90)) { memcpy(data, buffer, 16); return 0; } return 2; }写块稍微复杂要分两步先发送写命令0xA0加块地址卡片返回4位应答然后再发送16字节数据加CRC。第一步和第二步都要检查应答是否正确uint8_t FM17550_Write(uint8_t blockAddr, uint8_t *data) { uint8_t buffer[18]; uint16_t backBits; uint8_t status; buffer[0] 0xA0; buffer[1] blockAddr; FM17550_CalculateCRC(buffer, 2, buffer[2]); status FM17550_ToCard(0x0C, buffer, 4, buffer, backBits); if ((status ! 0) || (backBits ! 0x04) || ((buffer[0] 0x0F) ! 0x0A)) { return 2; } memcpy(buffer, data, 16); FM17550_CalculateCRC(buffer, 16, buffer[16]); status FM17550_ToCard(0x0C, buffer, 18, buffer, backBits); if ((status ! 0) || (backBits ! 0x04) || ((buffer[0] 0x0F) ! 0x0A)) { return 2; } return 0; }主流程串起来就是下面这样。实际项目中记得在死循环里加防抖逻辑避免快速重复刷卡导致状态混乱。int main(void) { // 初始化系统时钟、GPIO、串口、SPI引脚 SystemInit(); UART_Init(); SPI_GPIO_Init(); FM17550_Init(); uint8_t uid[4]; uint8_t tagType[2]; uint8_t key[6] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; uint8_t blockData[16]; printf(FM17550 RFID Reader Start\r\n); while (1) { if (FM17550_Request(0x26, tagType) 0) { if (FM17550_Anticoll(uid) 0) { printf(UID: %02X %02X %02X %02X\r\n, uid[0], uid[1], uid[2], uid[3]); uint8_t sak FM17550_Select(uid); if (sak ! 0xFF) { if (FM17550_Auth(0x60, 4, key, uid) 0) { if (FM17550_Read(4, blockData) 0) { printf(Read Block4: ); for (int i 0; i 16; i) { printf(%02X , blockData[i]); } printf(\r\n); } uint8_t newData[16] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10}; if (FM17550_Write(4, newData) 0) { printf(Write Block4 OK\r\n); } } } delay_ms(200); } } delay_ms(10); } }5. 常见问题排查与避坑实录5.1 读不到卡先查硬件再查时序这个问题在调试第一天几乎必现。我的排查顺序是先看FM17550模块上的LED是否闪烁或常亮不亮基本是电源问题然后用逻辑分析仪看SPI时钟和片选确认初始化时有没有波形最后读版本寄存器确认SPI通信是否正常。如果版本寄存器读回全FF大概率是MISO/MOSI接反了或者片选引脚控制反了。还有一个很容易被忽略的点FM17550模块的天线区域不要大面积覆盖金属物体否则会屏蔽射频场读卡距离骤减严重时完全读不到。调试的时候把模块垫高一点不要直接放在金属桌面上。5.2 认证失败密钥和块地址要仔细核对认证失败最常见的原因是密钥用错。S50卡出厂密钥是6字节全FF注意是6字节不是8字节。有些资料里把密钥描述成“FFFFFFFFFFFF”三个字节的十六进制组合实际发送时是要展开成6个字节0xFF 0xFF 0xFF 0xFF 0xFF 0xFF。第二个常见原因是块地址超过当前扇区范围。每个扇区只有4个块比如扇区0的块地址是0到3扇区1是4到7。如果你想读扇区1的数据块认证时用的块地址必须是4、5、6中的任何一个不能直接传16或17这种扇区编号。第三认证命令里必须带上正确的UID字节序。防碰撞读出来的UID是高位在前选卡和认证时要保持一致不要中间做大小端转换。5.3 写卡失败厂商块不能写CRC不能漏如果认证成功但写操作卡在第一步应答先检查目标块是不是厂商块。扇区0块0是S50卡的厂商信息块出厂只读任何情况下都写不进去。调试写功能时建议从扇区1的块4开始写这是最安全的数据块。如果写操作卡在第二步检查CRC有没有计算。部分简化的RC522库在发送写命令的第一阶段不做CRC也能工作但第二阶段的数据帧必须带CRC否则卡片不会返回写成功的应答。我实测发现两个阶段都严格按照规范附加CRC成功率最高尤其是在卡片质量一般的情况下。5.4 FM17550与RC522不能无脑替换FM17550是国产芯片主推的就是RC522的替代市场但这不意味着寄存器完全一样。我在调一个老项目时把RC522直接替换成FM17550发现寻卡偶尔失败读UID不稳定。对比数据手册后发现FM17550在某些控制寄存器的默认值和RC522有细微差别。所以如果你是从RC522代码迁移过来建议先跑一遍版本寄存器读取和初始化时序确认寄存器行为一致再继续。最稳妥的办法是以FM17550的数据手册为准不要盲抄RC522的库。6. 常见问题速查表现象可能原因处理建议读版本寄存器全FFSPI接线错误、片选无效用逻辑分析仪检查SPI波形确认MISO/MOSI是否接反寻卡超时天线匹配差、卡片不在射频范围、电源纹波大调整卡片位置给FM17550电源加10uF和0.1uF去耦电容读UID时校验失败防碰撞时序被中断、卡片质量差降低主频或SPI速率减少中断干扰选卡失败选卡帧漏了CRC用FM17550硬件CRC计算并附加认证失败密钥不对、块地址不对、UID字节序不对核对密钥字节数和块地址范围确认UID顺序写块失败写了厂商块、漏CRC、卡片访问位被改过从块4开始写附加完整CRC先检查数据块访问位读卡距离突然变短天线附近有金属、模块表面有污损远离金属物体清洁或重新焊接天线匹配电容7. 项目扩展与实际心得读卡功能跑通之后最自然的扩展方向是门禁终端、智能储物柜、会议签到设备这类场景。S50卡还有一大类用法是作为“票据”或“钥匙”在系统中保存用户ID、余额、权限码等数据。如果你想更进一步还可以把FM17550换成兼容方案后接入RTOS把读卡任务放到独立线程里通过队列把UID和块数据传递给上层业务逻辑。最后再分享一个经验S50卡的数据块写入次数是有限的虽然十万次级别的寿命对绝大多数场景够用但在调试阶段不要用死循环反复对同一个块执行写入。我调试时为了测试稳定性用for循环连续写了几万次后来发现该块偶尔写入失败虽然概率不高但确实不如新卡稳定。实际产品开发中建议写入时增加扇区磨损均衡逻辑或者至少不要频繁写同一个地址。另外涉及卡片密钥和数据安全时请务必只在你自己持有或已经获得明确授权的卡片上操作开发读卡设备本身是通用技术但不要把它用在未经授权的场景里。