32单片机学习记录2之按键

32单片机学习记录2之按键

前置

按键检测需要高速轮询但是流水灯的延迟函数会卡CPU执行时间,通过按键开启流水灯能不能实现?

不要延迟函数要计数思想达到相同效果,就不会卡CPU了。

原理图

分析

看不懂电路图,K2,3,4是同一组IO口,不是给低电平亮就是给高电平亮。

K1同样给高低电平测试。

K1 电路特性(与 PA0 相连)

  1. 按键 K1 的一端连接到 GND(地),另一端通过上拉电阻 R24 (10kΩ) 接到 +3.3V,并同时连接到 IO 引脚 PA0
  2. 特殊之处:
    • PA0 除了作为普通 GPIO,还标注了 WKUP(唤醒功能),可能与系统的低功耗唤醒机制有关。

K2、K3、K4 的电路特性

  1. 按键 K2、K3、K4 的接法相同:
    • 一端连接到 GND(地)。
    • 另一端通过各自的上拉电阻(R12、R23、R26,均为 10kΩ)接到 +3.3V,并同时连接到 IO 引脚 PE2、PE3、PE4。
  2. 功能对比:
    • 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 配置建议

  1. K1(PA0,带唤醒功能)
    • 配置为输入模式。检测传递过来的电平值
    • 若使用唤醒功能,需根据芯片规格手册选择唤醒边沿(通常是下降沿触发,即高变低)。
    • 如果不使用唤醒功能,则按普通 GPIO 输入使用即可。
  2. K2、K3、K4(PE2、PE3、PE4)
    • 配置为输入模式,用于普通按键检测。
    • 直接读取对应 GPIO 引脚电平,高电平表示未按下,低电平表示按下。

疑问1

IO口应该配置输出还是输入?是输出电平?还是接收电平?

在这个按键电路中,IO 口应该配置为输入模式,用于接收按键的电平信号。以下是详细解释:

按键电路的原理回顾

  1. 每个按键(K2~K4)与 IO 引脚相连,同时通过上拉电阻拉至高电平:
    • 按键未按下:上拉电阻使 IO 引脚维持高电平(+3.3V)。
    • 按键按下:按键闭合,IO 引脚通过按键直接连接到地(GND),电平变为低电平(0V)。
  2. 因此,IO 引脚的任务是检测电平变化(高电平和低电平),用以判断按键状态。

为什么 IO 口应该配置为输入?

  • 输入模式的作用
    • 配置为输入模式后,IO 引脚能够检测外部电路(如按键)产生的电平信号。
    • 它不会主动输出电平,而是依赖电路(如上拉电阻和按键状态)来确定输入信号的高低。
  • 输出模式不适合的原因
    • 若配置为输出模式,IO 引脚会主动驱动电平(高或低),与按键电路的设计目的冲突。
    • 例如:按键按下时,IO 引脚被按键短路到地,若同时输出高电平,将引发电流冲突,甚至可能损坏芯片。

IO 配置细节

  1. 模式
    • 配置为输入模式
    • 若硬件电路中已接有外部上拉电阻(接+3.3V)(如 10kΩ),不需要再启用内部上拉功能。
  2. 电平读取
    • 高电平(+3.3V):按键未按下(默认状态)。
    • 低电平(0V):按键按下。
  3. 特殊情况(针对 PA0 的唤醒功能)
    • 如果使用唤醒功能,需在低功耗模式下配置 IO 引脚为边沿触发模式(例如下降沿触发),以检测按键按下事件。

疑问2

如果外部没有上拉电阻呢?是不是要开启上拉电阻提供一个参考电平?

是的,如果外部没有上拉电阻(比如电路中没有10kΩ电阻),就需要启用 IO 引脚的内部上拉电阻,以提供一个稳定的参考电平。以下是详细解释:

为什么需要上拉电阻?

  1. 提供默认状态(高电平)
    • 当按键未按下时,IO 引脚必须通过上拉电阻连接到高电平 (+3.3V),确保默认电平稳定。
    • 如果没有上拉电阻,IO 引脚的电平可能悬空(浮空),容易受到外界噪声干扰,导致检测结果不可靠。
  2. 工作原理
    • 按键未按下时:上拉电阻将 IO 引脚拉到高电平。
    • 按键按下时:按键将 IO 引脚直接接地,电平变为低电平。

外部没有上拉电阻的情况下

  • 如果外部电路未提供上拉电阻(比如 10kΩ 上拉电阻缺失),则需要启用芯片内部的上拉功能。
  • 大多数微控制器(如 STM32)都支持内部上拉功能,具体方法是在配置 IO 口时设置为输入模式 + 内部上拉

配置步骤

  1. 设置 IO 引脚为输入模式
    • 配置 GPIO 为输入模式,能够检测外部电平。
  2. 启用内部上拉电阻
    • 在 IO 配置寄存器中启用内部上拉电阻(通常是 GPIO_PUPDR 或类似的寄存器)。
    • 启用后,IO 引脚在未按下时会通过内部上拉电阻(通常是几十 kΩ)保持高电平。

注意事项

  • 内部上拉电阻 vs 外部上拉电阻
    • 内部上拉电阻的阻值通常较大(一般 30kΩ ~ 50kΩ),相比外部电阻(如 10kΩ),拉高能力稍弱。
    • 如果电路对噪声抗干扰要求较高,建议使用外部 10kΩ 上拉电阻来代替内部上拉。
  • 不要同时使用外部和内部上拉
    • 如果已经接了外部上拉电阻,就不需要再启用内部上拉,避免多重上拉影响电路性能。

