ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于单片机的RFID消费管理系统设计全解析

基于单片机的RFID消费管理系统设计全解析

最近好几个做课程设计和项目实训的读者都来问我同一个题目——基于单片机的RFID消费管理系统设计。这个题目听起来挺经典,网上方案也很多,但很多人照着做出来的东西只能算“演示板”,离真正能跑起来的消费系统差得远。问题基本都出在同一个地方:把RC522模块接上单片机,能读到卡号,就以为完成了大半,结果一涉及余额扣减、重复刷卡、数据写不稳这些事,整个逻辑就崩了。

这篇文章我把当时完整做完这套系统的思路整理一遍,从需求拆解到硬件选型,再到SPI通信链路、扣费状态机,最后是调试中真正踩过的坑。为了便于复现,接口定义、协议帧结构、读写流程、排查顺序我都会写清楚,多数内容是基于这套系统在课程设计和工程实践中的常见做法扩展而来,你拿到之后直接当参考骨架用就行。

1. 先搞清楚这套系统真正要解决的问题

RFID消费管理系统,本质是一台“电子钱包终端”。它模拟的场景是食堂、小卖部、图书馆自助消费这类地方:用户拿一张IC卡,靠近读卡器,终端识别身份,从卡内余额里扣掉一笔消费金额,再把余额写回卡内,同时给你看余额、发出提示声。整个过程要求在1秒内完成,还得防止同一张卡在同一笔交易里被重复扣款。

很多人第一步就栽在需求理解上。网上大量例程只做“读UID”,也就是把卡内唯一序列号读出来显示在液晶屏上。这在门禁、考勤场景够用,但消费系统绝对不行。消费系统关心的是卡内的钱,钱存在M1卡的扇区数据块里,不是固定在UID里的,你需要主动去读余额、写新余额,而不是只认卡号。所以,你在设计之前就得把“识别”和“支付”这两件事分开,不能混为一谈。

另一个容易被忽略的点是系统必须能区分操作类型。消费机器上通常有几种按键:消费、充值、查询、设置单价。按下一个键,再刷卡,系统做的事完全不同。这不是“读卡然后显示”能糊弄过去的,软件上必须有一个交互状态机来管理当前处于什么模式。很多同学拿到板子后就在主循环里写“读到卡就扣钱”,于是出现了充值也扣钱、按查询也扣钱的荒谬结果。原因就是没有状态划分。

再有就是可靠性。消费系统不像门禁,刷错了顶多开不了门,消费刷错了就涉及钱。所以卡内余额的读写顺序、掉电中断、重复触发、误读远距离卡这类情况,都要在功能和代码层面做防御。我后面会专门讲这部分。

1.1 需求拆解成功能清单

一个能交出去、能演示完整流程的消费终端,至少要包含以下功能:刷卡识别、余额读取、消费扣款、IC卡充值、余额查询、单价设置、扣费成功提示、防重复扣费、掉电保护、余额越界检查。除了功能,还得定几个性能指标:读卡距离稳定在3~5cm左右;完成一次完整扣费流程在1秒之内;连续刷卡100次不出现余额错乱;非法卡(比如UID为0或未初始化的卡)给出报警而不是死机。

如果把这些功能落到硬件上,就是:RFID读卡模块(射频前端)、主控芯片(处理通信和业务逻辑)、LCD显示模块(显示余额与菜单)、矩阵键盘(输入单价、切换模式)、EEPROM或Flash存储(记录日志和参数)、蜂鸣器和LED(声光提示)、电源模块(稳定供电)。看起来器件多,实际上都是很常规的模块,单独拿出来都不难,难就难在它们之间如何协同工作。

1.2 功能之间的优先级

