【STM32F4】基于RTOS的智能门禁系统实战:人脸识别+RFID+OTA 升级

一、项目简介

这是一个完整的 STM32 智能门禁系统(Smart IDT)。硬件原理图和 PCB 都是自己画的,软件从 CubeMX 配置到驱动代码也都是自己弄的,最后还写了一个pycharm的上位机。
演示视频:

基于RTOS的智能门禁系统 人脸识别演示


OTA升级演示视频:

OTA演示

整体功能如下:

功能 实现方式 硬件
人脸识别开门 OV7670 抓图 → WiFi 发电脑 → 电脑识别 → 回传结果 → 舵机开门 OV7670 + AL422B(FIFO) + ESP8266
RFID 刷卡开门 RC522 读卡号,白名单比对 RC522(SPI)
OTA 远程升级 电脑发固件 → 存 W25Q → 重启由 Bootloader 搬运 ESP8266 + W25Q + Bootloader
时间同步 + 实时时钟 设备问电脑要时间,RTC 用纽扣电池掉电走时 RTC + LSE + ML621纽扣电池
本地交互 1.8寸屏 + 旋钮 + 四键 + 语音 ST7735 + 旋钮/按键 + SU-03T

文章会按「先硬后软」的顺序讲:先讲原理图和 PCB 是怎么设计的,再讲 CubeMX 配置,最后讲每一块驱动代码和上位机。

二、硬件设计(原理图 + PCB)

这一节把硬件从头到尾讲清楚。所有器件、网络、电源结构都来自实际打样验证过的原理图。

2.1 整体架构

系统的核心是一块 **STM32F401 核心板和各个外设模块。整体框图:
请添加图片描述
原理图:
请添加图片描述

2.2 完整元件清单(BOM)

这是导出的完整器件表:

位号 器件型号 封装/规格 作用
U1 STM32F401RCT6开发板 40 脚排针 主控
U2 AMS1117-3.3 SOT-223 外设 3.3V 稳压(1A)
U3 ML621-TZ1 纽扣电池 3V 5.8mAh RTC/VBAT (可充电)
USB1 TYPE-C 16PIN 母座 插件 仅供电(CC 5.1k 下拉)
U4 ST7735S 1.8" LCD 8P 模块 SPI 彩屏
U5 OV7670 + AL422B FIFO 18P 模块 摄像头
U6 SU-03T 18P 模块 语音(内置功放)
U7/U8 PH2.0 母座 2P 喇叭 / 麦克风接口
U9 W25Q 6P 模块 16MB SPI Flash
U10 RC522 8P 模块 13.56MHz RFID
H1 ESP8266(ESP-01) 2x4P 排母 WiFi 模块
U11 旋钮(电位器) 3P ADC 模拟输入
U12 红色 LED 1206 充电/电源指示
U13~U16 6×6×7.5 轻触开关 4 个 上/下/返回/确认 四键
U17 PH2.54 4P 直插 SU-03T 串口烧录口
U18 PH2.54 3P 直插 SG90 舵机接口
SW1/SW2 SS12D10G3 拨动开关 2 个 电源总开关 / 充电使能
D2 1N5819 肖特基 40V 1A 纽扣电池充电防倒灌
R1/R2 5.1kΩ 0805 Type-C CC1/CC2 下拉
C1 10uF 0805 稳压输入滤波
C3 22uF 钽 16V 稳压输出滤波
C2/C4/C5/C6/C7 100nF 0805 去耦
R3 330Ω 0805 充电限流
R4 10kΩ + C6(100nF) 0805 LCD 复位 RC 电路
R5 10kΩ 0805 旋钮/按键上拉
R6~R9 1k/2.2k/4.7k/10kΩ 0805 按键梯形电阻
R10 1kΩ 0805 LED 限流

2.3 电源系统设计(重点)

这是整个硬件里最有含金量的部分,讲清楚「为什么这样供电」。

2.3.1 主供电链

请添加图片描述

  • Type-C 母座只用来供电(D+/D- 没接,CC 脚各接 5.1k 下拉识别为从设备)。
  • SW1 是电源总开关,拨到关就切断整板供电。

2.3.2 两路独立的 3.3V

板上有两路 3.3V,由于负载的模块较多,所以除开发板外的模块单独使用LDO供电,防止stm32开发板过载

网络 来源 供谁 电流
E3V3 U2 AMS1117-3.3 LCD / 摄像头 / W25Q / RC522 / ESP8266 / 充电 大电流外设
3V3 核心板板载 LDO 仅旋钮上拉 R5 + 旋钮 U11 ~0.7mA

