STM32之beep、多文件、延迟、按键以及呼吸灯
一、Beep控制
原理图分析:
- 蜂鸣器三极管控制引脚对应 MCU PB8。
- 当前蜂鸣器对应的电路中,三极管是 NPN 三极管,当前【基极】存在小电流,当前三极管导通。要求对应 PB8 引脚对外输出电压 / 电流。
- 当前 PB8 输出高电平,当前三极管导通,BEEP 工作。PB8 提供一个低电平,三极管未导通,BEEP 不工作。当前 PB8 要求的 GPIO 工作模式必须是【推挽模式】。
代码流程:
- 时钟使能,RCC->APB2ENR。
- 设置当前 GPIO PB8 工作模式必须是【推挽模式】。需要通过 GPIOB->CRH 配置 PB8 引脚工作模式。需要配置的寄存器位置是【位 3:0】。
- 利用 GPIOx_ODR 控制输出电平情况。高电平 BEEP 工作,低电平 BEEP 停止工作。
代码实现:
#include "stm32f10x.h"
/*
STM32 核心头文件,标准头文件,当前对应 STM32F10x 系列,
当前使用的芯片是 STM32F103ZET6
开发中,需要使用相关函数,相关类型,相关配置都在当前
头文件中。
*/
void Delay(u32 tim);
int main(void)
{// 1. 时钟使能,RCC 控制使用 GPIOB GPIO组RCC->APB2ENR = 0x01 << 3;// 2. 配置 GPIOB --> PB8 推挽模式,需要使用 CRHGPIOB->CRH &= ~(0x0F);GPIOB->CRH |= 0x03; // 推挽模式,同时输出速度最高位 50MHz/*3. ODR 控制输出高电平或者低电平高电平 BEEP 工作低电平 BEEP 不工作*/while (1){// 控制输出高电平GPIOB->ODR |= 0x01 << 8;Delay(200000);// 控制输出低电平GPIOB->ODR &= ~(0x01 << 8);Delay(200000);}
}
void Delay(u32 tim)
{while (tim--){for (int i = 0; i < 72; i++){}}
}
二、keil的多文件编程
将 LED 和 BEEP 控制模式进行函数封装,同时利用多文件方式,将不同的模块划分为不同的文件,从而实现模块化开发。
需要创建不同模块的 .c 和 .h。同时完成 Keil5 工程配置。
估计模块需求创建新文件,需要提供 .c 和 .h:
- 利用 Ctrl + N 创建新文件。
- Ctrl + S 保存当前文件。
- 后续操作如图。
代码实现:
led.c:
#include "led.h"/*** @brief LED初始化函数:配置GPIO引脚为推挽输出模式*/
void Led_Init(void)
{// 1. 使能GPIOB和GPIOE时钟RCC->APB2ENR |= 0x01 << 3; // GPIOB时钟使能RCC->APB2ENR |= 0x01 << 6; // GPIOE时钟使能// 2. 配置PB5为推挽输出模式(50MHz)GPIOB->CRL &= ~(0x0F << 20); // 清除PB5原有配置GPIOB->CRL |= 0x03 << 20; // 配置PB5为推挽输出// 3. 配置PE5为推挽输出模式(50MHz)GPIOE->CRL &= ~(0x0F << 20); // 清除PE5原有配置GPIOE->CRL |= 0x03 << 20; // 配置PE5为推挽输出// 4. 默认输出高电平,LED初始状态为熄灭GPIOB->ODR |= 0x01 << 5; // PB5输出高电平,LED0灭GPIOE->ODR |= 0x01 << 5; // PE5输出高电平,LED1灭
}/*** @brief 控制LED0的亮灭* * @param flag 1-点亮LED0,0-熄灭LED0*/
void Led0_Ctrl(u8 flag)
{if (flag){// 输出低电平,点亮LED0GPIOB->ODR &= ~(0x01 << 5);}else{// 输出高电平,熄灭LED0GPIOB->ODR |= 0x01 << 5;}
}/*** @brief 控制LED1的亮灭* * @param flag 1-点亮LED1,0-熄灭LED1*/
void Led1_Ctrl(u8 flag)
{if (flag){// 输出低电平,点亮LED1GPIOE->ODR &= ~(0x01 << 5);}else{// 输出高电平,熄灭LED1GPIOE->ODR |= 0x01 << 5;}
}/*** @brief 实现LED0和LED1交替闪烁效果*/
void Led_Blink(void)
{Led0_Ctrl(1); // 点亮LED0Led1_Ctrl(0); // 熄灭LED1Delay_ms(500); // 延时500msLed0_Ctrl(0); // 熄灭LED0Led1_Ctrl(1); // 点亮LED1Delay_ms(500); // 延时500ms
}
led.h:
#ifndef _LED_H
#define _LED_H#include "stm32f10x.h"
#include "delay.h"/*** @brief 当前开发板中 DS0 和 DS1 LED 灯配置初始化*/
void Led_Init(void);/*** @brief DS0 对应的 LED0 灯控制函数* * @param flag 控制标志:1-LED0亮,0-LED0灭*/
void Led0_Ctrl(u8 flag);/*** @brief DS1 对应的 LED1 灯控制函数* * @param flag 控制标志:1-LED1亮,0-LED1灭*/
void Led1_Ctrl(u8 flag);/*** @brief LED0 和 LED1 交替闪烁*/
void Led_Blink(void);#endif
三、__Nop()函数实现延时操作
-
delay.h:头文件
- 使用条件编译防止重复引用(
#ifndef _DELAY_H
等) - 声明了微秒级延时(
Delay_us
)和毫秒级延时(Delay_ms
)函数 - 包含函数注释,说明功能和参数含义
- 使用条件编译防止重复引用(
-
delay.c:源文件
- 实现了头文件中声明的两个延时函数
Delay_us
:通过连续调用 72 个__NOP()
空操作实现约 1 微秒的延时(基于 STM32F103ZET6 的 72MHz 主频设计)Delay_ms
:通过循环调用Delay_us(1000)
实现毫秒级延时(1 毫秒 = 1000 微秒)
使用时,只需在需要延时的地方调用这两个函数即可,例如:
Delay_us(500)
:延时 500 微秒Delay_ms(100)
:延时 100 毫秒
该延时模块采用纯寄存器级操作,不依赖系统滴答定时器,适合在初始化阶段或对延时精度要求不极高的场景使用。
delay.c:
#include "delay.h"/*** @brief 微秒级延时函数* * @param us 要延时的微秒数* @note 基于STM32F103ZET6的72MHz主频设计,1个__NOP()占用约13.889ns* 72个__NOP()约占用1us(72 * 13.889ns ≈ 1000ns)*/
void Delay_us(u32 us)
{while (us--){// 连续调用72个空操作,实现约1us的延时__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();}
}/*** @brief 毫秒级延时函数* * @param ms 要延时的毫秒数* @note 基于微秒延时函数实现,1ms = 1000us*/
void Delay_ms(u32 ms)
{while (ms--){Delay_us(1000); // 调用1000次微秒延时,实现1ms延时}
}
delay.h:
#ifndef _DELAY_H
#define _DELAY_H
#include "stm32f10x.h"/*** @brief 延时微秒控制函数,延时单位是 us** @param us 延时微秒时间(取值范围:0~4294967295)*/
void Delay_us(u32 us);/*** @brief 延时毫秒控制函数,延时单位是 ms** @param ms 延时毫秒时间(取值范围:0~4294967295)*/
void Delay_ms(u32 ms);#endif
四、可编程按键控制
原理图分析:
- 根据原理图分析,KEY0、KEY1、KEY2 按键按下之后,当前电路接地,对应低电平。
- KEY_UP 按键按下之后,当前电路接 3.3V,对应高电平。
- 如果想要通过按键按下之后的电平切换作为按键判断条件。KEY0、KEY1、KEY2 对应 MCU 引脚默认【高电平】,KEY_UP 对应 MCU 引脚默认【低电平】。
- KEY0、KEY1、KEY2 对应 MCU 引脚为【上拉输入模式】。
- KEY_UP 对应 MCU 引脚为【下拉输入模式】。
KEY0、KEY1、KEY2 引脚对应关系:
- KEY0 ==> PE4;KEY1 ==> PE3;KEY2 ==> PE2。
KEY_UP 引脚对应关系:
- KEY_UP ==> PA0。
开发流程:
- 时钟使能,需要使能的 GPIO 组为 GPIOE 和 GPIOA
- RCC->APB2ENR 使用 GPIOE 和 GPIOA
- 控制 GPIO 工作模式
- PA0【下拉输入模式】
- PE2 ~ PE4【上拉输入模式】
- 利用 GPIOx_IDR 寄存器,读取对应引脚的电平状态,判断对应按键是否按下。
- PA0 对应 KEY_UP 按下,电平状态从低电平切换到高电平状态。
- PE2 ~ PE4 对应 KEY2、KEY1、KEY0 按下,电平状态从高电平切换到低电平状态。
寄存器文档分析:
代码实现:
key.c:
#include "key.h"/*** @brief 按键初始化函数* 配置KEY0(PE4)、KEY1(PE3)、KEY2(PE2)为上拉输入* 配置KEY_UP(PA0)为下拉输入*/
void Key_Init(void)
{// 使能GPIOA和GPIOE时钟RCC->APB2ENR |= (1 << 2); // 使能GPIOA时钟RCC->APB2ENR |= (1 << 6); // 使能GPIOE时钟// 配置PE2、PE3、PE4为上拉输入GPIOE->CRL &= ~(0x0F << (2*4)); // 清除PE2配置GPIOE->CRL &= ~(0x0F << (3*4)); // 清除PE3配置GPIOE->CRL &= ~(0x0F << (4*4)); // 清除PE4配置GPIOE->CRL |= (0x08 << (2*4)); // PE2: 上拉输入(1000)GPIOE->CRL |= (0x08 << (3*4)); // PE3: 上拉输入(1000)GPIOE->CRL |= (0x08 << (4*4)); // PE4: 上拉输入(1000)GPIOE->ODR |= (1 << 2); // PE2上拉GPIOE->ODR |= (1 << 3); // PE3上拉GPIOE->ODR |= (1 << 4); // PE4上拉// 配置PA0为下拉输入GPIOA->CRL &= ~(0x0F << (0*4)); // 清除PA0配置GPIOA->CRL |= (0x08 << (0*4)); // PA0: 下拉输入(1000)// PA0默认下拉,无需额外配置ODR
}/*** @brief 获取按键状态* @return 按键值,分别对应KEY0、KEY1、KEY2、KEY_UP或无按键*/
u8 Key_GetValue(void)
{// 检测KEY0(PE4)if (!(GPIOE->IDR & (1 << 4))) // 低电平表示按下{Delay_ms(10); // 消抖if (!(GPIOE->IDR & (1 << 4))){while (!(GPIOE->IDR & (1 << 4))); // 等待释放return KEY0_VALUE;}}// 检测KEY1(PE3)if (!(GPIOE->IDR & (1 << 3))) // 低电平表示按下{Delay_ms(10); // 消抖if (!(GPIOE->IDR & (1 << 3))){while (!(GPIOE->IDR & (1 << 3))); // 等待释放return KEY1_VALUE;}}// 检测KEY2(PE2)if (!(GPIOE->IDR & (1 << 2))) // 低电平表示按下{Delay_ms(10); // 消抖if (!(GPIOE->IDR & (1 << 2))){while (!(GPIOE->IDR & (1 << 2))); // 等待释放return KEY2_VALUE;}}// 检测KEY_UP(PA0)if (GPIOA->IDR & (1 << 0)) // 高电平表示按下{Delay_ms(10); // 消抖if (GPIOA->IDR & (1 << 0)){while (GPIOA->IDR & (1 << 0)); // 等待释放return KEY_UP_VALUE;}}return NO_KEY_PRESSED; // 无按键按下
}
key.h:
#ifndef _KEY_H
#define _KEY_H
#include "stm32f10x.h"
#include "delay.h"// 按键值定义
#define KEY0_VALUE (0) // KEY0按键对应返回值
#define KEY1_VALUE (1) // KEY1按键对应返回值
#define KEY2_VALUE (2) // KEY2按键对应返回值
#define KEY_UP_VALUE (3) // 上拉按键对应返回值
#define NO_KEY_PRESSED (4) // 无按键按下返回值// 函数声明
void Key_Init(void); // 按键初始化函数
u8 Key_GetValue(void); // 获取按键值函数#endif
main.c:
#include "stm32f10x.h"
#include "led.h"
#include "key.h"
#include "delay.h"int main(void)
{// 初始化外设Delay_Init(); // 延时初始化Led_Init(); // LED初始化Key_Init(); // 按键初始化while (1){u8 key_value = Key_GetValue(); // 获取按键值// 根据按键值控制LEDswitch (key_value){case KEY0_VALUE:Led0_Ctrl(1); // 打开LED0break;case KEY1_VALUE:Led0_Ctrl(0); // 关闭LED0break;case KEY2_VALUE:Led1_Ctrl(1); // 打开LED1break;case KEY_UP_VALUE:Led1_Ctrl(0); // 关闭LED1break;default:// 无按键操作时无需处理break;}}
}
五、闪烁灯与呼吸灯
完整代码:
led.c:
#include "led.h"void Led_Init(void)
{//RCC对PB端口和PE端口时钟使能RCC->APB2ENR |= (0x01 << 3 | 0x01 << 6);//GPIO寄存器配置GPIOB->CRL &= ~(0xf << 20);GPIOE->CRL &= ~(0xf << 20);GPIOB->CRL |= (0x1 << 20);GPIOE->CRL |= (0x1 << 20);
}
void Led_Ctrl(int value)
{if(value){GPIOB->ODR &= ~(0x1 << 5);GPIOE->ODR &= ~(0x1 << 5);}else{GPIOB->ODR |= (0x1 << 5);GPIOE->ODR |= (0x1 << 5);}
}
void Led_Blink(void)
{//低电平LED0灯亮 高电平LED1灯灭 GPIOB->ODR &= ~(0x1 << 5);GPIOE->ODR |= (0x1 << 5);Delay_ms(300);//高电平LED0灯灭 低电平LED1灯亮GPIOB->ODR |= (0x1 << 5);GPIOE->ODR &= ~(0x1 << 5);Delay_ms(300);
}
void Led_Breath(void)
{int j = 0;while(1){GPIOB->ODR &= ~(0x1 << 5);GPIOE->ODR |= (0x1 << 5);Delay_us(5000 - j);GPIOE->ODR &= ~(0x1 << 5);GPIOB->ODR |= (0x1 << 5);Delay_us(j);j += 10;if(j == 5000){break;}}j = 0;while(1){GPIOE->ODR &= ~(0x1 << 5);GPIOB->ODR |= (0x1 << 5);Delay_us(5000 - j);GPIOB->ODR &= ~(0x1 << 5);GPIOE->ODR |= (0x1 << 5);Delay_us(j);j += 10;if(j == 5000){break;}}
}
led.h:
#ifndef __LED_H
#define __LED_H#include "stm32f10x.h"
#include <stdio.h>
#include "delay.h"void Led_Init(void);
void Led_Ctrl(int value);
void Led_Blink(void);
void Led_Breath(void);#endif
main.c:
#include "stm32f10x.h"
#include "led.h"
#include "delay.h"int main(void)
{Led_Init();while(1){//Led_Ctrl(3);Led_Blink();//Led_Breath();}
}
代码解析:
一、
Led_Blink(void)
函数:LED 交替闪烁功能该函数实现 两个 LED 灯(LED0 和 LED1)固定频率交替亮灭 的效果,具体逻辑如下:
初始状态设置
GPIOB->ODR &= ~(0x1 << 5);
:将 GPIOB 端口的第 5 位(PB5)置为低电平(假设 LED0 接 PB5,低电平点亮),此时 LED0 亮。GPIOE->ODR |= (0x1 << 5);
:将 GPIOE 端口的第 5 位(PE5)置为高电平(假设 LED1 接 PE5,高电平熄灭),此时 LED1 灭。延时保持
Delay_ms(300);
:维持 “LED0 亮、LED1 灭” 的状态 300 毫秒。状态反转
GPIOB->ODR |= (0x1 << 5);
:PB5 置为高电平,LED0 灭。GPIOE->ODR &= ~(0x1 << 5);
:PE5 置为低电平,LED1 亮。再次延时
Delay_ms(300);
:维持 “LED0 灭、LED1 亮” 的状态 300 毫秒。效果总结:
LED0 和 LED1 以 600 毫秒为一个周期 交替闪烁(各亮 300ms,灭 300ms),状态切换清晰、节奏固定,类似 “红绿灯交替” 的效果。二、
Led_Breath(void)
函数:LED 呼吸灯效果该函数通过 改变 LED 亮灭时间的占空比,实现两个 LED 灯(LED0 和 LED1)交替 “呼吸”(亮度逐渐变化)的效果,具体分为两个阶段:
阶段 1:LED0 逐渐变暗,LED1 逐渐变亮
初始化变量
j = 0
,通过while(1)
循环逐步调整亮灭时间:
- 先让 LED0 亮(PB5 低电平)、LED1 灭(PE5 高电平),并延时
5000 - j
微秒(随j
增大,此延时逐渐减小)。- 再让 LED0 灭(PB5 高电平)、LED1 亮(PE5 低电平),并延时
j
微秒(随j
增大,此延时逐渐增大)。- 每次循环
j += 10
,直到j = 5000
时退出循环。过程解析:
- 初始时
j=0
:LED0 亮 5000us(常亮),LED1 灭 0us(常灭)→ LED0 最亮,LED1 最暗。- 随着
j
增大:LED0 亮的时间缩短、灭的时间增长 → 亮度逐渐降低;LED1 亮的时间增长、灭的时间缩短 → 亮度逐渐升高。- 结束时
j=5000
:LED0 亮 0us(常灭),LED1 亮 5000us(常亮)→ LED0 最暗,LED1 最亮。阶段 2:LED1 逐渐变暗,LED0 逐渐变亮
重置
j = 0
,再次进入while(1)
循环,逻辑与阶段 1 对称:
- 先让 LED1 亮(PE5 低电平)、LED0 灭(PB5 高电平),延时
5000 - j
微秒(随j
增大而减小)。- 再让 LED1 灭(PE5 高电平)、LED0 亮(PB5 低电平),延时
j
微秒(随j
增大而增大)。- 直到
j = 5000
时退出循环。过程解析:
- 初始时
j=0
:LED1 常亮,LED0 常灭 → LED1 最亮,LED0 最暗。- 随着
j
增大:LED1 亮度逐渐降低,LED0 亮度逐渐升高。- 结束时
j=5000
:LED1 常灭,LED0 常亮 → LED1 最暗,LED0 最亮。效果总结:
两个 LED 灯交替实现 “呼吸” 效果:
- 先 LED0 从最亮逐渐变暗至熄灭,同时 LED1 从熄灭逐渐变亮至最亮;
- 再 LED1 从最亮逐渐变暗至熄灭,同时 LED0 从熄灭逐渐变亮至最亮。
整体效果类似人呼吸时的 “明暗渐变”,过渡平滑自然。
https://github.com/0voice