在正式开始设计时,我给功能排了优先级。第一梯队是读卡、扣款、显示,这三件事做成,系统就“能用了”。第二梯队是规范流程、按键操作、状态提示,做成后系统“好用了”。第三梯队是防重复、防掉电、防误读、日志记录,做成后系统“敢用了”。前两梯队大多数同学都会去做,第三梯队才是拉开差距的地方。这篇文章里三个阶段都会覆盖,但你心里最好有数,哪些部分是演示评价重点,哪些是真正工程化要解决的点。

2. 主控与RFID模块的选型逻辑

2.1 为什么读卡模块选RC522系列

市面上常见的RFID方案有这么几类:RFID-RC522系列工作在高频13.56MHz,支持ISO14443A协议,能读写M1系列卡片(S50/S70);ID卡读头(比如125kHz的低频方案)只能读固定ID,无法写块数据,直接排除;编码式读头或网口读卡器成本高,不合适DIY和课程设计场景。所以RC522基本是这个题目的默认选项,配套资源多、例程丰富、模块成本低,甚至很多核心板上直接集成了天线电路。

RC522模块常见的有两种形态。一种是插针状的独立模块,自带天线,用排针引出接口,适合飞线搭建和快速验证。一种是邮票孔或者贴片方式焊到主板上、天线单独画的形态,适合做产品化PCB。如果你只是做课程设计,买独立模块最省事,稳定性和一致性都已经调好了;你要是想打样成一块完整板子,那就要连天线匹配、谐振电容这些一起处理,工作量会大很多。

RC522和主控的通信方式有两种常用模式。一种是SPI,接线少、速率快,RC522作为从设备挂到MCU的SPI总线上。另一种是模拟SPI,用普通IO口按时序手动翻转电平来模拟SPI通信,优点是任意单片机都能接,缺点是占用CPU、代码依赖延时。我建议能用硬件SPI就用硬件SPI,速度、稳定性都好;如果主控太老没有硬件SPI,模拟SPI也能跑,但你要注意时序参数的容差,后面调试部分会详细说。

2.2 主控选择:STC系列还是STM32

主控的选择直接决定了你能跑起来的软件复杂度。如果选经典的STC89C52系列(比如STC89C52RC),好处是和单片机课程衔接紧密、教程多、51内核的代码网上遍地都是。坏处也明显:工作频率低,RC522的SPI速率被拉得很低,同时内存小、没有硬件SPI时完全依赖模拟时序,代码一旦复杂一点就到处堆全局变量。

如果选STM32F103系列(或者国产的类似Cortex-M3芯片),优势是有硬件SPI、有充足Flash和RAM、主频高,处理RC522的协议栈非常从容。坏处是入门门槛稍微高一点,但现在的标准库/HAL库例程已经很成熟,学习成本没有想象中高。我的建议是:如果你只想快速复现、跑通流程,用STC89C52足够,网上模板多;如果你想往后做产品化、扩展上位机、加加密功能,直接上STM32,省得后面推倒重来。

实际系统里,我用的是某款国产Cortex-M0内核芯片,主频48MHz,硬件SPI跑RC522绰绰有余,这个配置比STC89C52顺滑太多,代码层面几乎不用为性能做妥协。

2.3 显示、存储、电源这些周边怎么定

显示方面,LCD1602字符屏最常用,能显示两行,每行16个英文字符,显示“余额:12.5 元”这类信息够了。缺点是中文点阵要自己拼或者外挂字库,显示不了中文。如果你想显示中文菜单,就得用LCD12864或者OLED,OLED又小又省电,显示信息量却很大,只是驱动稍微复杂些。我最终选的是LCD1602,主要考虑是中文需求不强烈、代码成熟、对比度调节只有一个电位器的事情。

存储方面,RC522里的卡有自己的EEPROM,这是余额的核心存储位置。但系统本身还要存点东西,比如最近几笔交易日志、设置的消费单价、系统参数。这些存MCU内部的EEPROM或者外挂一颗AT24C02都行。AT24C02是I2C接口的EEPROM,2Kbit(256字节),存几十条日志绰绰有余。这个芯片便宜、协议简单、教程满地都是,成本几乎可以忽略。

