项目概述
本项目设计并实现了一款基于STM32F103RC的RFID电子钱包系统,使用YMC1501 S50 Mifare高频读写模块完成对IC卡的电子钱包操作,并通过128×128 SPI LCD屏幕与3个物理按键提供完整的人机交互界面。
核心功能: 初始化钱包(余额归零)、充值、扣款、余额查询——完全对标真实电子钱包的增、减、查、改四项基本操作。
系统架构
┌──────────────────────────────────────────┐│ STM32F103RC ││ ││ ┌──────────┐ ┌──────────┐ ┌───────┐ ││ │ UI状态机 │ │ 协议栈 │ │ I2C │ ││ │ (ui.c) │ │ (rfid.c) │ │(LDC) │ ││ └────┬─────┘ └────┬─────┘ └───┬───┘ ││ │ │ │ ││ ┌────┴─────┐ ┌────┴─────┐ ┌──┴───┐ ││ │LCD 128px │ │ USART2 │ │PB10/ │ ││ │SPI (PB4- │ │9600 8N1 │ │ PB11 │ ││ │ PB9) │ │ PA2/PA3 │ │ │ ││ └──────────┘ └────┬─────┘ └──┬───┘ ││ │ │ │└─────────────────────┼────────────┼──────┘ │ │ ┌───────┴──┐ ┌─────┴─────┐ │ YMC1501 │ │ LDC1614 │ │ S50 读写器│ │ 电感传感器 │ └─────┬────┘ └───────────┘ │ ┌─────┴────┐ │ M1 S50 │ │ IC卡 │ └──────────┘硬件选型与接线
| 设备 | 型号/引脚 | 说明 |
|---|---|---|
| 主控芯片 | STM32F103RC | ARM Cortex-M3, 72MHz |
| RFID读写器 | YMC1501 (S50 Mifare) | UART接口, 13.56MHz |
| LCD屏幕 | 128×128 SPI | 驱动IC ST7735, 16位色 |
| 电感传感器 | LDC1614 | 4通道, I2C接口 |
| 按键 ×3 | KEY1=PA0, KEY2=PC8, KEY3=PC9 | 下拉/上拉输入 |
| 指示灯 | LED=PA8 | 操作指示 |
关键接线:
| 外设 | STM32引脚 | 接口类型 |
|---|---|---|
| YMC1501 TX | PA3 (USART2 RX) | UART 9600 8N1 |
| YMC1501 RX | PA2 (USART2 TX) | UART 9600 8N1 |
| LCD SCL/CS/BLK | PB4~PB9 | 软件SPI |
| LDC1614 SCL | PB10 | 软件I2C |
| LDC1614 SDA | PB11 | 软件I2C |
| LDC1614 ADDR | PA11 | 地址选择(低=0x2A) |
| LDC1614 SD | PC13 | 关断控制(低有效) |
PB10/PB11 用于软件I2C而非硬件I2C的原因是:原硬件I2C引脚PB8/PB9与LCD的CS/BLK冲突,因此改用GPIO模拟。
YMC1501 通信协议驱动
协议帧格式
YMC1501 采用自定义的UART二进制帧协议(文档版本 V1.0.5),帧格式如下:
上位机发送帧: [CMD_TYPE][LEN][CMD][ADDR][...DATA...][CS]
读写器应答帧: [CMD_TYPE][LEN][CMD][ADDR][STATUS][...DATA...][CS]CMD_TYPE: 命令类型,固定为0x01(IC卡操作)LEN: 整帧长度(含自身)CMD: 命令码ADDR: 读写器地址,默认0x20STATUS:0x00=成功,0x01=失败,0x03=操作成功但读余额失败CS: 校验和 =~(Buf[0] ^ Buf[1] ^ ... ^ Buf[LEN-2])
校验算法实现
每个字节依次异或,最终取反得到校验码:
void RFID_TxCheckSum(u8 *buf, u8 len){ u8 i, cs = 0; for(i = 0; i < (u8)(len - 1); i++) cs ^= buf[i]; buf[len - 1] = ~cs;}
u8 RFID_RxVerify(u8 *buf, u8 len){ u8 i, cs = 0; for(i = 0; i < (u8)(len - 1); i++) cs ^= buf[i]; cs = ~cs; return (cs == buf[len - 1]) ? RFID_OK : RFID_ERR;}接收端重新计算校验并与帧中校验字节比对,一致则校验通过。这种异或取反方案虽然是简单校验,但对串口常规误码有较好的检测效果。
命令码一览
| 命令码 | 名称 | 功能 | 是否需要密钥 |
|---|---|---|---|
| 0xA1 | 读卡号 | 读取IC卡4字节UID | 否 |
| 0xA3 | 读数据块 | 读取指定块16字节数据 | 是(KEYA) |
| 0xA4 | 写数据块 | 写入指定块16字节数据 | 是(KEYA) |
| 0xA6 | 初始化钱包 | 将指定块初始化为钱包,设置初始值 | 是(KEYA) |
| 0xA7 | 钱包减值 | 从钱包扣除指定金额 | 是(KEYA) |
| 0xA8 | 钱包加值 | 向钱包充值指定金额 | 是(KEYA) |
| 0xA9 | 余额查询 | 读取钱包当前余额 | 是(KEYA) |
核心命令详解
读卡号 (0xA1) — 探测是否有卡:
发送: 01 08 A1 20 00 01 00 [CS] (8字节)成功: 01 0C A1 20 00 04 00 [UID4] [CS] (12字节)失败: 01 08 A1 20 01 00 00 76 (8字节)第5字节 0x01 表示操作时蜂鸣提示,0x00 表示静默。
钱包操作 (0xA6/A7/A8) — 初始化/增值/减值,使用统一的11字节命令帧:
发送: 01 0B [CMD] 20 [BLOCK] [BEEP] [VALUE_LE4] [CS] | | | | | | | | 0 1 2 3 4 5 [6..9] 10
应答: 01 0A [CMD] 20 00 [VALUE_LE4] [CS] (成功, 10字节) 01 08 [CMD] 20 01 00 00 [CS] (失败, 8字节)金额采用4字节小端序存储(S50钱包块原生格式),以”分”为单位避免浮点运算:
static void s32ToLE(s32 val, u8 *out){ u32 v = (u32)val; out[0] = (u8)(v & 0xFF); // 最低字节 out[1] = (u8)((v >> 8) & 0xFF); out[2] = (u8)((v >> 16) & 0xFF); out[3] = (u8)((v >> 24) & 0xFF); // 最高字节}USART2 中断接收
void USART2_IRQHandler(void){ if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { u8 d = (u8)USART_ReceiveData(USART2); if(rfid_rx_cnt < RFID_RX_BUF_LEN) rfid_rx_buf[rfid_rx_cnt++] = d; USART_ClearITPendingBit(USART2, USART_IT_RXNE); }}接收缓冲为32字节的环形缓冲区,每次收到数据触发中断逐字节存入。等待应答时先清空计数器,然后轮询超时检测:
u8 RFID_WaitResponse(u32 timeout_ms){ u32 t; rfid_rx_cnt = 0; rfid_rx_flag = 0; for(t = 0; t < timeout_ms; t++) { delay_ms(1); if(rfid_rx_cnt > 0) { delay_ms(50); /* 等整帧收完 */ rfid_rx_flag = 1; return RFID_OK; } } return RFID_TIMEOUT;}检测到第一个字节后延时50ms确保整帧收完,再返回给上层处理。这是基于9600bps下11字节约需11ms的保守估计。
用户界面与交互设计
状态机设计
界面采用二层状态机架构:
第一层 (UI_State_t): MAIN ──→ INIT ──→ RESULT ──→ MAIN │ │ ↑ │ │ │ ├──→ WALLET ────→ RESULT │ ↑ │ │ └──→ QUERY ────→ RESULT
第二层 (Wallet_SubState_t): SELECT ──→ AMOUNT ──→ SWIPE ──→ RESULT所有页面按任意键返回主菜单,操作有5秒超时机制。
按键映射
| 按键 | 主菜单 | 增/减选择 | 金额选择 | 结果页 |
|---|---|---|---|---|
| K1 | 初始化钱包 | 选择”增值” | 上移选项 | 返回 |
| K2 | 增/减值 | 选择”减值” | 下移选项 | 返回 |
| K3 | 查询余额 | 确认进入 | 确认刷卡 | 返回 |
金额档位
预设4个金额档位(单位为分):
| 索引 | 金额(分) | 显示 |
|---|---|---|
| 0 | 100 | 1.00 Y |
| 1 | 500 | 5.00 Y |
| 2 | 1000 | 10.00 Y |
| 3 | 5000 | 50.00 Y |
金额显示
采用 “分 → X.XX Y” 格式化,避免浮点数运算:
static void fmt_money(s32 fen, u8 *buf){ if(fen < 0) sprintf((char*)buf, "-%d.%02dY", (int)(-fen/100), (int)(-fen%100)); else sprintf((char*)buf, "%d.%02dY", (int)(fen/100), (int)(fen%100));}配色方案
LCD界面采用白底色系配色方案,通过色块区分不同类型的UI元素:
| 用途 | 颜色值 | 效果 |
|---|---|---|
| 全局背景 | WHITE | 白色 |
| 标题栏 | 0x4A49 | 深灰蓝 |
| 选中项 | 0x04FF | 蓝色高亮 |
| 成功提示 | 0x0660 | 绿色 |
| 失败/减值 | 0xC000 | 红色 |
| 增值金额 | 0x0400 | 深绿 |
| 减值金额 | 0xC000 | 红色 |
非阻塞超时轮询机制
刷卡等待采用非阻塞倒计时方案,避免死等导致界面无响应:
#define WAIT_TICKS 500u /* 500×10ms = 5秒 */
static u8 wait_poll(void){ u8 r; if(g_tick == 0) return RFID_TIMEOUT; g_tick--; r = RFID_ReadUID(g_uid); if(r == RFID_OK) return RFID_OK; return RFID_NONE; /* 本次未读到,继续等 */}主循环每10ms调用一次,500次(5秒)后自动超时。期间可以检测按键、处理中断,不会被阻塞。YMC1501的读卡号命令0xA1发送/应答约需50ms,在500次循环中足以轮询多次。
LDC1614 电感传感器驱动
项目预留了LDC1614电感传感器的驱动支持,用于测量物体距离。该芯片是TI的4通道电感数字转换器,通过I2C接口通信。
软件I2C实现
由于硬件I2C引脚冲突,采用GPIO模拟I2C(位带操作):
// PB10=SCL, PB11=SDA 开漏输出SDA_IN() // 切换为输入模式SDA_OUT() // 切换为输出模式SMBus写操作(写寄存器):
START → 设备地址+W → ACK → 寄存器地址 → ACK → 数据高字节 → ACK → 数据低字节 → ACK → STOPSMBus读操作(读寄存器):
START → 设备地址+W → ACK → 寄存器地址 → ACKRESTART → 设备地址+R → ACK → 数据高字节 → ACK → 数据低字节 → NACK → STOPLDC1614初始化流程
- 软件复位:写入
RESET_DEVICE寄存器0x8000,延时20ms - 读取设备ID寄存器(0x7F)验证芯片型号(兼容 0x3055 / 0x3054)
- 配置4个通道的参考计数(0xFFFF)、偏移量(0x0000)、稳定计数(0x0400)、时钟分频器(0x1001)
- 配置MUX选择、系统时钟、驱动电流(0xF000)
- 延时100ms等待传感器稳定
S50卡电子钱包原理
Mifare S50卡的每个扇区由4个数据块组成(每块16字节),第4块为密钥/访问控制块。YMC1501在后台完成所有S50底层操作(认证、读写块、值块格式转换),上位机只需要发送高层钱包命令。
钱包块(值块) 是S50卡的特殊数据格式:
- 4字节值(小端)+ 4字节反值 + 4字节值(冗余)+ 1字节地址
- 钱包操作(增/减/恢复)在卡内由芯片硬件执行,保证原子性
本系统使用扇区1的块0(绝对块号4)作为钱包块,密钥为默认KEYA(0xFFFFFFFFFFFF)。
编译信息
- 编译器: Keil5 ARMCC
- 启动文件: startup_stm32f10x_hd.s (高密度Flash)
- 外设库: STM32F10x Standard Peripheral Library
- 优化级别: 默认
调试顺序建议
- 先烧录程序,检查LCD启动画面是否正常显示
- 用串口助手连接USART1 (115200bps),观察printf调试输出
- 放一张M1 S50卡在读写器上,按K3测试读卡号功能
- 按K1初始化钱包(会提示放卡,5秒内放卡即写余额为0)
- 按K2测试充值/扣款(选择增/减 → 选金额 → 刷卡确认)
- 按K3查询余额验证操作结果
完整代码和工程文件位于 GitHub Repository
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时










