简介:基于STM32的智能门禁人体感应报警毕业设计资料包,面向电子与嵌入式相关专业学生、毕设开发者,提供一套涵盖软硬件设计的综合参考方案。设计围绕人体感应触发报警、蜂鸣器驱动、LCD1602显示与按键交互等核心功能,资料包含完整源码、电路原理图和实物图,便于对照学习GPIO、定时器、LCD驱动及人体感应模块的编程思路。压缩包以7z格式封装,大小约4.56MB,整体聚焦于核心工程文件,解压后即可查看关键设计与代码。目前已有1122人浏览学习这份资源。借助原理图可梳理硬件连接与电路设计,借助实物图可辅助焊接和排错;按键控制与液晶显示的状态联动,也为功能扩展和毕业答辩演示提供了清晰支撑。无论课程设计还是毕业设计,都能从中获取可直接参考的门禁报警整体方案。
1. 基于STM32的智能门禁:为什么用“人体感应+蜂鸣器+LCD1602+按键”这套组合
STM32 智能门禁这个题目,在嵌入式毕业设计里出现频率极高:PIR 人体感应模块负责“看到人”,蜂鸣器负责“发出警报”,LCD1602 把布防、撤防、报警状态显示出来,按键承担布防切换和报警复位。整条链路由 4 个基础外设拼成,却要处理输入检测、输出控制、显示刷新和状态机切换四类逻辑,对 GPIO、外部中断、定时器和 HAL 库都是完整的训练。适合做课程设计收尾,也适合想在简历上写“独立完成带传感输入和声光报警的单片机系统”的应届生。这篇文章按硬件选型、固件实现、状态机设计、调试优化的顺序展开,最后给到可以直接移植的代码片段。
2. 门禁硬件链路:HC-SR501人体感应接线、蜂鸣器驱动电路与LCD1602引脚分配
2.1 人体感应模块选型:HC-SR501 输出信号与跳帽模式怎么设
毕业设计里人体感应几乎默认用 HC-SR501,原因是它内部集成了 BISS0001 红外处理芯片和菲涅尔透镜,输出端直接给出高电平,不需要额外放大电路。供电范围标称 4.5V~20V,系统里常见做法是直接给模块供 5V,输出引脚串一个 1kΩ 电阻再进 STM32 的 GPIO,既能限流又能在杜邦线较长时减轻干扰。信号脚推荐接 PA8,配置为浮空输入或带上拉输入均可,因为模块静态时输出低电平,只有检测到人体时才拉高。
HC-SR501 板上有两个十字电位器,分别调灵敏度和延时。灵敏度旋钮控制检测距离,通常能覆盖 3~7 米;延时旋钮控制输出高电平保持时间,范围约 0.3~10 秒。门禁场景里建议把延时调短,保持 1 秒左右。原因是延时过长时,主控读到的是持续几秒的高电平,状态机切换会变得迟钝;而且人一靠近就一直响,适合做“有人进入”的提示,但不适合做“非法闯入”的报警。延时调短后,主控可以通过软件自行延长报警时间,逻辑更可控。
HC-SR501 还有一个触发模式跳帽。L 模式是“不可重复触发”,检测到人后输出一次高电平,计时结束后恢复低电平,期间人即使连续移动也不会重新计时;H 模式是“可重复触发”,只要人在感应范围内持续运动,输出就始终保持高电平。门禁报警建议选 H 模式。闯入者一般不会站着不动,H 模式下的信号更可靠,不容易出现“人还在现场,输出却已经回落”的漏报情况。
2.2 蜂鸣器驱动电路:三极管基极电阻计算与续流二极管的作用
STM32 的 GPIO 推挽输出时灌入和拉出电流典型值只有 20mA 左右,直接驱动蜂鸣器往往音量不够,甚至会把引脚拉垮。常见做法是用一颗 NPN 三极管,比如 SS8050,搭共射极开关电路:集电极接蜂鸣器负极,蜂鸣器正极接 VCC,发射极接地,基极通过限流电阻接 STM32 引脚。GPIO 输出高电平时三极管饱和导通,蜂鸣器得电发声;输出低电平时截止,蜂鸣器停响。
基极电阻取值需要算一步。SS8050 的直流放大倍数 hFE 典型值在 100 到 300,蜂鸣器工作电流按 30mA 算,基极电流取集电极电流的 1/10 到 1/20,也就是 1.5~3mA。STM32 引脚高电平按 3.3V 算,三极管基极-发射极饱和压降约 0.7V,电阻上压降约 2.6V,2.6V 除以 2mA 约等于 1.3kΩ,所以常见做法是选 1kΩ 或 2.2kΩ,既能保证进入饱和区,又不会让基极电流过大。
蜂鸣器两端要反并联一个 1N4148 二极管,阴极接 VCC,阳极接集电极。很多教程只在继电器驱动电路里强调续流,实际上无源蜂鸣器内部是线圈结构,断电瞬间会产生反向感应电动势,如果没有这个二极管泄放,三极管集电极-发射极之间可能被击穿。有源蜂鸣器虽然内部封装了振荡电路,但同样带线圈,续流二极管应该保留。选型上,有源蜂鸣器通电就响,只适合做固定频率报警音;无源蜂鸣器需要外部提供 2kHz 到 4kHz 方波才能发声,好处是可以用 PWM 改变频率,做出“叮咚”门铃音或急促的警笛效果。毕设时间紧就选有源,想多展示一些“技术含量”就选无源。
2.3 LCD1602 四线并行接线、对比度电位器与按键保护电路
LCD1602 驱动方式有 8 位并行、4 位并行和 I2C 转接板三种。毕设里最常见的是 4 位并行模式,只占用 6 个 GPIO:RS、EN、D4 到 D7。8 位并行需要 11 个引脚,对 STM32F103C8T6 这种 48 脚芯片并不是不能用,但 4 位模式能省下引脚给传感器或串口调试。I2C 方式用 PCF8574 转接板,只需要 SCL 和 SDA 两根线,但要先确认板载 I2C 地址,常见地址是 0x27 或 0x3F,买回来的模块地址可能不一致,最好在初始化之前用 I2C 扫描程序探测一下。
LCD1602 的 Vo 引脚是很多人被坑的地方。Vo 悬空时屏幕要么全黑要么全白,字符完全看不清,跟代码没关系。正确接法是 Vo 接一个 10kΩ 电位器的中间抽头,两端分别接 VCC 和 GND,上电后边看屏幕边转动电位器,把对比度调到字符清晰、背景不发黑的位置。背光引脚串一个 10Ω 到 100Ω 的限流电阻再接 VCC,否则背光 LED 电流偏大,长时间工作会加速老化。
| 外设 | 信号 | STM32 引脚 | 配置项 |
|---|---|---|---|
| HC-SR501 PIR | 输出 | PA8 | GPIO 输入,无上拉 |
| 蜂鸣器 | 控制端 | PA9 | GPIO 推挽输出 |
| LCD1602 | RS | PB10 | GPIO 推挽输出 |
| LCD1602 | EN | PB11 | GPIO 推挽输出 |
| LCD1602 | D4~D7 | PB12~PB15 | GPIO 推挽输出 |
| 按键 1 | 布防/复位 | PB0 | 外部中断下降沿触发,内部上拉 |
| 按键 2 | 报警静音 | PB1 | 外部中断下降沿触发,内部上拉 |
按键的电路设计也不能省。按键一端接 GPIO,另一端接 GND,GPIO 内部使能上拉,未按下时读到高电平,按下时读到低电平。比按键另一端接 VCC 更安全,因为按下瞬间不会有额外灌入电流。每个按键两端并联一个 100nF 电容做硬件消抖,再在按键和 GPIO 之间串一个 330Ω 电阻,构成简单的 RC 保护结构。这个按键保护电路在实验板上效果明显,尤其是按键线较长的时候,能减少毛刺对 EXTI 的误触发。
3. STM32固件实现:CubeMX初始化、LCD1602驱动与按键消抖读取
3.1 基于 CubeMX 的引脚初始化与时钟配置路径
固件开发用 STM32CubeMX 先生成工程,省去手动配时钟的步骤。工程新建时选择型号 STM32F103C8T6,在 Pinout 界面按硬件接线表配置引脚:PA8 设为 GPIO_INPUT,模式选 Input Mode,不使能上下拉;PA9 设为 GPIO_OUTPUT,初始电平设为 Low;PB10 到 PB15 都设为 Output Push-Pull;PB0 和 PB1 设为 External Interrupt Mode with Falling edge trigger detection,Pull-up/Pull-down 选 Pull-up。CubeMX 会自动生成HAL_GPIO_Init()和中断回调函数骨架。
时钟配置注意一个常见错误:STM32F103C8T6 的核心板并不都焊接了外部 8MHz 晶振。如果板子上没有晶振,而 CubMX 里又把 HSE 选为时钟源,程序会卡在HAL_RCC_ClockConfig()里面,表现为下载后不运行。此时要把 PLL Source 改成 HSI,系统时钟配置为 64MHz,这是没有外部晶振时的最可靠方案。如果核心板自带了 8MHz 晶振,则保持 HSE,把 SYSCLK 设为 72MHz,这样串口波特率和定时器频率都更标准。工程生成后,在main.c的用户代码区/* USER CODE BEGIN 2 */后面加 LCD 初始化和状态变量初始化。
3.2 LCD1602 4位并行驱动代码与位操作时序
LCD1602 4 位模式的核心是:每次写入 8 位数据时,先送高 4 位,再送低 4 位,通过 EN 引脚从高到低的跳变把数据锁存进液晶控制器。整套驱动可以分成写命令和写数据两个函数。下面是完整的最小驱动,文件lcd1602.c。
#include "lcd1602.h" #include "main.h" // 数据脚定义:D4->PB12, D5->PB13, D6->PB14, D7->PB15 #define LCD_D4 GPIO_PIN_12 #define LCD_D5 GPIO_PIN_13 #define LCD_D6 GPIO_PIN_14 #define LCD_D7 GPIO_PIN_15 static void LCD_Delay_us(uint16_t us) { for (uint16_t i = 0; i < us; i++) { __NOP(); } } static void LCD_WriteNibble(uint8_t nibble) { HAL_GPIO_WritePin(GPIOB, LCD_D4, (nibble & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, LCD_D5, (nibble & 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, LCD_D6, (nibble & 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, LCD_D7, (nibble & 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET); } static void LCD_Strobe(void) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET); LCD_Delay_us(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET); LCD_Delay_us(5); } static void LCD_WriteByte(uint8_t data, uint8_t rs) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, rs ? GPIO_PIN_SET : GPIO_PIN_RESET); LCD_WriteNibble((data >> 4) & 0x0F); LCD_Strobe(); LCD_WriteNibble(data & 0x0F); LCD_Strobe(); } void LCD_Init(void) { HAL_Delay(50); // 初始化时的特殊时序,必须严格按顺序来 LCD_WriteNibble(0x03); LCD_Strobe(); HAL_Delay(5); LCD_WriteNibble(0x03); LCD_Strobe(); HAL_Delay(5); LCD_WriteNibble(0x03); LCD_Strobe(); HAL_Delay(5); LCD_WriteNibble(0x02); LCD_Strobe(); HAL_Delay(5); // 4 位模式,2 行,5x7 点阵字符 LCD_WriteByte(0x28, 0); // 开显示,关光标,不闪烁 LCD_WriteByte(0x0C, 0); // 清屏 LCD_WriteByte(0x01, 0); HAL_Delay(5); // 写入后地址指针自动加一 LCD_WriteByte(0x06, 0); } void LCD_SetCursor(uint8_t row, uint8_t col) { uint8_t addr = (row == 0) ? (0x80 + col) : (0xC0 + col); LCD_WriteByte(addr, 0); } void LCD_ShowString(uint8_t row, uint8_t col, char *str) { LCD_SetCursor(row, col); while (*str) { LCD_WriteByte((uint8_t)(*str++), 1); } }代码里最关键的是LCD_WriteNibble和LCD_Strobe的配合。LCD_WriteNibble只把 4 位数据放到 D4~D7 引脚上,不产生时序;LCD_Strobe把 EN 拉高再拉低,形成上升沿加下降沿的信号。LCD1602 在 EN 下降沿时锁存数据,所以先写数据再拉高 EN 的顺序不能反过来。整个初始化序列里的 0x03 三次重复,是为了让 LCD 控制器在未知状态下强制进入 4 位模式,少一步不行。LCD_WriteByte的第二个参数 rs 决定当前写入的是命令还是字符数据,命令写在LCD_Init和光标控制里,字符数据写在LCD_ShowString的循环里。
3.3 按键消抖:先外部中断标记再主循环确认
按键接在 PB0 和 PB1 上,配的是下降沿外部中断。直接在中断回调函数里加 HAL_Delay 做延时消抖是不对的,因为中断回调里阻塞会拖死整个系统,而且 HAL_Delay 依赖 SysTick,在中断上下文里工作的行为并不可靠。更规范的做法是中断回调里只置一个标志位,记录触发时刻,真正的消抖判断放到主循环的任务函数里。
volatile uint8_t g_key1_pending = 0; volatile uint8_t g_key2_pending = 0; volatile uint32_t g_key1_tick = 0; volatile uint32_t g_key2_tick = 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == GPIO_PIN_0) { g_key1_pending = 1; g_key1_tick = HAL_GetTick(); } else if (GPIO_Pin == GPIO_PIN_1) { g_key2_pending = 1; g_key2_tick = HAL_GetTick(); } } void Key_Scan(void) { if (g_key1_pending) { g_key1_pending = 0; // 延时 20ms 后再确认电平,此时应为稳定的低电平 while (HAL_GetTick() - g_key1_tick < 20) { // 等待消抖窗口结束 } if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) { // 有效按键:切换布防/撤防 System_ToggleArm(); } } if (g_key2_pending) { g_key2_pending = 0; while (HAL_GetTick() - g_key2_tick < 20) { } if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) { Buzzer_Silence(); } } }这个设计的核心思路是:外部中断只负责“喊一声有大动作”,具体按键是否真的按下,要靠延时后重新读 GPIO 电平来确认。20ms 的消抖窗口覆盖了机械按键常见的抖动时间,也就是按下去瞬间触点弹开又闭合的过程。System_ToggleArm()和Buzzer_Silence()是应用层函数,分别放在主程序状态机里和蜂鸣器控制里,这样按键逻辑不跟具体业务耦合,后面改功能时只需要动这两个函数。还有人会问:中断回调里直接调用HAL_Delay(20)不是更省事?实际情况是外部中断回调优先级较高,如果在里面阻塞 20ms,系统里其它依赖时间标志的工作会集体卡住,比如 LCD 刷新和蜂鸣器提示音节奏。所以“中断置标志、主循环消抖”这种模式,不是一个风格问题,是可靠性问题。
4. 门禁主程序状态机:布防、撤防、报警三段逻辑与LCD实时刷新
4.1 用枚举定义门禁状态,状态切换集中在同一处
门禁系统实时响应外部事件,用一个有限状态机来组织最合适。这里只定义三个状态:撤防态、布防态、报警态。撤防态下 PIR 可以照常感应人,但不触发报警,适合主人白天在家的场景;布防态下 PIR 一旦检测到人体移动,立即进入报警态;报警态下蜂鸣器按设定节奏响,LCD 显示报警信息,直到按复位键回到布防态,或者按撤防键回到撤防态。
typedef enum { SYS_DISARMED = 0, SYS_ARMED, SYS_ALARM } SystemState; SystemState g_sys_state = SYS_DISARMED; uint32_t g_alarm_start_tick = 0;状态变量g_sys_state是整个程序的主心骨,所有外设任务都围绕它做分支。状态切换尽量收敛在一个函数里,不要在多个地方随意改g_sys_state,否则后面排查“为什么状态跳变异常”会非常痛苦。g_alarm_start_tick用来记录进入报警态的时刻,后续如果要做“报警持续 30 秒后自动关闭蜂鸣器”的逻辑,直接拿当前时间和它相减就行。
void System_ToggleArm(void) { if (g_sys_state == SYS_ARMED) { g_sys_state = SYS_DISARMED; } else if (g_sys_state == SYS_DISARMED) { g_sys_state = SYS_ARMED; } else if (g_sys_state == SYS_ALARM) { // 报警态按复位键,回到布防态 g_sys_state = SYS_ARMED; } } void System_TriggerAlarm(void) { if (g_sys_state == SYS_ARMED) { g_sys_state = SYS_ALARM; g_alarm_start_tick = HAL_GetTick(); } }这两个函数内部都检查了当前状态,避免在错误的状态下触发切换。比如System_TriggerAlarm()只在布防态才生效,如果当前是撤防态或者已经在报警态,函数什么也不做,直接返回。这种防御式写法能防止 PIR 信号抖动导致的状态错乱。
4.2 PIR 检测与蜂鸣器报警逻辑,如何处理持续报警
主循环按三件事来跑:读 PIR、刷蜂鸣器、刷 LCD。PIR 的检测逻辑最简单,读引脚电平,布防态下为高电平就触发报警。
void Pir_Task(void) { GPIO_PinState level = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8); if (level == GPIO_PIN_SET) { System_TriggerAlarm(); } }蜂鸣器任务分两种情况。有源蜂鸣器直接用 GPIO 翻转,每隔 200ms 翻转一次电平,就能得到“嘀-嘀-嘀”的间隔报警声;用 PWM 的无源蜂鸣器则是切换频率,形成起伏的警笛音调。这里先给有源蜂鸣器的实现,无源蜂鸣器的高级音效放到最后一章展开。
void Buzzer_Task(void) { static uint32_t last_toggle = 0; if (g_sys_state == SYS_ALARM) { if (HAL_GetTick() - last_toggle >= 200) { last_toggle = HAL_GetTick(); HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_9); } } else { // 非报警态:蜂鸣器必须保持低电平,避免误响 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_RESET); } }注意静态变量last_toggle只在 Buzzer_Task 内部有效,这是为了让翻转节奏有个基准点。每次报警状态时基不同,如果从 0 开始计时,第一次翻转的间隔可能是 0 到 200ms 之间任意值,听起来节奏不稳。静态变量保持上次翻转的时刻,进入报警态后第一次翻转也会紧跟上一次的时间基准,声音更均匀。
4.3 LCD1602 按状态刷新,避免每帧重复写入
LCD 刷新这里最容易犯的错是:主循环每一圈都把同样的字符串往 LCD 上写一遍。LCD1602 写入字符是比较耗时的操作,尤其是主循环里还有其他任务时,重复刷新会拖慢整体响应,而且屏幕会闪。正确做法是记录上一次显示的状态,只有状态变化时才重新写屏幕。
void Lcd_Task(void) { static SystemState last_state = 0xFF; // 初始值设成不可能出现的值,强制首次刷新 if (last_state == g_sys_state) { return; } last_state = g_sys_state; if (g_sys_state == SYS_DISARMED) { LCD_ShowString(0, 0, "System: DISARM"); LCD_ShowString(1, 0, "SW1: Arm"); } else if (g_sys_state == SYS_ARMED) { LCD_ShowString(0, 0, "System: ARMED"); LCD_ShowString(1, 0, "Welcome!"); } else { LCD_ShowString(0, 0, "! ALARM ! ALARM"); LCD_ShowString(1, 0, "PIR Triggered"); } }last_state初始化成 0xFF,因为枚举里合法值是 0、1、2,第一次进入函数时必然不相等,所以上电后一定会刷新一次屏幕。这个“首帧强制刷新”的做法,用在 LCD、OLED、数码管这类显示外设上都很通用,可以避免刚上电时屏幕残留上一次的数据。液晶的静态显示不需要持续刷新,保持电平就能一直显示,所以状态不变化时跳过早刷新行为,对整个系统的实时性有明显提升。
5. 参数调优与实物调试:人体感应距离、蜂鸣器音效和无源蜂鸣器非阻塞播放
5.1 四个必检硬件参数与排查方法
实物板子焊完或者面包板插好后,上电第一件事不是下载程序,而是先测量供电和电平。用万用表检查 STM32 的 3.3V 和 5V 是否正常,再检查 HC-SR501 的输出脚静态电压是否为低,蜂鸣器两端是否有意外压差。另一件容易被忽略的事是 Keil5 项目安装的芯片支持包版本是否匹配,STM32F103C8T6 用的是 F1 系列支持包,如果芯片包没装好,下载程序会报错。以下表格列出调试时最常遇到的几个现象、可能原因和检查顺序。
| 现象 | 优先检查项 | 处理建议 |
|---|---|---|
| LCD1602 有背光无字符 | Vo 对比度电位器 | 缓慢转动 10k 电位器,找字符最清晰的点 |
| LCD1602 显示乱码或全方块 | 数据线 D4~D7 接错位 | 用万用表逐个量 PB12~PB15 到 LCD 各引脚的通断 |
| PIR 一直输出高电平 | 延时旋钮位置和跳帽模式 | 延时旋到最小,跳帽切到 H 模式 |
| PIR 感应距离太短 | 灵敏度旋钮和透镜 | 灵敏度顺时针旋到底,确认菲涅尔透镜未被遮挡 |
| 蜂鸣器声音低沉无力 | 三极管型号和基极电阻 | 确认使用 SS8050 而非 S8550,基极电阻降到 1kΩ |
| Proteus 仿真里蜂鸣器不响 | 仿真模型频率参数 | 有源蜂鸣器模型需设置额定电压,无源模型需加方波源 |
其中 PIR 距离问题的坑最深。HC-SR501 的灵敏度电位器顺时针调节时是增大灵敏度,但要注意它和延时电位器互相干扰:调整灵敏度的过程中手指靠近模块,会同时触发模块内部的延时逻辑,导致输出电平短暂拉高,容易被误判成“坏了”。正确的调校顺序是:先固定延时旋钮到最小,再调灵敏度,最后根据实际场景重新微调延时。整个过程必须断电后调整电位器,避免人体接近对测试结果的影响。
5.2 进阶:无源蜂鸣器非阻塞播放高低交替警笛音
如果蜂鸣器选用的是无源款,用 PWM 输出可以做一个比固定频率“嘀嘀声”更有辨识度的警笛音效。原理很简单:无源蜂鸣器需要外部给方波才能发声,方波频率由定时器的自动重装载值决定,只要把自动重装载值在两个频率之间来回切换,就能产生“呜-呜-呜”的起伏效果。核心是用定时器通道输出 PWM,在报警任务里定期修改自动重装载值。
// 假设 PA9 配置为 TIM1_CH1,输入时钟 72MHz // 通过修改下面两个值,即可改变蜂鸣器发声频率 void Buzzer_SetFreq(uint32_t freq) { uint32_t period = 72000000UL / freq; __HAL_TIM_SET_AUTORELOAD(&htim1, period - 1); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, period / 2); } void Buzzer_Task(void) { static uint32_t last_switch = 0; static uint8_t freq_high_phase = 1; if (g_sys_state == SYS_ALARM) { if (HAL_GetTick() - last_switch >= 300) { last_switch = HAL_GetTick(); freq_high_phase = !freq_high_phase; if (freq_high_phase) { Buzzer_SetFreq(2000); // 高频段,听感更尖锐 } else { Buzzer_SetFreq(1200); // 低频段,听感更低沉 } } // 启动一次 PWM,避免每次切换频率后重复启动 HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); } else { HAL_TIM_PWM_Stop(&htim1, TIM_CHANNEL_1); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_RESET); } }Buzzer_SetFreq函数里的 72000000UL 是 TIM1 的输入时钟频率,如果系统时钟不是 72MHz,这个值要跟着改。period = 72000000UL / freq计算的是一个完整方波周期需要多少时钟节拍,然后写入自动重装载寄存器。占空比设成 50%,即比较值等于周期的一半,这样蜂鸣器驱动信号的谐波分量最干净,音量也最大。非阻塞的体现是:每次频率切换只改一个寄存器,不需要阻塞延时,主循环的其他任务照常执行。失谐的关键点是HAL_TIM_PWM_Start和HAL_TIM_PWM_Stop不能放错位置,如果每 300ms 都启动一次 PWM,定时器会反复从零计数,声音节奏反而混乱。把 PWM 启动放在报警分支的公共路径里,频率切换只改自动重装载值,就能得到干净的警笛效果。HAL 库的这套写法直接套用在各个毕业设计里,从有源蜂鸣器换成无源蜂鸣器,只需要改这一个任务函数和 GPIO 复用配置。
本文还有配套的精品资源,点击获取