电源方面是个容易翻车的地方。RC522模块标称3.3V供电,工作电流峰值能到30mA左右,天线驱动瞬间电流更大。如果直接从USB转串口模块的3.3V取电,经常因为电流不足导致读卡距离极短甚至读不到卡。更稳的做法是:用5V主电源统一供电,RC522模块自带板载LDO或者电平转换电路的话就接5V;没有的话,单独用一个AMS1117-3.3做稳压给RFID供电,模拟地和数字地单点连接,避免射频电流干扰单片机。这些细节看着不起眼,但直接决定读卡稳不稳定。

3. SPI通信链路:数据是怎么从卡片流进单片机再显示到屏幕的

这一节是整篇文章的技术核心。很多系统“能读卡但读卡距离短”“偶尔读不出”“显示乱码”,根子都出在通信链路上,而不是卡片或代码逻辑的问题。

3.1 RC522与MCU的物理连接与SPI接口定义

RC522在SPI模式下的引脚定义很固定:SDA是片选(CS,低有效),SCK是时钟,MOSI是主出从入,MISO是主入从出,RST是复位引脚(RC522还有一个独立的复位脚,不能和MCU复位脚并在一起),IRQ是中断请求脚,在轮询模式可以悬空不接。

接线的几个原则:SCK、MOSI、SDA这三个信号是MCU输出到RC522的,走线要短、不能太长太细;MISO是RC522输出到MCU的,如果用的是5V单片机而RC522是3.3V模块,这个引脚的电平匹配要做对,否则MCU可能读不到稳定的高电平;RST最好由MCU的IO单独控制,上电后拉低再拉高完成一次硬件复位,别偷懒直接接RC522的3.3V,那样首次上电容易进入奇怪的状态。

SPI参数方面,RC522支持的最高SPI时钟在10MHz级别,但实际使用中我建议跑在1~2MHz左右,特别是飞线连接时更快更稳。SPI模式可以配置为模式0(CPOL=0,CPHA=0)也可配置为模式1(CPOL=0,CPHA=1),取决于你的库和初始化代码,关键是主机与从机必须一致。STM32的硬件SPI支持模式0,STC模拟SPI通常也是模式0,所以默认按模式0写就行。

3.2 电平匹配:很多板子死在这上面

RC522模块内部核心芯片是3.3V器件。如果你用的是STM32这类3.3V主控,接起来万事大吉;如果你用的是5V的51单片机(比如STC89C52),那就有电平转换问题。SPI的MOSI、SCK、SDA这些信号是主机发给从机的,5V直接灌进3.3V的RC522引脚,虽然很多模块引脚本身有保护二极管,不至于立刻烧芯片,但长期运行会有风险,而且电平阈值判断可能出问题。

反过来,MISO信号是RC522用3.3V输出的,接到5V单片机的IO脚,逻辑高电平只有3.3V。对5V单片机来说,输入高电平阈值VIH通常是2.0~2.4V之间,3.3V是能算作高电平的,所以很多情况下不转换也能工作——但不是所有芯片都这样,有些51单片机IO配置了比较弱的上拉或者内部施密特触发器阈值偏高,就会偶尔读不到正确数据。

最稳妥的做法是加一个电平转换方案:单向转换用两个MOS管搭的电平转换电路,或者用一颗专用的电平转换芯片(如TXS0108E、SN74LVC245A等),再简单一点也可以用电阻分压把5V降到3.3V给RC522供电和信号,同时RC522的MISO用上拉电阻接5V兼容。我不推荐靠“能跑就行”赌,因为这种不稳定坑你在调试时根本分不清是代码问题还是硬件问题,排查成本远高于一个转换器的价格。

注意:如果RC522模块上标了“5V兼容”字样,通常说明模块带电平转换电路,这时候直接接5V单片机是可以的。但如果模块上只标3.3V,那就老老实实做电平转换。

