STM32图书馆空位检测系统:传感器选型、扫描与串口上报
发布时间:2026/9/19 1:29:19 作者:尧图编辑部 阅读量:1,286

简介这是一份面向嵌入式开发学习者和高校课程设计者的STM32图书馆空位检测系统项目资料以PDF文档形式完整记录从硬件选型到算法部署的全流程方案。文档围绕STM32设备端与服务器端两部分展开设备端以STM32F103RCT6为主控配合ESP8266无线模块和1.44寸SPI接口LCD屏实时显示座位占用与人流量服务器端部署于Windows通过NGINX与RTMP模块接收网络摄像头推流使用OpenCV与YOLO完成人体检测和座位状态判断再经网络将结果回传设备端。内容涵盖系统框架图与原理图、硬件连线与实物图、KEIL工程与取模软件使用、TCP/UDP通信协议制定、图像识别分割原理以及项目如何描述、常见问答等总结章节。资源包共1个PDF文件约43.67MB图文并茂、目录按前言、硬件选型、服务器部署、图像处理、STM32开发、总结六大部分组织便于按模块检索。已有132人学习适合需要完整项目参考或毕业设计素材的读者。1. 图书馆空位检测系统为什么选STM32做座位节点图书馆自习区最折磨人的不是找不到书而是绕三圈看不到空位管理员也难实时知道哪片区域满座。基于STM32的图书馆空位检测系统要解决的就是把“座位有人/无人”变成可采集、可上报、可展示的数据。选STM32不是因为算力过剩而是它成本低、外设全、资料多GPIO能直接接红外对射或压力薄膜定时器能做周期扫描和消抖串口能接ESP8266或RS485。做 stm32项目 的人常把这类题目归为入门综合题但真正落地要处理多路传感器、误报、供电布线和上位机映射。适合电子、物联网、自动化方向的学生和想快速搭原型的工程师新手能按引脚和代码跑通熟手可以继续加低功耗与校验。2. 图书馆空位检测的传感器选型与STM32引脚资源规划2.1 图书馆座位状态到底测什么四类传感器对比座位状态只有两种有人、无人。但“有人”这个物理事实映射到传感器上差别很大。红外对射把发射管和接收管装在座位两侧人坐下挡住光路输出电平翻转薄膜压力传感器埋在坐垫或椅面靠体重触发超声波装在桌面下方测椅子或人体到探头距离毫米波雷达检测微动抗遮挡但价格高。做基于stm32的毕业设计 时最容易踩的坑是只看器件单价不看安装、误报和维护。方案单座成本安装方式误报来源STM32接口适合场景红外对射低两侧对射书本、包遮挡GPIO输入低成本多座位薄膜压力中坐垫内重物、坐垫老化GPIO/ADC单座精判超声波中桌下或顶部邻座串扰、温度定时器捕获少量座位毫米波雷达高顶部隐蔽安装角度UART/GPIO高预算场景图书馆里书本和背包会长时间占座单靠红外对射容易把“放书”判成“有人”。常见做法是红外对射做主检测压力薄膜做复核只有红外被遮挡且压力持续超过阈值才把座位标为占用。这样误报下降明显代价是每个座位多一根线和一路GPIO或ADC。注意红外对射输出常见5V电平STM32引脚耐受3.3V必须分压或加电平转换直接接会损伤IO。2.2 STM32F103C8T6 管 8 个座位的引脚分配与扫描周期一个STM32F103C8T6管8个座位足够。PA0到PA7做8路红外输入配置为上拉输入PB0、PB1做压力复核输入也可以扩展ADC。PA9、PA10做USART1调试口PA2、PA3做USART2接ESP8266。PB6、PB7可接OLED显示本机座位位图。扫描周期用10ms消抖计数5次即50ms确认一次状态上报周期500ms避免频繁发帧。下面是引脚映射和座位结构体定义。#include stm32f10x.h #define SEAT_NUM 8 typedef struct { GPIO_TypeDef *port; // 传感器所在端口 uint16_t pin; // 引脚号 uint8_t active_low; // 1: 遮挡输出低电平 uint8_t stable_cnt; // 消抖计数 uint8_t state; // 0空位, 1占用 } SeatSensor; SeatSensor seat[SEAT_NUM] { {GPIOA, GPIO_Pin_0, 1, 0, 0}, {GPIOA, GPIO_Pin_1, 1, 0, 0}, {GPIOA, GPIO_Pin_2, 1, 0, 0}, {GPIOA, GPIO_Pin_3, 1, 0, 0}, {GPIOA, GPIO_Pin_4, 1, 0, 0}, {GPIOA, GPIO_Pin_5, 1, 0, 0}, {GPIOA, GPIO_Pin_6, 1, 0, 0}, {GPIOA, GPIO_Pin_7, 1, 0, 0} };这段代码只定义数据结构真正的扫描放在定时器中断里。active_low表示红外接收管被遮挡时输出低电平避免在扫描函数里反复写死判断。stable_cnt用于累计连续相同状态state是上位机最终看到的座位位图。参数上8路GPIO扫描一次不到10微秒10ms节拍留给串口和OLED足够余量。若座位数增加到16可换GPIOB、GPIOC但要注意JTAG占用PB3、PB4、PA13到PA15必要时禁用JTAG保留SWD。2.3 选型结论红外对射加压力复核的常见组合对图书馆这种大面积、多座位、预算有限的场景红外对射加压力复核比纯视觉更可控。视觉方案要处理摄像头角度、光线变化、隐私争议和边缘算力毕业设计周期内很难调到位超声波每个座位都要分时触发多个探头同时工作会互相收到回波。压力薄膜单独用也不够因为书包和厚书会压出类似体重的一小段波形需要配合红外遮挡逻辑。供电上每块采集板就近取5VSTM32用AMS1117转3.3V红外对射和压力模块单独走线避免电机或继电器动作时拉低MCU电压。若用普通排线PA0到PA7建议每路串100Ω电阻并加0.1μF去耦减少长线引入的毛刺。软件上不要在主循环里裸判电平必须用stm32定时器建立固定节拍这对后面做状态机和上报校验很关键。3. 用STM32定时器扫描GPIO空位采集节点最小代码3.1 Keil5新建STM32标准库工程与最小外设清单keil5新建stm32工程 时先装好器件包再准备标准库的启动文件、stm32f10x.h、system_stm32f10x.c和stm32f10x_gpio.c、stm32f10x_tim.c、stm32f10x_usart.c、stm32f10x_rcc.c。目录可以这样分SeatNode/ ├── CMSIS/ ├── FWLib/ │ ├── inc/ │ └── src/ ├── User/ │ ├── main.c │ ├── seat_scan.c │ └── seat_scan.h └── Output/标准库新建工程 的核心不是复制多少文件而是把启动文件和中断向量表选对。STM32F103C8T6用中容量启动文件宏定义写STM32F10X_MD, USE_STDPERIPH_DRIVER。最小外设清单只要GPIO、TIM3、USART1、USART2和NVIC。调试口先留USART1波特率115200方便用串口助手看每个座位的原始电平。3.2 用stm32定时器做10ms节拍与500ms消抖扫描不要写在while(1)里靠延时硬等TIM3开10ms更新中断在中断里扫8路GPIO。10ms内连续扫5次也就是50ms确认一次每50次中断即500ms打包上报一次。下面是TIM3初始化和扫描逻辑。#include stm32f10x.h #include seat_scan.h volatile uint16_t tick_10ms 0; void TIM3_Init(void) { TIM_TimeBaseInitTypeDef tim; NVIC_InitTypeDef nvic; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 72MHz / 72 1MHz计数10000次为10ms tim.TIM_Prescaler 71; tim.TIM_Period 9999; tim.TIM_ClockDivision TIM_CKD_DIV1; tim.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, tim); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); nvic.NVIC_IRQChannel TIM3_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 1; nvic.NVIC_IRQChannelSubPriority 1; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); TIM_Cmd(TIM3, ENABLE); } void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); tick_10ms; Seat_Scan10ms(); // 每10ms扫一次 if (tick_10ms % 50 0) { Seat_UpdateReport(); // 每500ms整理上报位图 } } }TIM_Prescaler71把72MHz分频到1MHzTIM_Period9999对应10000个计数正好10ms。tick_10ms % 50得到500ms上报窗口。中断里只做电平采样和计数不做串口发送串口发送放到主循环避免中断执行时间过长。若传感器线较长Seat_Scan10ms里可连续读两次GPIO间隔几微秒降低偶发毛刺。3.3 座位状态打包与USART1/USART2上报代码8个座位用1字节位图就够bit0到bit7对应座位0到7。帧格式建议$SEAT,位图,异或校验\r\n例如$SEAT,0A,5C\r\n。这样上位机不用解析长JSONSTM32也不容易因为sprintf栈溢出。#include stdio.h #include string.h extern SeatSensor seat[SEAT_NUM]; uint8_t Seat_Bitmap(void) { uint8_t map 0; for (int i 0; i SEAT_NUM; i) { if (seat[i].state) { map | (1 i); } } return map; } void Seat_UpdateReport(void) { uint8_t map Seat_Bitmap(); uint8_t xor map; char frame[32]; // 帧格式$SEAT,位图,异或\r\n sprintf(frame, $SEAT,%02X,%02X\r\n, map, xor); USART1_SendString(frame); // 调试口 USART2_SendString(frame); // 接ESP8266或RS485 }Seat_Bitmap每500ms重新计算一次避免状态分散。xor是简易校验后续第6章会换成CRC8。USART1和USART2共用同一帧调试时可以直接从串口助手看STM32发出的内容不必先连Wi-Fi。参数上USART1、USART2都用115200、8位数据、1位停止、无校验。若接RS485要在发送前后控制DE/RE引脚发送完最后一个字节再切回接收。字段长度示例含义$SEAT5字节$SEAT帧头位图2字节0Abit3和bit1为1异或2字节5C校验字节结束2字节\r\n帧尾4. ESP8266与STM32串口组网图书馆空位表如何更新4.1 RS485总线、ESP8266直连和LoRa在图书馆的取舍一个图书馆阅览室可能有几十到上百个座位全部用ESP8266直连会让无线信道拥挤每个节点还要独立配网。更常见的做法是分区每块STM32管8到16个座位板间用RS485总线接到集中器集中器再用ESP8266或以太网向上位机上报。若只有十几个座位ESP8266与STM32直接串口相连也能快速跑通。LoRa适合跨楼栋但带宽低只适合传位图。组网方式布线节点成本维护难度适合规模RS485集中器需总线低中几十到上百座ESP8266直连少中配网麻烦小区域LoRa少高中跨区域以太网需网线中低固定机房图书馆现场更推荐RS485做座位采集总线集中器负责轮询各采集板。轮询周期200ms每个采集板地址不同帧里带地址后上位机才能把座位号映射到具体楼层和区域。4.2 ESP8266与STM32连接原理图要点和AT指令上报esp8266与stm32连接原理图 要盯三件事ESP8266是3.3V供电峰值电流可能到300mA以上不能用STM32的3.3V引脚直接带USART2的TX接ESP8266 RXRX接ESP8266 TX交叉连接ESP8266的EN引脚要上拉GPIO0悬空或上拉进入运行模式。若ESP8266固件支持AT指令STM32可以用ATCIPSTART建立TCP连接再用ATCIPSEND发座位帧。void ESP_SendFrame(const char *frame) { char cmd[32]; int len strlen(frame); USART2_SendString(ATCIPSEND); sprintf(cmd, %d\r\n, len); USART2_SendString(cmd); Delay_ms(20); // 等待ESP8266返回 USART2_SendString(frame); // 发送$SEAT帧 Delay_ms(50); // 等待SEND OK }ATCIPSEND要先告诉ESP8266本次发送多少字节再发正文。Delay_ms(20)和Delay_ms(50)是保守等待实际项目更建议用串口接收中断判断和SEND OK避免主循环卡死。若ESP8266返回ERROR先查波特率是否匹配再查供电是否在发送瞬间掉到3.0V以下。STM32串口调试 PID 在电机控制里很常见但空位检测不靠PID靠的是帧格式和重发策略。4.3 上位机Python解析空位帧并维护座位表上位机可以用Python的serial读集中器串口按$SEAT帧解析位图再把每个bit映射成座位号。下面的例子把座位表存到SQLite便于后续做楼层热力图。import serial import sqlite3 conn sqlite3.connect(seats.db) cur conn.cursor() cur.execute( CREATE TABLE IF NOT EXISTS seat_state ( seat_id INTEGER PRIMARY KEY, occupied INTEGER, ts DATETIME DEFAULT CURRENT_TIMESTAMP ) ) def parse_frame(line: str, base_seat: int): line line.strip() if not line.startswith($SEAT,): return parts line.split(,) bitmap int(parts[1], 16) xor int(parts[2], 16) if xor ! bitmap: print(xor error, line) return for bit in range(8): seat_id base_seat bit occupied 1 if (bitmap (1 bit)) else 0 cur.execute( REPLACE INTO seat_state(seat_id, occupied, ts) VALUES (?, ?, CURRENT_TIMESTAMP), (seat_id, occupied) ) conn.commit() ser serial.Serial(COM3, 115200, timeout0.5) while True: raw ser.readline().decode(ascii, errorsignore) parse_frame(raw, base_seat101)base_seat101表示当前采集板从101号座位开始bit0到bit7依次对应101到108。REPLACE INTO让同一座位只保留最新状态避免表膨胀。若上位机还要展示“空位总数”直接统计occupied0即可。这里没有解析RS485地址实际多板系统中应在帧头加$SEAT,地址,位图,校验Python按地址查基址表。5. 图书馆空位检测系统现场部署标定、误报与排错5.1 红外对射与超声波阈值标定步骤红外对射装好后先不接STM32用万用表测接收管输出无遮挡时高电平遮挡时低电平。若反过来就在结构体里把active_low改成0。安装高度建议离桌面5到10cm避开显示器、台灯和手臂扫过的区域。超声波若用于测椅子距离公式是距离 回波时间 × 声速 / 2。声速随温度变化可用v 331.4 0.607 × T做补偿。float Ultra_DistanceCm(uint32_t echo_us, float temp_c) { float v 331.4f 0.607f * temp_c; // 温度补偿后的声速单位m/s return (echo_us * v) / 20000.0f; // us转s除以2再转cm }echo_us由stm32定时器捕获得到temp_c可来自DS18B20或SHT30。若没有温度传感器按20℃写v343.5m/s也能用但冬天和空调房会有几厘米偏差。标定表建议记录每个座位的空位距离、有人距离和误报次数。座位号空位距离/cm有人距离/cm判定阈值/cm备注101621835椅子靠里102582235邻座干扰小103652538靠窗光线强5.2 误报抑制中值滤波、状态机与座位映射红外对射的毛刺来自手臂、书页、空调风。最简单的方法是在Seat_Scan10ms里做三次采样取中值再进入状态机连续5次占用才置1连续10次空位才置0。这样人短暂起身拿书不会立刻变空位。压力复核可以设为“红外占用且压力大于阈值才占用红外空位且压力小于阈值才空位”其余保持原状态。void Seat_StateMachine(int i, uint8_t raw_occupied, uint8_t press_ok) { if (raw_occupied press_ok) { if (seat[i].stable_cnt 5) seat[i].stable_cnt; if (seat[i].stable_cnt 5) seat[i].state 1; } else if (!raw_occupied !press_ok) { if (seat[i].stable_cnt 0) seat[i].stable_cnt--; if (seat[i].stable_cnt 0) seat[i].state 0; } }stable_cnt在0到5之间滑动占用确认快释放确认慢。若现场没有压力传感器把press_ok固定为1即可但误报会升高。座位映射要用“采集板地址bit位”而不是安装顺序否则换板后上位机全部错位。部署时在接线端子贴座位号Python基址表同步维护。5.3 串口调试与常见故障排查表stm32串口调试 先用USB转TTL接USART1打开终端看原始帧。命令可以这样python -m serial.tools.miniterm COM3 115200如果看到乱码先查波特率、时钟配置和USB转TTL电平。如果帧正确但上位机不更新查Python串口号、权限和数据库路径。如果ESP8266频繁掉线查供电和AT指令等待逻辑不要在主循环里用死延时。现象可能原因排查动作串口无输出时钟未使能、TX/RX接反量PA9波形换交叉线某座位一直占用红外被遮挡、上拉错误遮挡测试改active_low上位机座位错位基址表与接线不一致打印板地址和bit位ESP8266返回ERROR波特率、供电、固件单独用AT测试查3.3V数据偶尔跳变长线毛刺、无消抖加中值滤波和状态机6. 图书馆空位检测的进阶状态机、低功耗与上报校验6.1 用CRC8校验座位帧避免上位机错位异或校验只能发现奇数个bit错误现场电磁干扰强时不够。把第3章的异或换成CRC8帧格式改成$SEAT,地址,位图,CRC\r\n。下面是一个查表法CRC8多项式0x07。static const uint8_t crc8_table[256] { /* 省略表项可用脚本生成 */ }; uint8_t CRC8_Calc(const uint8_t *data, uint16_t len) { uint8_t crc 0x00; while (len--) { crc crc8_table[crc ^ *data]; } return crc; } void Seat_SendWithCRC(uint8_t addr, uint8_t bitmap) { char frame[40]; uint8_t buf[2] {addr, bitmap}; uint8_t crc CRC8_Calc(buf, 2); sprintf(frame, $SEAT,%02X,%02X,%02X\r\n, addr, bitmap, crc); USART2_SendString(frame); }上位机收到后先算CRC再解析位图。CRC不通过直接丢弃不要做“纠正”。若同一采集板连续3帧CRC错误上位机可以把该区域标灰提示维护而不是继续显示旧空位。6.2 低功耗与定时器唤醒在电池节点上的取舍图书馆座位节点大多有市电低功耗不是第一优先级。如果某些区域无法布线用电池加STM32停止模式TIM3改成RTC唤醒每2秒扫一次GPIO扫完立即回停止模式。红外对射静态电流要选低功耗型号否则电池撑不过一周。ESP8266也不要每次上报都联网可在集中器侧缓存节点只发RS485帧。模式扫描周期平均电流适用位置常供电10ms20mA以上阅览桌停止模式2s1mA级临时座位集中器轮询200ms节点无Wi-Fi多座位区最后上位机收到CRC通过且地址在基址表内的帧只更新对应座位位图收到NACK后节点只重发当前座位位图不重发历史帧。本文还有配套的精品资源点击获取