基于STM32与RT-Thread的WiFi门禁考勤系统设计

发布时间:2026/9/13 1:47:11

基于STM32与RT-Thread的WiFi门禁考勤系统设计 简介基于C语言与RT-Thread操作系统的WiFi门禁考勤系统完整工程主控采用STM32F103ZET6适合毕业设计、课程设计及嵌入式项目二次开发。系统支持刷卡、指纹双方式验证身份并通过ESP8266 Mesh组网与服务器端通信服务端可完成人员信息增删改、考勤数据读取及Excel表格导出。项目内含RT-Thread v2.1.0移植、STemWin5.28图形界面移植以及触摸屏、液晶、SD卡、SRAM、RC522、DS1307、蜂鸣器等外设驱动源码通信协议与考勤数据库均已实现。资源共2000个文件以888个C源码、807个H头文件、188个HTML文档及PDF、MD说明、Python脚本等构成压缩包约38.6MB代码结构清晰并附项目文档便于直接阅读与扩展。目前已有97人学习适合正在开展相关课题或希望掌握RT-Thread实际开发流程的读者参考。1. 为什么 WiFi 门禁考勤系统要选 F103ZET6 RT-Thread 而不是裸机很多人在毕设或者课设里做“STM32 ESP8266 传数据”时习惯把所有功能塞进一个大循环查询按键、读卡、继电器、发串口全靠 poll。做纯数据采集还可以接受但一做到门禁考勤就会暴露问题刷卡的同时要判断继电器动作网络断线时考勤记录不能丢卡片重复刷卡要过滤还要在串口打印日志。主循环一长任意外设的耗时都会拖累其他模块的响应。把这个项目换到 STM32F103ZET6 RT-Thread 操作系统上等于把“串联执行”变成“多线程按优先级抢占”电磁锁开锁由高优先级线程单独响应WiFi 上报慢就不拖累刷卡路径。这篇文章会从 F103ZET6 的引脚分配、RT-Thread 工程搭建讲到考勤记录缓存和断网补传最后给出一组运行时验证手段适合正在做毕设、课设或者想从裸机过渡到 RTOS 的人参考。2. F103ZET6 外设资源盘点与 WiFi 模块选型2.1 先从引脚图看 F103ZET6 能扛住多少设备F103ZET6 是 STM32F1 系列里引脚较多的一颗芯片144 脚 LQFP 封装内部 Flash 512K、SRAM 64K主频最高 72MHz。放到门禁考勤这种场景这些资源并不宽裕但很够用。背一个物联网可视化平台、刷一个 RT-Thread再做 128 字节级别的考勤记录缓存Flash 预留给日志就可以按计划分配。引脚分配的原则是硬件上尽量避开复用冲突软件上要把容易出问题的外设单独拉出来。以我常用的分配方案为例功能模块引脚说明电磁锁继电器PA0高电平开锁软件上做 200ms 脉冲读卡模块 SPI1PA5 / PA6 / PA7 / PA4接 RC522 的 SCK / MISO / MOSI / SSN读卡复位PG10接 RC522 的 RSTESP8266 UARTPA2 / PA3USART2 的 TX / RX波特率 115200人体红外PB3检测进门人员配合红外传感器矩阵键盘或轻触按键PB0 / PB1 / PB10 / PB11用于管理员操作RTC 晶振PC14 / PC1532.768kHz用于考勤时间记录这样分配有两个好处一是 SPI 和 USART 各有独立引脚调试时不会多根杜邦线夹在一起二是继电器这种感性负载控制器远离模拟引脚避免上电瞬间干扰读卡数据。2.2 WiFi 模块选型ESP8266 还是直接上自带协议栈的芯片门禁考勤系统的“WiFi”端通常不直接用 F103ZET6 去内嵌网络协议栈。最省事也最稳定的做法是选一颗串口转 WiFi 的模组MCU 只负责通过 AT 指令或者透传串口去访问网络。ESP8266 价格低小批量也好买但要注意它和服务器之间是短连接还是长连接、透传还是 AT 模式。透传模式下 ESP8266 会主动把串口收到的数据推给服务器适合门禁上报这类纯上传业务AT 模式下更适合需要云端下发指令的场景比如远程开门、远程禁用某个 RFID 卡。连接上还有一个容易被忽略的问题ESP8266 逻辑电平是 3.3VF103ZET6 的 UART 引脚也是 3.3V可以直接连。只是 ESP8266 在发射瞬间电流可能到 300mA不能用主板上的 LDO 直接带常见做法是给模块单独配一个 AMS1117-3.3 或者 MP1584输入取 5V 或 12V。2.3 读卡、开锁、红外的 C 语言驱动接口设计读卡模块用 RC522 或者兼容 13.56MHz 协议的读卡头。RC522 走 SPI读卡时序由 F103ZET6 的 SPI1 外设去处理C 语言里只需要把驱动拆成最小组件初始化、寻卡、防碰撞、选卡、读卡号。typedef struct { uint8_t status; // 0 为成功 uint8_t card_id[4]; // 四字节卡序列号 uint8_t id_len; } rfid_result_t; rfid_result_t rfid_poll_once(void) { rfid_result_t res {0}; uint8_t buffer[2] {0}; if (rc522_request(PICC_REQALL, buffer) ! MI_OK) { res.status 1; return res; } if (rc522_anticoll(buffer) ! MI_OK) { // buffer 前 4 字节为卡号 res.status 2; return res; } memcpy(res.card_id, buffer, 4); res.id_len 4; res.status 0; return res; }这段驱动体现的是典型的读卡流程先请求所有在射频场内的卡片再做防碰撞避免两张卡同时放在读写器上导致数据错乱。拿到卡号后门禁逻辑只需要比对卡号不需要关心射频通信细节。开锁控制相对粗暴但要加保护。继电器默认是断电上锁状态软件只在验证通过后拉高 200ms然后立即释放。拉高时间太久会让电磁锁一直通电线圈发热这是外部继电器方案最容易踩的坑。红外传感器则用来做“有人经过”检测走进门禁区域后如果一段时间内没有刷卡就触发超时告警这个作为独立线程去读不用阻塞主逻辑。3. 在 STM32F103ZET6 上裁剪 RT-Thread 并划分线程优先级3.1 工程从哪来RT-Thread Studio 生成还是手动移植源码RT-Thread 在这类项目里的接入方式有两条路。一条是用 RT-Thread Studio 直接创建基于芯片的工程在图形化配置界面里打开内核、Shell、设备驱动框架然后生成工程另一条是从源码仓库拿一份 bsp/stm32f103 的裸工程再手动复制 rt-thread 内核源码。对毕设来说前者更省时间因为你不用自己处理启动文件和链接脚本对想练底层能力的人来说后者更能看清调度器是怎么跑起来的。夹在中间还有一种常见做法用 Keil RT-Thread Nano 或完整版在分散加载文件里把 RT-Thread 源码路径加进去。Nano 只有内核没有设备驱动框架完整版会带 SPI、UART、PIN 等设备驱动更适合本项目的 RC522 和 ESP8266。选完工程路线后要在 rtconfig.h 里打开几个关键开关。我一般必开下面这些#define RT_USING_HEAP #define RT_USING_SMP #define RT_USING_COMPONENTS_INIT #define RT_USING_CONSOLE #define RT_CONSOLE_DEVICE_NAME uart3打开 HEAP 是为了让线程的栈可以动态分配组件自动初始化是为了让板级外设的初始化不再散落在 main 函数里。控制台设备建议指向调试串口日志和 msh 命令都从同一个口出连线时就少一根 UART。3.2 线程怎么划分读写卡、门锁控制、网络上报分开门禁考勤系统的典型场景有四个人来了要读卡读到卡要开锁开门要记考勤考勤要上传服务器。这四个动作如果只用一个线程做就会被网络阻塞拖死。划分线程的方案是线程/组件优先级栈大小主要职责rfid_thread81024循环读卡向事件队列投递刷卡事件door_thread71024接收门锁控制事件执行开锁/超时上锁net_thread122048与服务器保持 TCP/UDP 连接补传离线考勤key_thread101024扫描矩阵键盘处理管理员操作led_timer15512周期性刷新状态指示灯这里把 door_thread 设置得比 rfid_thread 高是因为门锁动作必须立即响应哪怕卡片还没读完也要先让继电器动作跟上。net_thread 的优先级放低是因为网络收发经常要等待等待中的任务不占 CPU。线程创建可以用动态方式直接在初始化时分配句柄static rt_thread_t rfid_tid RT_NULL; static char rfid_stack[1024]; static struct rt_thread rfid_thread; rt_err_t rfid_thread_init(void) { rfid_tid rt_thread_create(rfid, rfid_entry, RT_NULL, 1024, 8, 20); if (rfid_tid ! RT_NULL) { rt_thread_startup(rfid_tid); return RT_EOK; } return RT_ERROR; } INIT_APP_EXPORT(rfid_thread_init);INIT_APP_EXPORT 是 RT-Thread 的自动初始化宏系统启动到应用阶段会自动调用这个函数。用它之后 main 函数里就不用再手动声明和调用所有初始化函数对工程后期加模块很有利。3.3 线程间通信事件集比裸机全局变量干净很多系统里读卡线程产生刷卡事件门锁线程消费事件。如果不用 RTOS 的消息机制代码会退化成两个线程抢一个全局变量。推荐方式是用事件集或者消息队列。事件集适合“哪几件事同时发生”的判定门禁里更适合消息队列因为刷卡记录本身是一批批有序数据。消息队列创建和发送的代码如下static rt_mq_t door_mq RT_NULL; #define MAX_MSG_SIZE 8 door_mq rt_mq_create(door, sizeof(uint32_t), MAX_MSG_SIZE, RT_IPC_FLAG_PRIO); if (door_mq RT_NULL) { return RT_ERROR; } void door_open_stub(uint32_t card_no) { rt_mq_send(door_mq, card_no, sizeof(card_no)); }发送端把卡号作为消息内容放进队列door_thread 里用 rt_mq_recv 阻塞等待收到一条就打开一次门锁。这里的优点是接收线程不会被唤醒多次同一张卡短时间重复刷卡时可以在接收侧做 3 秒去重去重逻辑比裸机里的延时过滤简单得多。4. 考勤记录与门禁逻辑的 C 语言实现状态机、离线队列与断网补传4.1 考勤记录的数据结构结构体 环形缓存比数据库更实用F103ZET6 上没有 Sqlite 这类数据库的生存空间存储考勤记录更适合用结构体数组。设计考勤结构体时要考虑对齐问题和 Flash 写入次数。记录里放时间戳、卡号、事件类型、校验值时间戳用 RTC 秒数不存字符串能省下很多空间。typedef struct { uint32_t timestamp; // 绝对秒数UTC uint32_t card_id; // 4 字节卡号的高 32 位 uint8_t event_type; // 0x01 进门 0x02 出门 0x03 超时未关门 uint8_t result; // 0 成功 1 拒绝 uint16_t crc16; } attendance_entry_t; #define ATTENDANCE_MAX_ENTRIES 256 static attendance_entry_t attendance_pool[ATTENDANCE_MAX_ENTRIES]; static volatile uint16_t write_idx 0;数组大小 256 条每条 12 字节总共只有 3KB SRAM对 64K SRAM 的 F103ZET6 来说很小。但要考虑 Flash 寿命如果每刷一次卡都直接写内部 Flash会把页写坏。常见做法是 SRAM 里开一个 128 条记录的环形缓存攒到一定数量或者掉电前再整体写入。4.2 门禁控制核心状态机IDLE - CHECKING - OPENING - OPEN门锁如果不做状态机写起来很容易变成一堆 if 嵌套。整理后的状态非常简单typedef enum { DOOR_IDLE, DOOR_CHECKING, DOOR_OPENING, DOOR_OPEN, } door_state_t; door_state_t door_state DOOR_IDLE; static uint32_t door_open_ticks 0; void door_state_machine(void) { uint32_t card_no; switch (door_state) { case DOOR_IDLE: // 收到刷卡消息就进入校验状态 if (rt_mq_recv(door_mq, card_no, sizeof(card_no), 0) RT_EOK) { door_state DOOR_CHECKING; } break; case DOOR_CHECKING: if (check_card_permission(card_no) 0) { digital_high(RELAY_PIN); door_open_ticks rt_tick_get() rt_tick_from_millisecond(200); door_state DOOR_OPENING; } else { door_state DOOR_IDLE; set_buzzer(2); } break; case DOOR_OPENING: if (rt_tick_get() door_open_ticks) { digital_low(RELAY_PIN); door_state DOOR_OPEN; log_attendance(card_no, 0x01, 0); } break; case DOOR_OPEN: // 关门到位或超时后回到 IDLE if (door_closed_sensor() 1) { door_state DOOR_IDLE; } break; } }状态机的好处是把“正在校验”、“正在开锁”、“门开着”三个阶段的超时与条件是分开处理的。特别是 DOOR_OPENING 状态200ms 继电器脉冲结束后立刻置低避免线圈长时间通电。这里的 card_no 在校验阶段空闲时是无效的但状态机保证只有先进入 CHECKING 才会使用它不存在跨状态访问问题。4.3 WiFi 断线时考勤不丢离线消息队列 补传机制WiFi 门禁系统最大的痛点不是 WiFi 本身而是服务器不可达时考勤记录会不会丢。系统里维护一个独立的离线消息队列结构体里带上写入时间和重试计数网络恢复后从队头按顺序补传。// 伪代码网络断开时先落内存再落 Flash void attendance_upload_worker(void) { attendance_entry_t entry; while (1) { if (offline_queue_pop(entry) 0) { if (wifi_is_available() upload_entry(entry) 0) { continue; } else { // 补传失败放回队尾重试次数 1 entry.retry_cnt; if (entry.retry_cnt MAX_RETRY) { offline_queue_push(entry); } } } rt_thread_mdelay(2000); } }上传失败时不能死循环要考虑服务器重启或临时维护的情况。用 2 秒间隔重试一段时间超过最大重试次数就丢弃最老记录防止队列把 SRAM 占完。这段逻辑里对 WiFi 的检测尽量用模块返回的状态而不是 AT 指令的回复因为 AT 指令本身可能阻塞串口导致读卡线程被卡。4.4 考勤日志落盘内部 Flash 还是外挂 SPI Flash考勤日志在 F103ZET6 内部 Flash 上存储需要把记录按页对齐128KB 的扇区多次擦除会坏。更稳妥的路径是外挂一颗 4MB SPI Flash把考勤记录、黑名单、WiFi 配置全部放进去MCU 内部 Flash 只保留程序和少量运行时变量。外挂 SPI Flash 的读取和写入都比较慢但考勤是低频事件读卡后最多只写一条记录。用 RT-Thread 的 MTD 设备框架挂载成一个小文件系统可以统一管理但对 F103ZET6 这种低资源单片机来说直接按块地址读写更直观也不依赖文件系统组件。5. 用 RT-Thread msh 控制台验证门禁考勤系统定位线程栈溢出与死锁5.1 不需要反复烧录就能在板子上验证外设都有哪些命令msh 是 RT-Thread 自带的命令行。调试时不用反复改代码烧录直接在 shell 里敲命令对项目调试效率帮助很大。常见做法是给自己编写的外设驱动模块导出命令static int cmd_rfid_test(void) { rfid_result_t res; for (int i 0; i 10; i) { res rfid_poll_once(); rt_kprintf(rfid status%d card%02X%02X%02X%02X\n, res.status, res.card_id[0], res.card_id[1], res.card_id[2], res.card_id[3]); rt_thread_mdelay(100); } return 0; } MSH_CMD_EXPORT(cmd_rfid_test, test rfid read 10 times);MSH_CMD_EXPORT 宏把命令导出到命令表里编译后输入 cmd_rfid_test 就会执行这段测试函数。我这里在循环里加了 100ms 延时让读卡模块有足够时间完成上电复位。5.2 线程栈溢出检查把优先级调低再看最大值RT-Thread 里可以在运行时动态查看线程状态重点看线程的最大栈使用量。先在 rtconfig.h 中打开#define RT_USING_SMP #define RT_DEBUG_THREAD_STACK_CHECK重新编译烧录后在 msh 里输入 ps 命令会输出每个线程的名称、优先级、状态和栈用量。如果某个线程的最大栈空余很小就是栈分配太紧。我遇到过 esp8266 线程的栈 1024 字节在 printf 格式化参数较多时爆栈的情况这时把栈调到 2048 即可。类型检查重点关注 net_thread 和 rfid_thread因为它们涉及串口和 WiFi 协议栈栈使用路径最深。调试时可以把这两种线程的栈临时调小故意制造栈溢出看系统是否进入硬错误从而确认线程的实际峰值深度。5.3 排查死锁与优先级反转留意互斥锁和信号量的嵌套门禁系统里最常见的死锁来自两个线程各持有一把锁又在等待对方的锁。比如 door_thread 持有门锁状态的互斥量同时又想获取 WiFi 配置锁而 net_thread 持有 WiFi 配置锁又在等门锁互斥量。此时两个线程互相等待谁也无法继续。规避方式是“统一锁顺序”在项目里约定凡是需要同时拿两把锁的场合锁的获取顺序必须一致比如先取门锁锁再取 WiFi 配置锁。用 RT-Thread 的话可以在互斥量初始化时指定优先级继承减少低优先级线程占锁导致高优先级线程空转的情况。5.4 最后的调试手法用 log 分级把问题隔离到具体模块在实际验证中建议给系统加三级日志err 级只打印开关门失败和网络异常info 级打印刷卡记录和上传结果debug 级打印 AT 指令的完整交互。日志输出走控制台串口但注意不要在中断上下文里调用 rt_kprintf 打印整串日志否则输出时间过长会干扰实时任务。可以用 RT-Thread 的 log 组件配合循环缓冲区把日志收集在内存里再统一输出。这样既不影响刷卡响应也能在 WiFi 断连或者按键失灵时拿到足够长的现场数据。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/9/13 1:47:11

Rust+WASM地理引擎:地形/3D Tiles/大气一体化渲染

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/13 1:42:10

提示词工程实战:10个技巧与模板库,让AI输出可控

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/13 5:57:21

MIMIC III重症数据库实战指南:表结构、申请流程与SQL查询案例

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/13 5:57:21

Vue nextTick 原理与 DOM 更新时机详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/13 5:52:21

vLLM与Ray分布式大模型推理环境配置指南

1. 项目背景与核心价值在大模型推理场景中,单机部署往往面临显存不足、计算资源受限的问题。vLLM作为高性能推理框架,结合Ray分布式计算引擎,能够实现跨节点的模型并行推理。而环境变量的正确配置,则是保障分布式集群稳定运行的关…

2026/9/13 0:01:16

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/13 0:01:16

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/12 6:29:36

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/12 14:32:17

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/12 6:37:43

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码