1. 项目概述
最近在折腾一个挺有意思的DIY项目——基于STM32的离线人脸识别门铃系统。这个项目最大的特点就是完全离线运行,不需要联网,所有数据处理都在本地完成,既保护隐私又稳定可靠。作为一个嵌入式开发老鸟,我觉得这种将AI模型部署到微控制器上的实践特别有挑战性,也很有实用价值。
整套系统由STM32F407主控、OV2640摄像头、人体红外传感器和几个简单的输出设备组成。核心功能是通过轻量级神经网络模型实现人脸识别,然后根据识别结果控制门铃、LED指示灯和门锁继电器。我花了大概三周时间从零搭建完成,现在家里用着感觉挺不错的,识别速度在1秒以内,准确率也能达到90%以上。
2. 硬件设计与选型
2.1 核心硬件选型思路
选型时我主要考虑三个因素:性能足够、功耗低、成本可控。经过多次对比测试,最终确定的硬件配置如下:
主控芯片选择了STM32F407ZGT6,这款芯片有1MB Flash和192KB RAM,带FPU浮点运算单元,跑轻量级模型完全够用。相比F103系列,F407的DCMI接口直接支持摄像头数据采集,开发起来更方便。
摄像头用的是OV2640,200万像素,支持JPEG输出。实际使用中发现160x120的灰度图就足够人脸识别用了,再高的分辨率反而会增加处理负担。OV2640通过I2C配置,DCMI接口传输图像数据,硬件连接简单。
人体检测用了常见的HC-SR501红外传感器,灵敏度可调,检测距离最大7米。这个模块的作用是在没人时让系统进入低功耗模式,检测到人移动时才唤醒主控和摄像头,大大降低整体功耗。
2.2 外设模块选型
显示部分用了0.96寸OLED屏,I2C接口,128x64分辨率,主要用来显示系统状态和识别结果。选择OLED是因为它功耗低、显示清晰,在阳光直射下也能看清。
提示模块包括一个有源蜂鸣器和红绿黄三色LED。蜂鸣器用于门铃提醒,LED则用不同颜色表示不同识别结果:绿灯家人、黄灯访客、红灯陌生人或黑名单。
执行机构是一个5V继电器模块,用来控制电子门锁。继电器线圈电流约70mA,STM32的IO口驱动能力不够,所以中间加了一个S8050三极管做电流放大。
存储用了W25Q128 SPI Flash芯片,16MB容量,存放人脸模型权重、注册的人脸特征和识别日志。选择SPI Flash是因为它价格便宜、容量大、读写速度也能满足需求。
3. 硬件连接与搭建
3.1 核心板与外设接线
接线时需要特别注意电源分配和信号完整性。我的接线方案如下:
OV2640摄像头:
- D0-D7接PA6、PC1、PC0、PC4、PC5、PB1、PB2、PA4(DCMI数据线)
- VSYNC接PB7,HSYNC接PA4,PCLK接PA6(同步信号)
- XCLK接PA8(由TIM1产生8MHz时钟)
- SDA接PB9,SCL接PB8(I2C配置)
人体红外传感器:
- OUT接PA0(外部中断唤醒)
- VCC接5V,GND共地
OLED屏:
- SDA接PB7,SCL接PB6(I2C1)
- VCC接3.3V
W25Q128 Flash:
- CS接PA15,SCK接PB13
- MOSI接PB15,MISO接PB14(SPI3)
蜂鸣器、LED和继电器都接在PB0-PB4上,每个IO口串联220Ω限流电阻。
3.2 电源设计要点
整个系统由5V/2A电源适配器供电,通过AMS1117-3.3转换为3.3V给STM32和部分外设使用。需要注意以下几点:
- 摄像头和继电器直接从5V取电,不经过STM32的IO口,避免电流过大损坏芯片
- 所有模块的GND必须共地,否则会出现信号干扰问题
- 在电源输入端加了一个100μF电解电容和0.1μF陶瓷电容,滤除电源噪声
- 如果想用电池供电,可以加TP4056充电模块和升压电路
4. 软件开发环境搭建
4.1 工具链配置
开发环境用的是STM32CubeIDE 1.12.1,这个IDE集成了CubeMX配置工具和GCC编译链,一站式开发很方便。也可以选择Keil MDK5,但需要自己安装STM32F4的Device Family Pack。
软件架构基于STM32CubeMX生成的HAL库,相比标准外设库,HAL库的移植性更好,但执行效率稍低。对于这个人脸识别项目,HAL库的性能完全够用。
模型推理框架选择了TensorFlow Lite for Microcontrollers(TFLite-Micro),这是Google专门为微控制器优化的轻量级推理框架,支持ARM Cortex-M系列芯片。
4.2 工程目录结构
项目代码按功能模块划分,主要目录如下:
- Core/Inc:HAL库头文件
- Core/Src:HAL库源文件
- Drivers:STM32硬件驱动
- Middlewares:TFLite-Micro中间件
- Application:
- User/inc:用户头文件
- User/src:用户源文件
- Model:训练好的TFLite模型
5. STM32CubeMX工程配置
5.1 时钟树配置
系统时钟配置为外部8MHz晶振,通过PLL倍频到168MHz。具体设置:
- PLLM = 8(分频)
- PLLN = 336(倍频)
- PLLP = 2(分频)
- SYSCLK = 168MHz
- HCLK = 168MHz
- PCLK1 = 42MHz
- PCLK2 = 84MHz
5.2 外设接口配置
DCMI接口:
- 模式:Snapshot(单帧模式)
- 极性:VSYNC、HSYNC、PCLK均为High active
- 数据宽度:8位
- JPEG模式:禁用
I2C接口:
- I2C1(OLED):Standard Mode(100kHz)
- I2C2(摄像头):Fast Mode(400kHz)
SPI接口:
- SPI3(Flash):Full-Duplex Master
- 预分频:256(约0.33MHz)
- 数据大小:8位
- 片选:软件控制(PA15)
5.3 定时器配置
TIM1用于产生摄像头XCLK时钟:
- 时钟源:Internal Clock
- 通道1:PWM Generation CH1
- 预分频:0
- 自动重装载:10
- PWM占空比:50%
- 输出频率:8MHz(168MHz/(0+1)/(10+1)/2)
5.4 中断优先级设置
系统使用了三个中断:
- EXTI0(PA0):人体检测唤醒,优先级1
- DCMI:帧捕获完成,优先级0(最高)
- RTC:定时日志,优先级2
6. 外设驱动开发
6.1 延时函数实现
精确延时对摄像头初始化很重要,我利用Cortex-M4的DWT周期计数器实现了us级延时:
c复制void delay_us(uint32_t us)
{
uint32_t ticks = us * (SystemCoreClock / 1000000);
uint32_t start = DWT->CYCCNT;
while ((DWT->CYCCNT - start) < ticks);
}
void delay_ms(uint32_t ms)
{
for (uint32_t i = 0; i < ms; i++)
delay_us(1000);
}
使用前需要先启用DWT:
c复制CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
6.2 OLED驱动优化
OLED驱动基于SSD1306芯片,我优化了显示函数以减少I2C通信开销:
c复制void OLED_ShowString(uint8_t x, uint8_t y, char *str, uint8_t size)
{
uint8_t buf[20], *p = buf;
*p++ = 0x40; // 数据模式
while (*str) {
uint8_t c = *str++ - 32;
const uint8_t *font = &Font_6x8[c][0];
for (uint8_t i = 0; i < 6; i++)
*p++ = *font++;
if (p - buf > sizeof(buf) - 7) {
HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, buf, p - buf, 100);
p = buf;
*p++ = 0x40;
}
}
if (p > buf + 1)
HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, buf, p - buf, 100);
}
6.3 SPI Flash驱动
W25Q128驱动实现了基本的读写和擦除功能。特别注意Flash写入前必须先擦除:
c复制void W25Q128_Write(uint32_t addr, uint8_t *buf, uint16_t len)
{
W25Q128_CS_LOW();
W25Q128_SendByte(0x06); // 写使能
W25Q128_CS_HIGH();
W25Q128_CS_LOW();
W25Q128_SendByte(0x02); // 页编程
W25Q128_SendByte((addr >> 16) & 0xFF);
W25Q128_SendByte((addr >> 8) & 0xFF);
W25Q128_SendByte(addr & 0xFF);
for (uint16_t i = 0; i < len; i++)
W25Q128_SendByte(buf[i]);
W25Q128_CS_HIGH();
W25Q128_WaitBusy();
}
7. 摄像头驱动与图像处理
7.1 OV2640初始化
OV2640需要配置大量寄存器才能正常工作。我从官方驱动中提取了关键配置序列:
c复制uint8_t OV2640_Init(void)
{
// 复位
OV2640_WriteReg(0x12, 0x80);
delay_ms(100);
// JPEG禁用,输出RGB565
OV2640_WriteReg(0xff, 0x01);
OV2640_WriteReg(0x15, 0x00);
// 分辨率配置
OV2640_WriteReg(0xff, 0x00);
OV2640_WriteReg(0x2c, 0xff);
OV2640_WriteReg(0x2e, 0xdf);
// 更多配置...
// 设置输出160x120灰度图
OV2640_SetSize(160, 120);
return 0;
}
7.2 图像采集与处理
DCMI接口配置为单帧模式,采集完成后触发中断:
c复制void HAL_DCMI_FrameEventCallback(DCMI_HandleTypeDef *hdcmi)
{
// 图像数据已存入ov2640_frame_buf
frame_ready = 1;
}
图像预处理包括裁剪、灰度化和归一化:
c复制void Image_Normalize(uint8_t *in, float *out, uint16_t len)
{
for (uint16_t i = 0; i < len; i++) {
// 归一化到[-1,1]
out[i] = ((float)in[i] / 127.5f) - 1.0f;
}
}
8. 人脸模型部署
8.1 模型训练与转换
在PC端用TensorFlow训练了一个轻量级人脸识别模型:
python复制model = tf.keras.Sequential([
tf.keras.layers.Conv2D(8, (3,3), activation='relu', input_shape=(120,160,1)),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Conv2D(16, (3,3), activation='relu'),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(32, activation='relu'),
tf.keras.layers.Dense(5) # 5类:家人/访客/黑名单/陌生人/未知
])
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=10)
model.save('face_model.h5')
转换为TFLite格式并生成C数组:
bash复制tflite_convert --keras_model_file=face_model.h5 --output_file=face_model.tflite
xxd -i face_model.tflite > face_model_data.h
8.2 TFLite-Micro移植
在STM32上初始化TFLite解释器:
c复制uint8_t TFLite_Face_Init(void)
{
static tflite::MicroMutableOpResolver<5> micro_op_resolver;
micro_op_resolver.AddConv2D();
micro_op_resolver.AddDepthwiseConv2D();
micro_op_resolver.AddReLU();
micro_op_resolver.AddMaxPool2D();
micro_op_resolver.AddSoftmax();
static tflite::MicroInterpreter static_interpreter(
tflite::GetModel(face_model_tflite),
micro_op_resolver,
tensor_arena,
TENSOR_ARENA_SIZE
);
interpreter = &static_interpreter;
if (interpreter->AllocateTensors() != kTfLiteOk) {
return 1;
}
input = interpreter->input(0);
output = interpreter->output(0);
return 0;
}
推理函数实现:
c复制uint8_t TFLite_Face_Recognize(float *input_data, float *score)
{
// 拷贝输入数据
for (uint32_t i = 0; i < input->dims->data[1] * input->dims->data[2]; i++) {
input->data.f[i] = input_data[i];
}
// 执行推理
if (interpreter->Invoke() != kTfLiteOk) {
return 255; // 错误
}
// 获取最高分结果
uint8_t id = 0;
float max_score = output->data.f[0];
for (uint8_t i = 1; i < output->dims->data[1]; i++) {
if (output->data.f[i] > max_score) {
max_score = output->data.f[i];
id = i;
}
}
*score = max_score;
return id;
}
9. 系统集成与优化
9.1 低功耗设计
系统大部分时间处于STOP模式,功耗约20μA:
c复制void Enter_LowPower_Mode(void)
{
// 关闭外设时钟
__HAL_RCC_GPIOA_CLK_DISABLE();
__HAL_RCC_GPIOB_CLK_DISABLE();
__HAL_RCC_GPIOC_CLK_DISABLE();
// 配置唤醒引脚
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 进入STOP模式
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// 唤醒后重新初始化时钟
SystemClock_Config();
MX_GPIO_Init();
}
9.2 人脸库管理
人脸特征和身份信息存储在SPI Flash中:
c复制typedef struct {
uint8_t id; // 0:家人 1:访客 2:黑名单
char name[16]; // 人名
float feature[128]; // 人脸特征向量
} FaceInfo;
FaceInfo face_db[MAX_FACE_NUM];
uint8_t FaceDB_Load(void)
{
W25Q128_Read(FACE_DB_ADDR, (uint8_t *)face_db, sizeof(face_db));
return 0;
}
uint8_t FaceDB_Save(uint8_t index, FaceInfo *info)
{
W25Q128_EraseSector(FACE_DB_ADDR + index * sizeof(FaceInfo));
W25Q128_Write(FACE_DB_ADDR + index * sizeof(FaceInfo), (uint8_t *)info, sizeof(FaceInfo));
return 0;
}
9.3 主控制逻辑
系统主循环实现状态机控制:
c复制while (1) {
// 低功耗待机
Enter_LowPower_Mode();
// 唤醒后初始化摄像头
OV2640_Init();
OV2640_StartCapture();
// 等待图像采集完成
while (!frame_ready);
frame_ready = 0;
// 图像预处理
float input[160*120];
Image_Normalize(ov2640_frame_buf, input, 160*120);
// 人脸识别
float score;
uint8_t id = TFLite_Face_Recognize(input, &score);
// 根据识别结果执行动作
if (score > 0.8f) {
switch (id) {
case 0: // 家人
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); // 绿灯
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4, GPIO_PIN_SET); // 开门
OLED_ShowString(0, 4, "Family: Open", 12);
break;
case 1: // 访客
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_SET); // 黄灯
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // 响铃
OLED_ShowString(0, 4, "Visitor: Ring", 12);
break;
case 2: // 黑名单
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_SET); // 红灯
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // 响铃
OLED_ShowString(0, 4, "Blacklist: Alert", 12);
break;
}
} else {
// 陌生人
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_SET); // 红灯
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // 响铃
OLED_ShowString(0, 4, "Stranger", 12);
}
// 记录日志
Log_Save(id, score);
// 延时后复位状态
HAL_Delay(3000);
HAL_GPIO_WritePin(GPIOB, 0x1F, GPIO_PIN_RESET); // 关闭所有输出
OLED_Clear();
}
10. 调试经验与优化建议
10.1 常见问题排查
-
摄像头无图像输出
- 检查XCLK时钟是否正常(用示波器测量PA8应有8MHz方波)
- 确认I2C通信正常,读取OV2640的PID/VID(0x0A/0x0B寄存器)
- 检查DCMI数据线连接,特别是VSYNC/HSYNC信号
-
模型推理结果异常
- 确认输入数据归一化范围与训练时一致
- 检查Tensor Arena大小是否足够(至少模型大小的2-3倍)
- 验证模型在PC端的推理结果,确保模型转换正确
-
Flash写入失败
- 写入前必须先擦除对应扇区
- 检查WP引脚是否被拉高
- 降低SPI时钟频率(建议<1MHz)
10.2 性能优化技巧
-
图像处理优化
- 使用DMA传输图像数据,减少CPU占用
- 将灰度转换和归一化合并为一个步骤
- 使用查表法加速归一化计算
-
模型优化
- 使用量化感知训练,将模型转换为int8格式
- 剪枝去除不重要的神经元连接
- 使用深度可分离卷积代替标准卷积
-
电源管理
- 在STOP模式下关闭所有不必要的外设时钟
- 降低主频到48MHz(识别阶段再升到168MHz)
- 使用动态电压调节(需硬件支持)
10.3 扩展功能建议
-
增加WiFi模块
- 使用ESP8266/ESP32实现远程通知
- 通过MQTT协议上报识别结果
- 保留离线优先架构,网络不可用时自动切换本地模式
-
添加语音提示
- 使用SYN6288语音合成芯片
- 预录"欢迎回家"、"有访客"等提示音
- 通过PWM或DAC输出音频
-
改进用户交互
- 增加按键用于录入新人脸
- 使用OLED菜单系统管理人脸库
- 添加NFC模块支持刷卡开门
这个项目最让我满意的地方是成功将AI模型部署到了资源有限的STM32上,而且整套系统完全离线运行,响应速度快,隐私有保障。在实际部署时,建议将摄像头安装在门铃位置约1.5米高,确保正对人脸角度。光线条件对识别率影响很大,可以考虑增加红外补光灯用于夜间照明。
