我最早接触“基于stm32单片机教室人数统计”这个课题,是被一个很现实的需求推着走的。某高校的开放自习教室经常出现座位被“幽灵占座”的情况,管理人员根本不知道每间教室实际坐了多少人,保洁和安保也只能凭感觉安排。当时手里正好有一批STM32F103C8T6核心板,就想着能不能把这件事做成一个能落地的小系统。整个项目从硬件选型到算法调试,前后花了差不多三周,踩过的坑不少,但最终跑出来的效果让我觉得这套方案特别适合作为单片机学习者的综合练手项目,也适合真正部署到小型教室场景里做人数统计。
这套系统的核心其实就两个词:传感器判断和进出计数。传感器负责判断“有人经过门”,算法负责分辨“人是进来还是出去”,STM32作为主控负责采集信号、处理状态、显示结果。做出来的东西可以独立运行,用OLED屏实时显示当前教室人数,也可以把数据通过串口发给上位机,做进一步的数据分析。
如果你正在学STM32,或者想在课程设计里做一个“有实际意义”的单片机项目,那这节课的内容很适合你。我会把整个项目的方案选型、电路设计、状态机算法、代码实现、调试心得,以及我实际踩过的坑全部拆开讲,照着做基本能复现。
1. 项目整体设计与思路拆解
1.1 为什么选STM32F103C8T6作为主控
做教室人数统计,主控的首要任务是稳定处理传感器信号,其次是低功耗和低成本。STM32F103C8T6这颗芯片在单片机圈子里堪称“国民级”MCU,72MHz的主频对这类轻量级逻辑任务绰绰有余,而且它有丰富的GPIO、定时器和串口资源,代码生态非常成熟,哪怕你是第一次接触ARM内核芯片,网上随便一搜就能找到大量参考工程。
对比过几种替代方案后,我依然坚持用它。
- 用纯数字逻辑电路(比如74系列芯片搭计数器)虽然能实现基础计数,但完全无法做方向判断,更没法扩展显示和数据上传功能。
- 用树莓派这类Linux开发板做人数统计,性能确实强,但成本高、功耗大、启动慢,放在教室里还需要单独配电和散热,运维成本很明显不划算。
- STM32F103C8T6核心板的价格只需要十几块,功耗非常低,一张5V充电宝就能带得动,而且单片机实时性很强,处理传感器信号几乎没有延迟,这对进出判断来说非常重要。
从教学和工程两个角度看,STM32都是这个场景下的“性价比之王”。更关键的是,你可以在它上面完整地体验“GPIO读取-信号消抖-状态机迁移-外设展示”这一整套嵌入式开发流程,做完之后对嵌入式系统的理解会上一个台阶。
1.2 双传感器方案的选择逻辑
人数统计的第一个难题,是如何判断“一个人经过了门”。如果你只用单个传感器,比如门口装一个红外对射,那只能知道“有人挡了一下光”,根本分不清这个人是进还是出。所以我采用了双传感器差分判断的方案。
常见的双传感器组合有几种:
| 传感器类型 | 工作方式 | 优点 | 缺点 |
|---|---|---|---|
| 红外对射管 | 发射管和接收管对射,遮挡时电平跳变 | 价格极低,响应快,检测准确 | 需要成对安装,且只能检测遮挡 |
| 人体红外热释电 | 检测人体辐射的红外线 | 可检测移动人体,无需成对 | 容易受环境温度干扰,且对静止人体不敏感 |
| 超声波传感器 | 发射声波并检测回波 | 可以测距离,能区分物体远近 | 响应速度相对较慢,有盲区 |
我在第一版方案里尝试过两个热释电传感器并排安装。这种做法的优点是无需成对布线,方便安装。但热释电传感器有个致命弱点是它只对“移动中的人体”敏感,如果有人在门口站住不动,信号会丢失;如果室内有空调出风口对着传感器吹,也可能产生误触发。实测下来误报率偏高。
最后我换成了两个对射式红外传感器,并排安装在门框两侧,一高一低组成“红外光幕”。当有人穿过时,会按照“先遮挡A→再遮挡B→离开A→离开B”或者“先遮挡B→再遮挡A→离开B→离开A”的顺序触发信号,这两组顺序正好对应“进”和“出”两个方向。这个方案在物理层面就解决了方向判断的问题,而且信号处理起来非常简单可靠。
1.3 状态机计数思路的引入
传感器装好之后,真正的核心难点变成了软件层面的算法设计。传感器只会输出0和1,也就是遮挡和未遮挡,你要从这些0和1的序列中还原出人的行为轨迹。
最开始我想到的是用简单的“先来后到”逻辑:先遮挡哪个传感器,就认为人往哪个方向走。但如果有人在门口停下来,或者只挡住其中一个传感器,这种逻辑就会失效。后来我把思路换成了状态机模型。
状态机本质上就是定义一个“系统当前处于什么状态”,然后根据传感器的输出决定“状态如何迁移”。比如:
- 状态0:无人遮挡任何传感器
- 状态1:有人遮挡了传感器A
- 状态2:有人遮挡了传感器B
- 状态3:有人同时遮挡了两个传感器
只有当系统按照“状态0→状态1→状态3→状态0”这样的顺序完整走完一圈,才能判定为一次有效通过,并根据路径方向加1或减1。这个方案能过滤掉大部分无效触发,比如有人只是伸手在门口挥一下,或者有物体从门前掠过但没穿过门,都不会被计为一次进入或离开。
这个设计思路是整套系统最值得深入理解的地方。你掌握了状态机之后,不止能做人数统计,做按键消抖、做交通灯控制、做流量检测,思路都是相通的。
2. 核心硬件细节与电路搭建
2.1 传感器安装位置与接线方式
双红外对射传感器的安装位置直接决定计数的准确性。我踩过的第一次坑,就是把两个传感器装在同一水平高度上。后来才发现,如果两者完全水平,人经过时可能因为身体遮挡顺序不明显,导致状态跳变太快,程序来不及捕捉完整的遮挡序列。
正确的安装方式应该是:
- 两个传感器对射方向垂直于门的通行方向
- 传感器A安装在门框内侧约20厘米高度处
- 传感器B安装在门框内侧约60厘米高度处
- 两者间距至少40厘米
这样设计的原因很简单。人走路时身体不同部位经过传感器的顺序非常固定:先经过低处的传感器,再经过高处的传感器,或者反过来。拉开垂直间距后,两次遮挡之间就有了时间差,程序能从容地记录状态变化,不会因为信号抖动造成误判。
接线方面,每个对射式红外传感器模块通常有三个引脚:VCC、GND和OUT。OUT引脚在未被遮挡时输出高电平,遮挡时输出低电平。我直接把两个模块的OUT分别接到STM32F103C8T6的PA0和PA1引脚上。
注意:千万不要给传感器模块供5V电压却把OUT直接连到STM32的3.3V引脚上。部分STM32的GPIO不是5V容忍的,长期接5V信号有烧毁风险。稳妥的做法是给传感器统一供3.3V,或者加一个电平转换电路。
2.2 显示模块与数据输出方案
人数统计的结果需要一个直观的展示方式。我在第一版使用了LCD1602,但它的可视角度小,而且在暗光环境下看不太清楚。后来换成了一块0.96寸的OLED屏幕,用I2C接口通信,只需要接SDA和SCL两根线,代码驱动起来也特别简单。
OLED屏的显示内容我设计成了三行:
- 第一行显示当前状态:是“空闲”还是“有人”
- 第二行显示当前教室人数
- 第三行显示累计进人数和出人数
除了在线显示,这个系统还通过串口把数据输出到上位机。STM32的USART1接一个USB转TTL模块,波特率设置成115200,电脑端用串口助手就能实时看到数据流。如果后续想接WiFi模块或者蓝牙模块,直接复用这个串口即可。
我在这里把硬件清单整理出来,方便你照着备料:
| 元器件 | 型号/规格 | 数量 | 备注 |
|---|---|---|---|
| 主控板 | STM32F103C8T6核心板 | 1 | 蓝色板常见 |
| 红外对射模块 | 槽型或对射式,3.3V供电 | 2 | 选择带比较器输出的模块 |
| OLED显示屏 | 0.96寸,I2C接口 | 1 | 四脚模块 |
| USB转TTL | CH340模块 | 1 | 调试和上位机通信 |
| 电源 | 5V/2A USB电源 | 1 | 核心板自带稳压到3.3V |
| 杜邦线 | 母对母/公对母 | 若干 | 快速调试 |
| 洞洞板或PCB | 5x7cm | 1 | 用于固定电路 |
2.3 供电与抗干扰设计
教室里电磁环境不算特别复杂,但为了稳定运行,供电方面我还是做了一些额外的处理。
STM32F103C8T6核心板通常会板载AMS1117稳压芯片,可以把5V降到3.3V。但AMS1117本身是线性稳压,效率不太高,如果传感器模块直接从3.3V取电,多个模块同时工作时的瞬间电流可能导致电压跌落,造成坞机或误触发。
我的做法是:传感器模块和OLED屏幕都用3.3V供电,但电源入口处并联一个100uF电解电容和一个0.1uF瓷片电容。100uF负责储能缓冲,0.1uF负责滤除高频噪声。实测下来,加了这两个电容之后,传感器偶尔出现的“幽灵信号”明显少了很多。
如果现场走线比较长,信号线最好用双绞线或者屏蔽线。教室门到主控板的距离如果超过2米,信号线太长会引入噪声,建议把主控板放在门框上方或者门的侧面,尽量缩短传感器到主控的距离。
3. 软件实现:状态机人数统计算法
3.1 工程初始化和GPIO配置
软件部分用STM32标准外设库开发,工程结构非常简单:main.c负责初始化并调用主循环,timer.c负责定时扫描传感器状态,counter.c负责状态机人数统计逻辑。
首先看GPIO初始化。PA0和PA1设置为上拉输入模式,这样在传感器未遮挡时读到高电平,遮挡时读到低电平。
void GPIO_InitConfig(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); }这里有三点需要注意:
- 上拉输入模式下引脚默认是高电平,正好配合红外对射模块的“遮挡输出低电平”逻辑。
- GPIO速度设置为50MHz,实际上这个项目根本不需要这么快的翻转速度,但设置高一点可以避免一些奇怪的时序问题。
- 两个传感器分别接入PA0和PA1,在程序里用宏定义别名,方便后续维护。
3.2 传感器扫描与消抖处理
红外对射传感器的输出看起来是干净的0和1,但在真实环境中,人脚步踏过门槛时产生的振动会让传感器在短时间内出现多次抖动。抖动在信号上表现为连续几个毫秒内电平反复跳变。
处理抖动的方式有硬件消抖和软件消抖。硬件上可以在传感器输出端并联一个小电容,比如10uF,让电平变化变“钝”;软件上则是采用连续多次读取确认的方案。
我用的是定时器中断扫描法。设置一个1毫秒的定时器中断,每次中断里读取一次两个传感器的电平状态,将结果存入一个环形缓冲区。只有当连续5次采样(也就是5毫秒)都读到相同的电平值时,才更新一次有效状态。
void TIM2_IRQHandler(void) { static uint8_t filter_cnt[2] = {0, 0}; static uint8_t stable_level[2] = {1, 1}; uint8_t raw[2]; if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); raw[0] = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); raw[1] = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1); // 用连续滤波法消除抖动 for (int i = 0; i < 2; i++) { if (raw[i] == stable_level[i]) { filter_cnt[i] = 0; } else { filter_cnt[i]++; if (filter_cnt[i] >= 5) { stable_level[i] = raw[i]; filter_cnt[i] = 0; } } } sensor_state = (stable_level[0] << 1) | stable_level[1]; } }这段代码读完你会注意到,我没有直接操作全局变量进行计数,而是先更新sensor_state变量。这样做的好处是让主循环里的人数统计逻辑只关心一个稳定的状态变量,而不必在意底层的抖动问题。
3.3 进出方向判断的状态机实现
状态机是整个程序的灵魂。我用两个传感器的高低位置定义“从门里向外走”和“从门外向里走”两条路径。
定义如下:
- sensorA:位于门内侧较低处
- sensorB:位于门内侧较高处
- 当人从外面走进教室时,先遮挡B,再遮挡A
- 当人从教室走出去时,先遮挡A,再遮挡B
现在看状态机的定义。总共有4个状态,用state变量表示。
#define STATE_NONE 0 #define STATE_BLOCK_A 1 #define STATE_BLOCK_B 2 #define STATE_BLOCK_BOTH 3 static uint8_t current_state = STATE_NONE; static uint8_t prev_state = STATE_NONE; void update_counter(void) { switch (current_state) { case STATE_NONE: if (sensor_state == 0b01) { current_state = STATE_BLOCK_A; } else if (sensor_state == 0b10) { current_state = STATE_BLOCK_B; } break; case STATE_BLOCK_A: if (sensor_state == 0b00) { // 两个传感器都被遮挡,说明人正在通过门 current_state = STATE_BLOCK_BOTH; } else if (sensor_state == 0b11) { // 信号消失,说明是无效触发 current_state = STATE_NONE; } break; case STATE_BLOCK_B: if (sensor_state == 0b00) { current_state = STATE_BLOCK_BOTH; } else if (sensor_state == 0b11) { current_state = STATE_NONE; } break; case STATE_BLOCK_BOTH: if (sensor_state == 0b10) { // 从双遮挡恢复到只挡B,说明人从A的方向离开,判定为进入教室 if (prev_state == STATE_BLOCK_A) { room_count++; current_state = STATE_BLOCK_B; } } else if (sensor_state == 0b01) { // 从双遮挡恢复到只挡A,说明人从B的方向离开,判定为离开教室 if (prev_state == STATE_BLOCK_B) { room_count--; current_state = STATE_BLOCK_A; } } else if (sensor_state == 0b11) { current_state = STATE_NONE; } break; } prev_state = current_state; }这段代码设计的核心逻辑是:必须走完“单遮挡→双遮挡→反向单遮挡”的完整路径才计数。从这个角度看,系统不会被“只挡一下就消失”的错误信号骗到。
不过这段代码里有个地方需要特别细心:prev_state的更新时机。如果粗心地在进入某个case之前就更新prev_state,会导致方向判断失效。我的建议是把prev_state的更新放在整个switch执行完之后统一处理。
3.4 计数阈值与人数上下限保护
状态机设计完成之后,还有一个很现实的工程问题:人数会不会越数越歪?比如有人从门口探个头又缩回去,状态机走了半圈,虽然没有异常计数,但某次误触发导致room_count变成了负数,或者超出教室实际容量。
我加了两层保护:
- 下限保护:如果room_count已经为0,则不再执行减1操作。
- 上限保护:根据教室座位数设置一个人数上限值,比如100人,超过这个数就自动清零并输出警告。
#define MAX_PERSONS 100 #define MIN_PERSONS 0 void adjust_count(int8_t delta) { if (delta > 0) { if (room_count < MAX_PERSONS) { room_count++; } } else { if (room_count > MIN_PERSONS) { room_count--; } } }或许有人会问,为什么不直接让系统休眠然后重启清零。我的想法是:人数统计系统最忌讳的就是无故清零,一旦数据丢失,管理人员就无法判断当天教室的使用情况。所以我在OLED屏上增加了“手动清零”的提示,管理员可以通过按钮清零,而不是程序自动清。
3.5 OLED显示驱动与串口输出
显示驱动部分我直接用了常见的SSD1306驱动库,在初始化时把I2C配置好,然后在主循环里调用显示函数刷新数据。
void OLED_Display_Init(void) { I2C_InitTypeDef I2C_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; // PB6 -> SCL, PB7 -> SDA GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_ClockSpeed = 400000; I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_Init(I2C1, &I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); }主循环里每200毫秒刷新一次OLED显示,每100毫秒通过串口发送一次当前人数。串口频率别太高,太快的数据流对人工查看不友好。
int main(void) { SystemInit(); GPIO_InitConfig(); TIM2_Init(); OLED_Init(); USART1_Init(); while (1) { update_counter(); display_room_count(); printf("Room_count:%d\r\n", room_count); delay_ms(100); } }4. 实操过程与核心环节实现
4.1 从面包板到固定实装
第一版我是在面包板上搭的电路。STMicro的F103核心板、两个红外对射模块、一块OLED屏,用杜邦线连好,代码烧录进去,人来回走了几趟,计数功能确实能跑。但面包板的问题是接触不良。教室现场走动的振动会导致杜邦线松动,传感器信号偶尔中断。
后来我把整个电路焊到了一块5x7cm的洞洞板上。具体流程是:
- 先固定STM32核心板排针,用双面胶和扎带绑在洞洞板中心。
- 预留传感器接口端子,方便拆卸和维护。
- OLED屏的四个引脚接到预留的排针上,屏幕用亚克力支架固定在门口墙面。
- 所有走线采用短线并扎带整理,尽量避免信号线与电源线平行长距离走线。
在实装之前,我先在实验室模拟了教室门的环境。用两个纸箱模拟门框,把传感器固定在一高一低两个位置,然后反复人穿行测试。这个过程非常重要,因为你不实际走几遍,根本发现不了传感器安装角度的问题。
4.2 传感器阈值与响应时间调试
红外对射模块上通常有一个电位器,可以调节检测灵敏度。我在调试中发现,灵敏度太高会导致模块对手指、书本等小物体也产生遮挡信号;灵敏度太低则可能导致人在快速经过时漏检。
调试方法是:先把电位器拧到中间位置,人站在门口正常速度来回走动,观察串口输出。如果出现漏检,就稍微调高灵敏度;如果出现误触发,就调低灵敏度。这个迭代过程可能需要反复十几次。
另一个关键参数是系统对传感器状态变化的最小响应时间。我在状态机代码里加入了一个简单的超时机制:如果进入STATE_BLOCK_A之后超过800ms还没有进入STATE_BLOCK_BOTH,就判定为无效触发并复位到STATE_NONE。
if (current_state != STATE_NONE) { if (state_timer > 800) { current_state = STATE_NONE; state_timer = 0; } }这个超时机制一加上,误报率立刻下降了一个数量级。因为在真实场景中,正常通过一扇门大概耗时在200到600毫秒之间,超过800毫秒的基本都是异常情况。
4.3 真实场景测试与数据校正
系统安装到教室门框上之后,我在多个人流量不同的时间段分别测试了两次:
- 课间时段:大量学生同时进出,状态机偶尔会因为人多拥挤导致顺序错乱。
- 单人次进出:计数准确率达到了98%以上。
- 有携带书包或拉杆箱场景:书包遮挡传感器的顺序与人体的顺序基本一致,没有额外误判。
课间时段的问题其实很难靠算法完全解决。因为当两个人并排走,或者一个人刚进一半另一个人从旁边挤出去时,传感器只能看到“遮挡”,看不到“是谁在遮挡”,逻辑上就无法完美区分。我的处理办法是:在门口贴上“请依次通过”的提示标签,并在软件上增加锁存机制,也就是当状态机处于中间状态时,5秒内不响应反向路径。
经过现场数据校正之后,整套系统的误报率控制在了一个课间统计误差3到5人的水平。对于不需要精确考勤的教室管理场景,这个精度已经足够。
4.4 数据上位机展示
如果光有OLED显示,管理人员必须走到设备旁边才能看到人数,这显然不够方便。后来我通过STM32的串口把数据发到电脑上,用简单的串口助手和Excel实时记录数据变化。
更进一步,我预留了ESP8266 WiFi模块的接口。只要把串口数据重新打包成TCP报文,就能把人数数据推送到一个局域网网页上。这个扩展留给了后续优化,但硬件资源已经有富余了。STM32F103C8T6的串口2和串口3都是空闲的,通过AT指令控制WiFi模块不需要修改主逻辑代码。
5. 常见问题与排查技巧实录
5.1 传感器不响应或信号不稳定
这个问题在排查时几乎有80%的可能是电源导致的。传感器模块对供电电压非常敏感,如果电源电压低于3.0V,红外对射模块的接收端可能无法稳定识别发射端的光信号。
排查步骤可以按照这个顺序来:
- 用万用表量传感器VCC和GND之间的电压,确认是否为3.3V左右。
- 观察模块上的指示灯是否常亮,正常状态下对射无遮挡时指示灯常亮,遮挡后熄灭。
- 如果电源电压正常但信号不稳定,用示波器或者逻辑分析仪看OUT引脚的波形,确认是否有毛刺。
我曾经遇到过一种特殊情况:传感器模块和电机类设备共用电源,电机启动瞬间电压跌落导致传感器复位。这种问题在教室场景不常见,但如果现场有其他大功率设备,还是建议传感器单独供电。
5.2 人数统计逐渐偏差过大怎么办
长期运行之后,计数偏差几乎是必然的。原因有两类:一是现场环境变化,比如有人把书包放在门口堵住了传感器;二是偶然误触发一直在累积。
针对这个问题,我设计的系统增加了“当前人数”和“总进/总出人数”的对比显示。管理员通过OLED屏上的累计进出人数,可以大致判断当前统计是否可信。如果累计进入人数减去累计出人数明显不合理,管理员可以手动清零重新统计。
同时在软件上我增加了“连续无遮挡超时自动待机”的功能。当传感器连续10分钟没有检测到任何人通过时,系统自动进入低功耗模式,同时把当前人数保存到STM32内部Flash中。这样即使断电重启,人数数据也不会丢失。
5.3 代码烧录失败的排查
STM32核心板烧录方式一般有两种:SWD和串口ISP。我遇到过的典型问题是串口ISP在烧录时始终连不上,后来发现是BOOT0引脚没有正确拉高。
解决办法是:
- 用串口ISP烧录时,把BOOT0拨到1,BOOT1拨到0,上电进入系统存储器模式。
- 烧录完成后,把BOOT0拨回0,复位重新运行。
- 用ST-Link加SWD方式烧录则不需要改BOOT引脚,但需要确认接线顺序是否接反。
如果烧录过程中报错“No target connected”,优先检查SWDIO和SWCLK两根线是否接反,以及目标板供电是否足够。曾有同学因为ST-Link供电能力太弱导致烧录失败,换成外部独立供电后问题就解了。
5.4 调试中常见的逻辑错误
代码层面的问题比硬件问题更隐蔽。我列出三个最容易犯的逻辑错误:
- 状态方向搞反。上电后不经过测试就直接使用,结果发现人数越数越少。解决办法很简单:实测之前先在串口打印出传感器状态值0b01和0b10分别对应哪个传感器,再写死方向判断逻辑。
- 状态机没有复位路径。比如当系统进入STATE_BLOCK_BOTH后,如果传感器信号异常直接跳到STATE_NONE,代码里必须对这个迁移路径做处理,否则状态机卡死。
- 全局变量竞争问题。定时器中断里更新sensor_state,主循环里读取sensor_state,如果中断发生频率远高于主循环读取频率,大多数情况下没问题,但极端情况下可能会读到中间值。解决办法是尽量让读取和写入操作原子化,或者用关中断的方式保护临界区。
__disable_irq(); temp_state = sensor_state; __enable_irq();虽然这个项目逻辑不算复杂,但养成对全局变量加访问保护的习惯,对以后做RTOS多线程开发非常有帮助。
6. 扩展方向与个人实操体会
6.1 算法层面如何进一步优化
目前的双传感器状态机方案已经能满足基础需求,但如果你希望把这个项目做成一个真正能应对复杂人流的产品,有几个方向可以继续深入。
其一是加入机器学习判断。用传感器输出的完整遮挡序列作为特征向量,采集大量“单人进出”“多人并排”“逆行折返”等场景数据,训练一个简单的分类模型,把分类结果交给STM32执行。F103的RAM只有20KB,跑太复杂的模型会比较吃力,但换到STM32F407或者F429系列之后,跑轻量级神经网络模型是完全可行的。
其二是融合多模态数据。除了红外遮挡信息,还可以加入地磁传感器、加速度传感器或者毫米波雷达,通过多个维度判断人体运动轨迹。这个方向在智能家居的人体存在检测领域已经有成熟应用。
6.2 场景联动和价值延展
人数统计系统单独看只是个计数器,但和教室里的其他设备联动之后,价值就完全不一样了。
当检测到教室人数为0时,可以自动关闭空调和灯光,做到节能减排;当人数接近教室容量上限时,可以提醒安保人员及时疏散;如果接入到课表系统,还能自动判断当前教室是否“应该有人”,异常情况及时报警。
我把这套系统接到了一套简易的灯光控制模块上。人数大于0时点亮“有人”指示灯,人数为0时点亮“无人”指示灯。宿舍楼的管理员阿姨看了之后主动问我能不能给每个房间都装一套,说这样她就不用挨个敲门检查了。
6.3 做这个项目最大的收获
回到项目本身,我最想强调的是:这个项目最大的难点不在传感器,也不在STM32本身,而在于把物理世界的连续行为映射成有限状态的离散逻辑。传感器负责感知,但感知到的是嘈杂、混乱、充满误判的原始信号;状态机负责决策,但决策必须建立在“对真实场景的深入理解”之上。
在做这个项目之前,我写代码的习惯是“顺着功能需求直接写”,但从这个项目之后,我开始习惯“先把所有可能的输入情况罗列出来,再设计状态迁移图,最后才动笔写代码”。这个思维转变对我的影响远超项目本身。
如果你准备复现这个项目,我的建议是:不要直接抄别人的完整代码。先在自己的洞洞板上点亮一颗LED,然后让串口输出传感器状态,再手动模拟各种进出路径,最后再把状态机代码写进去。每一步都亲手做一遍,踩过的坑才会真正变成你自己的经验。
这套系统做完之后,如果你还想继续折腾,可以试试把这些数据通过ESP8266上传到本地MQTT服务器,做一个简单的Web端仪表盘。那时候你会发现,单片机学习的有趣之处在于——每一个小项目都可以像积木一样,不断累加出更复杂的系统。