简介:一份基于STM32与RFID技术的停车场管理系统完整项目源码,面向毕业设计、期末大作业和课程设计场景,也适合嵌入式新手对照学习。压缩包共240个文件,约8.33MB,以C源代码、H头文件、Keil工程文件为主体,同时包含编译生成的HEX固件、MAP映射、依赖文件与调试配置;项目内涉及TIM、FLASH、RCC、ADC、I2C、CAN、USART等常用外设驱动层文件,底层模块覆盖较全,便于二次开发和功能扩展。代码注释详细,模块划分清晰,系统具备车辆出入识别、RFID读卡验证、车位信息管理、状态显示与交互等环节,操作流程直观,并经过严格调试可直接运行。压缩包内还附带MD说明文件与全部工程数据,可快速部署为毕设演示或课设原型。作者自述该项目为98分高分项目、获导师认可,页面已有171人浏览学习,整体具有较强的参考与复用价值。
1. 这套 STM32 + RC522 停车场源码,能让你少熬两个通宵
离交卷还有两晚,这种带硬件、带显示、带流程逻辑的 STM32 课设最怕的不是代码量,而是不知道先动哪一块。这套基于 STM32 + RC522 的 RFID 停车场项目,是典型的课设高分形态:刷卡、计时、计费、道闸模拟、LCD 回显一条龙全有。它不是"读个卡号就完事"的玩具 demo,而是把进场、出场、计时、扣费、余额更新串成了一个完整业务闭环,正好能覆盖评委最爱追问的几个点:卡号是怎么读出来的、费用怎么算、如何防止重复扣费。适合三类人:正在赶毕设或期末大作业的在校生、想拿一个完整工程模板来理解嵌入式业务逻辑的入门者、以及需要一套能改的 MFRC522 读写框架做二次开发的从业者。源码包里工程文件、驱动库和配套数据资料都齐,能直接烧录跑通,也能当骨架往上加功能。
2. 硬件链路与 RFID 读卡:RC522 的 SPI 时序、驱动初始化和读卡流程
2.1 接口选型:为什么 RC522 用 SPI 而不是 UART 或 I2C
RC522 这颗 13.56MHz 的读卡芯片,对外提供了 SPI、UART、I2C 三种主机接口,但项目里基本都走 SPI,原因很实际:I2C 的地址线和速率限制多,调试时还要额外挂上拉电阻,经常因为地址冲突读不到设备;UART 虽然线少,但 MFRC522 的 UART 帧格式要按字节拼装,代码写起来绕。SPI 是全双工、时序直观,读卡时的命令帧和响应数据都能通过 MOSI/MISO 一眼看到,配合逻辑分析仪排查问题最方便。
具体的引脚分配是硬件连线里最需要先确认的部分,我按 STM32F103 的 SPI1 外设整理了一张对照表:
| 功能 | STM32 引脚 | 接 RC522 引脚 | 说明 |
|---|---|---|---|
| SCK | PA5 | SCK | SPI 时钟,模式 0(CPOL=0, CPHA=0) |
| MISO | PA6 | MISO | 主机输入,从机输出 |
| MOSI | PA7 | MOSI | 主机输出,从机输入 |
| CS | PA4 | NSS | 软件片选,低电平有效 |
| RST | PA8 | RST | 复位引脚,高电平复位 |
| 电源 | 3.3V / GND | VCC / GND | 注意 RC522 模块不能直接接 5V |
有个细节要留意:PA4 虽然可以被 SPI1 硬件 NSS 复用,但工程里更稳妥的做法是用普通 GPIO 推挽输出手动拉低拉高,这样片选时机完全可控,不会因为 SPI 硬件自动管理 NSS 导致时序错乱。
2.2 驱动初始化:MFRC522 寄存器配置的先后顺序
MFRC522 的寄存器初始化有严格的先后顺序,顺序错了会出现"能读到偶尔的卡号、大多数时候没响应"这类问题。常见做法是先软件复位,让芯片回到默认状态,再配置定时器和发送参数,最后才打开天线。初始化代码一般长这样:
void RC522_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE); // 配置 PA5/PA6/PA7 为 SPI1 复用推挽 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); // 配置 PA4 片选、PA8 复位为普通推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); 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; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); RC522_RST_LOW(); // 硬件复位:拉低 delay_ms(10); RC522_RST_HIGH(); // 拉高进入工作状态 delay_ms(10); RC522_WriteRawRC(MFRC522_REG_COMMAND, PCD_RESETPHASE); // 软件复位 delay_ms(10); // 配置定时器:自动开启,分频 0xA9,重装载 0x03E8 RC522_WriteRawRC(MFRC522_REG_T_MODE, 0x80); RC522_WriteRawRC(MFRC522_REG_T_PRESCALER, 0xA9); RC522_WriteRawRC(MFRC522_REG_T_RELOAD_HI, 0x03); RC522_WriteRawRC(MFRC522_REG_T_RELOAD_LO, 0xE8); RC522_WriteRawRC(MFRC522_REG_TX_ASK, 0x40); // 发送时开启 ASK 调制 RC522_WriteRawRC(MFRC522_REG_MODE, 0x3D); // CRC 初始值 0x6363,TxWait 相关 RC522_AntennaOn(); // 打开天线 }这段代码里最关键的是PCD_RESETPHASE和定时器配置。软件复位会让所有寄存器回到数据手册的默认值,后续配置必须基于这个默认态展开;定时器分频 0xA9 和重装载值 0x03E8 是一组配合 13.56MHz 晶振的经验参数,决定读卡超时窗口。SPI_BaudRatePrescaler_32把 SPI 时钟降到约 2.25MHz(72MHz 主频下),这个速率对 RC522 完全够用,而且能减少高速 SPI 带来的信号过冲问题。
2.3 读卡流程:从寻卡到防碰撞再到选卡
RC522 读一张卡,完整链路是寻卡(Request)→ 防碰撞(Anticoll)→ 选卡(Select)→ 认证扇区 → 读写块。停车场项目只需要拿到卡号做身份识别,所以走到选卡拿到 4 字节序列号就够了。寻卡命令的作用是探测读卡器天线场区内有没有卡片,并返回卡型:
u8 RC522_Request(u8 reqMode, u8 *pTagType) { u8 status; u8 ucNumBits; u8 TagType[2]; // 先把命令寄存器切回空闲,清掉上一轮残留状态 RC522_WriteRawRC(MFRC522_REG_COMMAND, PCD_IDLE); // 7 位短帧格式,用于发送寻卡请求命令 RC522_WriteRawRC(MFRC522_REG_BIT_FRAMING, 0x07); TagType[0] = reqMode; // 0x26 是 ISO14443A 的 Request 命令 RC522_WriteRawRC(MFRC522_REG_TX_LAST_BYTES, 0x00); RC522_WriteRawRC(MFRC522_REG_COMMAND, PCD_TRANSCEIVE); RC522_WriteRawRC(MFRC522_REG_BIT_FRAMING, 0x07); // 0x80 表示启动发送并等待接收,发送 1 字节,接收结果存入 TagType status = RC522_ComMF522(0x80, TagType, 1, TagType, &ucNumBits); if (status == MI_OK) { pTagType[0] = TagType[0]; // 卡型高字节,常见 0x04 pTagType[1] = TagType[1]; // 卡型低字节,常见 0x00 } return status; }0x26是 REQA 短帧命令,卡片收到后会返回 ATQA(2 字节卡型应答)。RC522_ComMF522是 SPI 通信的核心函数,它把命令字节和要发送的数据通过 MOSI 逐字节发出去,同时从 MISO 读回卡片响应,读数据时 CRC 校验由 MFRC522 硬件完成,不需要手动算。
拿到卡型后,如果同时有多个卡片在天线场区内,数据会碰撞,所以要先防碰撞拿到单独一张卡的序列号:
u8 RC522_Anticoll(u8 *pSnr) { u8 i, status; u8 ucNumBits; u8 snr[5]; // 防碰撞不能用短帧,要切回完整字节模式 RC522_WriteRawRC(MFRC522_REG_BIT_FRAMING, 0x00); snr[0] = PCD_ANTICOLLISION; // 0x93:防碰撞命令 snr[1] = 0x20; // 级联级别 1 RC522_WriteRawRC(MFRC522_REG_TX_LAST_BYTES, 0x00); RC522_WriteRawRC(MFRC522_REG_COMMAND, PCD_TRANSCEIVE); RC522_WriteRawRC(MFRC522_REG_BIT_FRAMING, 0x00); status = RC522_ComMF522(0x80, snr, 2, snr, &ucNumBits); if (status == MI_OK) { for (i = 0; i < 4; i++) pSnr[i] = snr[i]; // 取出 4 字节卡序列号 } return status; }这里snr数组前两个字节是命令本身,后 4 个字节会被卡片返回的序列号覆盖。拿到序列号后再做一步 Select 选卡,把卡片的 SAK(选择确认)读回来,整个防碰撞流程才算闭环。选卡命令的第 6 字节是 CRC 校验,这个值由 MFRC522 发送时自动补齐,代码里不需要填。很多初学的翻车点就在这:以为 CRC 要自己算,手动填充反而把命令帧搞坏了。
3. 停车场核心逻辑:状态机、计时扣费与 LCD 回显
3.1 状态机拆分:无车、有车、正在出场三个阶段
停车场系统的业务逻辑比读卡本身更容易写乱。如果全用 if 嵌套,刷卡进场、再刷卡出场、重复读卡这些情况交织在一起,代码很快就成一团乱麻。常见的做法是引入一个三态状态机,让每次读卡事件驱动状态迁移:
typedef enum { STATE_IDLE, // 无车,等待进场刷卡 STATE_PARKED, // 车辆在场内,等待出场刷卡 STATE_LEAVING // 正在出场结算,防止重复扣费 } ParkState; ParkState g_state = STATE_IDLE; u8 g_currentCard[4]; // 当前车辆卡号 u32 g_enterTimeSec; // 进场时间戳(秒) u16 g_balance; // 卡内余额,单位分 u8 g_freeSpaces = 20; // 剩余车位void Parking_OnCardDetected(u8 *cardId) { switch (g_state) { case STATE_IDLE: // 进场逻辑:查卡号是否在白名单,写入进场时间,车位减一 memcpy(g_currentCard, cardId, 4); g_enterTimeSec = RTC_GetCounter(); g_freeSpaces--; g_state = STATE_PARKED; break; case STATE_PARKED: // 出场逻辑:结算费用、扣费、抬杆、状态复位 if (memcmp(g_currentCard, cardId, 4) == 0) { Billing_Settle(g_enterTimeSec); g_freeSpaces++; g_state = STATE_LEAVING; // 抬杆延时后回到 IDLE DelayAndResetState(); } else { // 非本车卡,提示错误 } break; case STATE_LEAVING: // 结算中,忽略任何读卡事件 break; } }状态机的关键收益是让"正在结算"成为一个独立状态。出场刷卡后,如果卡还没拿开、又触发了一次读卡,STATE_LEAVING会把这次请求直接吞掉,从根上杜绝重复扣费。这个设计在课设答辩时非常加分,评委问到"如果我连续刷两次卡怎么办"就能直接回答。
3.2 计时与扣费:时间戳运算和金额结算
计时有两种方案:用 STM32 的 RTC 外设,或者用 SysTick 维护一个开机以来的秒计数。RTC 的优势是掉电后靠备份电池还能继续走,适合模拟真实停车场跨天计费;SysTick 方案则简单直接,代码量少。项目源码里用的是 RTC 计数器,读取RTC_GetCounter()拿到的就是自设定的时间起点以来的秒数,进场和出场两个时间戳相减就得到停车秒数。
扣费逻辑要兼顾"免费时长"和"阶梯计费"两个常见需求,我一般把金额单位设为分,全程用整数运算避开浮点:
u16 Billing_Calc(u32 enterTime, u32 exitTime) { u16 fee = 0; u32 diffMin = (exitTime - enterTime) / 60; // 停车总分钟数 if (diffMin <= 15) { fee = 0; // 15 分钟内免费 } else { diffMin -= 15; // 减去免费时长 // 首小时 4 元,超出部分每小时 2 元,不足一小时按一小时算 u8 hours = (diffMin + 59) / 60; if (hours <= 1) fee = 400; else fee = 400 + (hours - 1) * 200; } return fee; }void Billing_Settle(u32 enterTime) { u32 exitTime = RTC_GetCounter(); u16 fee = Billing_Calc(enterTime, exitTime); if (g_balance >= fee) { g_balance -= fee; // 余额足够,直接扣 // 显示扣费金额和余额 LCD_ShowFeeAndBalance(fee, g_balance); } else { // 余额不足,提示补缴 LCD_ShowString(20, 160, (u8*)"余额不足", RED, WHITE, 16, 0); } }(diffMin + 59) / 60是向上取整的整数写法,保证停车 1 分钟也按 1 小时收费。这些计算规则都在源码的billing.c里,想改成"每 30 分钟 1.5 元"之类的计费规则,只需要改这个函数,不用动状态机。
3.3 LCD 回显与蜂鸣器:把状态变成可见可听的反馈
LCD 用的是 ILI9341 驱动的 2.4 寸屏,SPI 接口,和 RC522 共用 SPI1 但片选不同。LCD 初始化的第一步是读设备 ID,这里藏着一个常见的坑:部分兼容屏读回来的 ID 不是 0x9341,而是 0xA1A1 这类非标准值。如果初始化代码里死等 0x9341,就会一直卡在初始化流程里出不来。
void LCD_Init(void) { u16 id = LCD_ReadID(); // 兼容屏的 ID 可能是 0xA1A1 或 0x9341,都按 9341 时序初始化 if ((id != 0x9341) && (id != 0xA1A1)) { // 打印警告但不阻塞,继续按默认时序初始化 printf("LCD ID: 0x%04X\r\n", id); } LCD_Reset(); // ILI9341 的标准初始化序列,逐条发送 LCD_WriteCmd(0xCF); LCD_WriteData(0x00); LCD_WriteData(0x83); LCD_WriteData(0x30); // ... 省略中间若干初始化命令 LCD_WriteCmd(0x29); // 开显示 }显示部分要特别注意中文字符编码。源码里所有中文字符串常量在 Keil 工程中保存为 GBK 编码,LCD 字库取模工具也按 GBK 取模,两边才能对上。如果你用 VS Code 打开源文件改了中文再拷回 Keil,文件可能被存成 UTF-8,显示就全是乱码。解决方案是统一用 Keil 的 Edit → Configuration → Editor → Encoding 设为 GB2312 或 Chinese Simplified。这个细节我在第四部分还会重点展开。
蜂鸣器反馈就简单了:进场刷成功响一声短音,出场扣费成功响两声,余额不足响三声长音。用定时器 PWM 驱动蜂鸣器可以控制音调,但课设场景只需要开关电平加延时就能做出区分度。
4. 常见问题排查与避坑:五次翻车换来的调试经验
4.1 SPI 引脚接错,读卡全 FF
现象:RC522 初始化后读卡,返回的卡号全是 0xFF,或者RC522_Request一直返回超时。
原因:最常见的是 SPI 引脚复用配置错误。比如把 RC522 的 MISO 接到 PA7(MOSI)、MOSI 接到 PA6(MISO),数据通路就是错的,读回来的内容自然全是 FF;其次是片选极性搞反,RC522 的 NSS 是低有效,代码里如果默认拉高,芯片永远不会被选中。
解决:先用逻辑分析仪或示波器抓 SCK 和 CS 的波形,确认 CS 在通信期间有低电平脉冲;再用万用表对着原理图逐脚量 PA4/PA5/PA6/PA7 到 RC522 模块引脚的连通性。没有仪器时,写一个 GPIO 翻转测试程序,把 PA4~PA7 分别拉高拉低,看模块端对应引脚电平变化,也能快速定位接线错误。
4.2 第一次读卡正常,第二次死活读不出来
现象:程序上电后第一次刷卡能读到卡号,按下复位再刷也能读,但连续操作时第二次总是失败。这个现象在课设演示时最容易翻车。
原因:MFRC522 一轮收发结束后,命令寄存器还停留在PCD_TRANSCEIVE状态,没有回到PCD_IDLE,寄存器的 FIFO 缓冲区里残留了上一次的数据。下一轮RC522_Request发命令时,上一轮的残留数据干扰了收发状态机,导致命令发不出去。
解决:在每次RC522_Request和RC522_Anticoll的开始处,强制写一次PCD_IDLE命令,再清除 FIFO 指针。源码里RC522_Request的第一行就是RC522_WriteRawRC(MFRC522_REG_COMMAND, PCD_IDLE),这个顺序不能省。我自己的习惯是封装一个RC522_ClearFIFO(),在每次读卡前调用,把FIFO_LEVEL清零。
4.3 卡片识别率低,供电和天线是最常见的玄学
现象:卡片要贴着天线才能读到,距离稍微远一点就没反应,或者 10 次里有 3 次读不到。
原因:两个,一是模块供电不足,RC522 天线驱动需要较大的瞬时电流,如果 3.3V 是从 STM32 板子的 AMS1117 直接拉的,且板上还有其他大电流外设,电压会被拉低;二是天线线圈本身,RC522 模块的天线尺寸小,读卡距离本来就只有 3~5 厘米,如果模块靠近金属桌面或跟随动线缆走线杂乱,性能会进一步恶化。
解决:给 RC522 的 VCC 和 GND 之间并一个 100uF 电解电容加一个 0.1uF 陶瓷电容,这是最立竿见影的改善。测试时不要把模块放在金属面上,手持或用塑料支架垫高。如果想加大读卡距离,可以把模块天线部分改为外接更大面积的线圈天线,但天线匹配参数要调,否则可能适得其反。
4.4 LCD 中文乱码,GBK 和 UTF-8 的编码战争
现象:LCD 上英文和数字显示正常,中文全部变成乱码方块,或者干脆不显示。
原因:工程里的中文字符串常量是 GBK 编码,但显示函数按 UTF-8 字节流去取模,字库索引对不上。更隐蔽的是:Keil 默认按 GBK 编译源码,如果你用 VS Code 等 UTF-8 编辑器改过文件并保存,混合编码会导致部分字符串乱。
解决:工程内所有源文件统一用 GB2312/GBK 编码保存。在 Keil 中设置Edit → Configuration → Encoding → Chinese GB2312 (Simplified),然后全选源文件重开一遍。从别处拷贝代码进来时,先粘贴到纯文本编辑器里确认编码,再贴进 Keil。源码里所有中文字符串已经按 GBK 取模,只要编码统一就能直接显示。
4.5 出场重复扣费,状态机缺失或标志位提前清零
现象:一张卡进场后出场结算,扣了一次费,紧接着同一个卡再次刷卡,又扣了一次。
原因:出场结算完成后,状态机直接回到了STATE_IDLE,这张卡就变成了"新进场车辆"的状态。如果读卡函数在主循环里每 100ms 轮询一次,而卡还没拿开,第二次轮询就把它当成新卡处理了。
解决:结算完成后必须让这张卡进入一个"冷却期",比如把状态切到STATE_LEAVING,抬杆动作完成后延时几秒再回到STATE_IDLE。最严谨的做法是同时记录上一张已结算卡号,在STATE_IDLE状态下如果读到和上一张相同的卡号且时间差小于 3 秒,就忽略这次读卡。这两个机制源码里都实现了,测试时把卡放在天线上一动不动地刷,观察是否只扣一次费。
5. 进阶改造:把单机 demo 升级成可配置的完整工程
5.1 卡号白名单挪进外部 EEPROM,免重烧换卡
停车场系统必然要管理"哪些卡能进场"。直接把白名单数组写在代码里,换了卡就得重新编译烧录,这是 demo 和真实工程的差距。常见做法是把白名单挪到 I2C 接口的 AT24C02 上,开机时从 EEPROM 加载到内存数组,并预留一个"管理卡"来现场增删白名单:
#define MAX_CARD_COUNT 10 u8 g_cardList[MAX_CARD_COUNT][4]; u8 g_cardCount = 0; void CardList_LoadFromEEPROM(void) { g_cardCount = AT24C02_ReadOneByte(0x00); // 第一字节存卡的数量 if (g_cardCount > MAX_CARD_COUNT) g_cardCount = 0; for (u8 i = 0; i < g_cardCount; i++) { for (u8 j = 0; j < 4; j++) { g_cardList[i][j] = AT24C02_ReadOneByte(1 + i * 4 + j); } } }这样设备交付后配卡、删卡都在现场完成,再也不需要接下载器。AT24C02的 I2C 地址是 0xA0(写)和 0xA1(读),注意 STM32 的 I2C 外设地址不带读写位,库函数里传参用的是 0xA0 >> 1,这是 I2C 驱动最常见的笔误点。
5.2 道闸模拟换成 SG90 舵机,动作更真实
原来工程里道闸是用一个 LED 亮灭模拟的,答辩时说服力不够。换成一个 9g 舵机当道闸杆,效果立刻不一样。SG90 是模拟舵机,用 20ms 周期、1ms~2ms 脉宽的 PWM 控制角度,STM32 的定时器输出比较刚好能实现:
void Servo_Init(void) { TIM_TimeBaseInitTypeDef TIM_InitStructure; TIM_OCInitTypeDef OC_InitStructure; // TIM2 输出比较通道 1,PA0 输出 PWM TIM_InitStructure.TIM_Period = 1999; // 20ms 周期,2000 计数 TIM_InitStructure.TIM_Prescaler = 719; // 72MHz / 720 = 100kHz TIM_InitStructure.TIM_ClockDivision = 0; TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_InitStructure); OC_InitStructure.TIM_OCMode = TIM_OCMode_PWM1; OC_InitStructure.TIM_OutputState = TIM_OutputState_Enable; OC_InitStructure.TIM_Pulse = 100; // 初始 1ms 脉宽,对应 0 度 OC_InitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &OC_InitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_Cmd(TIM2, ENABLE); }void Servo_SetAngle(u8 angle) { // 0 度 = 0.5ms,180 度 = 2.5ms,折算成计数 u16 pulse = 50 + angle * 200 / 180; TIM_SetCompare1(TIM2, pulse); }落杆时从 90 度转到 0 度直接跳变看起来生硬,更自然的做法是在主循环里每 20ms 递增或递减 1 度,模拟匀速落杆的动作过程。PWM 的脉宽上限因舵机品牌而异,有些舵机超出 2.5ms 会堵转发热,调试时先从 1ms 开始一点点加。
5.3 串口日志分级,调试效率翻倍
读卡这种时序敏感的操作,全靠 Keil 的断点调试不现实。我在工程里加了三级日志宏,发布时关掉调试输出,定位问题时打开:
#define DBG_LEVEL 2 // 0=关闭, 1=错误, 2=警告, 3=信息 #if DBG_LEVEL >= 3 #define DBG_INFO(fmt, ...) printf("[INFO] " fmt "\r\n", ##__VA_ARGS__) #else #define DBG_INFO(fmt, ...) #endif #if DBG_LEVEL >= 2 #define DBG_WARN(fmt, ...) printf("[WARN] " fmt "\r\n", ##__VA_ARGS__) #else #define DBG_WARN(fmt, ...) #endif #if DBG_LEVEL >= 1 #define DBG_ERR(fmt, ...) printf("[ERR] " fmt "\r\n", ##__VA_ARGS__) #else #define DBG_ERR(fmt, ...) #endif// 使用示例 DBG_INFO("Card detected: %02X %02X %02X %02X", id[0], id[1], id[2], id[3]); DBG_WARN("Balance insufficient, fee=%d balance=%d", fee, g_balance); DBG_ERR("RC522_Request timeout, status=0x%02X", status);从那以后我每次做完一版改动,都强制自己走一遍"冷启动读卡 → 进场 → 断电重启 → 再读卡 → 出场扣费 → 余额核对"的完整流程,串口日志开到 INFO 级全程盯着状态迁移。这套基于 STM32 + RC522 的停车场源码已经把这个流程固化在工程里了,你拿到手先别急着改功能,按这套顺序跑一遍,确认状态机、扣费和 LCD 三条链路都通,再动手加自己的业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取