
1. 项目缘起与整体设计思路1.1 为什么选择STM32加RFID做仓库管理做过仓库管理的人都知道最头疼的不是货物多而是“账实不符”。手工记账、Excel表格、扫码枪这些方式在小规模场景下勉强能用一旦货物种类超过几百种、每天出入库频次上来之后漏记、错记、重复录入几乎是必然的。我见过太多小型仓库还在用纸质单据加人工核对的方式盘点一次要停业半天盘完之后数据还对不上。RFID技术恰好能解决这个痛点。它跟条形码最大的区别在于不需要对准、不需要逐个扫描读写器天线覆盖范围内可以一次性读取几十上百个标签。这意味着叉车推着一托盘货物经过通道门几秒钟之内所有货物的信息就全部采集完毕了。这种批量、非接触、自动化的特性放在仓库管理场景里简直是量身定做。那为什么主控选STM32而不是51单片机或者树莓派51单片机做RFID读写不是不行但它的RAM和Flash资源太紧张了跑一个完整的文件系统加网络协议栈基本不可能而且SPI时序在高速读写时容易出问题。树莓派虽然性能强但成本高、功耗大、工业环境下的稳定性也不如单片机方案。STM32F103系列或者F407系列刚好卡在一个甜点位置性能够用、外设丰富、成本可控、生态成熟而且工业级温度范围宽仓库环境夏天热冬天冷都能扛住。这个项目的定位很明确面向中小型仓库、工厂线边库、实验室物料管理这类场景提供一套低成本、可定制、能离线运行的RFID仓库管理系统。整套方案的核心逻辑是RFID读写器负责采集标签数据STM32负责数据处理和业务逻辑上位机或者本地屏幕负责展示和交互数据存储用SD卡或者Flash芯片做本地持久化。1.2 系统架构的取舍与考量整套系统的架构我反复调整过好几版最终定下来的方案是“STM32主控加RFID读写模块加本地存储加上位机通信”的四层结构。这里面的取舍值得展开说说。第一层是RFID读写模块。市面上常见的方案有MFRC522、RC522、PN532这几类。MFRC522便宜、资料多但只支持13.56MHz的ISO 14443A协议读距短大概3到5厘米。PN532支持ISO 15693协议读距能到10厘米以上而且支持卡片模拟和点对点通信但价格贵一些。我最终选了MFRC522作为基础版本原因是成本敏感型项目里3到5厘米的读距在手持盘点场景下够用了而且它的SPI接口跟STM32对接非常方便社区资料也最丰富。如果后续需要更远读距可以换成RC663或者专用UHF模块接口层留好抽象就行。第二层是STM32主控。我选的是STM32F103C8T6也就是常说的“蓝板”或者“最小系统板”。72MHz主频、64KB Flash、20KB RAM对于这个项目来说刚好够用。有人会问为什么不选F407性能更强。我的考虑是F103的生态最成熟标准库和HAL库资料都多而且这个项目不需要跑复杂的GUI或者网络协议栈F103完全能胜任。省下来的成本可以花在RFID模块和存储芯片上整体性价比更高。第三层是本地存储。我用了两种方案并行一个是SD卡通过SPI接口挂载FatFS文件系统用来存历史记录和导出数据另一个是AT24C02 EEPROM用来存系统配置参数和关键状态防止断电丢失。SD卡的好处是容量大、可以直接拔下来插电脑读数据缺点是文件系统操作有磨损风险不适合频繁写入。EEPROM的好处是字节级写入、寿命长适合存配置。两者配合使用各取所长。第四层是通信与交互。本地用0.96寸OLED显示当前状态和操作提示通过I2C接口连接。上位机通信走UART串口用CH340或者CP2102转USB波特率115200。协议方面我自定义了一套简单的帧格式包含帧头、命令字、数据长度、数据和校验和。没有用Modbus是因为这个项目的数据结构比较简单自定义协议更轻量解析起来也更快。注意RFID模块的供电一定要稳定MFRC522在发射射频场的时候瞬间电流能到100mA以上如果跟STM32共用一路LDO建议加一个100uF的电解电容做储能否则容易出现读卡失败或者死机。1.3 软件层面的分层设计软件这块我分了三层硬件驱动层、业务逻辑层、应用交互层。硬件驱动层负责SPI、I2C、UART、定时器这些外设的初始化和读写操作直接操作寄存器或者调用HAL库。业务逻辑层负责RFID卡片的寻卡、防冲突、选卡、读写块数据以及库存的增删改查、盘点比对、数据持久化。应用交互层负责OLED显示刷新、按键扫描、串口命令解析和响应。这样分层的好处是换RFID模块的时候只需要改驱动层业务逻辑不用动换显示屏的时候同理。我在实际开发中就遇到过从OLED换成LCD的情况因为分层做得好只改了一个显示驱动文件半天就搞定了。防冲突算法是RFID读写里的一个关键点。当多张卡片同时进入天线场区时MFRC522会返回冲突信号需要用防冲突算法逐张识别。MFRC522内部硬件支持防冲突但需要软件配合发送特定的命令序列。我用的流程是寻卡、防冲突、选卡、认证、读块。每一步都有超时重试机制防止某张卡片响应慢导致整个流程卡死。2. 核心硬件选型与电路设计要点2.1 主控芯片与最小系统STM32F103C8T6的最小系统包括主控芯片、8MHz晶振、32.768kHz晶振可选用于RTC、复位电路、BOOT选择跳线、SWD调试接口。晶振的负载电容一般选20pF但实际值要根据晶振规格书来定我试过用22pF也能起振但稳定性不如20pF。复位电路用10k上拉电阻加100nF电容这是标准配置。电源部分要注意STM32的VDDA和VDD要分开滤波VDDA用磁珠或者0欧电阻隔离然后加1uF和100nF电容。ADC如果不用可以悬空但VDDA一定要接。BOOT0和BOOT1的跳线要引出来方便切换启动模式。SWD接口至少引出SWCLK、SWDIO、GND、3.3V四根线用四针排针就行。我踩过的一个坑蓝板上的LED接在PC13但PC13的驱动能力很弱只能灌电流不能拉电流而且翻转速度慢。如果你要用PC13做状态指示记得LED要接成低电平点亮否则亮度不够。2.2 RFID读写模块的选型对比模块型号支持协议读距接口价格区间适用场景MFRC522ISO 14443A3-5cmSPI/I2C/UART5-15元手持盘点、近距离读写PN532ISO 14443A/B, ISO 156935-10cmSPI/I2C/UART30-60元需要ISO 15693或卡模拟RC663ISO 14443A/B, ISO 15693, FeliCa10-15cmSPI/I2C/UART80-150元远距离、多协议UHF模块ISO 18000-6C1-5mUART200-500元通道门、远距离批量读取这张表是我实际用过之后总结的。MFRC522的SPI接口最高支持10Mbps但实际用的时候建议降到4Mbps左右太高了容易出错。PN532的UART接口默认波特率是115200但它的SPI接口时序比较特殊需要仔细看数据手册。RC663性能最好但价格也最高适合对读距有硬性要求的场景。我最终选MFRC522还有一个原因它的天线是PCB板载的不需要外接线圈省去了天线调试的麻烦。如果你要自己做天线那又是另一个大坑匹配电容和线圈电感都要用网络分析仪调没有专业设备很难做好。2.3 存储电路的设计细节SD卡电路要注意几点SPI模式下CS、MOSI、MISO、SCK四根线要接对MISO是输入到STM32的不要接反。SD卡的供电是3.3V但有些卡在初始化时需要更大的电流所以LDO的输出能力至少要500mA。另外SD卡座子的CD卡检测引脚可以接一个GPIO用来检测卡是否插入这个功能在实际使用中很有用。AT24C02的I2C地址是0xA0写和0xA1读A0、A1、A2三个引脚决定地址的低三位。如果总线上只挂一个EEPROM这三个脚都接地就行。上拉电阻用4.7k不要用10k10k在高速模式下上升沿太慢。WP引脚是写保护接高电平就禁止写入接低电平允许写入。我一般把它接一个GPIO需要写保护的时候拉高平时拉低。提示SD卡和EEPROM共用SPI和I2C总线时要注意片选信号的管理。SD卡的CS和RFID模块的CS不能同时拉低否则会冲突。我在代码里用了一个互斥锁机制确保同一时间只有一个SPI设备被选中。2.4 显示与交互外设OLED我用的是0.96寸SSD1306I2C接口分辨率128x64。这个屏幕的优点是自发光、对比度高、视角广在仓库的灯光环境下看得很清楚。I2C地址一般是0x78写和0x79读但有些模块是0x7A具体要看模块上的电阻配置。初始化序列比较长建议直接找一个现成的驱动库不要自己从头写。按键我用的是五个独立按键上、下、确认、返回、功能。接在GPIO上配置成上拉输入按下时接地。消抖用软件延时20ms再读一次简单有效。如果按键数量多可以考虑用矩阵键盘或者ADC按键但五个键用独立GPIO最省事。蜂鸣器用一个NPN三极管驱动基极串1k电阻蜂鸣器接在集电极和VCC之间发射极接地。STM32的GPIO输出高电平时三极管导通蜂鸣器响。注意蜂鸣器要选有源蜂鸣器给电就响不需要PWM驱动。如果要用无源蜂鸣器那就需要定时器输出PWM频率控制在2kHz到4kHz之间。3. 软件实现与核心代码解析3.1 开发环境搭建与工程配置开发环境我用的是Keil MDK 5配合STM32CubeMX做初始化配置。CubeMX的好处是图形化配置时钟树、外设引脚和中断优先级生成初始化代码省去了查手册配寄存器的麻烦。但要注意CubeMX生成的代码框架比较臃肿如果Flash紧张可以手动精简掉不用的外设初始化。时钟配置外部8MHz晶振PLL倍频到72MHzAHB不分频APB1分频系数236MHzAPB2分频系数172MHz。SPI1挂在APB2上时钟72MHz分频后SCK频率为72/89MHz但MFRC522建议不超过10MHz所以这个配置没问题。I2C1挂在APB1上36MHz标准模式100kHz快速模式400kHz。工程目录结构我习惯这样组织Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── stm32f1xx_hal_conf.h │ │ └── stm32f1xx_it.h │ └── Src/ │ ├── main.c │ ├── stm32f1xx_hal_msp.c │ ├── stm32f1xx_it.c │ └── system_stm32f1xx.c ├── Drivers/ │ ├── STM32F1xx_HAL_Driver/ │ └── CMSIS/ ├── Middlewares/ │ └── FatFs/ ├── App/ │ ├── rfid/ │ │ ├── mfrc522.c │ │ └── mfrc522.h │ ├── storage/ │ │ ├── sd_fatfs.c │ │ └── eeprom.c │ ├── display/ │ │ ├── oled.c │ │ └── oled.h │ └── business/ │ ├── inventory.c │ └── inventory.h └── MDK-ARM/ └── Project.uvprojx这样分目录的好处是驱动、中间件、业务逻辑分开找代码的时候一目了然。App目录下再按功能分子目录每个模块一对.c和.h文件接口清晰。3.2 MFRC522驱动移植与关键函数MFRC522的驱动移植主要是SPI读写函数的适配。它有两个关键函数MFRC522_Write和MFRC522_Read分别对应写寄存器和读寄存器。写寄存器的地址格式是(寄存器地址 1) 0x7E读寄存器是((寄存器地址 1) 0x7E) | 0x80。这个格式在数据手册里有说明但很容易搞错我第一次写的时候就把读写地址搞反了结果一直读不到正确的版本号。初始化流程void MFRC522_Init(void) { // 1. 硬件复位 MFRC522_Reset(); // 2. 软复位 MFRC522_Write(MFRC522_REG_COMMAND, PCD_RESETPHASE); HAL_Delay(50); // 3. 配置定时器 MFRC522_Write(MFRC522_REG_T_MODE, 0x8D); MFRC522_Write(MFRC522_REG_T_PRESCALER, 0x3E); MFRC522_Write(MFRC522_REG_T_RELOAD_L, 30); MFRC522_Write(MFRC522_REG_T_RELOAD_H, 0); // 4. 配置发射功率 MFRC522_Write(MFRC522_REG_TX_CONTROL, 0x03); MFRC522_Write(MFRC522_REG_TX_AUTO, 0x40); MFRC522_Write(MFRC522_REG_MODE, 0x3D); // 5. 打开天线 MFRC522_AntennaOn(); // 6. 读取版本号验证 uint8_t version MFRC522_Read(MFRC522_REG_VERSION); // 正常应该返回0x91或0x92 }寻卡函数MFRC522_Request发送REQA或WUPA命令返回卡片类型。防冲突函数MFRC522_Anticoll返回卡片的序列号。选卡函数MFRC522_SelectTag根据序列号选中卡片。认证函数MFRC522_Auth用密钥认证扇区。读块函数MFRC522_ReadBlock和写块函数MFRC522_WriteBlock分别读写数据块。注意MFRC522的认证密钥默认是6个0xFF但很多卡片出厂时密钥是6个0xFF用户区可以修改。如果你买的卡片是加密卡需要先知道密钥才能读写。我建议项目里用默认密钥方便调试量产时再改。3.3 库存管理业务逻辑实现库存管理的核心数据结构是一个链表或者数组每个节点包含标签UID、货物名称、数量、入库时间、出库时间、状态。我用的是固定长度的数组因为STM32的RAM有限动态内存分配容易产生碎片。数组长度定义为200每个节点占32字节总共6.4KB在20KB RAM里占三分之一可以接受。typedef struct { uint8_t uid[4]; // 标签UID char name[16]; // 货物名称 uint16_t quantity; // 数量 uint32_t in_time; // 入库时间戳 uint32_t out_time; // 出库时间戳 uint8_t status; // 0在库, 1出库 } InventoryItem; InventoryItem inventory[200]; uint16_t inventory_count 0;入库流程寻卡、读取UID、在数组中查找是否已存在。如果存在数量加一如果不存在在数组末尾新增一条记录。出库流程类似找到对应UID数量减一如果数量为零则标记为出库状态。盘点流程遍历数组中所有在库记录逐张读取当前场区内的卡片UID比对哪些卡片在数组中有记录但当前不在场哪些卡片在场但数组中没有记录。比对结果通过OLED显示或者串口输出。数据持久化每次入库或出库操作后将整个数组写入SD卡的一个二进制文件。写入时先写到一个临时文件写完后再重命名为正式文件防止写入过程中断电导致数据损坏。这个技巧在嵌入式文件系统里很常用FatFS支持f_rename函数。3.4 串口通信协议设计与上位机对接串口协议我设计得很简单帧格式如下字节位置内容说明00xAA帧头10x55帧头2CMD命令字3LEN数据长度4~NDATA数据域N1CHECKSUM校验和命令字定义0x01查询库存0x02入库操作0x03出库操作0x04盘点0x05清空库存。校验和是CMD、LEN、DATA所有字节的异或值。上位机我用Python写了一个简单的GUI用PySerial库做串口通信用Tkinter做界面。功能包括连接串口、发送命令、显示库存列表、导出CSV文件。这个上位机代码不长大概三百行左右但足够演示整个系统的功能。import serial import struct def send_command(ser, cmd, datab): frame bytearray([0xAA, 0x55, cmd, len(data)]) frame.extend(data) checksum 0 for b in frame[2:]: checksum ^ b frame.append(checksum) ser.write(frame) return ser.read(256) # 简化处理实际要按协议解析提示串口通信最容易出问题的地方是波特率不匹配和地线没接。如果上位机收不到数据先用示波器或者逻辑分析仪看STM32的TX引脚有没有波形有波形说明STM32在发问题在接收端没波形说明STM32没发检查代码里的发送函数有没有被调用。4. 实操调试与常见问题排查4.1 硬件调试步骤与工具使用硬件调试我一般按这个顺序来先测电源再测晶振然后测复位最后测通信。电源用万用表测3.3V和5V是否正常纹波大不大。晶振用示波器测8MHz和32.768kHz有没有起振幅度够不够。复位引脚在上电后应该是高电平按下复位键应该拉低再恢复高电平。通信接口用逻辑分析仪抓波形看时序对不对。逻辑分析仪我用的是Saleae Logic 8配合它的软件可以解码SPI、I2C、UART协议。抓SPI波形的时候重点看CS有没有拉低、时钟极性对不对、数据在上升沿还是下降沿采样。MFRC522的SPI模式是CPOL0、CPHA0也就是时钟空闲低电平、上升沿采样。如果配成CPOL1、CPHA1数据就全乱了。万用表测通断的时候注意不要在线测电阻要把电路断电。我见过有人带电测电阻结果万用表读数不准还把电路搞短路了。测电压的时候黑表笔接地红表笔点测点不要同时碰两个点容易短路。4.2 常见问题速查表现象可能原因排查方法解决方案读不到卡天线未开启测TX1、TX2引脚有无13.56MHz信号检查AntennaOn函数是否调用读卡距离短供电不足测3.3V在读卡瞬间是否跌落加100uF电容换大电流LDO读卡不稳定SPI时钟太快降低SPI分频系数从4Mbps降到2Mbps版本号读不到SPI接线错误检查MOSI、MISO是否接反交换MOSI和MISOSD卡初始化失败卡格式不对用电脑格式化为FAT32重新格式化簇大小4096OLED不显示I2C地址错误用I2C扫描程序找地址修改驱动里的地址定义串口乱码波特率不匹配确认两边都是115200统一波特率检查晶振频率数据丢失写入未完成就断电检查文件写入流程用临时文件加重命名机制按键不灵敏消抖时间不够示波器看按键波形延时20ms再读系统死机中断优先级冲突检查NVIC配置调整优先级RFID中断设最低这张表是我在实际调试中积累的每一条都对应一个真实踩过的坑。比如“读卡距离短”这个问题我一开始以为是天线设计问题后来用示波器测3.3V电源发现读卡瞬间电压从3.3V跌到2.8V明显是供电不足。换了一个输出电流更大的LDO问题就解决了。4.3 实操心得与避坑经验第一个心得RFID卡片不要贴在金属表面上。金属会吸收射频能量导致读卡距离急剧缩短甚至完全读不到。如果货物是金属材质要么用抗金属标签要么把标签贴在非金属的包装盒上。我试过把普通标签贴在铝板上读距从5厘米降到几乎为零。第二个心得多张卡片同时读取时防冲突算法需要时间。如果卡片数量超过10张整个读取流程可能需要几百毫秒。这时候如果主循环里还有其他任务要注意任务调度不要让RFID读取阻塞太久。我的做法是把RFID读取放在定时器中断里每次中断读一张卡读完一张再读下一张主循环只负责处理已读取的数据。第三个心得SD卡的文件系统操作比较耗时尤其是写入大文件的时候。如果每次入库都写一次SD卡系统响应会变慢。我的优化方案是在RAM里维护一个脏标志位只有脏标志位置位时才写入SD卡而且写入操作放在空闲任务里执行不阻塞主流程。第四个心得EEPROM的写入寿命是100万次左右如果频繁写入同一个地址很快就会坏掉。所以配置参数不要每次修改都写EEPROM而是等系统关机或者用户主动保存时才写。另外EEPROM写入后需要5ms左右的内部写周期这段时间不能操作I2C总线否则数据会出错。第五个心得OLED屏幕长时间显示固定内容会烧屏尤其是高对比度的白色文字。如果系统长时间运行建议加一个屏幕保护功能比如5分钟无操作就关闭显示或者定期移动显示内容的位置。4.4 系统优化与扩展方向系统跑通之后我做了几项优化。第一项是降低功耗把STM32的主频从72MHz降到48MHzRFID模块在不读卡的时候进入软掉电模式OLED亮度调低。实测下来整体功耗从120mA降到60mA左右如果用电池供电续航能翻一倍。第二项是提高读取速度把SPI时钟从4MHz提高到8MHz同时优化防冲突算法的重试次数。原来读一张卡需要50ms优化后降到20ms左右。对于需要快速盘点的场景这个提升很明显。第三项是增加数据校验在SD卡存储的每条记录后面加一个CRC16校验值读取的时候先校验再使用。这样即使SD卡有坏块导致数据损坏也能及时发现并跳过不会把错误数据当成真实库存。扩展方向有几个一是加一个ESP8266或者ESP32模块把数据上传到云端实现远程查看库存。二是加一个热敏打印机盘点完成后直接打印盘点单。三是加一个蜂鸣器和LED指示灯入库和出库时给出声光提示减少操作员看屏幕的次数。四是把RFID模块换成UHF模块实现通道门式的批量读取叉车经过就能自动完成出入库记录。注意加无线模块的时候要注意电源干扰。WiFi模块发射瞬间电流能到300mA如果跟RFID模块共用电源可能导致RFID读卡失败。建议给无线模块单独一路LDO或者在电源上加LC滤波。5. 写在最后的一些个人体会这个项目我从画原理图到打板、焊接、调试、写代码前后花了大概三周时间。中间踩了不少坑也学到了很多东西。最大的体会是嵌入式项目里硬件和软件是分不开的。很多时候软件跑不通问题出在硬件上硬件测着没问题软件配置错了照样不工作。所以调试的时候一定要软硬结合用示波器、逻辑分析仪这些工具去观察实际信号不要只盯着代码看。另一个体会是不要追求一步到位。我一开始想把所有功能都做进去结果代码越写越乱调试越来越难。后来我改变策略先做最核心的读卡功能跑通了再加显示再加存储再加通信。每加一个功能就测试一次确保新功能不影响旧功能。这种增量式的开发方式虽然看起来慢但实际上比一次性写完再调试要快得多。还有一点文档和注释很重要。我写代码的时候习惯在每个函数上面写清楚功能、参数、返回值关键变量加注释。这样过几天再回来看或者别人接手你的代码都能很快理解。我见过太多项目因为没文档换个人就维护不下去了。最后说一个实际使用中的小技巧RFID卡片如果放在钱包里或者口袋里可能会被其他卡片干扰。我建议给每张卡片贴一个标签纸写上货物名称这样即使读卡失败也能手动输入UID。另外卡片不要弯折内部的线圈断了就废了读卡距离会变得极短或者完全读不到。