注意事项: 旋钮是「比例式测量」:ADC 的参考电压(VREF)和旋钮分压源必须同轨。如果把旋钮分压接在 AMS1117 的 E3V3 上、而 ADC 参考是核心板自己的 3V3,两者电压有细微差异,旋钮读数就会漂。所以单独引核心板的 3V3 出来,只给旋钮和它的上拉电阻供电,让「分压源」和「ADC 参考」是同一个电压。

2.3.3 纽扣电池备份 + 充电

  • U3(ML621)是 3V 可充电纽扣电池,容量 5.8mAh。
  • 它通过 VB 网络接到核心板的 VBAT 脚,给 RTC 备份域和 32.768kHz 晶振供电,实现「掉电时间不丢、继续走时」。
  • D2(1N5819 肖特基)串在充电回路上,防止电池电流倒灌回 E3V3;SW2 是充电使能开关。
E3V3 ──R3(330Ω)──┬── SW2 ── D2(1N5819) ──┬── VB → 核心板 VBAT
                 │                       │
                 └──────────────────── ──┴── U3 ML621(纽扣电池)

2.4 主控核心板(U1)

用STM32F401 核心板,40 脚排针引出。几个和普通开发板不同的注意点:

  • 板载 32.768kHz LSE 晶振(接 PC14/PC15),给 RTC 用,这两个脚不能当 GPIO

  • 核心板 5V 脚由外部 5V 网络供电,核心板自己再板载 LDO 出 3V3。

2.5 摄像头模块(U5)

OV7670 后面挂了一块 AL422B FIFO(3Mb 帧缓存),好处是:摄像头输出一整帧先存进 FIFO,MCU 可以慢慢读,不用追着摄像头的像素时钟(PCLK)跑,对单片机友好。

  • 数据线 D0~D7 接 PA0~PA7(连续 8 位,可一次读 GPIOA 低 8 位);
  • SCCB(配置寄存器)走 I2C1,PB6/PB7;
  • FIFO 控制 5 根线:RCLK(读时钟) OE(输出使能) WRST(写复位) RRST(读复位) WEN(写允许);
  • VSYNC(帧同步) 接 PB14,用外部中断界定一帧的开始和结束。

2.6 显示模块(U4)

ST7735S 1.8 寸彩屏,SPI 接口,160×128 横屏使用(RGB565)。复位脚不走 GPIO,用硬件 RC(R4=10k + C6=100nF)上电自动复位,软件用 SWRESET 兜底。

2.7 存储(U9)与 RFID(U10)

  • W25Q:16MB SPI Flash,三件事——暂存 OTA 固件、暂存人脸图像、镜像时间。
  • RC522:13.56MHz RFID,SPI 接口,RST 常接 3.3V 使能。

重要:屏幕、W25Q、RC522 共用同一个 SPI1(PB3/PB4/PB5),只有片选不同。SPI1 跑 21MHz 给屏幕和 W25Q,但 RC522 上限只有 10MHz,所以代码访问 RC522 前要把分频临时降到 5.25MHz,用完再切回。

2.8 WiFi(H1)与语音(U6)

  • ESP8266(ESP-01):USART1(PA9/PA10),115200,AT 指令连 WiFi、连 TCP。
  • SU-03T 语音:USART6(PA11/PA12),9600,内置功放,直接接喇叭和麦克风;U17 是它的串口烧录口。

2.9 舵机(U18)

SG90 舵机,PWM 50Hz 控制角度,接口 U18(GND/5V/PWM),PWM 接 PA15(TIM2_CH1)。舵机是 5V 供电,直接从 5V 网络取电。

2.10 人机交互(U11/U13~U16/U12)

  • 旋钮 U11:电位器,接 PB0(ADC_IN8),配合 R5 上拉,分压输出 0~3.3V,读 ADC 就是 0~100。
  • 四按键 U13~U16:梯形电阻接法,四键共用一个 ADC 脚 PB1(ADC_IN9),按下不同键分压不同:
物理位置(左→右) 功能 分压电阻 按下电压
键4 R9=10kΩ 1.65V
键3 R8=4.7kΩ 1.06V
键2 返回 R7=2.2kΩ 0.60V
键1 确认(OK) R6=1kΩ 0.30V
  • LED U12:红色,R10 限流,做充电/电源指示。

2.11 引脚分配总表

