简介:一套基于STM32 F103C8T6微控制器的完整工程源码包,利用ESP8266无线模块将MFRC522射频模块读取到的卡号信息上传至中国移动ONENET物联网平台。工程中主控负责读卡和串口转发,ESP8266负责Wi-Fi联网与云端上报,覆盖射频识别、网络接入、数据交互三条核心链路,面向物联网与嵌入式学习者,适用于门禁考勤、智能终端等场景。压缩包约5.09MB,包含211个文件,以STM32标准外设库的.c/.h源码为主,同时附有Keil工程配置(.uvprojx)、编译产物(.hex/.axf/.map)及过程文件(.crf/.o/.d),目录结构清晰,可直接定位到初始化、外设驱动和上传相关代码。已有1770人浏览学习。工程从MFRC522读卡、ESP8266初始化到ONENET平台数据上传的完整流程均有对应实现,可对照学习射频识别读写、串口数据解析、Wi-Fi入网和物联网平台接口对接细节,便于按模块查阅和二次开发。初学者可按“读卡—串口—Wi-Fi—云端”的路径逐步调试,有经验者可以直接复用其中的驱动与通信框架;整个工程将硬件采集、无线传输与云端存储串联起来,作为课程设计、毕业设计或物联网入门项目的参考都非常合适,也可作为实际产品原型的底层基础。
1. 把 MFRC522 读到的卡号送上 OneNet:这套链路最容易被低估的不是读卡,而是联网
把一张 IC 卡的卡号,从 MFRC522 读出来,经过 STM32 整理,再借 ESP8266 送到 OneNet 平台——听起来只是三颗芯片之间的几条线加两个串口,实际调起来,绝大多数时间会耗在 ESP8266 的回包时序和 OneNet 的 HTTP 拼接上。你会同时接触 SPI 设备驱动、串口 AT 指令状态机、云端数据点上报,正好是一条能延伸到门禁、考勤、共享设备管理的小而完整的物联网数据链路。这个方向适合两类人:一类是在校生做课设或电子竞赛,一类是硬件工程师想用最低成本验证云上业务。核心难点只有一个——让三颗芯片和一个平台在时序上互相迁就。先说结论:读卡部分跑通只需要一个下午,真正让你反复烧录的是联网环节。工程文件拿到手先别急着编译,先找 readme 和引脚定义,把硬件接线对完再点烧录按钮。
2. 硬件链路与时序选择:SPI 留给读卡,串口留给联网,供电单独走
整条链路上,STM32 的引脚是唯一的信息会合点:MFRC522 通过 SPI 把卡号送进来,ESP8266 通过串口把卡号送出去。这两条总线的选择不是随意的,接口类型直接决定了驱动代码的复杂度和联调时你手里的调试工具是否顺手上。
2.1 为什么 MFRC522 走 SPI、ESP8266 走串口:三个选型依据
第一个依据是器件本身的控制方式。MFRC522 是一个寄存器映射的射频控制芯片,驱动里最频繁的操作就是读写一组内部寄存器:发命令、查 FIFO、读版本号、查中断标志。SPI 一次读写就能完成一个寄存器操作,全双工且带硬件片选,时序非常干净。I2C 当然也能用,但多了器件地址和内部寄存器地址两段开销,速率上限只有 400Kbps,在连续寻卡场景下延迟明显偏高。更现实的原因是,网上现成的 MFRC522 驱动绝大部分都是 SPI 版本,直接移植的工作量最小。
第二个依据是 ESP8266 的出厂形态。绝大多数 ESP8266 模块出厂烧录的是 AT 固件,你主控只需要通过串口发文本指令,不需要去搭乐鑫 SDK 的交叉编译工具链。AT 协议是文本化的,意味着你可以用一根 USB-TTL 把模块单独接到电脑上,用串口助手验证模块本身是否正常,再回过来查自己的代码——这一步在联调时极为关键,因为它把“模块坏了”和“代码错了”这两类问题隔离开。只有当你对功耗、启动时间或数据透传吞吐有硬指标时,才需要考虑用 SDK 重新写 ESP8266 内部程序,那是另一个层面的工作,主控侧的外围协议反而不用动。
第三个依据是外设占用与数据流特性。SPI1 加 USART2 在 STM32F103 上是两套独立外设,引脚不冲突,GPIO 压力很小;而卡号是低频小数据,一次上报也就一两百字节,115200 波特率绰绰有余。这条链路的瓶颈永远不在串口速率,而在云平台的 API 鉴权和请求格式上,所以选型时把“可调试性”放在和“速率”同等的权重上,是值得的。
2.2 接线表与供电分配:一次画对,避免乱飞线
三颗芯片的逻辑电平都是 3.3V,STM32F103 的 GPIO 输出也是 3.3V,所以直连没有电平转换问题。真正需要小心的是 ESP8266 的供电,它在上电联网瞬间的峰值电流能到 300mA 以上,从 STM32 的 3.3V 引脚直接取电,启动瞬间会把系统电压拉低,表现为反复复位上电,这是新手第一个会踩的坑。下面是我常用的接线方式,以 STM32F103C8 最小系统板为例:
| 角色 | 模块引脚 | STM32 引脚 | 备注 |
|---|---|---|---|
| SPI 片选 | MFRC522 SDA/CS | PA4 | GPIO 推挽输出 |
| SPI 时钟 | MFRC522 SCK | PA5 | SPI1_SCK |
| SPI 主入从出 | MFRC522 MISO | PA6 | SPI1_MISO,浮空输入 |
| SPI 主出从入 | MFRC522 MOSI | PA7 | SPI1_MOSI |
| 复位 | MFRC522 RST | 任意 GPIO,如 PB0 | 低电平复位 |
| 模块中断(可选) | MFRC522 IRQ | 不接 | 轮询方式读卡时不接 |
| 串口发送 | ESP8266 RXD | PA2 | USART2_TX,交叉连接 |
| 串口接收 | ESP8266 TXD | PA3 | USART2_RX |
供电分配我一般这样做:5V 进板后接一路独立稳压(常见的是 AMS1117-3.3)专门给 ESP8266 供电,STM32 和 MFRC522 共用另一路 3.3V。如果用电脑 USB 供电,整机电流约 200-400mA,USB 口能扛住,但不要再用同一个 USB 口拖其他大电流外设。MFRC522 的天线是 PCB 印制天线,天线区周围不要铺铜、不要贴金属外壳,读卡距离会明显缩水。
提示:ST-LINK 自带的 3V3 引脚能点灯,但带不动 ESP8266 连 Wi-Fi 时的峰值电流。调试时建议把 ESP8266 的串口和 STM32 的调试串口分开接 USB-TTL,这样 AT 回包和业务日志互不干扰。
接线完成后,SPI 初始化参数先对齐:模式 0(CPOL=0、CPHA=0)、8 位数据、MSB 优先、主模式、软件片选。这些参数写错任何一个,MFRC522 的版本寄存器都读不回来。下一章就从读卡驱动的裁剪说起。
3. MFRC522 读卡部分:驱动裁剪与 UID 的字节陷阱
读卡的过程本质上是一次 ISO14443A 的射频交互:MFRC522 唤醒天线场,卡响应,然后防碰撞、选卡,最后把卡的 UID 序列号放进 FIFO。驱动框架是现成的,但移植到 STM32 不是复制粘贴那么简单,需要做三处裁剪,同时理解 UID 的字节序和长度差异。
3.1 驱动移植的三个裁剪点:中断、延时和天线控制
第一处裁剪是把驱动和串口解耦。很多流传的 MFRC522 驱动最初面向 Arduino,代码里布满了 Serial.print 和 delay()。STM32 工程里这些必须替换成自己的调试宏或日志接口,否则直接编译会报一堆串口调用错误。替换的时候顺手把调试输出级别做一个开关,联调阶段打开,稳定后关掉,这样读卡流程里的时间片不受打印拖累。
第二处裁剪是延时方式。Arduino 的 delay(ms) 直接换成 HAL_Delay(ms) 没有错,但驱动里那些“等 FIFO 数据就绪”“等命令执行完”的循环,不能靠固定延时硬等,要用短循环反复读状态寄存器。原因是 MFRC522 的射频交互对时序敏感,固定长延时会吞掉防碰撞窗口,表现就是卡贴上去偶尔读得出、偶尔读不出。正确做法是给轮询加一个最大重试次数,读到状态位立刻继续。
第三处裁剪是天线控制。部分驱动在每次寻卡前会把天线关掉再打开,代码里表现为往 TxControlReg 写 0x00 再写 0x83。这种做法在切换卡片类型时有用,但对连续读卡场景反而制造了不必要的射频建立时间。我一般只在初始化时开一次天线,之后保持常开,连续读卡的稳定性会好很多。射频增益寄存器(RFCfgReg 附近)在多数场景不需要动,如果你发现卡片必须贴得很近才读得出,第一嫌疑是电源纹波,而不是增益配置。
// 以 STM32CubeMX 生成的 HAL 初始化为例,重点看速度和极性三行 hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0 hspi1.Init.NSS = SPI_NSS_SOFT; // 软件片选,不用硬件 NSS hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 72MHz/8=9MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;逻辑说明:MFRC522 要求 SPI 模式 0,速率上限 10MHz,预分频 8 在 72MHz 主频下输出 9MHz,刚好在规格之内。NSS 必须用软件 GPIO,因为驱动的每个命令阶段都要独立拉低 SDA 再拉高,硬件 NSS 的自动片选时序和命令边界对不齐,很容易出现第一字节丢失。标准外设库版本在 SPI_Cmd(SPI1, ENABLE) 前后同样适用这几行参数,只是寄存器封装形式不同。
移植完成后先做一个最小验证:读版本寄存器,地址 0x37,正常返回 0x91 或 0x92。如果返回 0x00 或 0xFF,不要急着查驱动,先检查 SPI 极性、接线顺序和供电,这三项占读卡失败原因的九成。版本号确认没问题,再进入 UID 读取逻辑。
3.2 UID 的字节序与长度:决定云端卡号是不是“看起来对”
MFRC522 完成寻卡、防碰撞、选卡后,拿到的是 UID 字节数组。最常见的是 4 字节的 MIFARE Classic 卡,也就是门禁里大量使用的白色 IC 卡;也有 7 字节的标签卡。问题通常出现在两个地方:一是直接把字节当成 ASCII 字符发送,0x12 被 printf 成控制字符,云端收到乱码;二是不知道字节序规则,同一张卡在不同设备上报出两个卡号,数据库里出现重复卡。
统一做法是:每个字节转成两位大写十六进制,按读取顺序拼接成一个字符串。UID 是 0xAB 0xCD 0x12 0x34,就转成 ABCD1234。这个字符串在云端就是设备的唯一身份标识,后续不管是门禁白名单还是考勤记录,都以它为准。7 字节 UID 的卡,如果驱动只做了一次防碰撞,只返回前 4 字节,那两张不同 7 字节卡的前 4 字节可能完全相同,导致撞号。如果你的工程要面向通用市场,需要确认驱动是否实现了级联防碰撞,也就是 ISO14443A 里的 Cascade Level 2 流程。
uint8_t uid[8]; // 驱动返回的 UID 缓冲,具体长度按驱动定义 uint8_t uidLen; // 实际 UID 字节数,可能是 4 或 7 char cardNo[16]; // 十六进制字符串,最大 14 字符 + '\0' uidLen = MFRC522_GetUID(uid); // 函数名以你的工程驱动为准 if (uidLen == 4 || uidLen == 7) { for (int i = 0; i < uidLen; i++) { sprintf(&cardNo[i * 2], "%02X", uid[i]); } } printf("[CARD] %s\r\n", cardNo); // 例:UID {AB,CD,12,34} -> "ABCD1234"逻辑说明:MFRC522_GetUID 内部通常包含 Request、Anticollision、Select 三步,不同驱动的函数名可能叫 PcdRequest、PcdAnticoll,但返回的 UID 字节顺序是统一的。sprintf 里的 %02X 会补齐前导零,所以 0x0A 会输出 A0 前面的 0,这一点比手动移位拼接更不容易出错。拼好的字符串不要再加空格或换行,后续拼 HTTP body 时这会变成多余的转义负担。
拿到卡号字符串后,建议先串口打印确认与读卡器工具读出的卡号一致,再继续下一步。这个验证半小时内能完成,但它能帮你把读卡问题和联网问题彻底分开——后面 Any 一眼看不出原因的云平台异常,只要确认卡号字符串是对的,排查方向就不会跑偏。
4. 卡号上云:ESP8266 的 AT 指令串与 OneNet 数据点写入
ESP8266 本身不带业务逻辑,主控的角色就是通过串口敲 AT 指令,把模块带上网,再让它发一次 HTTP 请求。OneNet 平台的接入有老平台和新版平台两套差异,本章按老平台最常见的 HTTP 数据点上报来写,这是多数存量工程和网盘工程文件采用的路径。
4.1 把 ESP8266 带上网:AT 指令的最小序列与超时判断
AT 指令在固件版本之间有少量差异,但核心序列是固定的:关回显、设 Station 模式、连路由器、建 TCP 连接、发数据。容易翻车的不是指令拼写,而是每条指令的“预期回包”和“等待时间”。AT+AT 会回复 OK,AT+CWJAP 连上后分步返回 WIFI CONNECTED 和 WIFI GOT IP,AT+CIPSEND 则要等模块先回一个 > 提示符,然后才能发数据,发完还要等 SEND OK。这些回包都可能被串口拆成多段到达,所以接收侧不要用“收满固定长度”的方式,而要用环形缓冲加关键字匹配:收到 OK 或 ERROR 就认为指令执行完毕。
// 伪代码,体现状态机:每条 AT 都确认收到预期回包后再进入下一步 ESP_WaitReady(2000); // 等模块上电后打印 ready ESP_SendCmd("ATE0\r\n", "OK", 500); // 关闭回显,回包更干净 ESP_SendCmd("AT+CWMODE=1\r\n", "OK", 500); // 如果 WiFi 密码含引号或逗号,请在字符串里转义 ESP_SendCmd("AT+CWJAP=\"MY_SSID\",\"MY_PASS\"\r\n", "WIFI GOT IP", 15000); ESP_SendCmd("AT+CIPSTART=\"TCP\",\"api.heclouds.com\",80\r\n", "CONNECT", 5000);逻辑说明:ESP_SendCmd 内部要做回包匹配,而不是简单地等够时间就读缓冲区。AT+CWJAP 的超时给到 15 秒,因为路由器关联、DHCP 获取 IP 是两个阶段;CIPSTART 用域名时模块需要先解析 DNS,首次解析可能 3 到 5 秒,超时给 5 秒以上。如果经常 DNF FAIL,大概率是固件版本太老或路由器开了 AP 隔离,可以先用 AT+CIPDOMAIN 手工解析域名拿到 IP,再用 IP 建 TCP。
参数说明:115200 是 ESP8266 出厂常见波特率,但二手模块可能被改过,上电无响应时用 AT+UART_DEF=115200,8,1,0,0 重置一次。注意 USART2 的波特率、数据位、停止位要和模块保持一致,STM32 这边初始化对不上,AT 指令会变成乱码。
建好 TCP 连接后,HTTP 请求的发送就交给下一节的拼接函数。这里有一个经验:不要每次上报都重新执行一遍完整的 AT 序列,模块联网和 TCP 连接在持续供电状态下一直是保持的,把联网状态做成一个全局标志位,只有掉线时才重新执行。这样既省时间,也减少了路由器侧频繁掉线的概率。
4.2 组装一次 HTTP 上报:URL、鉴权头与 Content-Length
OneNet 老平台的数据点写入接口是 POST /devices/{设备ID}/datapoints,头部需要四个字段:Host、api-key、Content-Type、Content-Length。鉴权用的是 api-key 头,不是 Authorization,这是新手最容易看错的地方。设备 ID 和 APIKey 是两串不同的数字,从平台控制台的设备详情和产品信息里分别复制,别混用。
请求体是一个 JSON 数组,里面的数据流 ID 要和控制台创建的数据流一致,卡号字符串作为 value 传入。最关键的细节是 Content-Length 必须和 body 的实际字节数完全一致,多一位少一位都会表现为“云端收不到数据”,而 ESP8266 这个层面看 TCP 是发送成功的。我习惯先拼 body,再用 strlen 算长度,最后把它填进头部,顺序上不要反过来。
char body[96]; char req[256]; int body_len, req_len; // 先拼 body,body_len 就是实际字节数 body_len = sprintf(body, "{\"datastreams\":[{\"id\":\"%s\",\"datapoints\":[{\"value\":\"%s\"}]}]}", "card_no", cardNo); // card_no 是控制台里的数据流 ID // 再把 body 长度填进头部,最后整体发送 req_len = sprintf(req, "POST /devices/%s/datapoints HTTP/1.1\r\n" "Host: api.heclouds.com\r\n" "api-key: %s\r\n" "Content-Type: application/json\r\n" "Content-Length: %d\r\n\r\n" "%s", "YOUR_DEVICE_ID", "YOUR_API_KEY", body_len, body); ESP8266_TCPSend(req, req_len); // 通过 AT+CIPSEND 发送这段请求内容逻辑说明:sprintf 的返回值是实际写入的字符数,用它当 Content-Length 不会算错。注意代码里 YOUR_DEVICE_ID 和 YOUR_API_KEY 是占位符,要替换成你控制台的真实值;数据流 ID “card_no” 也可以换成你自己的数据流名。ESP8266_TCPSend 内部做三件事:发 AT+CIPSEND=<长度>,等 > 提示符,然后发送数据,最后等 SEND OK。长度参数以 req_len 为准,因为这个值来自已经拼好的完整请求,不会出现“头部声明长度和实际发送长度不一致”的问题。
这段请求发出后,OneNet 会返回 HTTP 响应,正常是 200,你可以把响应内容打印到调试串口。如果返回 401,基本是 api-key 错误;返回 404,基本是设备 ID 或 URL 路径不对;返回 400 且响应体提示 JSON 格式问题,优先检查双引号是否被转义,或者数据流 ID 里有没有多余空格。
提示:如果你在平台控制台看到的是新版的 OneNET Studio,接入方式和本章的 api-key 请求不同,它改成了签名鉴权。对应做法是去平台文档里找 HTTP 数据上报接口,把请求头换成文档给出的 token 格式,上报逻辑和 TCP 链路本身是不变的。老平台与新平台的差异,会在后面的进阶章节里给出一个工程化的兼容写法。
5. 联调高频问题排查:五个翻车点与对应的解决路径
这一章把联调阶段最常见的五个问题按“现象、原因、解决”整理出来。这些问题我在搭建同类型链路时基本都遇到过,也是帮别人排查时出现频率最高的。建议按顺序逐一排除,不要跳着试。
现象一:MFRC522 读卡无反应,版本寄存器读回 0x00 或 0xFF。原因是 SPI 极性相位不对、接线顺序错误或模块没有正常上电复位。解决:先不跑业务代码,只读版本寄存器(地址 0x37,命令 0x02 类别下),正常返回 0x91 或 0x92。用逻辑分析仪看 SCK 空闲电平,MFRC522 要求空闲低电平、第一个边沿采样,和代码里 CPOL=0、CPHA=0 对应。RST 引脚在高电平下运行,确认没有误接低电平;IRQ 引脚在轮询模式下悬空即可。
现象二:ESP8266 发 AT 无任何回应。原因是模块没有供电、RXD/TXD 接反、波特率不匹配或 CH_PD 没有上拉。解决:把模块单独接到 USB-TTL,直接和电脑串口助手通信,先确认模块本身能回 OK。RXD 和 TXD 是交叉连接,ST32 的 TX 接模块的 RX,ST32 的 RX 接模块的 TX。3.3V 供电要用独立稳压,不要用 ST-LINK 的 3V3 引脚硬带。CH_PD(EN)引脚接 3.3V 上拉,这是模块能启动的前提。
现象三:AT+CWJAP 返回 ERROR 或一直在 CONNECT FAIL。原因是路由器开了 5G 与 2.4G 同名的双频合一、WiFi 密码含特殊字符、或者信号强度不够导致重连超时。解决:先用手机开一个纯 2.4G 热点,热点名改成简单英文字母,密码用纯数字测试。热点能连上就说明模块没问题,问题在路由器环境;这时候去路由器后台把双频合一关掉,或者把模块固定到 2.4G 频段。中文 SSID 在部分 AT 固件下会解析失败,工程里尽量避免。
现象四:AT+CIPSEND 发出了数据,也收到了 SEND OK,但 OneNet 平台没有数据。原因是 Content-Length 和 body 实际字节数不一致、HTTP 头部缺少 \r\n\r\n 分隔、或者 APIKey 头拼写有误。解决:把整段请求原样打印到串口,用网络调试助手在本机开一个 TCP Server,把请求发到本机,肉眼看头部空行是否存在,再用 strlen 计算 body 长度核对 Content-Length。这里有一个细节:HTTP 头部和 body 之间那个空行必须是 \r\n\r\n,少一个 \r 平台解析就会失败。建议直接复制本章代码里的字符串格式,不要手动敲。
现象五:云端有数据,但卡号看起来乱码,或者两张不同的卡上报了相同的卡号。原因是 UID 字节被当成 ASCII 字符发送、大小端顺序拼反、或者 7 字节卡只做了单级防碰撞。解决:统一用 sprintf 的 %02X 转十六进制字符串,卡号里的字符只允许 0-9 和 A-F。用读卡器工具和 STM32 打印的卡号对比,确认顺序一致。7 字节的卡如果驱动不支持级联防碰撞,建议先 4 字节卡跑通整个链路,后续再补驱动,不要一开始就混用卡型。
这一章的排查思路是“由底向上”:先把单模块验证过,再做链路验证,最后才怀疑云平台。我见过很多工程文件里的代码本身没有大问题,最终排查出来是 USB 供电不稳导致 ESP8266 反复重启,这个问题在一开始按第 2 章单独供电就可以完全避免。
6. 从能通到能用:缓存重传、平台接口差异与低功耗的三个习惯
链路跑通只是起点,真实环境下还要处理网络抖动、平台版本更迭和功耗问题。我在这套架构上沉淀了三个值得优先做的习惯,都改动量不大,但对稳定性的提升立竿见影。
第一个习惯是本地缓存与重传。ESP8266 在弱 Wi-Fi 环境下可能建连失败,这时候卡号如果直接丢弃,云端就永久丢了一条数据。我会在 STM32 内部开辟一个环形缓冲,上报失败时把卡号字符串存进去,等模块恢复联网后,下次上报前先把缓冲里的数据补发,再发新数据。缓存的容量不用大,几十条就够,因为刷卡本身是低频事件。
第二个习惯是用宏把平台差异隔离出来。老平台的 api-key 鉴权和新版平台的签名鉴权,接入路径不同,但业务层都只关心“上报一个卡号字符串”。我会在代码里定义几个宏:HTTP_HOST、API_KEY_HEADER、DEVICE_URL,把平台相关的差异全部收敛到一处,切平台时只改宏定义,主流程代码不动。后续如果要对接其他物联网平台,也是同样思路,业务逻辑和上报协议之间隔一层抽象,这层抽象用宏实现比用函数回调更省代码。
第三个习惯是低功耗与读卡唤醒。设备待机时不需要让 MFRC522 的天线一直开着,也不需要 ESP8266 一直保持 TCP 连接。我一般让主控进入 STOP 模式,MFRC522 的 IRQ 引脚配置为外部中断源,卡片进入天线场时会产生中断唤醒主控,主控再开天线、联网、上报,然后再次休眠。这样待机电流可以从几十毫安降到微安级别,电池供电的门禁模块才具备实用性。
我现在的习惯是先把串口回环打通,再谈上云。所有“平台不吐数据”的玄学,最后基本都能归因到链路里某一段没确认,而不是云端的问题。先把读卡打印、AT 回包、HTTP 响应这三段各自验证明白,再合并起来跑,整个过程会顺畅很多。希望帮到你。
本文还有配套的精品资源,点击获取