一、问题的发现
在做 RFID 刷卡消费充值实验时,我遇到了一个令人困惑的现象:实验过程中卡内余额一度变成了负数,但后续操作又一切正常,串口和 TFT 屏显示的结果看起来都没问题。一开始我以为是接触不良或者卡片没放稳,但反复核对每一步操作后发现,这个负数余额是稳定可复现的,而且根源就在第一次扣费操作里。
这篇博客从实验的初始状态出发,完整复盘每一步操作和余额变化,结合代码逐层拆解,找出背后的真正原因。
二、实验环境与初始状态
实验平台是 STM32L4 + RC522 模块,四个按键分别对应:KEY1 扣费 10 元,KEY2 扣费 50 元,KEY3 充值 100 元,KEY4 初始化卡。卡内余额存储在 M1 卡的 Sector 1 Block 0 数值块中,操作结果同时输出到串口和 TFT 屏。
代码中钱包初始值定义为:
uint8_t Wallet_vlaue[4] = {0x0a}; // 钱包初始值 = 10元执行 KEY4 初始化卡后,RC522_Write_Date()将按数值块格式打包好的 16 字节模板写入卡片,初始余额为10 元。此时卡片就绪,实验正式开始。
三、实验过程完整复盘
第一步:按 KEY2 扣费 50 元
按下 KEY2,key2_end_handle()将RCC522_Work_Mode设为 2,串口输出:
*****key2按下,请将RFID卡靠近感应区,当前工作模式为扣费金额50元*****将卡靠近感应区,RC522_Handle()每秒执行一次,完成寻卡、防碰撞、选卡、密码认证四步通信后,读取卡内余额为 10 元。程序进入 KEY2 扣费分支,执行余额判断:
if(Card_Money < 5) // 10 < 5 为假,不拦截注意这里的判断条件是< 5,而实际扣费金额是 50 元。10 元余额不小于 5,所以没有被拦截,直接进入else分支,调用RC522_Deduction_Recharge()发送PICC_DECREMENT命令,减值 50 元:
RFID卡扣费中... RFID卡扣费成功 RFID卡当前余额:-40元余额计算:10 - 50 = -40 元。TFT 屏底部同步显示"当前金额 = -40元",蜂鸣器鸣叫 400 毫秒。
这一步是整个实验中问题的爆发点:扣费金额(50 元)远大于余额(10 元),但由于余额判断阈值错误,系统照常执行了扣费,余额被透支为负数。
第二步:按 KEY3 充值 100 元
按下 KEY3,RCC522_Work_Mode被设为 3,串口输出:
*****key3按下,请将RFID卡靠近感应区,当前工作模式为充值金额100元*****刷卡后读取余额为 -40 元。KEY3 是充值分支,代码中没有余额判断,直接执行PICC_INCREMENT加值 100 元:
RFID卡充值中... RFID卡充值成功 RFID卡当前余额:60元余额计算:-40 + 100 = 60 元。TFT 屏显示"当前金额 = 60元"。
这一步把负数余额"拉回"了正数,表面上看一切正常,但实际上 60 元这个结果是建立在之前 -40 元异常状态的基础上的。如果只看这一步的结果,根本发现不了前面已经发生过透支。
第三步:再次按 KEY3 充值 100 元
再次按下 KEY3,刷卡后读取余额为 60 元,执行充值 100 元:
RFID卡充值中... RFID卡充值成功 RFID卡当前余额:160元余额计算:60 + 100 = 160 元。TFT 屏显示"当前金额 = 160元"。
第四步:按 KEY2 扣费 50 元
按下 KEY2,刷卡后读取余额为 160 元。余额判断160 < 5为假,不拦截,执行扣费 50 元:
RFID卡扣费中... RFID卡扣费成功 RFID卡当前余额:110元余额计算:160 - 50 = 110 元。TFT 屏显示"当前金额 = 110元"。
这一步余额充足,扣费正常,是一次没有触发问题的正常操作。
第五步:按 KEY1 扣费 10 元
按下 KEY1,RCC522_Work_Mode被设为 1,刷卡后读取余额为 110 元。进入 KEY1 分支,余额判断:
if(Card_Money <= 0) // 110 <= 0 为假,不拦截执行PICC_DECREMENT减值 10 元:
RFID卡扣费中... RFID卡扣费成功 RFID卡当前余额:100元余额计算:110 - 10 = 100 元。TFT 屏显示"当前金额 = 100元"。
完整余额变化链
把五步操作连起来,从初始 10 元到最终 100 元的完整链路是:
10 元(初始化)→ 扣 50 元 →-40 元(异常透支)→ 充 100 元 →60 元→ 充 100 元 →160 元→ 扣 50 元 →110 元→ 扣 10 元 →100 元(最终)
其中第一步扣费 50 元是唯一触发异常的操作,其余四步均为正常操作。如果没有第一步的异常扣费,从初始 10 元直接开始后面的操作,结果应该是:10 + 100 + 100 - 50 - 10 =150 元,而不是实验中得到的 100 元。两者相差正好 50 元,就是第一步被错误扣除的那笔钱。
四、现象总结
从完整复盘可以提炼出一个规律:当卡内余额高于某个阈值时,即使扣款金额大于余额,系统也会执行扣款,余额变成负数;当卡内余额低于或等于某个阈值时,系统直接提示余额不足,拒绝执行。而且 KEY1 和 KEY2 的阈值不一样。
这说明问题不在硬件,也不在 RC522 模块,而在软件的余额判断逻辑里。接下来深入代码看看。
五、代码层面的分析
核心逻辑都在handle.c的RC522_Handle()函数中。在完成寻卡、防碰撞、选卡、认证四步通信后,程序读取卡内余额:
Card_Money = (uint32_t)(card_money[0] | (card_money[1] << 8) | (card_money[2] << 16) | (card_money[3] << 24));M1 卡数值块的前 4 字节以小端模式存储余额,这里按位或合并成一个 32 位整数。注意Card_Money的类型是int32_t,所以它可以表示负数——当数值块中的值被扣减到 0 以下时,就会以补码形式呈现为负数。
KEY1 的扣费逻辑
if(RCC522_Work_Mode == 1) // 扣款模式一:扣10元 { if(Card_Money <= 0) { printf("\r\n*****余额不足请充值*****\r\n"); Gui_Printf_Display((uint8_t *)"*****余额不足请充值*****"); } else { printf("RFID卡扣费中...\r\n"); Value_Change(&Deduction_Value1, &HEXvalue[0]); status = RC522_Deduction_Recharge(PICC_DECREMENT, Sector_Addr_Date[RC522_SECTOR]+SECTOR_BLOCKNUM, HEXvalue); } }KEY1 的余额判断条件是Card_Money <= 0。也就是说,只要余额大于 0,不管是 1 元还是 5 元,都会进入else分支执行扣费 10 元。M1 卡的PICC_DECREMENT(减值)命令本身不做余额校验,它只是简单地把数值块中的值减去指定金额,所以 5 元减 10 元就变成了 -5 元,操作返回MI_OK,程序打印"扣费成功"。
只有当余额已经小于等于 0 时,才会被if(Card_Money <= 0)拦截,提示余额不足。
KEY2 的扣费逻辑
else if(RCC522_Work_Mode == 2) // 扣款模式二:扣50元 { if(Card_Money < 5) { printf("\r\n*****余额不足请充值*****\r\n"); Gui_Printf_Display((uint8_t *)"*****余额不足请充值*****"); } else { printf("RFID卡扣费中...\r\n"); Value_Change(&Deduction_Value2, &HEXvalue[0]); status = RC522_Deduction_Recharge(PICC_DECREMENT, Sector_Addr_Date[RC522_SECTOR]+SECTOR_BLOCKNUM, HEXvalue); } }问题更明显了。KEY2 扣的是 50 元,但余额判断条件却是Card_Money < 5。这意味着只要余额大于等于 5 元,哪怕只有 6 元,也会执行扣费 50 元,结果变成 -44 元。只有当余额小于 5 元时才会被拦截。
实验中第一步就是这种情况:余额 10 元,扣 50 元,10 不小于 5,执行扣费,变成 -40 元。
六、根本原因
把两个分支放在一起对比,根本原因就清楚了:余额判断的阈值与实际扣款金额不匹配。
KEY1 扣 10 元,判断条件是<= 0,正确的写法应该是< 10。当前的条件只拦截了非正余额,却放行了所有正数余额,导致 1~9 元的余额被扣 10 元后变成负数。
KEY2 扣 50 元,判断条件是< 5,这个阈值和 50 元完全对不上,大概率是代码编写时的笔误——可能是从 KEY1 复制过来后只改了扣款金额忘了改判断阈值,也可能是把 50 误写成了 5。正确的写法应该是< 50。
此外,M1 卡的PICC_DECREMENT硬件命令本身不具备余额保护机制,它只是执行减法运算,是否允许透支完全取决于上层软件的判断。所以一旦软件判断条件有漏洞,负数余额就必然出现。
还有一个值得注意的细节是Card_Money的类型。代码中声明为int32_t,但读取时做了uint32_t强制转换再赋值。当余额为正时没有问题,但当数值块中的值被扣减到 0 以下时,M1 卡存储的是补码,按uint32_t解析后是一个很大的数,再赋值给int32_t就会被解释为负数。这个类型设计本身让负数余额能够被正确显示出来,但也意味着如果上层不做拦截,负数就会一直累积。
七、修复方案
知道了原因,修复就很简单。核心原则是:余额判断阈值必须等于实际扣款金额。
KEY1 的修复:
if(RCC522_Work_Mode == 1) { if(Card_Money < Deduction_Value1) // 改为 < 10,而不是 <= 0 { printf("\r\n*****余额不足请充值*****\r\n"); Gui_Printf_Display((uint8_t *)"*****余额不足请充值*****"); } else { // 执行扣费10元 } }KEY2 的修复:
else if(RCC522_Work_Mode == 2) { if(Card_Money < Deduction_Value2) // 改为 < 50,而不是 < 5 { printf("\r\n*****余额不足请充值*****\r\n"); Gui_Printf_Display((uint8_t *)"*****余额不足请充值*****"); } else { // 执行扣费50元 } }直接引用金额变量Deduction_Value1和Deduction_Value2作为判断阈值,而不是硬编码数字,这样以后修改金额时判断条件会自动跟随,避免再次出现不一致的问题。
更进一步,可以把余额判断和扣费操作封装成一个统一的函数,避免每个分支重复写判断逻辑:
uint8_t Deduction_If_Sufficient(int32_t balance, uint32_t amount, uint8_t *pValue) { if(balance < amount) { return MI_ERR; // 余额不足 } Value_Change(&amount, pValue); return RC522_Deduction_Recharge(PICC_DECREMENT, 块地址, pValue); }这样两个扣费分支都调用同一个函数,判断逻辑只有一份,从根本上杜绝了阈值不一致的问题。
九、总结
RFID 实验中扣款金额超过余额时出现两种不同结果,根本原因是代码中两个扣费分支的余额判断阈值与实际扣款金额不匹配:KEY1 扣 10 元却只判断<= 0,KEY2 扣 50 元却判断< 5。当余额高于错误阈值但低于实际扣款金额时,M1 卡的 DECREMENT 指令照常执行减法,余额变成负数并显示"扣费成功";只有当余额低于错误阈值时,才会被软件拦截并提示"余额不足"。
实验中从初始 10 元开始,第一步按 KEY2 扣 50 元就触发了这个问题,余额变成 -40 元,后续充值和扣费操作把这个异常状态"掩盖"在了正数结果之下,最终得到的 100 元与理论值 150 元相差正好 50 元。修复方法是将判断条件改为与实际扣款金额一致,并建议封装统一的扣费函数避免重复代码带来的不一致。
这个小小的 bug 让我深刻体会到,嵌入式系统的稳定性不仅取决于硬件连接和通信协议,更取决于业务逻辑中每一个判断条件的严谨性。细节决定成败,在代码世界里尤其如此。