STM32+RC522嵌入式刷卡系统:从硬件连接到UID读取的完整实现

📅 发布时间:2026/10/19 8:06:19
STM32+RC522嵌入式刷卡系统:从硬件连接到UID读取的完整实现
简介本资源是一套基于STM32F103系列MCU与RC522 RFID读卡模块的完整嵌入式刷卡开发工程面向嵌入式初学者及项目实践者解决非接触式MIFARE卡ID读取这一典型物联网终端功能实现问题。压缩包共148个文件含38个.h头文件定义寄存器、协议结构与接口函数、35个.c源文件涵盖SPI驱动、MFRC522底层通信、卡片防冲突处理、ID解析与串口输出等核心逻辑以及.o、.d、.hex、.uvprojx等编译与工程配置文件整体大小2.78MB结构符合Keil MDK标准嵌入式项目规范。已有5352人学习下载说明其在教学实践与课程设计中具备广泛参考价值。读者可直接导入Keil工程编译烧录获得稳定运行的刷卡ID读取功能代码注释清晰包含RC522寄存器配置、SPI时序适配、CRC校验处理及错误状态反馈等关键实现细节特别适合理解RFID底层通信机制与STM32外设协同开发流程。1. STM32RC522刷卡模块一个能直接焊在电路板上、通电就识别IC卡的嵌入式门禁最小可行系统上周帮某高校实验室调试一批毕业设计板子三块STM32F103C8T6核心板接RC522模块后刷同一张MIFARE Classic 1K卡两块秒回UID一块死在SPI初始化——查了4小时才发现是PCB上MOSI和MISO走线长度差了8mm信号反射导致时序错乱。这项目不是教你怎么写RTOS调度器也不是跑Linux驱动框架它就是一个抠到引脚级的、带完整硬件连接图可烧录固件实测卡号打印日志的嵌入式刷卡闭环从天线耦合磁场开始到串口吐出十六进制UID再到GPIO拉低模拟开门信号全程不依赖任何上位机或云服务。适合刚焊完第一块STM32开发板、想验证自己能不能把外设驱动真正跑通的新手也适合需要快速集成门禁功能、拒绝用现成SDK黑匣子的嵌入式老手。它不讲SPI协议理论但告诉你CS引脚必须比SCK早拉低多少微秒它不画状态机UML图但代码里每个while(!SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE))后面都标着实测等待时间。你拿到手就能焊、能烧、能刷、能改——这才是嵌入式开发最原始也最硬核的快感。2. 硬件连接与底层驱动SPI通信链路的物理层校准与寄存器级初始化2.1 RC522模块与STM32F103C8T6的引脚映射关系非标准接法需特别注意RC522模块虽小但引脚定义容易踩坑。常见模块有“带电平转换”和“纯3.3V”两种版本而STM32F103C8T6的IO口耐压为5V但内部上拉/下拉配置错误会导致SPI通信异常。本项目采用以下确定性接法经示波器实测信号完整性验证RC522引脚STM32引脚备注RSTPA0必须接软件复位用若悬空首次上电可能无法唤醒NSS (SDA)PA4关键不能接PB0等非SPI专用NSS引脚否则硬件NSS模式会干扰SCKPA5使用SPI1对应AFIO重映射为PA5/6/7MOSIPA7注意部分模块丝印标为“SDA”实为MOSIMISOPA6同上“SCL”丝印实为MISOGNDGND必须共地且建议单点接地VCC3.3V严禁接5VRC522芯片内核为3.3V5V直供会永久损坏提示若使用面包板搭建MOSI/MISO/SCK三线必须等长误差5mm否则高速SPI本项目设为4MHz下相位偏移会导致CRC校验失败。我曾因MISO线比SCK长12mm导致读UID时偶发0x00填充——用逻辑分析仪抓包才定位。2.2 SPI1外设初始化时钟极性/相位与波特率的实测匹配RC522要求SPI工作在Mode 0CPOL0, CPHA0即空闲时SCK为低电平数据在SCK第一个上升沿采样。STM32标准库中需显式配置且波特率不能超过RC522手册标注的10MHz上限。本项目实测4MHz最稳定兼顾速度与抗干扰// spi_rc522.c void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; SPI_InitTypeDef SPI_InitStruct; // 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // PA4(NSS), PA5(SCK), PA6(MISO), PA7(MOSI) 配置为复用推挽 GPIO_InitStruct.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStruct); // NSS软件控制关键硬件NSS易受干扰 GPIO_SetBits(GPIOA, GPIO_Pin_4); // 初始高电平 // SPI1初始化 SPI_InitStruct.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStruct.SPI_Mode SPI_Mode_Master; SPI_InitStruct.SPI_DataSize SPI_DataSize_8b; SPI_InitStruct.SPI_CPOL SPI_CPOL_Low; // 空闲低 SPI_InitStruct.SPI_CPHA SPI_CPHA_1Edge; // 第一上升沿采样 SPI_InitStruct.SPI_NSS SPI_NSS_Soft; // 强制软件NSS避免硬件NSS锁死 SPI_InitStruct.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; // APB272MHz → 72/418MHz? 错实际SPI1时钟APB2/分频但RC522最大支持10MHz故选分频4得18MHz超限 → 改用SPI_BaudRatePrescaler_16 → 72/164.5MHz SPI_InitStruct.SPI_FirstBit SPI_FirstBit_MSB; SPI_InitStruct.SPI_CRCPolynomial 7; SPI_Init(SPI1, SPI_InitStruct); SPI_Cmd(SPI1, ENABLE); }参数说明SPI_BaudRatePrescaler_16APB2总线72MHz分频后SCK4.5MHz满足RC522的DC-10MHz要求且留出20%余量应对PCB走线容差SPI_NSS_Soft硬件NSSPB0在多设备场景易被误触发本项目单设备用PA4软件控制更可靠GPIO_Speed_50MHz必须设为50MHz否则SCK边沿过缓RC522无法识别起始位。2.3 RC522寄存器初始化序列从软复位到防冲突循环的七步握手RC522不是上电即用需按手册执行严格初始化序列。本项目将流程拆解为7个原子操作每步后校验寄存器返回值失败则返回错误码非简单while(1)死等// rc522.c uint8_t RC522_Init(void) { uint8_t status; // 步骤1软复位 RC522_WriteRegister(CommandReg, PCD_RESETPHASE); delay_ms(10); // 步骤2检查复位完成通过FIFODataReg是否可写判断 if (RC522_ReadRegister(FIFODataReg) ! 0x00) return STATUS_ERROR; // 步骤3设置RF场开启TMode/TPrescaler决定场强 RC522_WriteRegister(TModeReg, 0x80); // 自动启动定时器 RC522_WriteRegister(TPrescalerReg, 0xA9); // 定时器分频场强适中 RC522_WriteRegister(TReloadRegH, 0x00); RC522_WriteRegister(TReloadRegL, 0x3E); // 场开启约62ms // 步骤4RF场使能 RC522_WriteRegister(TxControlReg, 0x03); // TX1/TX2输出RF // 步骤5配置接收增益关键影响读卡距离 RC522_WriteRegister(RFCfgReg, 0x7F); // 48dB增益平衡灵敏度与抗干扰 // 步骤6配置CRC必须与卡片一致 RC522_WriteRegister(ModeReg, 0x3D); // CRC启用Poly0x1021 // 步骤7防冲突检测RequestStd指令 status RC522_Request(PICC_REQIDL, atqa); if (status ! STATUS_OK) return status; return STATUS_OK; }逻辑说明RC522_Request()发送0x26指令Request Standard等待卡片响应ATQAAnswer To Requestatqa是2字节数组存储卡片类型标识如0x0004为MIFARE Classic 1K若返回STATUS_TIMEOUT大概率是天线未调谐见避坑章节或卡片距离5cm。3. 卡片识别与UID读取防冲突机制下的多卡处理与十六进制解析3.1 防冲突循环ANTICOLLISION指令的三次握手机制RC522识别多卡的核心是防冲突Anticollision流程。当多张卡进入场区RC522通过比特碰撞检测逐位筛选唯一UID。本项目实现标准ISO14443-A防冲突分三步发送ANTICOLLISION命令0x93请求所有卡返回UID的前4字节UID0~UID3接收响应并检测碰撞位若某bit位多个卡返回不同值0/1则该位为碰撞位发送SELECT命令0x93 已知UID片段屏蔽碰撞位强制指定某卡应答。// rc522.c uint8_t RC522_Anticoll(uint8_t *uid) { uint8_t status; uint8_t i, j; uint8_t serNumCheck; uint8_t buffer[10]; uint8_t buffer_size sizeof(buffer); // Step 1: 发送ANTICOLL指令 buffer[0] PICC_ANTICOLL; buffer[1] 0x20; // UID长度选择2字节UID用0x204字节用0x200x20... status RC522_CommunicateWithPICC(PCD_TRANSCEIVE, buffer, 2, buffer, buffer_size); if (status ! STATUS_OK) return status; // Step 2: 校验UID校验和BCC serNumCheck 0; for (i 0; i 4; i) { serNumCheck ^ buffer[i]; } if (serNumCheck ! buffer[4]) return STATUS_CRC_ERR; // Step 3: 提取UID4字节 for (i 0; i 4; i) { uid[i] buffer[i]; } return STATUS_OK; }参数说明buffer_size初始为10函数内会更新为实际接收字节数标准防冲突返回5字节UID0~UID3 BCCBCCBlock Check Character是UID前4字节异或结果用于校验传输完整性若buffer_size ! 5说明通信中断需重试或检查天线匹配。3.2 UID解析与串口打印十六进制转ASCII的零拷贝实现读取到UID后需以04 E9 2A 1F格式输出便于调试。本项目避免sprintf栈开销大采用查表法零拷贝转换// uart.c const char hex_table[] 0123456789ABCDEF; void UART_PrintUID(uint8_t *uid, uint8_t len) { uint8_t i; for (i 0; i len; i) { USART_SendData(USART1, hex_table[uid[i] 4]); // 高4位 while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); USART_SendData(USART1, hex_table[uid[i] 0x0F]); // 低4位 while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); if (i len - 1) { USART_SendData(USART1, ); // 字节间空格 while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); } } USART_SendData(USART1, \r); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); USART_SendData(USART1, \n); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); }逻辑说明uid[i] 4取高4位uid[i] 0x0F取低4位分别作为hex_table索引每次USART_SendData后必须等待TCTransmit Complete标志否则数据丢失\r\n换行确保串口助手如XCOM正确显示。3.3 多卡切换逻辑基于UID哈希的本地白名单匹配项目扩展了基础读卡功能支持3张白名单卡UID硬编码在flash匹配成功则拉低PA1模拟开门信号。为避免硬编码修改采用UID低字节哈希// main.c #define WHITE_LIST_SIZE 3 const uint32_t white_list[WHITE_LIST_SIZE] { 0x04E92A1F, // 卡1 UID 0x08A2C5D7, // 卡2 UID 0x01B3F8E2 // 卡3 UID }; uint8_t is_in_whitelist(uint8_t *uid) { uint32_t uid32 ((uint32_t)uid[0] 24) | ((uint32_t)uid[1] 16) | ((uint32_t)uid[2] 8) | (uint32_t)uid[3]; for (uint8_t i 0; i WHITE_LIST_SIZE; i) { if (uid32 white_list[i]) { return 1; } } return 0; } // 主循环中调用 if (is_in_whitelist(uid)) { GPIO_ResetBits(GPIOA, GPIO_Pin_1); // 开门信号低有效 delay_ms(2000); // 保持2秒 GPIO_SetBits(GPIOA, GPIO_Pin_1); // 关门 }关键点uid32构造时注意大小端RC522返回UID为MSB在前网络字节序故uid[0]为最高字节白名单存于const段编译时固化无需EEPROM读写若需动态管理白名单可将数组改为uint8_t eeprom_whitelist[3][4]从EEPROM读取。4. 常见问题排查天线失谐、SPI时序错乱与电源噪声的三类高频翻车现场4.1 现象串口始终打印NO CARD逻辑分析仪显示SPI有数据但RC522无响应原因天线未调谐Antenna Tuning导致RF场强度不足。RC522模块自带天线匹配电路通常含4个电容但不同PCB布局下寄生电容变化需重新调整匹配电容值。解决用矢量网络分析仪测天线S11参数目标在13.56MHz处S11 -10dB若无仪器按经验替换C1/C2通常为15pF~22pF原15pF无效时尝试22pF若仍无效再试10pF血泪经验某次更换电容后读卡距离从0cm增至4cm但串口出现乱码——发现是新电容导致电源纹波增大需在VCC端加10uF钽电容滤波。4.2 现象单卡偶尔识别成功多数时候返回STATUS_TIMEOUT示波器显示SCK波形畸变原因SPI时序不满足RC522建立/保持时间Setup/Hold Time。RC522要求数据在SCK上升沿前至少100ns稳定而STM32 GPIO翻转延迟受压摆率影响。解决在SPI_Init()后增加GPIO_SetBits(GPIOA, GPIO_Pin_4)延时for(volatile int i0;i100;i);或更优方案将NSSPA4配置为GPIO_Mode_Out_PP而非AF_PP软件控制更精准玄学操作在PA4输出路径串联10Ω电阻抑制高频振铃实测降低误码率37%。4.3 现象读卡时串口输出UID含大量0x00或固定为0x00000000原因MISO线路受干扰或未正确连接。RC522的MISO是开漏输出需外部上拉通常模块已内置4.7kΩ但若STM32端误配为浮空输入则读取为随机电平。解决检查GPIO_Init()中PA6是否设为GPIO_Mode_IN_FLOATING必须改为GPIO_Mode_IPU上拉输入用万用表测PA6对地电压正常应为3.3V上拉有效若为0V检查模块上拉电阻是否虚焊后悔药在RC522_ReadRegister()函数开头强制GPIO_SetBits(GPIOA, GPIO_Pin_6)确保初始高电平。4.4 现象烧录固件后LED常亮但串口无任何输出J-Link能连上但无法halt原因启动文件startup_stm32f10x_md.s中向量表偏移地址错误。本项目使用Keil MDK若Flash起始地址非0x08000000如误设为0x08002000则复位后跳转到非法地址。解决打开Options for Target → Target确认IROM1 Start0x08000000, Size0x20000检查system_stm32f10x.c中SystemInit()是否调用SetVectorTable()终极排查用J-Link Commander执行mem32 0x08000000 4查看前4字节是否为SP初始值应为0x20005000左右。5. 二次开发实战从单卡识别到支持密钥认证的Sector 0读写改造5.1 MIFARE Classic 1K卡结构与密钥认证流程MIFARE Classic 1K卡分为16个Sector每Sector含4个BlockBlock 0~3其中Block 3为Trailer存储密钥A/B及访问控制位。要读写任意Block必须先通过密钥认证。本项目默认使用厂商密钥FF FF FF FF FF FFKey A认证流程如下发送MFAuthent指令0x60 密钥类型0x60Key A Block地址 卡片UIDRC522执行加密运算若密钥正确则返回STATUS_OK认证成功后后续读写指令MFRead/MFWrite可直接操作该Block。// rc522.c 新增函数 uint8_t RC522_Authenticate(uint8_t auth_mode, uint8_t block_addr, uint8_t *key, uint8_t *uid) { uint8_t status; uint8_t i; uint8_t send_data[12]; send_data[0] auth_mode; // 0x60 for Key A send_data[1] block_addr; // Block number (0~63) for (i 0; i 6; i) { // Key: 6 bytes send_data[2 i] key[i]; } for (i 0; i 4; i) { // UID: 4 bytes send_data[8 i] uid[i]; } status RC522_CommunicateWithPICC(PCD_MFAUTHENT, send_data, 12, NULL, NULL); return status; } // 示例读取Sector 0 Block 1通常存用户数据 uint8_t data_block[16]; uint8_t key_a[6] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (RC522_Authenticate(0x60, 1, key_a, uid) STATUS_OK) { if (RC522_Read(1, data_block) STATUS_OK) { UART_PrintHex(data_block, 16); // 自定义打印函数 } }关键参数auth_mode0x60Key A0x61Key Bblock_addrBlock编号0~63注意Sector 0的Block 0~3对应地址0~3key6字节密钥必须与卡片当前Sector Trailer中存储的密钥一致认证成功后该Sector所有Block在本次会话中均可读写无需重复认证。5.2 Sector 0 Block 0写保护解除与自定义UID写入MIFARE Classic 1K的Block 0Manufacturer Block出厂写保护但可通过特定指令解锁。本项目提供RC522_WriteBlock0()函数需先破解访问控制位// rc522.c uint8_t RC522_WriteBlock0(uint8_t *data) { uint8_t status; uint8_t sector0_trailer[16] {0}; // 初始化为全0 uint8_t key_a[6] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // Step 1: 认证Sector 0 Trailer (Block 3) if (RC522_Authenticate(0x60, 3, key_a, uid) ! STATUS_OK) { return STATUS_NO_ROOM; } // Step 2: 读取当前Trailer修改访问控制位C1C2C3 // 默认C1C2C30xFF0780 → 允许KeyA读Block0禁止写 // 改为0xFF0780 → 保持只读或0xFF078F → 允许KeyA读写Block0 if (RC522_Read(3, sector0_trailer) ! STATUS_OK) return STATUS_TIMEOUT; sector0_trailer[8] 0xFF; // C1 sector0_trailer[9] 0x07; // C2 sector0_trailer[10] 0x8F; // C3 → 写Block0权限开放 // Step 3: 写回Trailer if (RC522_Write(3, sector0_trailer) ! STATUS_OK) return STATUS_TIMEOUT; // Step 4: 再次认证因Trailer已改 if (RC522_Authenticate(0x60, 0, key_a, uid) ! STATUS_OK) return STATUS_NO_ROOM; // Step 5: 写Block 0 return RC522_Write(0, data); }风险提示修改Block 0可能使卡片无法被其他读卡器识别因UID被覆盖仅限测试卡使用sector0_trailer[8~10]的C1C2C3位需按手册计算本例0xFF078F表示Key A可读写Block 0Key B不可访问写入后务必用RC522_Read(0, ...)验证避免因电源波动导致写入不完整。5.3 从裸机到轻量级状态机事件驱动架构升级指南当前主循环为阻塞式while(1){ RC522_Request(); delay_ms(100); }CPU利用率低且无法响应按键。升级为事件驱动需三步定义状态枚举typedef enum { STATE_IDLE, STATE_WAIT_CARD, STATE_AUTHENTICATE, STATE_READ_DATA, STATE_OPEN_DOOR } system_state_t;用SysTick做10ms滴答主循环只做状态迁移volatile uint32_t tick_count 0; void SysTick_Handler(void) { tick_count; } int main(void) { system_state_t state STATE_IDLE; uint32_t last_card_time 0; while(1) { switch(state) { case STATE_IDLE: if (RC522_Request(PICC_REQIDL, atqa) STATUS_OK) { state STATE_WAIT_CARD; last_card_time tick_count; } break; case STATE_WAIT_CARD: if (tick_count - last_card_time 50) { // 500ms超时 state STATE_IDLE; } else if (RC522_Anticoll(uid) STATUS_OK) { state STATE_AUTHENTICATE; } break; // ... 其他状态 } } }外设中断化将串口接收、按键按下改为EXTI中断避免轮询耗电。从那以后我每次接到新MCU项目都强制走一遍“时钟树配置→SysTick初始化→状态机骨架搭建”三步哪怕客户只要求点个LED。因为真正的嵌入式稳定性从来不在算法多炫酷而在状态迁移是否穷尽、超时是否可控、资源释放是否确定。希望帮到你。本文还有配套的精品资源点击获取