ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32软件SPI驱动RC522读取UID并OLED显示实战

STM32软件SPI驱动RC522读取UID并OLED显示实战 1. 项目认知这套组合到底在做什么1.1 三个模块各司其职拿到这个项目标题的人大概和我当初一样手头有一块STM32F103C8T6最小系统板又淘了个RC522射频读卡模块再配上0.96寸OLED想把三样东西拼起来做个“刷卡显示卡号”的小玩意。先别急着接线我们把三者的角色捋清楚。STM32F103C8T6是主控Cortex-M3内核72MHz主频64KB Flash20KB RAM。这个配置在今天看不算高但驱动一个RFID读卡器和一块小屏绰绰有余。RC522是NXP出品的13.56MHz非接触式读写芯片市面上绝大多数门禁卡、校园卡、会员卡都是这个频段模块通过SPI、I2C或UART与主控通信本教程用SPI。OLED显示屏则负责把读到的卡号实时呈现出来我用的是最常见那种0.96寸、128x64分辨率、I2C接口的蓝黄双色屏。这套组合的完整工作链路就是STM32通过软件模拟SPI接口向RC522发送指令RC522感应到卡片后返回卡片UIDSTM32拿到这串卡号后再通过I2C驱动OLED把UID以十六进制字符串的形式显示出来。核心关键词就三个软件SPI、读卡、显示UID。这课适合刚入门STM32、对SPI时序一知半解、以及想把RC522和OLED从“跑通例程”提升到“自己写得出来”的开发者。1.2 为什么用软件SPI而不是直接上硬件SPI外设很多新手会问STM32F103C8T6明明自带SPI1和SPI2外设为什么非要GPIO模拟一个SPI出来你是不是吃饱了撑的答案分三层。第一引脚灵活性。硬件SPI模块的引脚是固定的比如SPI1的SCK、MISO、MOSI大多映射到PA5、PA6、PA7片选SS还不一定够用。如果你的项目里这几个引脚已经被占用或者PCB布线绕不开硬件SPI就很麻烦。而软件SPI可以在任意GPIO上跑想用PA0到PA3可以想换PB0到PB3也可以只要你改四行宏定义物理上随便接。第二调试友好度。RC522这个模块说好驱动也好驱动说坑也坑。坑在哪里硬件SPI一旦不通你很难判断是GPIO复用没配置对、SPI极性相位不对、还是模块本身坏了。软件SPI则可以把问题拆得很细GPIO能不能正常拉高拉低MOSI波形对不对MISO有没有返回数据每个环节都能单独验证。我第一次调RC522时先用软件SPI把寄存器读通了才敢回头碰硬件SPI。第三读卡场景对速率要求不高。RC522射频通信本身不追求高吞吐STM32主频72MHzGPIO翻转速度动辄几MHz软件SPI完全跑得动。刷卡识别这种低频交互场景瓶颈在射频层而不在SPI总线。当然软件SPI也有代价CPU占用高无法利用DMA如果你同时要跑复杂的协议栈或GUI动画就得斟酌。但对于本项目软件SPI是容错率最高的方案。1.3 一套能“无缝迁移”到硬件SPI的引脚规划我见过不少教程软件SPI的引脚随便挑PA0、PA1、PA2、PA3乱接一通。等你想从软件SPI切到硬件SPI时发现引脚完全不兼容只能飞线重接非常痛苦。建议从一开始就把软件SPI的引脚规划在SPI1默认引脚附近。我用的方案是RC522的SCK接STM32的PA5SPI1_SCK默认引脚RC522的MOSI接PA7SPI1_MOSI默认引脚RC522的MISO接PA6SPI1_MISO默认引脚RC522的SDA片选SS接PA4SPI1_NSS所在引脚但这里我用软件控制这样设计的好处是将来你想试试硬件SPI只需要把GPIO模式从通用推挽输出改成复用推挽再初始化SPI外设物理接线一根都不用动。OLED我放在PB6、PB7这两个引脚恰好是I2C1的默认SCL和SDA。同理以后想切硬件I2C同样不用飞线。软件驱动只是手段不是目的。给自己留好后路这才是老手做事的习惯。2. 硬件接线引脚分配背后的考量和坑2.1 RC522的SPI引脚定义与接线表很多第一次接触RC522的人会被模块上的丝印搞晕因为这模块的引脚标注在不同商家手里居然不一样。最常见的标注是SDA、SCK、MOSI、MISO、IRQ、GND、RST、VCC。这里有个巨大坑点模块上标SDA的那个引脚在SPI模式下其实是SPI的片选信号不是I2C的数据线。RC522本身有三种通信模式SPI、I2C、UART由模块上的引脚电平选择。市面上绝大多数模块出厂默认是SPI模式此时SDA引脚被当作SPI的从机选择线也就是我们常说的SS/CS。如果你把它当成I2C的SDA接那SPI时序根本跑不起来。我的接线表如下RC522模块引脚连接到STM32F103C8T6说明SDAPA4SPI片选CS软件控制高低电平SCKPA5SPI时钟软件模拟输出MOSIPA7主机输出从机输入MISOPA6主机输入从机输出必须接IRQ不接本教程用轮询方式不使能中断RSTPA3复位引脚接普通GPIO输出高3.3V3.3V模块供电GNDGND共地强调一下MISO这根线。很多简化教程说读卡只要三根线加电源就行实际上UID读取需要从RC522接收数据帧MISO必须接好。少了它你能发出寻卡命令但永远读不到卡号。IRQ引脚不是不能用但是用中断方式会让代码复杂度上一个台阶。轮询读卡对大多数场景都够用所以IRQ悬空即可。2.2 OLED接线与3.3V供电的统一OLED我选的是I2C接口的四针版本VCC、GND、SCL、SDA。接线也很简单SCL接PB6SDA接PB7VCC和GND分别接3.3V和GND。供电是整个项目中比较容易翻车的环节。RC522的射频功率比较大启动瞬间电流可能到几十毫安甚至更高如果从STM32最小系统板上的LDO直接取电而系统板又同时给OLED和别的外设供电电压容易跌到3.0V以下表现就是OLED显示花屏、RC522读卡不稳定、甚至芯片反复复位。我实测下来最稳的接法STM32和RC522、OLED共用一个干净的3.3V电源而不是让RC522去吃系统板的5V再板载稳压。很多RC522模块确实板载了AMS1117-3.3稳压芯片允许5V输入但5V与STM32引脚之间存在电平不匹配的风险。如果模块的IO口没有做电平转换5V供电的模块输出高电平可能达到5V直接灌进STM32的PA6引脚长期看有损坏风险。因此统一3.3V是省心方案。2.3 接线阶段最容易翻车的三个细节第一确认模块模式。有些RC522模块背面有跳线或0欧电阻用来配置SPI、I2C、UART模式。如果模块默认不是SPI模式你按SPI时序驱动寄存器读出来全是0xFF卡怎么刷都不认。拿到模块先看丝印和卖家说明确定是SPI模式再动手。第二注意RST不是必接线但对排错很重要。把RC522的RST接到一个普通GPIO初始化时先拉低再拉高相当于给芯片来一次硬件复位能解决很多“上电后状态不对”的诡异问题。如果你不接RSTRC522内部上电状态可能不对导致寄存器写入不生效。第三杜邦线尽量短。软件SPI本身容忍度很高但RFID模块周围如果有电机、继电器、大电流走线天线附近的干扰会让读卡距离急剧下降。我的经验是RC522天线区域不要和杜邦线交叉线材长度控制在15cm以内。3. 软件模拟SPI把GPIO变成一条正经总线3.1 SPI Mode 0时序拆解RC522的SPI接口支持一种经典模式Mode 0也就是CPOL0、CPHA0。拆开讲就是空闲状态下时钟线SCK保持低电平数据在SCK上升沿被采样数据在SCK下降沿发生切换。换句话说主机发送一位数据的流程是把MOSI引脚设置为当前要发送的bit值。把SCK拉高此时从机在上升沿采样MOSI。从机同时会把MISO引脚设置成它要返回的bit值。主机在SCK高电平期间读取MISO得到接收bit。把SCK拉低完成一位数据的收发。这八个步骤重复8次就完成了一个字节的全双工传输。SPI是全双工协议MOSI和MISO在同一时钟周期内同时工作所以“发送一个字节”的同时也“接收了一个字节”只是看你用不用接收到的数据。我第一次写软件SPI时犯过一个错误先把SCK拉高再去设置MOSI电平。这样在上升沿来临时MOSI还没稳定从机采样到的是上一个bit的电平。正确的顺序一定是先预备MOSI再拉高SCK。3.2 核心收发函数实现基于上面的时序我写了一个通用字节收发函数。这里用标准外设库的GPIO操作如果你用HAL库只需把GPIO读写宏替换成HAL_GPIO_WritePin和HAL_GPIO_ReadPin。#define RC522_SCK_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_5) #define RC522_SCK_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_5) #define RC522_MOSI_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_7) #define RC522_MOSI_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_7) #define RC522_MISO_READ() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) #define RC522_CS_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4) #define RC522_CS_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4) uint8_t RC522_SoftSPI_TransferByte(uint8_t byte) { uint8_t rx 0; for (uint8_t i 0; i 8; i) { if (byte 0x80) RC522_MOSI_HIGH(); else RC522_MOSI_LOW(); byte 1; RC522_SCK_HIGH(); rx 1; if (RC522_MISO_READ()) rx | 0x01; RC522_SCK_LOW(); } return rx; }这段代码里需要注意一个顺序细节SCK拉高之后先让rx左移一位再读取MISO。这样第一次循环读到的bit7会进到rx的bit0第二次循环左移后变到bit1依次类推8次循环后rx就是完整的接收字节。GPIO初始化部分要注意SCK、MOSI、CS、RST都要配置成推挽输出模式MISO配置成上拉输入或浮空输入。初始化完成后把RC522的RST引脚拉高让模块退出复位状态同时SCK保持低电平CS保持高电平这是SPI总线空闲状态的标准姿势。void RC522_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); RC522_CS_HIGH(); RC522_SCK_LOW(); RC522_RST_HIGH(); }3.3 RC522寄存器读写函数与地址字节的“历史坑”RC522的SPI帧格式是第一个字节是寄存器地址字节第二个字节对于写操作是数据字节。但寄存器地址字节的定义在不同库里有两种写法这是新手最容易踩的坑之一。正规的数据手册写法是地址字节由7位寄存器地址加1位读写标志组成读标志为1。所以读寄存器的命令字节应该是(addr 1) | 0x01写寄存器命令字节是(addr 1) 0x7E。但我实测过网上大量STM32例程它们用的是另一套写法读寄存器的命令字节是((addr 1) 0x7E) | 0x80也就是把读标志放在bit7。这两种写法竟然都能用原因是RC522内部对地址字节存在一定的宽容处理而各家的寄存器地址常量定义方式也不同。为了避免读者在移植时被折磨我直接给出我正在用、实测稳定的版本void RC522_WriteReg(uint8_t addr, uint8_t val) { RC522_CS_LOW(); RC522_SoftSPI_TransferByte((addr 1) 0x7E); RC522_SoftSPI_TransferByte(val); RC522_CS_HIGH(); } uint8_t RC522_ReadReg(uint8_t addr) { uint8_t val; RC522_CS_LOW(); RC522_SoftSPI_TransferByte(((addr 1) 0x7E) | 0x80); val RC522_SoftSPI_TransferByte(0x00); RC522_CS_HIGH(); return val; }注意读操作时主机发送完地址字节后还要继续发送一个0x00字节来提供时钟MISO上的数据才会被移出来。这就是为什么读函数里调用了两次TransferByte。我建议拿到一个陌生RC522例程时先读一下VersionReg寄存器地址0x37验证SPI通信是否正常。正常情况下返回值应该是0x92或0x91之类如果读到0xFF或者0x00说明接线、SPI模式或地址字节写法有问题。4. RC522读卡流程寻卡、防碰撞到拿UID4.1 复位与天线初始化RC522上电后不会自动处于可用状态需要按顺序做四件事软复位、关闭天线、配置定时器、开启天线。软复位操作很简单向CommandReg命令寄存器写0x0F然后延时至少5毫秒等芯片内部状态机归位。void RC522_Init(void) { RC522_GPIO_Init(); RC522_RST_HIGH(); RC522_WriteReg(CommandReg, 0x0F); // 软复位 delay_ms(10); RC522_WriteReg(TModeReg, 0x8D); // 定时器使能自动触发 RC522_WriteReg(TPrescalerReg, 0x3E); RC522_WriteReg(TReloadRegH, 0x00); RC522_WriteReg(TReloadRegL, 0x1E); // 定时器重装载值 RC522_WriteReg(TxASKReg, 0x40); // 强制ASK调制 RC522_WriteReg(ModeReg, 0x3D); // CRC初始值0x6363 RC522_AntennaOn(); } void RC522_AntennaOn(void) { uint8_t val RC522_ReadReg(TxControlReg); if (!(val 0x03)) RC522_WriteReg(TxControlReg, val | 0x03); }这里稍微解释一下TModeReg、TPrescalerReg、TReloadReg的作用。RC522内部有一个定时器用来控制射频通信的超时时间。如果卡片没有响应总线不能永远等下去。配置好定时器后每次通信都能在超时后返回错误状态程序才有机会继续做别的事。我把超时时间设得很短保证卡片没贴近时主循环能快速轮询。天线开启的本质是让RC522的天线驱动电路上电寄存器TxControlReg的bit0和bit1控制天线信号输出。上电初始化只做一次如果每次读卡前都开关天线反而会导致通信不稳定。4.2 寻卡请求REQA状态机RC522读卡流程可以拆成几个标准步骤寻卡请求、防碰撞、选卡、认证、读写。本项目只读取UID所以走到防碰撞拿到卡号就够了认证和读写都不需要。寻卡请求的目的是检测天线范围内有没有卡片。RC522发送一个0x26指令REQA卡片收到后会返回2字节的ATQA应答。uint8_t RC522_Request(uint8_t reqMode, uint8_t *atqa) { uint8_t status MI_OK; uint8_t irqEn 0x77; uint8_t waitIRq 0x30; RC522_WriteReg(ComIEnReg, irqEn); // 允许接收中断和定时器中断 RC522_WriteReg(CommandReg, PCD_IDLE); RC522_WriteReg(FIFOLevelReg, 0x80); // 清空FIFO RC522_WriteReg(BitFramingReg, 0x07); // TX最后发送7位 RC522_WriteReg(FIFODataReg, reqMode); RC522_WriteReg(CommandReg, PCD_TRANSCEIVE); RC522_WriteReg(BitFramingReg, 0x07); status RC522_WaitForIRq(waitIRq, 100); if (status ! MI_OK) return status; uint8_t fifoLevel RC522_ReadReg(FIFOLevelReg); if (fifoLevel 0) return MI_ERR; atqa[0] RC522_ReadReg(FIFODataReg); atqa[1] RC522_ReadReg(FIFODataReg); return MI_OK; }这里有几个容易出错的小细节。一是发送命令前要先把CommandReg写成PCD_IDLE让芯片回到空闲态。二是要清空FIFO缓冲区否则上次通信的残留数据会污染这次结果。三是有时候要设置BitFramingReg为0x07表示最后一个字节只发送7位这是ISO14443协议规定的REQA命令格式。等待中断状态的时候我推荐用轮询ComIrqReg寄存器而不是死等并加一个超时判断。RC522内部定时器会在超时后置位TimerIRq这样程序永远不会卡死。4.3 防碰撞与UID读取当卡返回ATQA后可以认为天线范围内至少有一张卡存在但现在还不知道卡号。下一步是防碰撞循环。防碰撞命令的结构是级联命令字节0x93表示第一级、0x20表示防碰撞操作然后卡片会返回4字节的UID序列号和1字节的校验值BCC。uint8_t RC522_Anticollision(uint8_t *uid) { uint8_t status MI_OK; uint8_t i, bcc, checksum 0; RC522_WriteReg(CommandReg, PCD_IDLE); RC522_WriteReg(FIFOLevelReg, 0x80); RC522_WriteReg(BitFramingReg, 0x00); RC522_WriteReg(FIFODataReg, 0x93); RC522_WriteReg(FIFODataReg, 0x20); RC522_WriteReg(CommandReg, PCD_TRANSCEIVE); RC522_WriteReg(BitFramingReg, 0x00); status RC522_WaitForIRq(0x30, 100); if (status ! MI_OK) return status; uint8_t fifoLevel RC522_ReadReg(FIFOLevelReg); if (fifoLevel ! 5) return MI_ERR; for (i 0; i 4; i) { uid[i] RC522_ReadReg(FIFODataReg); checksum ^ uid[i]; } bcc RC522_ReadReg(FIFODataReg); if (checksum ! bcc) return MI_ERR; return MI_OK; }FIFOLevelReg返回5意味着FIFO中有5个字节等待读取4字节UID加1字节BCC校验。校验原理很简单前四个字节按位异或的结果应该等于BCC。如果你的项目做多卡识别关键就在这一环节的CollReg寄存器处理。当两卡同时靠近时防碰撞命令可能返回冲突CollReg中会记录冲突位位置。RC522可以逐位解析出两张卡的UID这也就是热词里“rc522 多卡识别”的实现基础。本教程先不展开后面扩展部分我再提思路。拿到UID后我们其实已经完成了主要目标。要不要做选卡操作取决于后续是否需要读写扇区。只显示UID的话到这一步就可以把数据交给OLED了。4.4 校验与状态返回我在代码里用了一种简单的状态码约定MI_OK表示成功MI_ERR表示失败。这是从开源MFRC522库继承来的习惯好处是调用处代码非常清晰。#define MI_OK 1 #define MI_NOTAG 2 #define MI_ERR 0等待中断状态的函数是这样的uint8_t RC522_WaitForIRq(uint8_t waitIRq, uint16_t timeoutMs) { while (timeoutMs--) { uint8_t irq RC522_ReadReg(ComIrqReg); if (irq waitIRq) return MI_OK; delay_ms(1); } return MI_TIMEOUT; }这里有个经验点读取ComIrqReg后最好主动写一下把中断标志位清掉。我实际调试时发现不清标志位的后果是第二次读卡时上一次的中断状态仍然存在导致时序错乱表现为“第一次能读到卡第二次却一直报成功但数据是老的”。清标志位的操作很简单向ComIrqReg写入0x7F即可。我在初始化时做了一次并在每次通信前都把相关寄存器重置效果稳定。5. OLED显示与主循环整合5.1 SSD1306的最小驱动0.96寸OLED的控制芯片一般是SSD1306I2C接口地址通常是0x3C。驱动OLED的原理说白了就是两件事初始化屏幕参数、往显存写数据。SSD1306内部有1KB显存对应128x64像素每个bit代表一个像素。I2C传输时每帧数据需要先发控制字节0x00表示后面跟的是命令0x40表示后面是显示数据。我提供一个极简初始化函数足以点亮屏幕并显示内容void OLED_Init(void) { uint8_t initCmd[] { 0xAE, 0x20, 0x10, 0xB0, 0xC8, 0x00, 0x10, 0x40, 0x81, 0x7F, 0xA1, 0xA6, 0xA8, 0x3F, 0xA4, 0xD3, 0x00, 0xD5, 0x80, 0xD9, 0xF1, 0xDA, 0x12, 0xDB, 0x40, 0x8D, 0x14, 0xAF }; for (uint8_t i 0; i sizeof(initCmd); i) OLED_WriteCmd(initCmd[i]); }字符显示就比较麻烦了需要内置字符点阵库。我用的是8x16字体每个ASCII字符占16字节一个字符正好显示在一个16像素高的页里。如果你不想手工制作字模去网上搜“OLED 8x16字模”一抓一大把复制成C数组直接就能用。第一次做项目可以先用8x16字体因为它在0.96寸屏上可读性最好比6x8那种细字体强得多。显示函数我并不打算全部贴出来思路是每个字符8列宽乘以16行高逐列取出字节写入OLED显存。128x64屏幕能显示8列16行的字符如果8x16字体横向16个字符纵向4行。5.2 把UID显示出来拿到4字节UID后直接显示十六进制格式最直观。比如卡片UID是0xA1 0xB2 0xC3 0xD4屏幕上可以显示“UID: A1B2C3D4”。我封装了一个十六进制转换函数把字节数组转成字符串void UID_ToHexString(uint8_t *uid, char *str) { char hexTable[] 0123456789ABCDEF; for (uint8_t i 0; i 4; i) { str[i * 2] hexTable[uid[i] 4]; str[i * 2 1] hexTable[uid[i] 0x0F]; } str[8] \0; }显示的时候先清屏再定位到第0列第0页显示“UID:”然后在下一行显示卡号字符串。清屏操作要把OLED的整个显存区填0x00同时把内部的页地址指针复位到起始位置。这里有个容易忽略的问题OLED的显存地址不是连续自动递增到边界的写满一页128字节后需要重新设置页地址和列地址否则数据会写到屏幕外面去出现“显示一半、另一半花屏”的情况。我的OLED_WriteData函数里每写完一页就自动切换页地址规避这个问题。void OLED_ShowUID(uint8_t *uid) { char uidStr[9]; UID_ToHexString(uid, uidStr); OLED_Clear(); OLED_ShowString(0, 0, UID:); OLED_ShowString(0, 2, uidStr); OLED_Display(); }OLED的“显示”动作本质是把内存中的数据刷新到屏幕。如果你的代码选择每次都直接写显存而不用全屏缓冲刷新会闪烁全屏缓冲方式虽然多占1KB内存但显示效果干净利落。STM32F103C8T6有20KB RAM1KB显存完全能接受。5.3 主循环怎么写才不会重复触发主循环看起来很简单一个while里不断轮询但实际调试中有一个让我很头疼的问题卡片一直贴着天线程序就会每秒重复显示好几遍同一个卡号OLED不断闪烁看着非常烦躁。解决思路有两种。第一种是读到卡后加延时比如延时500毫秒再继续轮询。简单有效但缺点是如果卡片始终不拿走程序还是会周期性刷新。第二种是记录上一次读取的UID只有当前卡号和上次不同才更新显示。我推荐第二种代码也不复杂uint8_t lastUid[4] {0xFF, 0xFF, 0xFF, 0xFF}; uint8_t uid[4] {0}; while (1) { uint8_t atqa[2]; if (RC522_Request(PICC_REQALL, atqa) MI_OK) { if (RC522_Anticollision(uid) MI_OK) { if (memcmp(uid, lastUid, 4) ! 0) { OLED_ShowUID(uid); memcpy(lastUid, uid, 4); } } } delay_ms(50); }用memcmp比较新旧UID只有卡号变化时才刷新屏幕。这样卡片一直贴近时屏幕只刷新一次拿走再贴过来又能立即读取新卡。5.4 标准库与HAL库的迁移提示我上面所有代码用的是STM32标准外设库很多从Arduino转过来的同学可能不习惯。其实迁移到HAL库的差异很小核心就是把GPIO操作替换一下。我把常用的宏映射列出来标准库写法HAL库等价写法GPIO_SetBits(GPIOA, GPIO_Pin_5)HAL_GPIO_WritePin(GPIOA, GPIO_Pin_5, GPIO_PIN_SET)GPIO_ResetBits(GPIOA, GPIO_Pin_5)HAL_GPIO_WritePin(GPIOA, GPIO_Pin_5, GPIO_PIN_RESET)GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6)HAL_GPIO_ReadPin(GPIOA, GPIO_Pin_6)RCC_APB2PeriphClockCmd__HAL_RCC_GPIOA_CLK_ENABLE软件SPI的字节收发函数、RC522寄存器读写函数、寻卡防碰撞逻辑完全不用改。你只需要重新写GPIO初始化部分和延时函数。如果你用的是CubeMX生成的HAL工程直接在用户代码区加这些函数就行。6. 实测现象与常见问题排查6.1 首次上电后的正常现象全部接线完成烧录程序后正常现象应该是OLED背光亮起屏幕清空后显示“UID:”两行字符。没有卡片靠近时RC522的天线会持续发射13.56MHz的射频场OLED保持静止。当你把一张IC卡贴近RC522天线区域大约距离3到5厘米就能触发读取OLED会即刻刷新显示类似“UID: 5B3A91C2”这样的卡号。卡片移走后屏幕保持上次显示的内容不会自动清空这是合理行为因为本项目的目的就是显示卡号。我实测时用过的卡片包括空白M1卡、某写字楼的门禁卡、校园一卡通、还有手机上的NFC模拟卡。只要工作在13.56MHz、符合ISO14443A协议都能正确读出UID。如果你是第一次调试我建议先不要接OLED先用软件SPI读VersionReg寄存器验证通信正常再用示波器或逻辑分析仪观察MOSI和MISO波形。逻辑分析仪能看到SCK上清晰的方波以及MISO线上来自RC522的响应数据这就说明总线层面已经通了。6.2 问题排查表我在这个项目上踩过的坑不少有些问题折腾了我一晚上整理成一张表希望能帮你省点时间。现象可能原因排查与解决OLED完全不亮供电不足、I2C地址不对先查3.3V电压再用I2C扫描程序确认地址是0x3C还是0x3DOLED亮但无显示初始化序列不完整、对比度太低重新核对initCmd数组重点检查0x8D和0x14这条充电泵命令读不到任何卡RC522模式不在SPI、天线没开、MISO没接确认模块跳线为SPI模式检查TxControlReg用万用表测MISO引脚电平第一次能读第二次失败ComIrqReg中断标志未清除每次通信前向ComIrqReg写0x7F清标志读到的UID间歇性错误供电电压不稳、线序接触不良换短杜邦线加10uF到100uF电容在RC522电源脚VersionReg读到0xFFSPI时序不对、地址字节写法不对对照3.3节检查地址字节尝试0x80和0x01两种读标志写法读卡距离特别近天线阻抗不匹配、周围金属干扰天线区域不要贴着金属面杜邦线远离天线线圈特别提醒很多模块上的RST引脚如果不接GPIO而是悬空理论上RC522也能工作但我遇到过部分批次模块悬空时芯片内部状态不稳定导致初始化失败。最保险的做法还是接GPIO并在初始化时拉高。6.3 后续扩展与优化方向这个项目只是RC522和OLED玩法的开始基于这套基础框架我列出几个值得尝试的扩展方向。第一多卡识别。前文提过防碰撞的CollReg寄存器记录了冲突位位置。通过修改防碰撞命令可以在一张张卡片同时进入天线区域时逐个解析出每张卡的UID。这个原理和ISO14443A协议的多卡防碰撞机制有关想深入了解的话可以从“bCollReg寄存器 冲突位递归遍历”这个方向查资料。第二读取M1卡数据块。显示UID只是第一步RC522更强大的功能是读写卡片内部的数据块。执行密钥认证MFAuthent命令使用默认密钥0xFFFFFFFFFFFF就能读取M1卡前几个扇区的数据。如果把读到的数据也显示在OLED上就可以做一个简易的“卡数据查看器”。第三把卡片UID和门禁逻辑结合。加一个继电器模块和蜂鸣器当读到的UID匹配预设白名单时继电器吸合蜂鸣器响一声模拟完整的门禁系统。这个应用场景对学习非常有帮助因为这涉及“上位决策”和“外设联动”。第四移植到HAL库并加入低功耗模式。RC522读卡本身功耗不高如果用电池供电可以让单片机在无卡时进入睡眠模式通过IRQ引脚唤醒。这时候RC522的IRQ引脚就有用武之地了。我在这个项目上最大的体会是软件SPI虽然看起来“低端”但它是理解SPI协议最好的老师。当你用GPIO一踩一个时钟、一位一位地把数据抠出来之后再去看硬件SPI的寄存器配置会觉得一切都顺理成章。很多人卡在RC522上不是寄存器配置不会而是对底层的时序没有直觉只能靠复制粘贴别人的库函数。用软件SPI把这套流程走一遍你就能彻底摆脱“调不通只能换库”的困境。
返回列表