核心板脚 网络 外设 方向/复用
PA0~PA7 PA0~PA7 OV7670 D0~D7 GPIO 输入
PA8 PA8 LCD CS GPIO 输出
PA9/PA10 PA9/PA10 ESP8266 TX/RX USART1
PA11/PA12 PA11/PA12 SU-03T TX/RX USART6
PA15 PA15 舵机 PWM TIM2_CH1
PB0 PB0 旋钮 ADC1_IN8
PB1 PB1 四按键 ADC1_IN9
PB2 PB2 LCD DC GPIO 输出
PB3/PB4/PB5 PB3/PB4/PB5 SPI1 SCK/MISO/MOSI SPI1
PB6/PB7 PB6/PB7 OV7670 SCCB I2C1
PB8~PB13 PB8~PB13 摄像头 FIFO 控制 GPIO 输出
PB14 PB14 摄像头 VSYNC EXTI14
PB15 PB15 W25Q CS GPIO 输出
PC13 PC13 RC522 CS(与 LED 共用) GPIO 输出
PC14/PC15 LSE 晶振 RTC
VB VB 纽扣电池备份 VBAT

2.12 硬件设计要点小结

  1. 两路 3.3V 不并联:E3V3 供大电流外设,3V3 只供旋钮分压,保证 ADC 比例式测量准确。
  2. 纽扣电池用可充电的 ML621,经肖特基 D2 防倒灌,SW2 控制充电。
  3. 三个 SPI 设备共用一根 SPI1,靠片选区分,RC522 需降频。

请添加图片描述
请添加图片描述


三、STM32CubeMX 配置

硬件搭好后,开始配软件。

3.1 新建工程

  1. 打开 STM32CubeMX,选 STM32F401RCT6
  2. System Core → SYSDebugSerial Wire(保留 SWD 下载);Timebase SourceTIM4(把 HAL 的 1ms 时基从 SysTick 挪到 TIM4,避免和 FreeRTOS 的 SysTick 冲突)。

3.2 时钟配置(RCC)

本项目不用外部高速晶振(HSE 没接,用 HSI),系统时钟走 HSI → PLL → 84MHz

  • RCC → 使能 HSIHSE 不使能;
  • PLL:PLLM=8PLLN=84PLLP=2SYSCLK=84MHzAPB1=42MHzAPB2=84MHz
  • 同时使能 LSE(32.768kHz 晶振) 给 RTC 用(自动占用 PC14/PC15)。

3.3 SPI1(屏幕 / W25Q / RC522 共用)

Connectivity → SPI1

  • Mode:Full-Duplex Master
  • 分频 BaudRatePrescaler=4(21MHz,给屏幕和 W25Q);
  • 8bit,Mode 0(CPOL=0/CPHA=0),软件 NSS;
  • 引脚 PB3=SCKPB4=MISOPB5=MOSI

3.4 I2C1(摄像头 SCCB)

标准 I2C,100kHz,PB6=SCLPB7=SDA。OV7670 用 I2C 协议(SCCB 兼容)写寄存器。

3.5 ADC1 + DMA(旋钮 + 按键)

  • ADC1 → 使能 IN8(PB0)和 IN9(PB1);
  • Scan Conversion Mode = Enable
  • Continuous Conversion Mode = Enable
  • Number Of Conversion = 2
  • 采样时间 480 cycles
  • DMA SettingsADC1 的 DMA(DMA2_Stream0,Circular,HalfWord)。

这样 ADC 会不停地把两个通道的值循环存到数组里,主程序直接读数组。

3.6 USART1(ESP8266)+ DMA

  • USART1:Asynchronous,PA9=TXPA10=RX,115200;
  • USART1_RX 加 DMA(DMA2_Stream2,Circular,Byte),后台收 ESP8266 数据(避免丢字节的关键)。

3.7 USART6(SU-03T 语音)

USART6:Asynchronous,PA11=TXPA12=RX,9600。

3.8 TIM2(舵机 PWM)

TIM2Channel1PWM Generation CH1Prescaler=83Period=19999(50Hz)、Pulse=1500(中位),引脚 PA15

3.9 GPIO

  • PA0~PA7GPIO_Input(摄像头 D0~D7);
  • PB8/PB9/PB10/PB12/PB13GPIO_Output(摄像头 RCLK/OE/WRST/RRST/WEN);
  • PB14GPIO_EXTI14(摄像头 VSYNC 上升沿中断);
  • PB2(LCD DC)、PA8(LCD CS)、PB15(W25Q CS)、PC13(RC522 CS):GPIO_Output
  • PB0/PB1ADC_IN8ADC_IN9

3.10 NVIC