3.3 卡片数据的存储结构:扇区、块和控制

M1卡(S50)的存储结构是个盘点,搞清楚它就能理解余额读写为什么不能随便写。S50的卡内有1KB EEPROM,分成16个扇区(扇区0~15),每个扇区有4个块(块0~3),每个块16字节。扇区0的块0比较特殊,它存的是厂商代码和UID(卡的唯一序列号),出厂时写死,用户不能修改(某些卡可以写一次,但正常情况下不用碰它)。

每个扇区的块3是控制块,最后16字节里包含了KeyA(6字节)、存取控制位(4字节)、KeyB(6字节)。这是安全机制的核心。RC522读一个数据块(比如余额所在的块1),需要先验证该扇区的KeyA或者KeyB;验证通过后,才能对块0~2执行读操作或写操作。默认出厂KeyA和KeyB往往都是FFFFFFFFFFFF(6字节全FF),很多教程里的例程就用这个默认密钥,商用系统一定会改成自定义密钥。

余额放在哪个扇区哪个块,自己定义就行。系统中,我把余额放在扇区1的块1,也就是第5块(扇区编号从0开始,地址=扇区号×4+块号,所以扇区1块1对应的块地址是5)。每个块16字节,M1卡的余额我用4字节BCD码存储,单位为分,可表示最大余额999999.99元,这个范围对绝大多数消费场景足够。另外12字节可以存充值时间戳、交易序号、校验和等扩展信息。存结构的时候一定加校验,比如把这16字节的第15个字节作为前面所有字节的异或和,防止数据损坏。读出来如果校验不对,说明这块卡数据异常,要报警而不是直接扣费。

3.4 一次完整的读写卡流程

读卡和写卡的流程大致是:

  1. MCU复位RC522,然后发软件复位命令(比如对应命令字为0x0F),等待芯片就绪。
  2. 调用防冲突命令,读取一张卡的UID和SAK。如果有多张卡同时靠近天线,这一步会返回冲突,需要重试或提示“请单独放卡”。
  3. 用UID选中这张卡,然后发送验证密钥命令,选择扇区1,填入KeyA(6字节),校验通过后就可以操作该扇区。
  4. 读数据块5(扇区1块1),得到16字节原始数据。如果正在做消费,就先把“当前卡内余额”解析出来。
  5. 计算新余额(余额 - 消费金额),构造新的16字节写数据,再执行写块命令,把新余额写回块5。
  6. 读回块数据,校验是否等于写入值。这是关键的确认步骤,很多教程里没做,导致实际写入失败但界面已经提示“扣费成功”,钱没扣掉还让用户走了。

还有一件事我要特别提醒:RC522或M1卡在写块时,如果正好在MCU断电或者卡被拿走的瞬间,可能写入半个块,导致整个块数据损坏。后面会细说怎么处理。

4. 扣费状态机:消费系统最核心的软件设计

读卡写卡都是底层基础,软件最终呈现给使用者的是一个清晰的操作流程。这一节把整个系统的主循环和状态机结构拆开讲。

4.1 为什么需要状态机而不是一堆if

如果直接在主循环里写“检测到卡,就扣款”,那按键模式根本没法管理,而且刷卡触发和按键触发的优先级会乱。消费终端的真实行为是分状态的:待机、读卡、确认、扣费、完成、报警。每个状态能做的操作不一样。用一套有限状态机(FSM)来组织代码之后,逻辑会非常清晰,读卡事件只是触发状态转移的一个输入,而不是直接决定动作。

状态可以这样设计:

  • IDLE:待机状态,显示“请刷卡”。此状态下既能响应刷卡进入读卡流程,也能响应按键进入菜单。
  • READ:读卡状态。已识别到UID,读取卡片余额、校验卡片数据。
  • MENU:菜单状态。通过按键修改单价或切换操作类型。
  • CONFIRM:扣款确认状态。显示当前余额和扣费金额,等待按键确认(比如按“确认”扣款,按“取消”退回待机)。
  • WRITE:写卡状态。执行扣款写卡、备份日志、读回校验。
  • SUCCESS:成功状态。蜂鸣器发出提示音,显示“扣费成功”和最新余额。
  • ERROR:错误状态。蜂鸣器报警,显示错误类型。之后自动回到IDLE。

