ESP32-S3驱动RC522模块软件SPI实现M1卡读写与门禁系统实战最近有不少朋友在问用ESP32-S3做门禁或者数据采集项目怎么读写M1卡也就是我们常说的IC卡、门禁卡今天我就来分享一下我的实战经验用ESP32-S3通过软件模拟SPI的方式驱动RC522模块手把手带你实现完整的M1卡读写功能。这个方案特别适合那些ESP32-S3硬件SPI接口已经被占用或者想用任意GPIO引脚连接RC522的朋友。我会从硬件接线开始一步步讲解底层驱动函数最后给出一个可以直接用的门禁/数据读写示例。即使你是嵌入式新手跟着做也能搞定。1. 准备工作认识你的“装备”在动手写代码之前咱们得先搞清楚手头的两样东西ESP32-S3开发板和RC522模块。ESP32-S3是乐鑫推出的一款功能强大的Wi-Fi Bluetooth 5 (LE)双模微控制器性能足够应对我们这个小项目。RC522模块是一个基于NXP RC522芯片的射频识别读写模块它支持读写Mifare系列的卡片就是我们最常见的门禁卡、校园卡。它通过SPI接口和主控芯片通信。注意RC522模块的工作电压是3.3V和ESP32-S3的IO电平完美匹配直接连接就行不需要电平转换。2. 硬件连接把线接对是成功的一半RC522模块通常有8个引脚有的版本是9个但我们只需要连接其中6个。这里我们使用ESP32-S3的任意GPIO来模拟SPI非常灵活。根据原始资料里的引脚定义我们进行如下连接。你可以根据自己板子的情况调整只要在代码里改一下宏定义就行。RC522模块引脚功能说明连接至ESP32-S3引脚SDA (有的标CS/NSS)片选信号GPIO1SCK时钟信号GPIO2MOSI主设备输出从设备输入GPIO3MISO主设备输入从设备输出GPIO4RST复位信号GPIO5VCC电源 (3.3V)3.3VGND地GND接线图在脑子里过一下电源和地接好四根数据线CS、SCK、MOSI、MISO和一根复位线RST分别接到ESP32-S3的五个GPIO上。接线一定要牢固虚接是调试时最头疼的问题之一。3. 软件SPI驱动用代码“模拟”硬件因为我们要用软件模拟SPI所以第一步就是实现最底层的引脚控制和时序模拟。这部分代码在bsp_rc522.c中。3.1 GPIO初始化首先我们需要把用到的几个GPIO引脚配置好CS、SCK、MOSI、RST设置为输出模式MISO设置为输入模式。void RC522_Init(void) { // 配置CS、SCK、MOSI、RST为输出引脚 gpio_config_t out_config { .pin_bit_mask (1ULLGPIO_CS) | (1ULLGPIO_SCK) | (1ULLGPIO_MOSI) | (1ULLGPIO_RST), .mode GPIO_MODE_OUTPUT, .pull_up_en GPIO_PULLUP_ENABLE, .pull_down_en GPIO_PULLDOWN_DISABLE, .intr_type GPIO_INTR_DISABLE }; gpio_config(out_config); // 配置MISO为输入引脚 gpio_config_t in_config { .pin_bit_mask (1ULLGPIO_MISO), .mode GPIO_MODE_INPUT, .pull_up_en GPIO_PULLUP_DISABLE, .pull_down_en GPIO_PULLDOWN_DISABLE, .intr_type GPIO_INTR_DISABLE }; gpio_config(in_config); }3.2 软件SPI时序模拟SPI通信有四种模式RC522模块通常工作在模式0CPOL0 CPHA0。这意味着时钟空闲时为低电平在时钟的上升沿采样数据。我们用代码来模拟这个时序。/* 软件模拟SPI发送一个字节数据高位先行 */ void RC522_SPI_SendByte(uint8_t byte) { uint8_t n; for(n0; n8; n) { // 先放置数据位高位在前 if(byte 0x80) RC522_MOSI_1(); // 宏定义设置MOSI为高电平 else RC522_MOSI_0(); // 设置MOSI为低电平 delay_us(200); // 短暂延时稳定数据 RC522_SCK_0(); // 拉低时钟 delay_us(200); RC522_SCK_1(); // 拉高时钟产生上升沿RC522在此时采样MOSI数据 delay_us(200); byte 1; // 左移准备发送下一位 } } /* 软件模拟SPI读取一个字节数据先读高位 */ uint8_t RC522_SPI_ReadByte(void) { uint8_t n; uint8_t data 0; for(n0; n8; n) { data 1; // 左移为接收新数据位腾出空间 RC522_SCK_0(); delay_us(200); // 在时钟为低时读取MISO引脚状态 if(RC522_MISO_GET() 1) // 宏定义读取MISO电平 { data | 0x01; // 如果为高则最低位置1 } delay_us(200); RC522_SCK_1(); // 拉高时钟RC522在此时将下一位数据放到MISO上 delay_us(200); } return data; }这里的关键是delay_us(200)产生的时序。实际项目中如果发现通信不稳定可以适当调整这个延时。我一开始调的时候延时太短会导致数据出错太长了又影响速度200us是个比较稳妥的值。3.3 RC522寄存器读写有了底层的字节收发函数我们就可以封装对RC522内部寄存器的读写操作了。读写寄存器是控制RC522所有功能的基础。/* 读取RC522指定寄存器的值 */ uint8_t RC522_Read_Register(uint8_t Address) { uint8_t data, Addr; // RC522的SPI读命令格式地址左移1位最低位置1 (Addr 0x7E) | 0x80 Addr ((Address 1) 0x7E) | 0x80; RC522_CS_Enable(); // 拉低CS选中RC522 RC522_SPI_SendByte(Addr); // 发送读命令和地址 data RC522_SPI_ReadByte(); // 读取寄存器值 RC522_CS_Disable(); // 拉高CS结束通信 return data; } /* 向RC522指定寄存器写入数据 */ void RC522_Write_Register(uint8_t Address, uint8_t data) { uint8_t Addr; // RC522的SPI写命令格式地址左移1位最低位置0 (Addr 0x7E) Addr (Address 1) 0x7E; RC522_CS_Enable(); RC522_SPI_SendByte(Addr); // 发送写命令和地址 RC522_SPI_SendByte(data); // 发送要写入的数据 RC522_CS_Disable(); }4. RC522初始化与基础功能寄存器读写函数准备好了我们就可以开始初始化RC522并实现一些基础功能比如开关天线、复位等。4.1 初始化与复位上电后RC522需要一个复位过程来确保处于已知状态。void RC522_Rese(void) { // 硬件复位拉低再拉高RST引脚 RC522_Reset_Disable(); delay_us(1); RC522_Reset_Enable(); delay_us(1); RC522_Reset_Disable(); delay_us(1); // 发送软复位命令 RC522_Write_Register(CommandReg, 0x0F); // 等待复位完成 while(RC522_Read_Register(CommandReg) 0x10); delay_us(1); // 配置常用工作模式 RC522_Write_Register(ModeReg, 0x3D); RC522_Write_Register(TReloadRegL, 30); // 定时器重载值低字节 RC522_Write_Register(TReloadRegH, 0); // 定时器重载值高字节 RC522_Write_Register(TModeReg, 0x8D); // 定时器模式设置 RC522_Write_Register(TPrescalerReg, 0x3E); // 定时器预分频 RC522_Write_Register(TxAutoReg, 0x40); // 调制信号为100% ASK }4.2 开关天线天线是用来发射和接收射频信号的寻卡前需要打开天线。void RC522_Antenna_On(void) { uint8_t k; k RC522_Read_Register(TxControlReg); // 判断天线是否已经开启如果没有则开启 if(!(k 0x03)) RC522_SetBit_Register(TxControlReg, 0x03); // 设置Tx1和Tx2使能位 }5. M1卡操作核心流程好了底层驱动和RC522初始化都完成了现在进入最核心的部分如何操作M1卡。整个过程就像你和卡片的一次“握手对话”有固定的步骤。5.1 完整操作流程操作一张M1卡必须严格按照以下顺序进行一步都不能错寻卡 (PcdRequest)喊一声“附近有人吗”看看有没有卡片回应。防冲突 (PcdAnticoll)如果有多张卡通过防冲突机制选出一张来“对话”。选卡 (PcdSelect)正式选中这张卡准备后续操作。密码认证 (PcdAuthState)输入密码密钥验证你是否有权操作卡的某个区域。读/写数据 (PcdRead/PcdWrite)认证通过后才能读取或修改卡里的数据。休眠 (PcdHalt)操作完成让卡片进入休眠状态。下面我们拆解几个关键函数。5.2 寻卡与防冲突/* 寻卡寻找感应区内的卡片 */ char PcdRequest(uint8_t ucReq_code, uint8_t *pTagType) { char cStatus; uint8_t ucComMF522Buf[MAXRLEN]; uint32_t ulLen; // 准备寻卡命令 RC522_ClearBit_Register(Status2Reg, 0x08); RC522_Write_Register(BitFramingReg, 0x07); RC522_SetBit_Register(TxControlReg, 0x03); // 打开天线驱动 ucComMF522Buf[0] ucReq_code; // 寻卡指令0x52寻所有卡0x26寻未休眠卡 // 通过RC522发送寻卡命令并接收卡片返回的类型码 cStatus PcdComMF522(PCD_TRANSCEIVE, ucComMF522Buf, 1, ucComMF522Buf, ulLen); if((cStatus MI_OK) (ulLen 0x10)) // 成功且返回数据长度为16位 { // 卡片类型码2字节返回在缓冲区前两个字节 *pTagType ucComMF522Buf[0]; *(pTagType 1) ucComMF522Buf[1]; } else { cStatus MI_ERR; } return cStatus; } /* 防冲突获取卡片的唯一序列号UID */ char PcdAnticoll(uint8_t *pSnr) { char cStatus; uint8_t uc, ucSnr_check 0; uint8_t ucComMF522Buf[MAXRLEN]; uint32_t ulLen; ucComMF522Buf[0] 0x93; // 防冲突命令 ucComMF522Buf[1] 0x20; // 发送防冲突命令卡片返回4字节UID和1字节校验和 cStatus PcdComMF522(PCD_TRANSCEIVE, ucComMF522Buf, 2, ucComMF522Buf, ulLen); if(cStatus MI_OK) { for(uc 0; uc 4; uc) { *(pSnr uc) ucComMF522Buf[uc]; // 保存UID ucSnr_check ^ ucComMF522Buf[uc]; // 计算异或校验 } // 校验和正确则成功否则返回错误 if(ucSnr_check ! ucComMF522Buf[uc]) cStatus MI_ERR; } return cStatus; }5.3 密码认证与数据读写M1卡的数据存储结构像一栋大楼有16个“楼层”扇区每个扇区有4个“房间”块。每个扇区的第3个块块3是特殊的“钥匙间”里面存放着密码A、密码B和访问控制位。你要访问某个扇区的前3个块必须先拿正确的钥匙密码打开这个扇区的“门”。/* 验证卡片密码 */ char PcdAuthState(uint8_t ucAuth_mode, uint8_t ucAddr, uint8_t *pKey, uint8_t *pSnr) { char cStatus; uint8_t uc, ucComMF522Buf[MAXRLEN]; uint32_t ulLen; ucComMF522Buf[0] ucAuth_mode; // 0x60验证密钥A0x61验证密钥B ucComMF522Buf[1] ucAddr; // 要操作的块地址 // 填充密钥6字节 for(uc 0; uc 6; uc) ucComMF522Buf[uc 2] *(pKey uc); // 填充卡片UID4字节 for(uc 0; uc 6; uc) ucComMF522Buf[uc 8] *(pSnr uc); // 发送认证命令 cStatus PcdComMF522(PCD_AUTHENT, ucComMF522Buf, 12, ucComMF522Buf, ulLen); // 判断认证是否成功 if((cStatus ! MI_OK) || (!(RC522_Read_Register(Status2Reg) 0x08))) cStatus MI_ERR; return cStatus; } /* 向指定块写入数据16字节 */ char PcdWrite(uint8_t ucAddr, uint8_t *pData) { char cStatus; uint8_t uc, ucComMF522Buf[MAXRLEN]; uint32_t ulLen; ucComMF522Buf[0] PICC_WRITE; // 写命令 ucComMF522Buf[1] ucAddr; // 块地址 // 计算CRC校验 CalulateCRC(ucComMF522Buf, 2, ucComMF522Buf[2]); // 发送写命令和地址 cStatus PcdComMF522(PCD_TRANSCEIVE, ucComMF522Buf, 4, ucComMF522Buf, ulLen); if((cStatus ! MI_OK) || (ulLen ! 4) || ((ucComMF522Buf[0] 0x0F) ! 0x0A)) cStatus MI_ERR; if(cStatus MI_OK) { // 将要写入的16字节数据放入缓冲区 for(uc 0; uc 16; uc) ucComMF522Buf[uc] *(pData uc); // 计算数据的CRC CalulateCRC(ucComMF522Buf, 16, ucComMF522Buf[16]); // 发送16字节数据2字节CRC cStatus PcdComMF522(PCD_TRANSCEIVE, ucComMF522Buf, 18, ucComMF522Buf, ulLen); if((cStatus ! MI_OK) || (ulLen ! 4) || ((ucComMF522Buf[0] 0x0F) ! 0x0A)) cStatus MI_ERR; } return cStatus; }重要提示新出厂的M1卡所有扇区的默认密钥A和密钥B都是6个0xFF即{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}。访问控制块的默认设置通常允许用密钥A进行读、写、增减值等所有操作。在实际产品中务必修改默认密码和访问权限否则安全性形同虚设。6. 实战应用一个完整的门禁/读写示例理论讲完了我们来看一个在app_main函数中使用的完整例子。这个例子实现了检测卡片、读取UID、验证密码、写入数据和读取数据的功能。#include stdio.h #include RC522_IC/bsp_rc522.h u8 ucArray_ID[4]; // 存储卡片IDUID uint8_t ucStatusReturn; // 状态返回值 void app_main(void) { int i 0; uint8_t read_write_data[16] {0}; // 读写数据缓冲区 // 默认密钥6个0xFF uint8_t card_KEY[6] {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; printf(RC522 Init Start\r\n); RC522_Init(); // 初始化GPIO RC522_Rese(); // 复位RC522 printf(RC522 Init Done. Start Polling...\r\n); while(1) { // 步骤1寻卡寻找所有类型的卡 if((ucStatusReturn PcdRequest(PICC_REQALL, ucArray_ID)) ! MI_OK) { // 第一次寻卡失败再试一次提高稳定性 ucStatusReturn PcdRequest(PICC_REQALL, ucArray_ID); } if(ucStatusReturn MI_OK) // 寻卡成功 { // 步骤2防冲突获取卡片唯一UID if(PcdAnticoll(ucArray_ID) MI_OK) { // 打印卡片UID printf(Card UID: %02X %02X %02X %02X\r\n, ucArray_ID[0], ucArray_ID[1], ucArray_ID[2], ucArray_ID[3]); // 步骤3选卡 if(PcdSelect(ucArray_ID) ! MI_OK) { printf(PcdSelect failure\r\n); } // 步骤4密码认证这里认证扇区1的块4即绝对地址4 // 注意块4属于扇区1其密码存储在扇区1的块3中。 if(PcdAuthState(PICC_AUTHENT1A, 4, card_KEY, ucArray_ID) ! MI_OK) { printf(PcdAuthState failure (Wrong Key?)\r\n); } else // 认证成功 { printf(Authentication Success!\r\n); // 步骤5写入数据到块4 read_write_data[0] 0xAA; // 写入一个测试数据 read_write_data[1] 0x55; if(PcdWrite(4, read_write_data) ! MI_OK) { printf(PcdWrite failure\r\n); } else { printf(Write Data OK.\r\n); } // 清空缓冲区准备读取 for(int i 0; i 16; i) { read_write_data[i] 0; } delay_us(8); // 短暂延时 // 步骤6从块4读取数据 if(PcdRead(4, read_write_data) ! MI_OK) { printf(PcdRead failure\r\n); } else { // 打印读取到的16字节数据 printf(Read Data: ); for(i 0; i 16; i) { printf(%02X , read_write_data[i]); } printf(\r\n); } } // 步骤7让卡片进入休眠状态 PcdHalt(); printf(Operation Finished. Waiting for next card...\r\n\r\n); } } delay_ms(50); // 延时50ms避免过于频繁寻卡 } }把代码下载到ESP32-S3打开串口调试助手当你把一张M1卡放到RC522模块上方时就会看到类似下面的输出RC522 Init Done. Start Polling... Card UID: 5A 8B 3C 1D Authentication Success! Write Data OK. Read Data: AA 55 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Operation Finished. Waiting for next card...7. 常见问题与调试心得卡片没反应首先检查硬件连接尤其是VCC和GND。然后确认天线是否焊接牢固RC522模块上的线圈。最后用示波器或逻辑分析仪看看SPI线上有没有波形SCK时钟是否正常。认证总是失败99%的情况是密钥不对。确认你操作的扇区块地址是否正确以及使用的密钥card_KEY数组是否与该扇区块3中存储的密钥一致。新手最容易犯的错是搞错扇区和绝对块地址的对应关系。读写数据出错确保在读写操作前已经成功执行了寻卡、防冲突、选卡和密码认证这四个步骤缺一不可。另外M1卡每个块除了每个扇区的块3只能写入一次重复写入需要先执行“恢复”或“传输”命令或者直接写0再写新值。通信不稳定尝试调整软件SPI中的delay_us时长。如果导线较长可以适当增加延时。确保电源稳定可以在RC522的VCC和GND之间加一个10uF的电解电容。这个软件SPI的方案虽然速度不如硬件SPI但引脚配置灵活代码结构清晰非常适合学习和在GPIO资源紧张的项目中使用。掌握了这套流程你就能轻松让ESP32-S3和RC522对话实现自己的门禁、打卡或者智能储物柜系统了。