勾选并使能:DMA2_Stream0_IRQnDMA2_Stream2_IRQn(优先级 5)、USART1_IRQn(5)、EXTI15_10_IRQn(5)、TIM4_IRQn(15)。

3.11 FreeRTOS

Middleware → FREERTOS:Interface 选 CMSIS_V2configTOTAL_HEAP_SIZE=20480(20KB),勾选 configUSE_NEWLIB_REENTRANT

3.12 生成工程 + 加入 LVGL

  1. 生成 STM32CubeIDE 工程;
  2. 复制 LVGL 到 Middlewares/Third_Party/lvgllv_conf.hCore/Inc
  3. 确认关键宏:LV_COLOR_DEPTH 16LV_TICK_CUSTOM 1 + LV_TICK_CUSTOM_SYS_TIME_EXPR (HAL_GetTick())LV_MEM_SIZE (16U*1024U)

工程结构就搭好了。main.c 里的 SystemClock_Config() 就是 HSI→PLL 84MHz。


四、Bootloader(详细可见往期文章)

本工程用「Bootloader + App」双区实现 OTA:

  • Bootloader 在 0x08000000(32KB),App 在 0x08008000(224KB)。
  • App 启动第一句 SCB->VTOR = FLASH_BASE | 0x8000; 重定位中断向量表。
  • Bootloader 流程(bootloader/bootloader_main.c,纯寄存器、无 HAL):读 W25Q 的 OTA 标志(0x40000,magic 0xA5A5A5A5)→ 校验 CRC → 擦 App 区(扇区 2~5)→ 从 W25Q 拷贝 → 清标志 → 跳转。

注意:F401 内部 Flash 编程要用 32 位字写(PSIZE=x32),否则向量表写坏、跳转白屏。

烧录顺序:先 ST-Link 烧 bootloader.bin0x08000000,再烧 App 到 0x08008000(或走 OTA)。


五、工程文件结构

Core/ 下的自写文件:

文件 作用
main.c 入口,初始化所有外设和 RTOS
freertos.c FreeRTOS 任务创建、互斥锁
ui.c/h LVGL 界面(动画/菜单/状态页/OTA 确认页)
lv_port_disp.c/h LVGL 显示驱动对接 ST7735
st7735.c/h ST7735 屏幕驱动
ov7670.c/h OV7670 摄像头驱动
w25q128.c/h W25Q Flash 驱动 + 分区
esp8266.c/h ESP8266 驱动(AT + DMA 接收)
RC522.c/h RC522 RFID 驱动
input.c/h 旋钮 + 按键(ADC)
lock.c/h 舵机 + SU-03T 语音收发
rtc.c/h RTC 时钟(LSE + 纽扣电池)
face_task.c/h 人脸任务 + 后台联网任务
rfid_task.c/h 刷卡任务
ota.c/h OTA 状态机
ota_task.c/h OTA 任务

adc.c/dma.c/i2c.c/spi.c/tim.c/usart.c/stm32f4xx_hal_msp.c/stm32f4xx_it.c 等是 CubeMX 生成,一般不改。


六、RTOS 任务调度

一共 6 个任务freertos.c 创建):

任务 入口 职责
defaultTask StartDefaultTask 512B 开机 5 秒后播报「开始」语音
uiTask StartUITask 4KB LVGL 渲染 + 旋钮/按键 + 每秒刷时钟
faceTask StartFaceTask 2KB 人脸识别
rfidTask StartRfidTask 2KB 刷卡
otaTask StartOtaTask 2KB OTA 下载
netTask StartNetTask 2KB 后台连 WiFi + 连电脑
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
uiTaskHandle = osThreadNew(StartUITask, NULL, &uiTask_attributes);
faceTaskHandle = osThreadNew(StartFaceTask, NULL, &faceTask_attributes);
rfidTaskHandle = osThreadNew(StartRfidTask, NULL, &rfidTask_attributes);
otaTaskHandle  = osThreadNew(StartOtaTask,  NULL, &otaTask_attributes);
netTaskHandle  = osThreadNew(StartNetTask,  NULL, &netTask_attributes);

6.1 任务间通信

  1. 事件标志(osThreadFlags):UI 点菜单唤醒对应任务。
void face_trigger(void)
{
    osThreadFlagsSet(faceTaskHandle, FACE_FLAG);
}

faceTask 平时阻塞等标志:

for (;;)
{
    osThreadFlagsWait(FACE_FLAG, osFlagsWaitAny, osWaitForever);
    /* 收到标志,开始识别 */
}
  1. 互斥锁(osMutex):屏幕/W25Q/RC522 共用 SPI1,访问前加锁。