状态转移之间一定要有“超时”概念。比如用户刷了卡但30秒不按确认键,应该自动回到IDLE,避免卡处于半锁定状态。再比如扣费写卡超时,要回到READ并提示失败。

4.2 防重复扣费的两道防线

重复扣费是这个系统最容易出的“演示翻车现场”:同一张卡在感应区放久了,或者使用者手抖在感应区晃了一下,结果连续扣了两笔钱。解决这个问题的第一道防线是“单卡防抖窗口”:记录最近一次成功交易的UID和完成时间,在下一次读到卡时,先判断UID是否相同,且与上次交易结束时间间隔是否小于3~5秒。如果都满足,直接忽略并显示“请勿重复刷卡”,不进入扣费流程。

第二道防线是“硬件提示”:扣费成功之后强制要求卡离开感应区至少0.5秒以上才能进行下一次操作。这个可以用状态机实现——扣费完成后进入一个LOCK状态,只有当天线检测不到卡时才解锁。因为读卡是主动寻卡的,天线区域没有卡自然就不会连续扣费,这时候再加一个时间窗口就足够防误触了。

4.3 充值、查询、消费如何共用一套读卡流程

充值、查询、消费本质上都是读卡、写卡,差别只在“余额如何处理”。所以代码里不要复制三份读卡流程,而应该封装成一个通用函数:给定操作类型(OP_QUERY、OP_CONSUME、OP_RECHARGE)和附加参数(扣费金额、充值金额),函数内部完成读卡、校验、余额计算、写回、日志记录。主函数里按键设置好操作类型和金额,然后转去读卡,这样代码结构干净,调试也方便。

具体来讲,消费模式下金额来自单价设置和可能的多件数量;充值模式下金额来自键盘输入;查询模式下金额为0,只读余额。校验通过后计算新余额,直接在写卡函数里处理。不管哪种模式,写卡前后都要做余额合法性检查:新余额不能为负数,充值后不能超过上限(比如9999.99元),越界就报错。

充值还有一个细节:充值本身不是“凭空加钱”,真正商用系统里充值需要后台记账或管理员权限。课程设计里可以简化成“管理员在终端上输入金额并刷卡”,但代码里敲一个限制:只有某个管理员卡(UID在预设列表里)才能执行充值,其他卡不能触发充值流程。这个功能加分项很明显,也不难实现。

4.4 掉电保护与日志

消费系统最怕掉电瞬间数据错乱。前面说过M1卡整块写16字节,如果写完新余额之前断电,可能出现:读出来的余额是旧的、新的、还是不完整的数据?都有可能。处理办法有三个层面。

第一,写卡之前做完整备份:把本次交易前的旧余额、新余额、UID、时间戳先写到片外EEPROM(AT24C02)的交易日志区,再操作卡片。掉电后上电,程序可以从EEPROM恢复“上一次写入是否成功”,如果发现卡片余额和日志不一致,可以提示管理员介入。

第二,写卡时采用“先写新值再写校验”的方式:先写新余额到数据块的固定位置,再更新校验字节。读取时先做校验,校验失败说明写入过程不完整,提示“卡片数据异常”,而不去使用错误的余额。这里有个取舍:校验字节本身如果先更新了而余额没更新,也会导致校验不一致,但概率更低。

第三,也是最实际的:写卡完成后立刻读回校验,确认无误才在液晶屏上显示成功。读回校验失败则提示“写卡失败,请重试”,并把整个流程保持在读卡状态,不自动清除,这样用户可以把卡重新放上来再次处理。

