【STM32F4】基于RTOS的智能门禁系统实战:人脸识别+RFID+OTA 升级
【STM32F4】基于RTOS的智能门禁系统实战:人脸识别+RFID+OTA 升级
- 一、项目简介
- 二、硬件设计(原理图 + PCB)
- 三、STM32CubeMX 配置
- 四、Bootloader(详细可见往期文章)
- 五、工程文件结构
- 六、RTOS 任务调度
- 七、屏幕与界面
- 八、摄像头驱动(ov7670.c)
- 九、W25Q Flash(w25q128.c)
- 十、ESP8266(esp8266.c)
- 十一、人脸识别(face_task.c)
- 十二、RFID 刷卡(RC522.c + rfid_task.c)
- 十三、舵机与语音(lock.c)
- 十四、旋钮与按键(input.c)
- 十五、OTA 升级(ota.c + ota_task.c)
- 十六、RTC 实时时钟(rtc.c)
- 十七、main.c 启动顺序
- 十八、上位机(smartid.py)
- 十九、常见问题与注意事项
- 二十、总结
一、项目简介
这是一个完整的 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 硬件设计要点小结
- 两路 3.3V 不并联:E3V3 供大电流外设,3V3 只供旋钮分压,保证 ADC 比例式测量准确。
- 纽扣电池用可充电的 ML621,经肖特基 D2 防倒灌,SW2 控制充电。
- 三个 SPI 设备共用一根 SPI1,靠片选区分,RC522 需降频。


三、STM32CubeMX 配置
硬件搭好后,开始配软件。
3.1 新建工程
- 打开 STM32CubeMX,选
STM32F401RCT6。 - System Core → SYS:
Debug选Serial Wire(保留 SWD 下载);Timebase Source选TIM4(把 HAL 的 1ms 时基从 SysTick 挪到 TIM4,避免和 FreeRTOS 的 SysTick 冲突)。
3.2 时钟配置(RCC)
本项目不用外部高速晶振(HSE 没接,用 HSI),系统时钟走 HSI → PLL → 84MHz:
RCC→ 使能HSI,HSE不使能;- PLL:
PLLM=8、PLLN=84、PLLP=2→SYSCLK=84MHz,APB1=42MHz,APB2=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=SCK、PB4=MISO、PB5=MOSI。
3.4 I2C1(摄像头 SCCB)
标准 I2C,100kHz,PB6=SCL、PB7=SDA。OV7670 用 I2C 协议(SCCB 兼容)写寄存器。
3.5 ADC1 + DMA(旋钮 + 按键)
ADC1→ 使能IN8(PB0)和IN9(PB1);Scan Conversion Mode = EnableContinuous Conversion Mode = EnableNumber Of Conversion = 2;- 采样时间
480 cycles; DMA Settings加ADC1的 DMA(DMA2_Stream0,Circular,HalfWord)。
这样 ADC 会不停地把两个通道的值循环存到数组里,主程序直接读数组。
3.6 USART1(ESP8266)+ DMA
USART1:Asynchronous,PA9=TX、PA10=RX,115200;- 给 USART1_RX 加 DMA(
DMA2_Stream2,Circular,Byte),后台收 ESP8266 数据(避免丢字节的关键)。
3.7 USART6(SU-03T 语音)
USART6:Asynchronous,PA11=TX、PA12=RX,9600。
3.8 TIM2(舵机 PWM)
TIM2 → Channel1 选 PWM Generation CH1:Prescaler=83、Period=19999(50Hz)、Pulse=1500(中位),引脚 PA15。
3.9 GPIO
PA0~PA7:GPIO_Input(摄像头 D0~D7);PB8/PB9/PB10/PB12/PB13:GPIO_Output(摄像头 RCLK/OE/WRST/RRST/WEN);PB14:GPIO_EXTI14(摄像头 VSYNC 上升沿中断);PB2(LCD DC)、PA8(LCD CS)、PB15(W25Q CS)、PC13(RC522 CS):GPIO_Output;PB0/PB1:ADC_IN8、ADC_IN9。
3.10 NVIC
勾选并使能:DMA2_Stream0_IRQn、DMA2_Stream2_IRQn(优先级 5)、USART1_IRQn(5)、EXTI15_10_IRQn(5)、TIM4_IRQn(15)。
3.11 FreeRTOS
Middleware → FREERTOS:Interface 选 CMSIS_V2,configTOTAL_HEAP_SIZE=20480(20KB),勾选 configUSE_NEWLIB_REENTRANT。
3.12 生成工程 + 加入 LVGL
- 生成 STM32CubeIDE 工程;
- 复制 LVGL 到
Middlewares/Third_Party/lvgl,lv_conf.h放Core/Inc; - 确认关键宏:
LV_COLOR_DEPTH 16、LV_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,magic0xA5A5A5A5)→ 校验 CRC → 擦 App 区(扇区 2~5)→ 从 W25Q 拷贝 → 清标志 → 跳转。
注意:F401 内部 Flash 编程要用 32 位字写(PSIZE=x32),否则向量表写坏、跳转白屏。
烧录顺序:先 ST-Link 烧 bootloader.bin 到 0x08000000,再烧 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 任务间通信
- 事件标志(osThreadFlags):UI 点菜单唤醒对应任务。
void face_trigger(void)
{
osThreadFlagsSet(faceTaskHandle, FACE_FLAG);
}
faceTask 平时阻塞等标志:
for (;;)
{
osThreadFlagsWait(FACE_FLAG, osFlagsWaitAny, osWaitForever);
/* 收到标志,开始识别 */
}
- 互斥锁(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+CWJAP、AT+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=1 后 AT+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、不写日期 DR,INITS 永远为 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())

十九、常见问题与注意事项
- RTC 掉电不走时:先查有没有写日期寄存器
DR(INITS 靠它置位),再查 LSE 起振等待够不够长。 - SPI 冲突:屏幕/W25Q/RC522 共用 SPI1,访问前拿
spi1_mutexHandle锁。 - RC522 读不到卡:先查 SPI 分频(降到 5.25MHz),再查 PC13(与板载 LED 共用,驱动能力弱)。
- ESP8266 连不上/丢字节:接收用 DMA 环形缓冲,别轮询 。
- OTA 升级完没变化:检查「先擦 W25Q 再连服务器」、内部 Flash 用 32 位字写。
- 人脸识别一直转圈:查 WiFi 账号密码、静态 IP 是否与电脑同网段、电脑服务器是否启动。
- 烧录后白屏:确认 Bootloader 在 0x08000000、App 在 0x08008000、
SCB->VTOR已重定位。 - 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,非常适合新手练手。
如果这篇文章对你有帮助,欢迎点赞收藏;有问题评论区交流。
更多推荐



所有评论(0)