osMutexId_t spi1_mutexHandle;   // SPI1 锁
osMutexId_t lvgl_mutexHandle;   // LVGL 锁(递归锁)

6.2 UI 任务主循环

void StartUITask(void *argument)
{
    for(;;)
    {
        osMutexAcquire(lvgl_mutexHandle, osWaitForever);
        lv_timer_handler();
        osMutexRelease(lvgl_mutexHandle);
        osDelay(5);

        uint8_t knob = input_get_knob();
        ui_set_knob(knob);

        uint32_t key = input_poll_key();
        if (key) ui_key(key);

        ui_update_net(g_net_state);
        su03_poll();

        static uint32_t last_sec = 0, last_save = 0;
        uint32_t now = HAL_GetTick();
        if (now - last_sec >= 1000) {
            last_sec = now;
            ui_update_time();
            if (now - last_save >= 60000) {
                last_save = now;
                rtc_save_time();
            }
        }
    }
}

七、屏幕与界面

7.1 ST7735 屏幕驱动

160×128 横屏、RGB565:

#define ST7735_WIDTH  160
#define ST7735_HEIGHT 128
#define ST7735_ROTATION (ST7735_MADCTL_MY | ST7735_MADCTL_MV | ST7735_MADCTL_RGB)

发命令(DC=0)和发数据(DC=1):

static void ST7735_WriteCommand(uint8_t cmd) {
    HAL_GPIO_WritePin(ST7735_DC_GPIO_Port, ST7735_DC_Pin, GPIO_PIN_RESET);
    HAL_SPI_Transmit(&ST7735_SPI_PORT, &cmd, 1, HAL_MAX_DELAY);
}
void ST7735_WriteData(uint8_t *buff, uint32_t buff_size) {
    HAL_GPIO_WritePin(ST7735_DC_GPIO_Port, ST7735_DC_Pin, GPIO_PIN_SET);
    HAL_SPI_Transmit(&ST7735_SPI_PORT, buff, buff_size, HAL_MAX_DELAY);
}

7.2 LVGL 显示对接(lv_port_disp.c)

给 LVGL 注册 flush_cb,把「画像素」接到屏幕:

static void flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p)
{
    uint32_t n = (uint32_t)(area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1);
    if (spi1_mutexHandle != NULL) osMutexAcquire(spi1_mutexHandle, osWaitForever);
    ST7735_Select();
    ST7735_SetAddressWindow(area->x1, area->y1, area->x2, area->y2);
    ST7735_WriteData((uint8_t *)color_p, n * 2);
    ST7735_Unselect();
    if (spi1_mutexHandle != NULL) osMutexRelease(spi1_mutexHandle);
    lv_disp_flush_ready(drv);
}

7.3 界面(ui.c)

四页:开机动画 → 主菜单 → 状态页 → OTA 确认页。菜单项:

#define ITEM_NUM 4
static const char *ITEM_NAME[ITEM_NUM] = {"Face ID", "RFID Card", "OTA Update", "Time Sync"};

按键分发:

void ui_key(uint32_t key)
{
    ui_lock();
    switch (cur_page) {
    case PAGE_MENU:
        if (key == LV_KEY_UP)            set_focus(focus == 0 ? ITEM_NUM - 1 : focus - 1);
        else if (key == LV_KEY_DOWN)     set_focus((focus + 1) % ITEM_NUM);
        else if (key == LV_KEY_ENTER) {
            if (focus == 0 && face_cb) { face_cb(); }
            if (focus == 1 && rfid_cb) { rfid_cb(); }
            if (focus == 2 && ota_cb)  { ota_cb(); }
            if (focus == 3 && time_sync_cb) { time_sync_cb(); }
        }
        break;
    ...
    }
    ui_unlock();
}

回调在 freertos.c 注册:

ui_set_face_cb(face_trigger);
ui_set_rfid_cb(rfid_trigger);
ui_set_ota_cb(ota_trigger);
ui_set_time_sync_cb(time_sync_trigger);

时钟刷新读 RTC:

void ui_update_time(void)
{
    if (time_lbl == NULL) return;
    char buf[16];
    rtc_get_time_str(buf, sizeof(buf));
    ui_lock();
    lv_label_set_text(time_lbl, buf);
    ui_unlock();
}

请添加图片描述


八、摄像头驱动(ov7670.c)

OV7670 后面带 AL422B FIFO,先存一帧再慢慢读。配寄存器走 SCCB(硬件 I2C1,地址 0x21<<1);读图像靠 PB8(RCK) 翻转时钟,FIFO 吐字节到 PA0~PA7

