通过LL库在stm32F407实现按键扫描
最简单的按键就是类似一根导线通跟不通的问题,按键按下"导线"导通,按键松开"导线"开路。为了识别按键是按下还是松开状态,可以使用GPIO的输入功能,把按键的一个引脚接地(GND),按键的另一引脚接STM32引脚,这样按键按下时"导线"导通,STM32引脚就是低电平,程序读取到低电平就可以判断到按键处于按下状态。另外,为保证按键没有按下时为高电平,或者按键松开后引脚会自动变为高电平,一般需要在STM32引脚接一个上拉电阻,具体电路设计参考后面原理图。为读取按键输入状态,需要把对应的STM32引脚设置为普通输入模式。
cubmax配置在STM32之LL库使用(一)
一、GPIO配置
c
/* GPIO初始化定义 */
#define KEY1_RCC_CLK_ENABLE() LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOE)
#define KEY1_Pin LL_GPIO_PIN_0
#define KEY1_GPIO_Port GPIOE
#define KEY1_DOWN_LEVEL RESET /* 根据原理图,KEY1按下时引脚为低电平,所以这里设置为0 */
#define KEY2_RCC_CLK_ENABLE() LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOE)
#define KEY2_Pin LL_GPIO_PIN_1
#define KEY2_GPIO_Port GPIOE
#define KEY2_DOWN_LEVEL RESET /* 根据原理图,KEY2按下时引脚为低电平,所以这里设置为0 */
#define KEY3_RCC_CLK_ENABLE() LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOE)
#define KEY3_Pin LL_GPIO_PIN_2
#define KEY3_GPIO_Port GPIOE
#define KEY3_DOWN_LEVEL RESET /* 根据原理图,KEY3按下时引脚为低电平,所以这里设置为0 */
#define KEY4_RCC_CLK_ENABLE() LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOE)
#define KEY4_Pin LL_GPIO_PIN_3
#define KEY4_GPIO_Port GPIOE
#define KEY4_DOWN_LEVEL RESET /* 根据原理图,KEY4按下时引脚为低电平,所以这里设置为0 */
#define KEY5_RCC_CLK_ENABLE() LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOE)
#define KEY5_Pin LL_GPIO_PIN_4
#define KEY5_GPIO_Port GPIOE
#define KEY5_DOWN_LEVEL RESET /* 根据原理图,KEY4按下时引脚为低电平,所以这里设置为0 */
void KEY_GPIO_Init(void)
{
LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
/* 使能(开启)KEY引脚对应IO端口时钟 */
KEY1_RCC_CLK_ENABLE();
KEY2_RCC_CLK_ENABLE();
KEY3_RCC_CLK_ENABLE();
KEY4_RCC_CLK_ENABLE();
KEY5_RCC_CLK_ENABLE();
/************************* 配置KEY1 GPIO:输入上拉模式 *************************/
GPIO_InitStruct.Pin = KEY1_Pin; // 按键的I/O编号
GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;// 输入模式
GPIO_InitStruct.Pull = LL_GPIO_PULL_UP; // 按键有效电平是低电平的时候是上拉
LL_GPIO_Init(KEY1_GPIO_Port, &GPIO_InitStruct);
/************************* 配置KEY2 GPIO:输入上拉模式 *************************/
GPIO_InitStruct.Pin = KEY2_Pin; // 按键的I/O编号
GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;// 输入模式
GPIO_InitStruct.Pull = LL_GPIO_PULL_UP; // 按键有效电平是低电平的时候是上拉
LL_GPIO_Init(KEY2_GPIO_Port, &GPIO_InitStruct);
/************************* 配置KE3 GPIO:输入上拉模式 *************************/
GPIO_InitStruct.Pin = KEY3_Pin; // 按键的I/O编号
GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;// 输入模式
GPIO_InitStruct.Pull = LL_GPIO_PULL_UP; // 按键有效电平是低电平的时候是上拉
LL_GPIO_Init(KEY3_GPIO_Port, &GPIO_InitStruct);
/************************* 配置KEY4 GPIO:输入上拉模式 *************************/
GPIO_InitStruct.Pin = KEY4_Pin; // 按键的I/O编号
GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;// 输入模式
GPIO_InitStruct.Pull = LL_GPIO_PULL_UP; // 按键有效电平是低电平的时候是上拉
LL_GPIO_Init(KEY4_GPIO_Port, &GPIO_InitStruct);
/************************* 配置KEY5 GPIO:输入上拉模式 *************************/
GPIO_InitStruct.Pin = KEY5_Pin; // 按键的I/O编号
GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;// 输入模式
GPIO_InitStruct.Pull = LL_GPIO_PULL_UP; // 按键有效电平是低电平的时候是上拉
LL_GPIO_Init(KEY5_GPIO_Port, &GPIO_InitStruct);
}
二、读取按键状态
c
KEYState_TypeDef KEY1_StateRead(void)
{
/* 读取此时按键值并判断是否是被按下状态,如果是被按下状态进入函数内 */
if(LL_GPIO_IsInputPinSet(KEY1_GPIO_Port, KEY1_Pin) == KEY1_DOWN_LEVEL)
{
/* 延时一小段时间,消除抖动
* 延时时间后再来判断按键状态,如果还是按下状态说明按键确实被按下 */
LL_mDelay(30);
if(LL_GPIO_IsInputPinSet(KEY1_GPIO_Port, KEY1_Pin) == KEY1_DOWN_LEVEL)
{
/* 等待按键弹开才退出按键扫描函数
* 按键扫描完毕,确定按键被按下,返回按键被按下状态 */
while(LL_GPIO_IsInputPinSet(KEY1_GPIO_Port, KEY1_Pin) == KEY1_DOWN_LEVEL);
return KEY_DOWN;
}
}
/* 按键没被按下,返回没被按下状态 */
return KEY_UP;
}
KEYState_TypeDef KEY2_StateRead(void)
{
/* 读取此时按键值并判断是否是被按下状态,如果是被按下状态进入函数内 */
if(LL_GPIO_IsInputPinSet(KEY2_GPIO_Port, KEY2_Pin) == KEY2_DOWN_LEVEL)
{
/* 延时一小段时间,消除抖动
* 延时时间后再来判断按键状态,如果还是按下状态说明按键确实被按下 */
LL_mDelay(30);
if(LL_GPIO_IsInputPinSet(KEY2_GPIO_Port, KEY2_Pin) == KEY2_DOWN_LEVEL)
{
/* 等待按键弹开才退出按键扫描函数
* 按键扫描完毕,确定按键被按下,返回按键被按下状态 */
while(LL_GPIO_IsInputPinSet(KEY2_GPIO_Port, KEY2_Pin) == KEY2_DOWN_LEVEL);
return KEY_DOWN;
}
}
/* 按键没被按下,返回没被按下状态 */
return KEY_UP;
}
LL_mDelay 是基于 SysTick 硬件定时器实现的 ,但它的实现机制与 HAL 库的 HAL_Delay 有所本质区别。
在 LL 库中,LL_mDelay 通常位于 stm32f4xx_ll_utils.h(或对应的系列文件)中。它的工作原理可以概括为以下几点:
1. 它是如何工作的?
不同于 HAL 库依赖中断累加变量(uwTick),LL 库的 LL_mDelay 采用的是**阻塞式查询(Polling)**硬件寄存器的方式:
- 频率计算:它首先会根据当前系统主频(HCLK)计算出 1 毫秒对应的 SysTick 递减计数值。
- 寄存器监测 :它会直接读取 SysTick 的当前数值寄存器(VAL)和控制寄存器(CTRL)中的
COUNTFLAG位。 - 循环等待:通过循环检测硬件标志位来判断时间是否到达。
2. 使用前提
要让 LL_mDelay 正常工作,你必须在初始化时调用:
c
LL_InitTick(HCLK_Frequency, 1000); // 配置 SysTick 每秒产生 1000 次节拍(即 1ms)
如果这一步没有正确配置(CubeMX 生成 LL 代码时通常会自动在 SystemClock_Config 中处理),LL_mDelay 将无法获得准确的时基。
3. 与 HAL_Delay 的主要区别
| 特性 | HAL_Delay | LL_mDelay |
|---|---|---|
| 实现方式 | 依赖 SysTick 中断累加全局变量 | 阻塞式查询 SysTick 硬件寄存器 |
| 中断依赖 | 必须开启 SysTick 中断 | 不需要开启 SysTick 中断(仅需硬件运行) |
| 优先级影响 | 如果在比 SysTick 优先级高的中断里调用,会死机 | 不受中断优先级影响(因为它直接看硬件状态) |
| 灵活性 | 易于被 RTOS 接管 | 纯硬件行为,更接近裸机逻辑 |
4. 注意事项
- CPU 占用:由于是阻塞式查询,在延时期间 CPU 无法执行其他任务。
- 主频依赖:如果你手动修改了系统时钟频率而没有重新调用初始化函数更新时基参数,延时会变得不准。
核心总结:
LL_mDelay 是对 SysTick 硬件的直接包装。它不需要你像在 HAL 库中那样去维护一个 Tick++ 的中断服务函数,这使得它在一些极其精简、不希望频繁进入中断的场景下非常高效。
温馨提示:
为了提高处理效率 并确保核心逻辑清晰 ,如果您想深入了解 LL_mDelay 内部是如何通过操作 SysTick->CTRL 寄存器来实现精准计时的,或者需要对比它在低功耗模式下的表现,我们可以针对"底层寄存器实现逻辑"进行结构化拆解讨论。
三、主函数
c
int main(void)
{
/********* 系统初始化配置开始(必须要的,一般不改) *****************/
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SYSCFG);//使能系统配置控制器时钟
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR);//使能电源接口时钟
NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);//设置中断优先级组别为2
SystemClock_Config();//初始化系统时钟
/********* 系统初始化配置结束(必须要的,一般不改) *****************/
LED_GPIO_Init();//LED初始化
KEY_GPIO_Init();//按键初始化
while (1)
{
if(KEY1_StateRead() == KEY_DOWN) //判断KEY1是不是按下状态
{
LED1_ON();
}
if(KEY2_StateRead() == KEY_DOWN) //判断KEY2是不是按下状态
{
LED2_ON();
}
if(KEY3_StateRead() == KEY_DOWN) //判断KEY3是不是按下状态
{
LED3_ON();
}
if(KEY4_StateRead() == KEY_DOWN) //判断KEY4是不是按下状态
{
LED1_TOGGLE();
LED2_TOGGLE();
LED3_TOGGLE();
}
if(KEY5_StateRead() == KEY_DOWN) //判断KEY5是不是按下状态
{
LED1_OFF();
LED2_OFF();
LED3_OFF();
}
}
}