交易日志的内容我建议至少包含:UID八字节十六进制、交易类型(消费/充值)、交易金额分、交易前余额、交易后余额、发生时间。在AT24C02里做一个环形缓冲区,存最近20条即可。课程设计展示时可以加一个“查看最近交易”的菜单项,很直观。

5. 调试阶段踩过的坑与排查链路

这一节我专门给出一整条排查思路,而不是只给结论。因为卡在某个现象上看不到问题真正出在哪、只能靠“换模块试试”的人太多了。下面几个情况是我实际调试中遇到过的,注意顺序不要跳步。

5.1 读卡距离只有1cm甚至完全读不到卡

现象:初始化正常,液晶有反应,但卡片必须紧贴天线才能读到,稍微抬高2cm就无响应。

排查链路如下:

  1. 先测量RC522模块的供电电压。正常应该在3.3V附近,如果低于3.1V,很可能是供电电流不足或压降太大。用万用表测模块的VCC和GND之间电压,刷卡瞬间电压有没有跌。如果跌到3.0V以下,换供电路径。这是最常见的原因。
  2. 检查复位时序。用示波器或逻辑分析仪看RST引脚,确认上电后有明显的低电平脉冲再拉高。如果没有,检查MCU代码里有没有正确拉低再拉高RST脚。很多例程只初始化SPI,忘了单独复位RC522,会造成读卡异常。
  3. 检查天线电路。独立模块的天线已经调好,但如果你用的是自画板子,检查天线线圈的谐振电容是否匹配。C0、C1等电容容值和天线电感共同决定谐振频率,偏离13.56MHz导致读取距离大幅缩短。
  4. 降SPI速率。把SPI的波特率配置降到最低档再试。飞线连接时线间电容大、信号边沿慢,SPI速率太高时序容易解析错误。实测1MHz和10MHz下读卡成功率差异巨大。
  5. 排除金属干扰。面包板下面的金属桌面、铜柱、金属外壳都会吸收电磁场,导致读卡距离骤减。把天线区域架空或垫高1cm再试。

如果以上五步都试过还不行,再怀疑M1卡本身或RC522硬件损坏。用官方上位机(如有)或者另一块已知正常的模块交叉验证。飞线接触不良是另一个坑,杜邦线松动、插针氧化会导致间歇性读卡失败,注意检查。

5.2 液晶屏有背光但什么字都不显示 / 显示乱码

现象:LCD1602背光亮,但第一行没有字符,或者出现一团方块。

排查链路如下:

  1. 先调对比度。LCD1602的VO引脚通常接一个10K电位器,中间抽头调节对比度。背光亮不代表液晶能显示,对比度电压不对时会显示空白或满屏方块。这是90%的“空白屏”问题的来源。调电位器,找到刚好能清楚看到字符的位置。
  2. 检查初始化时序。LCD1602初始化需要严格的时序序列:上电后延时40ms以上,再发功能设置、显示开关、清屏等命令。如果MCU主频快,而延时不够,LCD可能没有正确进入8位或4位模式。用逻辑分析仪抓RS、RW、E引脚的时序,对照标准初始化序列。
  3. 确认数据线接对了。如果用4位模式,只接DB4~DB7;如果接线时把DB4接成DB5,显示就会错乱。拿一个最简单的“发0x01清屏,发0x80定位第一行”的程序逐字节核对。
  4. 检查电平。如果MCU是5V,LCD是5V,没问题。但如果LCD模块是3.3V版本却接了5V的MCU数据线,容易工作在不可靠状态,换个3.3V兼容的LCD或者加转换。

注意:LCD1602的E(使能)脉冲宽度要求至少几百纳秒,用MCU的IO翻转时,如果代码里翻转速度太快、脉宽太短,LCD会漏收命令导致乱码。标准做法是每个E脉冲之间加至少几微秒延时。

5.3 能读卡但扣费后余额没变 / 偶尔扣费后变成乱码