写寄存器:

static void ov7670_write_reg(uint8_t reg, uint8_t val)
{
    HAL_I2C_Mem_Write(&OV7670_I2C, OV7670_ADDR, reg,
                      I2C_MEMADD_SIZE_8BIT, &val, 1, 100);
}

读一个像素(RGB565,直接操作寄存器比 HAL 快):

static inline uint16_t ov7670_read_pixel(void)
{
    uint16_t c;
    RCK_LOW();  __NOP();
    c = DATA_READ();
    RCK_HIGH(); __NOP();
    RCK_LOW();  __NOP();
    c = (uint16_t)(c << 8) | DATA_READ();
    RCK_HIGH();
    return c;
}

其中:

#define RCK_HIGH()  do { OV7670_RCK_PORT->BSRR = OV7670_RCK_PIN; } while(0)
#define RCK_LOW()   do { OV7670_RCK_PORT->BSRR = (OV7670_RCK_PIN << 16); } while(0)
#define DATA_READ() ((uint8_t)(OV7670_DATA_PORT->IDR & 0xFF))

用 VSYNC 中断(PB14 上升沿)界定一帧:第 1 个上升沿复位写指针开始采集,第 2 个上升沿冻结当前帧:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin != OV7670_VSYNC_PIN) return;
    if (ov_sta < 2)
    {
        if (ov_sta == 0) {
            HAL_GPIO_WritePin(OV7670_WRST_PORT, OV7670_WRST_PIN, GPIO_PIN_RESET);
            HAL_GPIO_WritePin(OV7670_WRST_PORT, OV7670_WRST_PIN, GPIO_PIN_SET);
            HAL_GPIO_WritePin(OV7670_WEN_PORT, OV7670_WEN_PIN, GPIO_PIN_SET);
        } else {
            HAL_GPIO_WritePin(OV7670_WEN_PORT, OV7670_WEN_PIN, GPIO_PIN_RESET);
            HAL_GPIO_WritePin(OV7670_WRST_PORT, OV7670_WRST_PIN, GPIO_PIN_RESET);
            HAL_GPIO_WritePin(OV7670_WRST_PORT, OV7670_WRST_PIN, GPIO_PIN_SET);
        }
        ov_sta++;
    }
}

两个应用:OV7670_DrawToLCD() 抽点预览到屏幕,OV7670_CaptureToFlash() 抓 160×120 灰度图写 W25Q 供人脸识别。

int OV7670_CaptureToFlash(uint32_t w25q_addr, uint32_t *pixel_count)
{
    ...
    for (sy = 0; sy < 240; sy++)
        for (sx = 0; sx < 320; sx++) {
            uint16_t c = ov7670_read_pixel();
            if ((sy & 1) == 0 && (sx & 1) == 0) {
                uint8_t g = (uint8_t)(((c >> 5) & 0x3F) << 2);
                page[page_off++] = g;
                ...
            }
        }
    ...
}

九、W25Q Flash(w25q128.c)

16MB SPI Flash,干三件事:暂存 OTA 固件、暂存人脸图像、镜像时间。分区:

#define W25Q_FW_ADDR     0x00000000u   /* OTA 固件暂存(256KB) */
#define W25Q_FLAG_ADDR   0x00040000u   /* OTA 标志(4B) */
#define W25Q_FACE_ADDR   0x00050000u   /* 人脸帧缓冲 */
#define W25Q_TIME_ADDR   0x00080000u   /* 时间镜像(4KB) */

基本操作 W25Q_Read/W25Q_Write/W25Q_Erase4K/W25Q_Erase64K,写前先擦。初始化遍历 4 种 SPI 模式读 JEDEC ID 识别 Winbond(0xEF)/GigaDevice(0xC8)。


十、ESP8266(esp8266.c)

走 USART1,AT 指令。接收用 DMA 环形缓冲(不是轮询 RXNE,否则 UI 刷屏抢占时会丢字节、AT 响应丢失):

#define DMABUF_SZ 4096
static uint8_t dmabuf[DMABUF_SZ];

ESP8266_DMA_Read() 处理环形缓冲绕回,把新收到的字节取出来。连 WiFi/TCP 发 AT+CWJAPAT+CIPSTART 等,参数在 esp8266.h

#define WIFI_SSID     "Makoto"
#define WIFI_PASS     "ec392241"
#define OTA_IP        "192.168.20.253"
#define FACE_PORT     8080
#define OTA_PORT      8081
#define STA_IP        "192.168.20.200"

