32单片机学习记录2之按键
前置
按键检测需要高速轮询但是流水灯的延迟函数会卡CPU执行时间,通过按键开启流水灯能不能实现?
不要延迟函数要计数思想达到相同效果,就不会卡CPU了。
原理图
![](https://i-blog.csdnimg.cn/direct/9572350896344ddabacacbb87c09594d.png)
分析
看不懂电路图,K2,3,4是同一组IO口,不是给低电平亮就是给高电平亮。
K1同样给高低电平测试。
K1 电路特性(与 PA0 相连)
- 按键 K1 的一端连接到 GND(地),另一端通过上拉电阻 R24 (10kΩ) 接到 +3.3V,并同时连接到 IO 引脚 PA0。
- 特殊之处:
- PA0 除了作为普通 GPIO,还标注了 WKUP(唤醒功能),可能与系统的低功耗唤醒机制有关。
K2、K3、K4 的电路特性
- 按键 K2、K3、K4 的接法相同:
- 一端连接到 GND(地)。
- 另一端通过各自的上拉电阻(R12、R23、R26,均为 10kΩ)接到 +3.3V,并同时连接到 IO 引脚 PE2、PE3、PE4。
- 功能对比:
- K2、K3、K4 的按键电路用于普通 GPIO 按键检测 ,主要用于用户输入。
电平分析(K1 与 K2~K4 的共同点与区别)
K1(PA0)的电平变化:
- 未按下 :PA0 的电平通过下拉电阻(接地) R24 拉至低电平(GND)。
- 按下:按键 K1 闭合,PA0 直接接地,电平变为高电平(+3.3V)。
K2、K3、K4 的电平变化:
- 未按下:PE2、PE3、PE4 的电平通过各自的上拉电阻拉至高电平(+3.3V)。
- 按下:按键闭合,对应的 PE2、PE3、PE4 直接接地,电平变为低电平(0V)。
区别:
K1 与 K2~K4 的接法本质一致(上拉和接地相结合),电平变化规则完全相同,区别在于:
- K1 的功能特殊 :PA0 同时具备普通 GPIO 和 唤醒功能(WKUP)。
- 唤醒功能在低功耗模式下可能检测边沿触发的信号(高变低或低变高)来唤醒系统。
IO 配置建议
- K1(PA0,带唤醒功能) :
- 配置为输入模式。检测传递过来的电平值
- 若使用唤醒功能,需根据芯片规格手册选择唤醒边沿(通常是下降沿触发,即高变低)。
- 如果不使用唤醒功能,则按普通 GPIO 输入使用即可。
- K2、K3、K4(PE2、PE3、PE4) :
- 配置为输入模式,用于普通按键检测。
- 直接读取对应 GPIO 引脚电平,高电平表示未按下,低电平表示按下。
疑问1
IO口应该配置输出还是输入?是输出电平?还是接收电平?
在这个按键电路中,IO 口应该配置为输入模式,用于接收按键的电平信号。以下是详细解释:
按键电路的原理回顾
- 每个按键(K2~K4)与 IO 引脚相连,同时通过上拉电阻拉至高电平:
- 按键未按下:上拉电阻使 IO 引脚维持高电平(+3.3V)。
- 按键按下:按键闭合,IO 引脚通过按键直接连接到地(GND),电平变为低电平(0V)。
- 因此,IO 引脚的任务是检测电平变化(高电平和低电平),用以判断按键状态。
为什么 IO 口应该配置为输入?
- 输入模式的作用 :
- 配置为输入模式后,IO 引脚能够检测外部电路(如按键)产生的电平信号。
- 它不会主动输出电平,而是依赖电路(如上拉电阻和按键状态)来确定输入信号的高低。
- 输出模式不适合的原因 :
- 若配置为输出模式,IO 引脚会主动驱动电平(高或低),与按键电路的设计目的冲突。
- 例如:按键按下时,IO 引脚被按键短路到地,若同时输出高电平,将引发电流冲突,甚至可能损坏芯片。
IO 配置细节
- 模式 :
- 配置为输入模式。
- 若硬件电路中已接有外部上拉电阻(接+3.3V)(如 10kΩ),不需要再启用内部上拉功能。
- 电平读取 :
- 高电平(+3.3V):按键未按下(默认状态)。
- 低电平(0V):按键按下。
- 特殊情况(针对 PA0 的唤醒功能) :
- 如果使用唤醒功能,需在低功耗模式下配置 IO 引脚为边沿触发模式(例如下降沿触发),以检测按键按下事件。
疑问2
如果外部没有上拉电阻呢?是不是要开启上拉电阻提供一个参考电平?
是的,如果外部没有上拉电阻(比如电路中没有10kΩ电阻),就需要启用 IO 引脚的内部上拉电阻,以提供一个稳定的参考电平。以下是详细解释:
为什么需要上拉电阻?
- 提供默认状态(高电平) :
- 当按键未按下时,IO 引脚必须通过上拉电阻连接到高电平 (+3.3V),确保默认电平稳定。
- 如果没有上拉电阻,IO 引脚的电平可能悬空(浮空),容易受到外界噪声干扰,导致检测结果不可靠。
- 工作原理 :
- 按键未按下时:上拉电阻将 IO 引脚拉到高电平。
- 按键按下时:按键将 IO 引脚直接接地,电平变为低电平。
外部没有上拉电阻的情况下
- 如果外部电路未提供上拉电阻(比如 10kΩ 上拉电阻缺失),则需要启用芯片内部的上拉功能。
- 大多数微控制器(如 STM32)都支持内部上拉功能,具体方法是在配置 IO 口时设置为输入模式 + 内部上拉。
配置步骤
- 设置 IO 引脚为输入模式 :
- 配置 GPIO 为输入模式,能够检测外部电平。
- 启用内部上拉电阻 :
- 在 IO 配置寄存器中启用内部上拉电阻(通常是 GPIO_PUPDR 或类似的寄存器)。
- 启用后,IO 引脚在未按下时会通过内部上拉电阻(通常是几十 kΩ)保持高电平。
注意事项
- 内部上拉电阻 vs 外部上拉电阻 :
- 内部上拉电阻的阻值通常较大(一般 30kΩ ~ 50kΩ),相比外部电阻(如 10kΩ),拉高能力稍弱。
- 如果电路对噪声抗干扰要求较高,建议使用外部 10kΩ 上拉电阻来代替内部上拉。
- 不要同时使用外部和内部上拉 :
- 如果已经接了外部上拉电阻,就不需要再启用内部上拉,避免多重上拉影响电路性能。
为什么加一下普通电阻就拉高了电平呢?K1就更不理解了,因为有一端接地了,加一个电阻,也可以把K1拉高?
在这四个电路中,加入普通电阻确实能够拉高电平。这是因为这些电路存在上拉电阻的设计。
对于K1电路,即使一端接地,加入上拉电阻能将K1拉低。这是因为:
- K1的另一端连接到WKUP/PA0引脚,这个引脚通常是一个输入引脚。
- 当K1保持断开状态时,WKUP/PA0引脚的电平就会被下拉电阻R24拉低到0V。
- 当K1闭合时,WKUP/PA0引脚会被拉高到+3.3V。
所以,即使K1一端连接到GND,加上电阻R24后,WKUP/PA0引脚能够被拉低到0V。这样设计可以确保在K1断开时,WKUP/PA0引脚处于低电平状态,以便单片机能够检测到按键的松开。
分析一下K2、K3、K4这些电路。它们的电路结构是相似的。
以K2为例:
- K2的一端连接到KEY2/PE2引脚
- K2的另一端连接到正常的3.3V电源
- 在K2断开的情况下,KEY2/PE2引脚会通过R12这个10K的上拉电阻被拉高到3.3V
- 当K2闭合时,KEY2/PE2引脚会被拉低到接地
这种上拉电阻的设计能够确保在按键断开时,微控制器能检测到高电平信号,从而识别按键松开的状态。当按键闭合时,则会被拉低到接地电平,微控制器能检测到按键按下。
这就是为什么在这些电路中不使用微控制器内部的上拉/下拉电阻,而是在外部加上独立的上拉电阻的主要原因。
内部上拉/下拉电阻的驱动能力通常较弱,无法可靠地拉高或拉低电平,尤其是当电路中有较大的电容负载时。相比之下,在外部加上10K等较大阻值的上拉电阻,能够可靠地将引脚电平拉高到VCC电源电平,提高电路的抗干扰能力和稳定性。
同时,使用外部上拉电阻还能提供一定的电流源,有助于微控制器引脚的电平检测。内部上拉电阻通常只能提供微弱的电流,很容易受到噪音干扰。而外部10K上拉电阻能够为微控制器引脚提供足够的驱动能力,提高检测的准确性。
所以,在这种简单的按键检测电路中,使用外部独立的上拉电阻是一个较为常见且稳妥的做法,可以很好地满足电路设计的需求。
配置
不是矩阵按键,直接不断的扫描轮询这四个按键即可。
-
PA0
c开对应总线的时钟使能 RCC->AHB1ENR |= 1U; 清空第0和1位,置0,其他位不动 GPIOA->MODER &= ~(3U) 清空第0和1位,置0,其他位不动 GPIOA->PUPDR &= ~(3U)
-
PE2
c开对应总线的时钟使能 RCC->AHB1ENR |= (1U<<4); 清空第2和3位,置0,其他位不动 GPIOA->MODER &= ~((3U<<4)|(3U<<6)|(3U<<8)) 清空第0和1位,置0,其他位不动 GPIOA->PUPDR &= ~((3U<<4)|(3U<<6)|(3U<<8))
-
判断电平
c#define KEY1 ((GPIOA->IDR) & 1U) #define KEY2 !(((GPIOE->IDR) & (1U<<2))>>2) #define KEY3 !(((GPIOE->IDR) & (1U<<3))>>3) #define KEY4 !(((GPIOE->IDR) & (1U<<4))>>4)
按键扫描问题
c
//轮询按键的状态
u8 checkKey(){
u8 key_val = 0xff;
//KEY1没有按下是0,按下是1
if(KEY1){
//软件消抖
delay_ms(15);
//再检测一次
if(KEY1){
//赋值键值
key_val = 1;
}
//等待按键释放
while(KEY1);
//返回键值
return key_val;
}
return 0;
}
- 蜂鸣器没有问题,但是流水灯按键执行会被卡住 (按键不松手的话CPU会卡住)
- 这里还是单独检测按键状态即可,把状态返回出去,由其他函数把执行按键状态改变后的动作
- 如果按下按键不松开的话,CPU会卡死在程序里,无法进行其他操作
思考:
按键等待抬起的过程CPU在等待。导致执行程序卡死
怎么才能把等待抬起替换?
优化要求:
①不能卡程序(不能用等待抬起)
②按一次按键只能返回一次键值(按键按下去,就不会再次赋值键值)
改进1
c
//轮询按键的状态
u8 checkKey(){
//保存上一次的状态,用于检测是否释放,初始化为没有按下
static u8 key_val = 0;
//KEY1没有按下是0,按下是1
if(KEY1){
//软件消抖
delay_ms(15);
//再检测一次
if(KEY1){
//还没松手直接return保证一次按下只会触发一次
if(key_val==1){
return 0;
}
//赋值键值
key_val = 1;
}
//返回键值
return key_val;
}
//执行到这里就是所有按键都没有按下,键值改为没有按下状态
key_val = 0;
return key_val;
}
这个改进的问题就是虽然不用使用循环等待释放。但是每增加一个按钮就要判断新的值来确定赋值没有,不太方便。可见借鉴51的按键检测程序。先判断标志位再赋值
改进2的问题
重复进入消抖逻辑:一直按下不松手,以及没按都会进入消抖逻辑。导致流水灯计时异常。
你的流水灯被卡住的原因主要是以下几点:
waterLed
的计时依赖循环频率
主循环中还执行了按键扫描和按键处理的逻辑,导致waterLed
的计时变量time
无法及时累加,影响流水灯状态更新。- 按键扫描中的
delay_ms
阻塞问题
按键消抖中频繁调用delay_ms
(例如每次20ms),会让CPU停滞执行其他代码,阻塞了waterLed
的运行,尤其是在按键状态复杂时。 - 主循环任务冲突
主循环既要处理流水灯逻辑,又要扫描按键并切换灯的状态,二者之间存在时间分配上的冲突,导致某一方(如流水灯)运行不正常。
我没按啊?高速扫描按键状态和执行流水灯会冲突吗?他们有先后顺序啊?
确实,从代码上看,流水灯逻辑和按键扫描是有明确的先后顺序的,理论上不应该产生直接的冲突。如果没有按键操作,导致流水灯"卡住"的问题可能是由以下原因之一引起的:
waterLed
逻辑问题
- 计时单位误差
time
的累加依赖于主循环执行的速度。如果循环速度过慢,可能导致time
累加的时间远远超过预期。例如:
c
if (time >= (speed * 50000))
中的50000
是一个"经验值",如果主循环执行速度不够快,这个值可能需要重新调整。
变量范围溢出
time
是u32
,如果在某些编译器设置下超出范围,可能会导致溢出错误,间接导致流水灯逻辑异常。
主循环执行时间问题
虽然waterLed
和key_scan
是按顺序执行,但每次主循环都需要耗费一定的时间:
- 如果
key_scan
的延时(delay_ms(20)
)频繁执行,将显著拖慢整个循环。 waterLed
计时的逻辑就会受影响,表现为流水灯卡住。
即使没有按键按下,key_scan
中每次检查都会触发delay_ms
,这些累加的延时会让waterLed
的时间判断被拖延。
按键状态的干扰
按键状态的浮动(比如由于硬件没有外接上拉或下拉电阻)可能导致虚假的按键"按下"被检测到。虽然你没有按键操作,程序可能仍然检测到按键触发,导致多余的逻辑执行。
c
// 按键检测函数
u8 key_scan(void)
{
//循环很快,当按下时会被记录一次状态,没有松开,当再一次进入循环时,状态还是按下状态,不会返回值
//只有按下后松开,松开逻辑才会把状态改为松开状态,那么下一次按下时才会返回值
static u8 key_state = KEY_UNPRESS; // 记录上一次的按键状态
// 检测KEY1
if(KEY1 == 1) // 第一次检测到按下
{
delay_ms(20); // 消抖
if(KEY1 == 1) // 确认按下
{
if(key_state == KEY_UNPRESS) // 之前是未按下状态,可以赋值新状态
{
key_state = KEY1_PRESS;
return KEY1_PRESS;
}
}
}
else if(KEY2 == 1) // 检测KEY2
{
delay_ms(20);
if(KEY2 == 1)
{
if(key_state == KEY_UNPRESS)
{
key_state = KEY2_PRESS;
return KEY2_PRESS;
}
}
}
else if(KEY3 == 1) // 检测KEY3
{
delay_ms(20);
if(KEY3 == 1)
{
if(key_state == KEY_UNPRESS)
{
key_state = KEY3_PRESS;
return KEY3_PRESS;
}
}
}
else if(KEY4 == 1) // 检测KEY4
{
delay_ms(20);
if(KEY4 == 1)
{
if(key_state == KEY_UNPRESS)
{
key_state = KEY4_PRESS;
return KEY4_PRESS;
}
}
}
else // 所有按键都未按下或者按下后松开了
{
delay_ms(20); // 松开消抖
if(KEY1 == 0 && KEY2 == 0 && KEY3 == 0 && KEY4 == 0) // 确认都松开
{
key_state = KEY_UNPRESS; // 恢复未按下状态,static局部变量
}
}
return KEY_UNPRESS; // 返回未按下状态
}
在这个按键扫描函数中,一旦进入了某个按键的 if
分支,其他 else if
分支就不会被执行了。这是 C 语言条件语句的基本特性。
具体来说,一旦 KEY1 == 1
这个条件为真,程序会执行 if
块内的代码,并且不会再检查后续的 else if (KEY2 == 1)
和其他按键条件。这意味着如果同时按下多个按键,只有第一个被检测到的按键会被识别和处理。
改进2的疑问
如果我按下不松开的话,他就会一直进入一个循环,但是没有操作,这样没有返回值合法吗?
这是一个很好的观察。在这个函数中,如果按键保持按下状态,函数的行为确实会有一些特点:
- 当首次按下按键时,函数会返回对应的按键值(如
KEY1_PRESS
)。 - 只要按键保持按下状态,后续的函数调用会进入相应的
if
分支,但因为key_state
已经被设置为非KEY_UNPRESS
,所以不会再次返回按键值。 - 函数会不断执行,但不会有返回值变化。
从技术上讲,这是合法的。函数总是会返回 KEY_UNPRESS
,这意味着在按键持续按下期间,调用者会不断接收到 KEY_UNPRESS
的返回值。
也就是说我一直按下不松开,他会先进入一个if,进而进入到if分支,但是不会返回了,程序结束之后,会执行到最下面返回未按下状态,但是函数局部的按键状态依旧是按下状态?
- 首次按下按键时:
- 会进入第一个
if(KEY1 == 1)
分支 - 通过消抖
- 检查
key_state
是KEY_UNPRESS
- 返回按键值(例如
KEY1_PRESS
) - 将
key_state
设置为按下状态
- 会进入第一个
- 继续按住不放时:
- 会再次进入
if
分支 - 但因为
key_state
已经不是KEY_UNPRESS
- 不会再返回按键值
- 会执行到函数末尾
- 返回
KEY_UNPRESS
- 会再次进入
- 关键点是
key_state
是static
类型的局部变量:- 即使函数返回
KEY_UNPRESS
- 外面的switch语句会默认直接跳出,进而一直不松开,一直触发无操作
key_state
的值仍然保持为按下状态- 这意味着下次调用函数时,仍然知道按键是按下的
- 即使函数返回
这种设计可以防止重复触发,确保只有在按键状态从未按下变为按下时才返回按键值。
改进2最佳实践(逻辑绕死了,不建议)
解决:长按时重复进入消抖延迟逻辑
c
// 按键检测函数
u8 key_scan(void)
{
static u8 key_state = KEY_UNPRESS; // 上次按键状态记录
u8 current_key = KEY_UNPRESS; // 当前按下的按键状态
// 检测按键
if (KEY1 == 1) current_key = KEY1_PRESS;
else if (KEY2 == 1) current_key = KEY2_PRESS;
else if (KEY3 == 1) current_key = KEY3_PRESS;
else if (KEY4 == 1) current_key = KEY4_PRESS;
if (current_key != KEY_UNPRESS) // 检测到按键按下
{
if (key_state == KEY_UNPRESS) // 之前未按下,当前是第一次按下
{
delay_ms(20); // 消抖
if (current_key != KEY_UNPRESS) // 确认按下状态
{
key_state = current_key; // 更新按键状态
return current_key; // 返回当前按键值
}
}
}
else // 没有按键按下
{
if (key_state != KEY_UNPRESS) // 检测到松开
{
delay_ms(20); // 松开消抖
if (KEY1 == 0 && KEY2 == 0 && KEY3 == 0 && KEY4 == 0) // 确认松开
{
key_state = KEY_UNPRESS; // 恢复未按下状态
}
}
}
return KEY_UNPRESS; // 返回未按下状态
}
改进点
- 减少重复分支 :通过一个变量
current_key
统一记录当前按键状态,避免重复的按键检测逻辑。 - 优化消抖逻辑:按下和松开的消抖操作更明确且分离,确保不会多次进入消抖逻辑。
- 易于扩展 :若需要增加更多按键,只需在
if (KEYX == 1)
中添加分支即可。
优势
- 避免冗余消抖:只有状态发生变化时才会进行消抖。
- 逻辑清晰:按键状态管理更集中,便于阅读和维护。
- 性能优化:减少了不必要的状态判断和延迟操作,提高了代码效率。
最佳实践
单加一个标志位锁住按键状态。和标志位一起判断
这个只能一次检测单个按键的状态
c
// 按键检测函数
u8 key_scan(void)
{
u8 key_val = 0;
static u8 key_flag = 1; // 开锁
// 同时检测四个按键和锁定状态
if((KEY1 || KEY2 || KEY3 ||KEY4) && key_flag) // 有一个按键按下且没锁
{
delay_ms(20); // 消抖
if(KEY1) // 确认按下是哪个键?
{
key_val = 1;
//锁住
key_flag = 0;
}else if(KEY2){
key_val = 2;
//锁住
key_flag = 0;
}else if(KEY3){
key_val = 3;
//锁住
key_flag = 0;
}else if(KEY4){
key_val = 4;
//锁住
key_flag = 0;
}
}
//检测按键抬起
if(!KEY1 && !KEY2 && !KEY3 && !KEY4){//所有按键都抬起才解锁,一次只能检测一次按键操作
//解锁
key_flag = 1;
}
return key_val; //返回键值
}
F103最小系统板按键
怎么记接法?
-
IO口接在阳极上,则为推挽接法。
-
IO口接在阴极上,则为开漏接法。
-
阳能推和拉,阴能漏。
按键的接线
![](https://i-blog.csdnimg.cn/direct/cf4f106ac14d47e7950863735f5df54a.png)
输入上拉模式:
- 弱上拉,可以轻易被IO口的电平影响。仅在没有外部信号时做参考使用
- 按下,形成回路导通为0,松开回路断开,处于浮空状态,由于上拉的关系,还是呈现1
![](https://i-blog.csdnimg.cn/direct/5ac000e5de4e415f97b0d868a47c5bad.png)