这个是M1卡写入失败的表现。排查顺序:

  1. 先验证你用的密钥对不对。默认KeyA是否等于FFFFFFFFFFFF?很多新卡出厂是默认密钥,但如果卡被别人改过,验证就会失败。验证失败时RC522会返回错误状态,代码里不能忽略这个错误码。
  2. 检查写块流程。写块前是否已经验证过该扇区的密钥?验证通过后才能写,这是协议规定。如果只调用读卡函数然后直接发写块命令,RC522大概率返回错误。
  3. 检查写入的数据长度和内容。写块必须是16字节。如果余额字段和校验字节没有更新到同一个16字节块里,会导致校验不一致。
  4. 检查供电瞬时跌落。写卡瞬间电流比读卡时大,如果供电不足,M1卡写操作会异常。这种现象表现很隐蔽,刷卡扣费有时候成功有时候失败,成功率还不稳定。这时候就得按前面电源部分说的,单独给RFID供3.3V。

这里分享一个我调试时的做法:不管硬件还是软件问题,我都先在代码里加一个“调试模式”。调试模式下,把每次读卡、验证、读块、写块、读回的原始返回值都输出到串口。对着RC522的命令手册逐条比对返回值,比盲目调硬件快得多。我自己遇到的那个“偶尔写失败”的问题,就是通过这个串口日志发现写块命令在某一次返回了超时错误——实际是RC522在连续高速读写后需要一点恢复时间。后来我在多次操作之间加了1ms延时,成功率就到了100%。

另外一个常见的坑是卡内数据块写坏了,也就是卡“废了”。修复方法是,在初始化阶段扫描卡片时,如果读取的余额块数据校验失败,系统提示“请更换卡片”,并进入待机。别尝试自动修复写坏的数据,因为无法判断损坏前的余额到底是多少。商用设备里这种卡会交给发卡方重新初始化,咱们自己做的系统也一样,只要“不继续扣费、不造成错乱”就算守住了底线。

5.4 按键反应不灵敏或一次按下触发多次

按键抖动是单片机入门问题,但在消费系统里却影响体验。你按“确认”本来要扣一次钱,结果抖动判断成按了三次,就把状态机推到了奇怪的位置。

解决很简单:软件消抖(延时10~20ms再读一次,确认电平稳定),同时检测“按下沿”而不是“电平”。也就是说只在按键从松开变成按下的那一刻触发一次,按住不放也不重复触发。检测沿的做法是:每次主循环读取按键状态,和上一轮的按键状态比较,只有不同才处理。如果你用的是矩阵键盘,还需要配合扫描间隔,避免一次扫描周期内重复读同一个键。

菜单按键和扣费确认按键建议物理上分开。比如:数字键用来输入金额,“确认”键用来确认扣款,“取消”键用来退出。不要设计成“按一下充值键再按数字键又按确认键”这种容易误触的组合。

5.5 蜂鸣器不响或声音很小

这是小问题,但影响现场效果。蜂鸣器分有源和无源两种。有源蜂鸣器内部自带振荡电路,通直流电就响;无源蜂鸣器需要PWM方波驱动才能发声。如果买的是无源蜂鸣器,代码里不能直接给高电平,要输出一个几kHz的方波。驱动方式上,IO口直接驱动蜂鸣器往往声音太小,正确方式是IO口控制一颗三极管(如S8550)或芯片驱动蜂鸣器,再接一个续流二极管保护。

另外,扣费成功和失败的提示音要有所区分。成功用短促的“滴”一声,失败用连续“滴滴”两声,或者用蜂鸣器+LED双提示。这样演示时观众不用看屏幕就知道结果。

6. 往产品化靠拢时值得做的几个升级

如果你的目标是交课程设计、参加比赛,前面五节内容已经能产出一个完整可演示的系统。但我知道很多人做完基础版本之后,不甘心停在“能跑”,还希望更像一个真实产品。下面这几个升级点按性价比排序,从低到高,你挑着做。