传大块二进制用「透传模式」:AT+CIPMODE=1AT+CIPSEND,串口与 TCP 直连。退出透传发 +++(无回车,前后 1 秒静默)。


十一、人脸识别(face_task.c)

MCU 只负责抓图上传,识别在电脑做。协议帧:20 字节头 + 灰度数据 + {0x55,0xAA} 尾,头里含宽高、长度、CRC32、RTC 时间。

任务流程:预览 4 秒 → 检查 TCP 连上 → 连发两张图(擦 W25Q → 抓图 → 算 CRC → 透传发送 → 收结果)→ 成功开门。

后台联网任务 StartNetTask 维护 g_net_state(0=未连、1=连 WiFi、2=连 TCP),点「人脸识别」不用傻等联网。时间同步也是它在 TCP 连上后处理:发 TIME?\n,收 TIME:HH:MM:SS\n,写 RTC。


十二、RFID 刷卡(RC522.c + rfid_task.c)

RC522 访问前把 SPI 分频临时降到 5.25MHz(RC522 上限 10MHz):

static inline void RC522_SPI_Select(void) {
    hspi1.Instance->CR1 = (hspi1.Instance->CR1 & ~SPI_CR1_BR) | SPI_BAUDRATEPRESCALER_16;
}
static inline void RC522_SPI_Release(void) {
    hspi1.Instance->CR1 = (hspi1.Instance->CR1 & ~SPI_CR1_BR) | SPI_BAUDRATEPRESCALER_4;
}

读写寄存器:

void MFRC_WriteReg(uint8_t addr, uint8_t data)
{
    uint8_t AddrByte = (addr << 1) & 0x7E;
    RS522_NSS(0);
    SPI_RW_Byte(AddrByte);
    SPI_RW_Byte(data);
    RS522_NSS(1);
}

刷卡任务循环寻卡、取卡号、白名单比对,有效就 lock_open() 开门。


十三、舵机与语音(lock.c)

舵机用 TIM2 的 50Hz PWM(Prescaler=83/Period=19999),改占空比就是角度:

#define SERVO_OPEN   500
#define SERVO_CLOSE  2500
#define SERVO_MID    1500

void lock_open(void)
{
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, SERVO_OPEN);
    HAL_Delay(2000);
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, SERVO_MID);
}

SU-03T 语音用 USART6 收发,播报帧 AA 55 <消息号> [参数] 55 AA

void voice_success(void)
{
    uint8_t cmd[] = {0xAA, 0x55, 0x02, 0x55, 0xAA};
    HAL_UART_Transmit(&huart6, cmd, sizeof(cmd), 100);
}

SU-03T 也能反向发指令(喊「开门」触发识别),接收用中断 + 状态机解析 AA 55 <cmd> 55 AA 帧,UI 任务轮询 su03_poll() 处理。


十四、旋钮与按键(input.c)

ADC + DMA 连续采样两个通道:adc_buf[0]=旋钮,adc_buf[1]=按键。

void input_init(void)
{
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 2);
}

uint8_t input_get_knob(void)
{
    uint16_t v = adc_buf[0];
    return (uint8_t)((uint32_t)v * 100U / 4095U);
}

input_poll_key() 按电压阈值判断键位 + 20ms 去抖,只在「空闲→按下」下降沿返回一次事件。


十五、OTA 升级(ota.c + ota_task.c)

状态机 OTA_WIFI → OTA_TCP → OTA_DL → OTA_OK/FAIL,枚举在 ota.h

typedef enum{OTA_IDLE,OTA_WIFI,OTA_TCP,OTA_DL,OTA_OK,OTA_FAIL}OTA_St;

固件头 256 字节,含 magic 0x4F544148、长度、CRC32。关键:先擦 W25Q 暂存区再连服务器,否则擦除期 DMA 缓冲回绕冲掉固件头。

任务收完后弹「OK/Cancel」确认,确认就 NVIC_SystemReset() 复位进 Bootloader。net_paused 标志在 OTA 期间暂停 netTask。


十六、RTC 实时时钟(rtc.c)

RTC 靠 LSE 32.768kHz 晶振 + 纽扣电池(VBAT) 掉电走时。用直接寄存器操作(不依赖 HAL RTC)。

  • LSE 由 VBAT 供电,掉电走时;LSI(内部 RC)不在备份域,掉电停摆,所以必须用 LSE。
  • LSE 起振慢,等待超时要 5 秒左右。

核心坑:INITS 标志。 STM32 的 INITS(日历已初始化)由硬件在「日期寄存器 DR 的年份 != 0」时才置 1。只写时间 TR、不写日期 DRINITS 永远为 0,每次上电都以为没初始化、时间被清掉——这就是掉电不走时的根因。