为什么加一下普通电阻就拉高了电平呢?K1就更不理解了,因为有一端接地了,加一个电阻,也可以把K1拉高?

在这四个电路中,加入普通电阻确实能够拉高电平。这是因为这些电路存在上拉电阻的设计。

对于K1电路,即使一端接地,加入上拉电阻能将K1拉低。这是因为:

  1. K1的另一端连接到WKUP/PA0引脚,这个引脚通常是一个输入引脚。
  2. 当K1保持断开状态时,WKUP/PA0引脚的电平就会被下拉电阻R24拉低到0V。
  3. 当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的问题

重复进入消抖逻辑:一直按下不松手,以及没按都会进入消抖逻辑。导致流水灯计时异常。

你的流水灯被卡住的原因主要是以下几点:

  1. waterLed的计时依赖循环频率
    主循环中还执行了按键扫描和按键处理的逻辑,导致waterLed的计时变量time无法及时累加,影响流水灯状态更新。
  2. 按键扫描中的delay_ms阻塞问题
    按键消抖中频繁调用delay_ms(例如每次20ms),会让CPU停滞执行其他代码,阻塞了waterLed的运行,尤其是在按键状态复杂时。
  3. 主循环任务冲突
    主循环既要处理流水灯逻辑,又要扫描按键并切换灯的状态,二者之间存在时间分配上的冲突,导致某一方(如流水灯)运行不正常。

我没按啊?高速扫描按键状态和执行流水灯会冲突吗?他们有先后顺序啊?

确实,从代码上看,流水灯逻辑和按键扫描是有明确的先后顺序的,理论上不应该产生直接的冲突。如果没有按键操作,导致流水灯"卡住"的问题可能是由以下原因之一引起的:

waterLed逻辑问题

  • 计时单位误差
    time的累加依赖于主循环执行的速度。如果循环速度过慢,可能导致time累加的时间远远超过预期。例如:
c 复制代码
if (time >= (speed * 50000))

中的50000是一个"经验值",如果主循环执行速度不够快,这个值可能需要重新调整。

变量范围溢出
timeu32,如果在某些编译器设置下超出范围,可能会导致溢出错误,间接导致流水灯逻辑异常。

主循环执行时间问题

虽然waterLedkey_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的疑问

如果我按下不松开的话,他就会一直进入一个循环,但是没有操作,这样没有返回值合法吗?

这是一个很好的观察。在这个函数中,如果按键保持按下状态,函数的行为确实会有一些特点:

  1. 当首次按下按键时,函数会返回对应的按键值(如 KEY1_PRESS)。
  2. 只要按键保持按下状态,后续的函数调用会进入相应的 if 分支,但因为 key_state 已经被设置为非 KEY_UNPRESS,所以不会再次返回按键值。
  3. 函数会不断执行,但不会有返回值变化。

从技术上讲,这是合法的。函数总是会返回 KEY_UNPRESS,这意味着在按键持续按下期间,调用者会不断接收到 KEY_UNPRESS 的返回值。

也就是说我一直按下不松开,他会先进入一个if,进而进入到if分支,但是不会返回了,程序结束之后,会执行到最下面返回未按下状态,但是函数局部的按键状态依旧是按下状态?

  1. 首次按下按键时:
    • 会进入第一个 if(KEY1 == 1) 分支
    • 通过消抖
    • 检查 key_stateKEY_UNPRESS
    • 返回按键值(例如 KEY1_PRESS
    • key_state 设置为按下状态
  2. 继续按住不放时:
    • 会再次进入 if 分支
    • 但因为 key_state 已经不是 KEY_UNPRESS
    • 不会再返回按键值
    • 会执行到函数末尾
    • 返回 KEY_UNPRESS
  3. 关键点是 key_statestatic类型的局部变量:
    • 即使函数返回 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; // 返回未按下状态
}

改进点

  1. 减少重复分支 :通过一个变量 current_key 统一记录当前按键状态,避免重复的按键检测逻辑。
  2. 优化消抖逻辑:按下和松开的消抖操作更明确且分离,确保不会多次进入消抖逻辑。
  3. 易于扩展 :若需要增加更多按键,只需在 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口接在阴极上,则为开漏接法。

  • 阳能推和拉,阴能漏。


按键的接线

输入上拉模式:

  • 弱上拉,可以轻易被IO口的电平影响。仅在没有外部信号时做参考使用
  • 按下,形成回路导通为0,松开回路断开,处于浮空状态,由于上拉的关系,还是呈现1
相关推荐
优雅永不过时·2 小时前
Three.js编辑器百度搜索 Top 1
前端·javascript·学习·编辑器·three
sinat_360704823 小时前
STM32 RTC 实时时钟说明
stm32·单片机
無铭之辈4 小时前
学习Vue的必要基础
前端·vue.js·学习
weixin_452600694 小时前
芯麦GC6208:革新摄像机与医疗设备的智能音频解决方案
单片机·嵌入式硬件·音视频·健康医疗·医疗器械·白色家电电源
虾球xz5 小时前
游戏引擎学习第99天
javascript·学习·游戏引擎
亿道电子Emdoor5 小时前
【ARM】解决ArmDS Fast Models 中部分内核无法上电的问题
arm开发·stm32·单片机
小火球2.05 小时前
单片机简介
单片机·嵌入式硬件
练小杰6 小时前
【MySQL例题】我在广州学Mysql 系列——有关数据备份与还原的示例
android·数据库·经验分享·sql·学习·mysql
larry_dongy6 小时前
VMware Workstate 的 Ubuntu18 安装 vmware tools(不安装没法共享)
学习·ubuntu