6.1 卡片加密与密钥管理

默认一边,M1卡的出厂密钥是公开的,任何人都能用自己的读卡器读你卡里的余额。真正的消费系统肯定要换密钥。操作很简单:开发时用默认密钥验证,验证成功后调用写控制块命令,把KeyA和KeyB改成自定义的密钥。但要注意,改密钥是一锤子买卖,改完以后,默认密钥就失效了。所以写这个功能时,一定要先在备用的卡上测试,确认新密钥能正常验证再批量改卡。万一改完忘记密钥,这张卡就彻底废了。

密钥不要存死在代码里。虽然单片机里存密钥没太多办法防破解,但至少不要在串口调试信息里打印出来。密钥可以通过一个“发卡器模式”写入,平时程序读固件区的密钥配置来验证。这样即使源码在网络上误传,也不至于直接暴露所有卡的钱包密钥。

6.2 低功耗与电池供电

如果系统要从“插电源演示”变成“便携电池供电”,低功耗就是硬需求。RC522的功耗不算低,为了省电,普通做法是:平时让RC522进入硬掉电模式(通过寄存器控制PowerDown位),MCU进入低功耗睡眠,用一个外部按键或者定时唤醒,等到有人操作时再给RC522上电。读卡频率可以降低,比如每秒寻卡一次,大部分时间睡觉。这类优化的代价是代码复杂度和调试时间上升,但对于产品化非常有价值。

6.3 对接上位机或云平台

扩展方向里,最有演示效果的是加一个上位机统计系统。MCU通过串口或USB转串口连接PC上位机,把每笔交易日志实时上传。上位机用Python或C#写个简单界面,统计当天营业额、消费次数、单笔均值,甚至可以画个趋势图。这样系统的复杂度立刻从“单片机外设应用”上升到“嵌入式+上位机综合系统”,在答辩或评比赛时是很明显的加分项。

硬件上只需要在MCU上留一个串口,引出TX、RX、GND三条线,板子上加一个USB转串口芯片(比如CH340)或者预留排针。软件上,协议可以自定义成一行一行的明文帧,例如:

CONSUME,UID=XXXXXXXX,AMOUNT=1250,BALANCE=5000,STATUS=OK

一行帧代表一笔成功消费。上位机按行解析即可,开发成本很小。如果你还想加联网,可以用ESP8266/ESP32模块,通过WiFi把交易记录发到一个本地服务器,但这会儿已经不只是“单片机消费系统”的课题边界了,属于锦上添花。

6.4 显示界面的优化:从“能看”到“好用”

如果还有余力,把LCD1602换成一个带中文字库的LCD12864或者TFT屏,界面会舒服很多。状态机不变,只把显示驱动换成新屏的驱动就行。还可以加一个“欢迎刷卡”的主界面、消费时显示卡号尾号、余额不足时闪烁提示等。用TFT彩屏的话还能画个简易的进度条、按键图标,现场演示效果非常接地气。当然代价是接线更多、驱动更复杂,我个人建议在基础流程全部稳定之后再做这步,不要在刚开始调显示就去搞大屏。

收尾之前再说几句

我做完这套系统之后最大的体会是:别小看一个“课程设计常见题目”,它其实把射频通信、SPI总线、状态机、数据可靠性、人机交互全都串起来了,是一个五脏俱全的小工程。网上现成的代码一抓一大把,但真正有价值的不是抄来一份能跑的例程,而是把“读卡-扣费-写回-校验-状态反馈”这个链路理解透,然后敢改、能改、知道改了之后会不会出事。

最后分享一个小技巧:调试时把读卡、验证、读写块、状态切变每一步都通过串口打印出来,做成一个带时间戳的日志流。这样无论系统出什么问题,你只要看最后几条日志,就能定位到是卡没读到、密钥没过、写卡失败还是状态机卡住,至少能省下一个晚上的盲调时间。先把日志打好,再谈优化界面和低功耗。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进