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 小时前
STM32实战:数字音频播放器开发指南
stm32·单片机·嵌入式硬件
Chef_Chen3 小时前
从0开始学习R语言--Day18--分类变量关联性检验
学习
键盘敲没电3 小时前
【IOS】GCD学习
学习·ios·objective-c·xcode
海的诗篇_3 小时前
前端开发面试题总结-JavaScript篇(一)
开发语言·前端·javascript·学习·面试
AgilityBaby4 小时前
UE5 2D角色PaperZD插件动画状态机学习笔记
笔记·学习·ue5
AgilityBaby4 小时前
UE5 创建2D角色帧动画学习笔记
笔记·学习·ue5
想搞嵌入式的小白4 小时前
STM32外设问题总结
单片机·嵌入式硬件
武昌库里写JAVA5 小时前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
木子单片机6 小时前
基于STM32语音识别柔光台灯
stm32·单片机·嵌入式硬件·proteus·语音识别·keil
广药门徒6 小时前
澄清 STM32 NVIC 中断优先级
单片机·嵌入式硬件