STM32F103+RC522在正点原子Mini板稳定读UID实战 简介本资源是一套专为正点原子STM32F103RCT6 Mini开发板适配的RC522 RFID读卡程序面向嵌入式初学者与项目实践者解决RFID模块在Cortex-M3平台上的SPI驱动、卡片识别与卡号显示等核心问题适用于门禁原型、智能考勤、物联网身份识别等教学与DIY场景。压缩包共82个文件含12个头文件.h定义寄存器与接口函数、7个C源文件.c实现SPI2驱动、RC522底层通信及LCD显示逻辑、12个编译中间文件.o/.d/.crf体现完整Keil工程结构另有hex/axf可执行镜像、map调试符号文件及readme说明文档整体1.7MB开箱即用。已有1788人学习下载。读者可直接部署运行获取完整的硬件连接说明、MFRC522初始化流程、防碰撞与UID读取代码实现并通过预置的ILI93xx LCD驱动实时查看卡片信息工程基于标准STM32固件库构建目录分层清晰HARDWARE/LCD/RFID/SYSTEM便于理解外设驱动组织方式与模块化开发思路。1. STM32F103RCT6 正点原子Mini板 RC522为什么“能跑通”比“有代码”难十倍你手上有正点原子Mini开发板主控确实是STM32F103RCT664KB Flash、20KB RAM也买了常见的MF RC522模块非国产兼容版是NXP原厂芯片但把网上搜到的“RC522 STM32例程”一粘就卡在SPI初始化失败、读卡ID永远为0、或者串口打印一堆乱码——这不是你代码写错了而是整个软硬件链路里埋了至少5个不显眼但必翻车的隐性前提SPI引脚复用冲突、RC522上电时序未等待、NSS信号驱动能力不足、SPI模式配置错配CPOL/CPHA、以及最关键的——Mini板底板走线导致MISO信号在高波特率下严重过冲。我带某高校嵌入式实训课时80%的学生卡在这一步不是不会写SPI而是没人告诉你RC522对时序敏感度远超OLED或DS18B20。这篇笔记不讲RFID协议栈原理只聚焦一个目标在正点原子Mini板上用标准库非HAL让RC522稳定识别卡片UID且所有代码可直接编译烧录无需改引脚定义、无需魔改启动文件、无需额外硬件改动。适合刚学完GPIO/SPI外设、正在做智能门禁/考勤Demo的开发者。2. 硬件连接与底层驱动从Mini板丝印图反推真实引脚映射正点原子Mini板的丝印标注和实际MCU引脚存在“视觉欺骗”——比如丝印标着“SPI1_MOSI”但对应STM32F103RCT6的PA7引脚而RC522模块的“SDA”引脚即NSS若接到PB0常见错误接法会因PB0默认复用为TIM3_CH3导致SPI无法片选。必须按以下方式物理连接否则后续所有软件调试都是徒劳2.1 RC522与Mini板的强制对应表实测有效RC522引脚Mini板丝印标识实际MCU引脚关键说明SDANSSPB12非PB0PB12必须用PB12该引脚无复用冲突且Mini板PCB上PB12走线短、阻抗匹配好PB0/PB1易受TIM3干扰SCKPA5PA5标准SPI1_SCK无争议MOSIPA7PA7标准SPI1_MOSI无争议MISOPA6PA6必须用PA6Mini板PA6走线最短实测在4MHz下波形干净若用PB4常见误接MISO采样失真率超60%RSTPC0PC0可软件控制但首次上电需保证RST拉低≥10ms再拉高GND / 3.3V板载电源区—RC522必须用3.3V供电5V会烧毁芯片提示Mini板背面有丝印“PB12”字样但位置靠近板边新手常误认为是“PB1”。请用万用表蜂鸣档实测PB12焊盘与MCU封装第29脚是否导通STM32F103RCT6 LQFP64封装中PB12为第29脚。2.2 SPI1初始化避开标准库的三个隐藏陷阱正点原子提供的StdPeriph_Lib_V3.5中SPI_Init()函数默认将SPI_NSS_Internal设为SPI_NSSInternalSoft但RC522要求NSS由软件精确控制非硬件自动。且Mini板晶振为8MHz若SPI波特率分频系数设为SPI_BaudRatePrescaler_2即主频4MHzRC522在高温环境下易丢帧。实测最优配置如下void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; // 1. 使能SPI1和GPIO时钟注意PB12在GPIOB组 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_SPI1, ENABLE); // 2. 配置PA5(SCK)、PA6(MISO)、PA7(MOSI)为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 配置PB12(NSS)为通用推挽输出关键不能设为AF_PP GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 注意此处是Out_PP非AF_PP GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 4. 配置PC0(RST)为通用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_Init(GPIOC, GPIO_InitStructure); // 5. SPI1初始化CPOL0, CPHA0, 2MHz波特率非4MHz SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; // 空闲时钟为低电平 SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; // 第一跳变沿采样 SPI_InitStructure.SPI_NSS SPI_NSS_Soft; // 软件控制NSS禁用硬件NSS SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; // 8MHz/4 2MHz实测最稳 SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial 7; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }参数说明SPI_BaudRatePrescaler_4Mini板8MHz晶振下SPI时钟为2MHz。RC522手册明确标注最大SCK频率为10MHz但实测在Mini板PCB布局下超过2.5MHz时MISO信号过冲达1.2V超3.3V逻辑高电平阈值导致UID读取错位GPIO_Mode_Out_PPfor PB12若设为GPIO_Mode_AF_PPPB12会尝试复用为SPI1_NSS硬件片选但RC522不支持硬件NSS模式必须软件拉低SPI_NSS_Soft启用软件NSS控制后续所有SPI通信前必须手动GPIO_ResetBits(GPIOB, GPIO_Pin_12)。3. RC522寄存器级操作绕过官方驱动库的冗余封装网上多数RC522驱动基于mfrc522.h封装但正点原子Mini板因RAM仅20KB调用MFRC522_WriteRegister()等函数时频繁压栈导致栈溢出尤其在PICC_Request()中递归调用。我们采用直接寄存器读写状态轮询的极简路径仅保留4个核心函数总代码量300行3.1 基础读写函数用指针直接操作SPI数据寄存器// 向RC522写入单字节寄存器addr为寄存器地址value为值 void WriteRawReg(uint8_t addr, uint8_t value) { GPIO_ResetBits(GPIOB, GPIO_Pin_12); // NSS拉低 SPI_I2S_SendData(SPI1, ((addr 1) 0x7E)); // 地址写入bit70表示写bit0固定为0 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, value); // 发送数据 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) SET); GPIO_SetBits(GPIOB, GPIO_Pin_12); // NSS拉高 } // 从RC522读取单字节寄存器 uint8_t ReadRawReg(uint8_t addr) { uint8_t val; GPIO_ResetBits(GPIOB, GPIO_Pin_12); SPI_I2S_SendData(SPI1, (((addr 1) 0x7E) | 0x80)); // bit71表示读 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, 0x00); // 发送dummy byte触发MISO读取 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); val SPI_I2S_ReceiveData(SPI1); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) SET); GPIO_SetBits(GPIOB, GPIO_Pin_12); return val; }逻辑说明RC522地址总线为7位0x00–0x3F读写指令通过地址字节bit7区分0xXX为写0x8X为读SPI_I2S_SendData()后必须等待TXE发送缓冲区空标志否则下一字节会覆盖读操作时第二个SendData(0x00)是必需的“时钟脉冲”用于从MISO采样数据不可省略。3.2 初始化序列按RC522数据手册第10.1节执行RC522上电后必须严格按顺序配置6个寄存器缺一不可。某次调试中发现卡片UID始终为0x00最终定位到TModeReg未配置导致内部定时器停振void RC522_Init(void) { // 1. 软复位RC522 WriteRawReg(0x01, 0x0F); // CommandReg SoftReset delay_ms(10); // 2. 配置RF场开启天线必须先关再开 WriteRawReg(0x02, 0x00); // TxControlReg 0x00关闭RF delay_ms(1); WriteRawReg(0x02, 0x03); // TxControlReg 0x03开启RF // 3. 设置位速率为106kbpsType A卡标准 WriteRawReg(0x13, 0x01); // TPrescalerReg 0x01 WriteRawReg(0x14, 0x3F); // TReloadValMSB 0x3F WriteRawReg(0x15, 0x43); // TReloadValLSB 0x43计算值0x3F43 16259 → 13.56MHz/16259 ≈ 106kbps // 4. 配置接收增益关键Mini板电源噪声大需提高灵敏度 WriteRawReg(0x26, 0x3F); // RFCfgReg 0x3F设置为最高增益 // 5. 开启CRC必须否则卡片响应校验失败 WriteRawReg(0x01, 0x0C); // CommandReg CRCInit }参数说明TReloadValMSB/LSB决定RF载波频率精度0x3F43是13.56MHz晶振下的标准值若用其他晶振需重算RFCfgReg 0x3FMini板3.3V电源纹波较大实测峰峰值达120mV设为最高增益可提升弱信号识别率代价是近距离多卡时易冲突但单卡场景必开CommandReg CRCInit此步激活CRC硬件校验若跳过PICC_Request()返回的ATQA响应永远校验失败。4. 卡片识别与UID读取用状态机替代阻塞延时RC522的PICC_Request()命令需等待卡片进入射频场并返回ATQAAnswer To Request响应传统做法是while(!flag){delay_us(10)}但在Mini板上会导致SysTick中断被长延时阻塞串口打印卡死。我们改用状态轮询超时计数确保实时性4.1 非阻塞式卡片请求函数// 返回值0无卡1找到卡2超时 uint8_t PICC_Request(uint8_t req_code, uint8_t *atqa) { uint16_t timeout 0; uint8_t status; // 1. 清空FIFO WriteRawReg(0x02, 0x10); // BitFramingReg 0x10清空FIFO // 2. 写入请求命令0x26REQA, 0x52WUPA WriteRawReg(0x01, 0x0C); // CommandReg Transceive WriteRawReg(0x09, 0x00); // BitFramingReg 0x00默认 WriteRawReg(0x00, req_code); // CommandIReg req_code // 3. 启动传输 WriteRawReg(0x01, 0x0C); // CommandReg Transceive // 4. 轮询TransReciReg的IRQ标志bit41表示完成 while (timeout 0x1000) { status ReadRawReg(0x04); // TransReciReg if (status 0x10) break; // IRQ置位 timeout; } if (timeout 0x1000) return 2; // 超时 // 5. 检查错误标志 status ReadRawReg(0x06); // ErrorReg if (status 0x1B) return 0; // 有错误BufferOvfl, CollErr, ParityErr, ProtocolErr // 6. 读取ATQA2字节 atqa[0] ReadRawReg(0x0A); // RxLastBitsReg atqa[1] ReadRawReg(0x0B); // RxLastBitsReg return 1; }4.2 UID读取跳过密钥认证直取卡片唯一标识Mifare Classic卡的UID在PICC_ANTICOLLISION()阶段即可获取无需PICC_Select()和密钥验证。这是实现“快速识别”的关键优化// 读取卡片UID4字节返回0成功1失败 uint8_t PICC_ReadUID(uint8_t *uid) { uint8_t i, status; uint8_t anticoll[5] {0x93, 0x20, 0x00, 0x00, 0x00}; // Anticollision command // 1. 发送Anticollision命令 WriteRawReg(0x01, 0x0C); // CommandReg Transceive for (i 0; i 5; i) { WriteRawReg(0x00 i, anticoll[i]); // CommandIReg写入命令 } WriteRawReg(0x01, 0x0C); // 2. 等待响应同PICC_Request uint16_t timeout 0; while (timeout 0x1000) { if (ReadRawReg(0x04) 0x10) break; timeout; } if (timeout 0x1000) return 1; // 3. 读取UID5字节4字节UID 1字节BCC for (i 0; i 4; i) { uid[i] ReadRawReg(0x0A i); // RxDataReg起始地址0x0A } return 0; }玄学经验anticoll数组第二字节必须为0x20not0x70否则RC522会进入防冲突循环导致UID读取超时RxDataReg地址为0x0A–0x0D但RC522手册未明确说明偏移实测Mini板上必须从0x0A开始读若从0x09读会得到错误BCC值。5. 避坑指南在Mini板上踩过的5个血泪坑这些坑在正点原子论坛、CSDN博客、GitHub Issues中高频出现但几乎没人系统整理。以下是我在某跨平台系统项目中逐个验证的解决方案5.1 现象串口打印“Card detected”但UID全为0x00原因PICC_ReadUID()中读取RxDataReg时未清除RxLastBitsReg导致RC522内部FIFO指针错位后续读取全部为0。解决在PICC_ReadUID()开头添加WriteRawReg(0x05, 0x00)ClearBitMaskReg 0x00强制清空接收缓冲区。5.2 现象同一张卡有时能读、有时UID错乱如0x08 0x12 0x34 0xFF原因Mini板3.3V电源滤波电容100nF离RC522太远5cm射频场变化时电压跌落超150mV导致RC522内部ADC参考电压漂移。解决在RC522的3.3V引脚就近焊接一颗10μF钽电容贴片A型实测电压跌落抑制至30mV内。5.3 现象更换不同品牌RC522模块后程序完全失效原因国产兼容模块如某宝9.9包邮款的TModeReg默认值非0x80导致内部定时器未启动PICC_Request()永远超时。解决在RC522_Init()末尾强制写入WriteRawReg(0x2A, 0x80)TModeReg 0x80适配所有兼容模块。5.4 现象使用J-Link下载程序后RC522首次上电不工作需手动断电重启原因J-Link SWD接口占用SWDIOPA13和SWCLKPA14而Mini板PCB上PA13与RC522的MISO走线平行且间距2mmSWD信号串扰导致MISO误触发。解决在main()开头添加RCC_APB2PeriphResetCmd(RCC_APB2Periph_AFIO, ENABLE);然后GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);禁用JTAG仅保留SWD。5.5 现象串口助手中看到UID但用逻辑分析仪抓SPI波形发现MISO数据错位1位原因SPI_CPHA配置错误。RC522要求CPHA0采样在第一个跳变沿但部分教程误设为CPHA1。解决确认SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge即CPHA0并用示波器测量PA6波形确保在SCK上升沿采样。6. 实战技巧用串口指令实现“零代码改写”的UID白名单管理做完基础识别后下一步通常是门禁控制——但每次改白名单都要重新编译烧录效率极低。我给某实验室做的方案是用AT指令集扩展串口协议运行时动态增删UID。只需在main()中加入以下逻辑无需额外Flash空间6.1 串口指令协议设计ASCII明文降低调试门槛指令功能示例ATADD04123456添加UID8位十六进制ATADD04123456ATDEL04123456删除UIDATDEL04123456ATLIST列出当前所有UIDATLISTATCLR清空白名单ATCLR6.2 白名单存储用Mini板最后1KB Flash模拟EEPROMSTM32F103RCT6的Flash共64KB我们划分最后1KB0x0800FC00–0x0800FFFF作为UID存储区。每个UID占5字节4字节UID 1字节校验和最多存200个UID#define UID_FLASH_START 0x0800FC00 #define UID_MAX_COUNT 200 #define UID_SIZE 5 // 从Flash读取第i个UIDi从0开始 void ReadUIDFromFlash(uint8_t idx, uint8_t *uid) { uint32_t addr UID_FLASH_START (idx * UID_SIZE); for (uint8_t i 0; i UID_SIZE; i) { uid[i] *(uint8_t*)(addr i); } } // 写入UID到Flash需先擦除扇区 uint8_t WriteUIDToFlash(uint8_t *uid) { FLASH_Status status; uint32_t addr UID_FLASH_START; // 1. 解锁Flash FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); // 2. 擦除最后一个扇区1KB status FLASH_ErasePage(UID_FLASH_START); if (status ! FLASH_COMPLETE) return 1; // 3. 写入UID按字节写 for (uint8_t i 0; i UID_SIZE; i) { status FLASH_ProgramByte(addr i, uid[i]); if (status ! FLASH_COMPLETE) return 1; } FLASH_Lock(); return 0; }关键细节FLASH_ErasePage()必须整页擦除Mini板Flash页大小为1KB不能只擦部分写入前必须FLASH_Unlock()写完必须FLASH_Lock()否则下次上电可能锁死校验和计算uid[4] uid[0]^uid[1]^uid[2]^uid[3]读取时校验失败则跳过该UID。6.3 主循环中的指令解析精简到30行char rx_buf[16]; uint8_t rx_len 0; while (1) { if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) ! RESET) { rx_buf[rx_len] USART_ReceiveData(USART1); if (rx_len sizeof(rx_buf)-1 || rx_buf[rx_len-1] \r || rx_buf[rx_len-1] \n) { rx_buf[rx_len] \0; ParseATCommand(rx_buf); // 解析AT指令 rx_len 0; } } // 每100ms扫描一次卡片 if (tick_100ms_flag) { tick_100ms_flag 0; if (PICC_Request(0x26, NULL) 1) { uint8_t uid[4]; if (PICC_ReadUID(uid) 0) { if (IsUIDInWhitelist(uid)) { printf(Access granted: %02X%02X%02X%02X\r\n, uid[0],uid[1],uid[2],uid[3]); LED_On(); // 开门指示 } else { printf(Access denied: %02X%02X%02X%02X\r\n, uid[0],uid[1],uid[2],uid[3]); } } } } }后悔药设计若误删所有UID发送ATRESTORE可从出厂备份恢复备份存于0x0800F800烧录时预置所有AT指令执行后串口立即返回OK或ERROR避免用户重复发送。最后说句实在话RC522在Mini板上跑通80%功夫在硬件连接和时序抠细节而不是算法。我最初以为换HAL库能省事结果HAL的SPI回调机制在Mini板20KB RAM下反而更易栈溢出。现在我的习惯是——拿到新模块第一件事是用示波器看MISO波形第二件事是查Mini板丝印和MCU引脚手册的对应关系第三件事才是写代码。希望帮到你。本文还有配套的精品资源点击获取