正确写法,时间日期都要写:

RTC->TR = make_tr(h, m, s);   /* 写时间 */
RTC->DR = make_dr();          /* 写日期(年份=1)→ 置位 INITS */
static uint32_t make_dr(void)
{
    return (1UL << 16) |   /* YU = 1 */
           (1UL << 13) |   /* WDU = 1 */
           (1UL << 8)  |   /* MU = 1 */
           (1UL << 0);     /* DU = 1 */
}

初始化:

void rtc_init(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
    PWR->CR |= PWR_CR_DBP;

    bool inits = (RTC->ISR & RTC_ISR_INITS) != 0;
    bool using_lse = ((RCC->BDCR & RCC_BDCR_RTCSEL) == RCC_BDCR_RTCSEL_0);

    if (inits && using_lse) {
        while (!(RTC->ISR & RTC_ISR_RSF));
        return;
    }
    /* 读旧时间 → BDRST 复位备份域 → 起 LSE(等5s) → 选 LSE → 写 TR/DR */
}

读时间:

void rtc_get_hms(uint8_t *h, uint8_t *m, uint8_t *s)
{
    uint32_t tr = RTC->TR;
    *h = bcd_val(tr, 20, 16);
    *m = bcd_val(tr, 12, 8);
    *s = bcd_val(tr, 4, 0);
}

RTC 注意事项汇总:① INITS 靠写日期置位;② LSE 等 5 秒;③ F401 没有 LSEMOD 位;④ 换时钟源要 BDRST;⑤ 读 TR 前等 RSF;⑥ 写 BDCR 前开 DBP。完整调试过程见工程 WORKFLOW.md


十七、main.c 启动顺序

int main(void)
{
  SCB->VTOR = FLASH_BASE | 0x8000;
  __enable_irq();
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_ADC1_Init();
  MX_I2C1_Init();
  MX_SPI1_Init();
  MX_TIM2_Init();
  MX_USART1_UART_Init();
  MX_USART6_UART_Init();

  lv_init();
  lv_port_disp_init();
  ui_init();
  input_init();

  OV7670_Init();
  W25Q_Init();
  lock_init();
  rtc_init();

  osKernelInitialize();
  MX_FREERTOS_Init();
  osKernelStart();
  while (1) { }
}

LVGL 和界面要在 osKernelStart() 之前建好。


十八、上位机(smartid.py)

Python + Tkinter,三个功能页:人脸识别服务器(TCP 8080)、人脸训练、OTA 服务器(TCP 8081)。人脸和 OTA 互斥(共用一个 ESP8266)。

对时协议(电脑端):

if head == b"TIME?\n":
    now = datetime.now()
    conn.sendall(f"TIME:{now.hour:02d}:{now.minute:02d}:{now.second:02d}\n".encode())

请添加图片描述


十九、常见问题与注意事项

  1. RTC 掉电不走时:先查有没有写日期寄存器 DR(INITS 靠它置位),再查 LSE 起振等待够不够长。
  2. SPI 冲突:屏幕/W25Q/RC522 共用 SPI1,访问前拿 spi1_mutexHandle 锁。
  3. RC522 读不到卡:先查 SPI 分频(降到 5.25MHz),再查 PC13(与板载 LED 共用,驱动能力弱)。
  4. ESP8266 连不上/丢字节:接收用 DMA 环形缓冲,别轮询 。
  5. OTA 升级完没变化:检查「先擦 W25Q 再连服务器」、内部 Flash 用 32 位字写。
  6. 人脸识别一直转圈:查 WiFi 账号密码、静态 IP 是否与电脑同网段、电脑服务器是否启动。
  7. 烧录后白屏:确认 Bootloader 在 0x08000000、App 在 0x08008000、SCB->VTOR 已重定位。
  8. F401 RAM 只有 64KB:FreeRTOS 堆 20KB,别开大静态数组。

二十、总结

这个项目从自己画原理图/PCB 开始,串起了 STM32 的几乎所有常用外设——SPI(屏/Flash/RFID)、I2C(摄像头配置)、UART+DMA(WiFi)、ADC+DMA(旋钮按键)、TIM(舵机 PWM + 时基)、EXTI(帧同步)、RTC+LSE(掉电时钟),再叠加 FreeRTOS 多任务、LVGL 界面、Bootloader 双区 OTA,非常适合新手练手。

如果这篇文章对你有帮助,欢迎点赞收藏;有问题评论区交流。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