在第一讲中曾经提到,GPIO有输入输出两种模式。在点亮LED时,我们已经使用了GPIO输出模式,在按键识别中,我们将要使用GPIO输入模式。首先来看看按键的电路原理图(下图在选手资源数据包——CT117E-M4产品手册中):
其中,B1~B4为4个不同的按键,它们通过PB0、PB1、PB2、PA0四个端口以上拉电阻的方式连接到单片机中。当按键松开时,PB0等端口处于高电平状态;当按键按下后,端口处于低电平状态。因此,我们可以把这些端口设置为GPIO输入+上拉电阻(pull-up)模式,通过读取其电平的高低状态来判断按键是否被按下。(所谓上下拉电阻,其实决定的就是GPIO输入端口断路时的初始电平状态,有关介绍可以自行搜索)
例如,需要判断B1是否被按下时,我们只需要判断PB0的电平状态:
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET)//读取PB0的电平状态,判断是否为低电平
{/* 执行任务 */
}
在主循环中,利用按键扫描,我们就可以通过不同的按键操作来执行不一样的任务,例如:
while (1)
{//B1按下,点亮LD1和LD2if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET){LED_On(LD1|LD2);}//B2按下,点亮LD3和LD4if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET){LED_On(LD3|LD4);}//B3按下,点亮LD5和LD6if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_RESET){LED_On(LD5|LD6);}//B4按下,点亮LD7和LD8if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){LED_On(LD7|LD8);}
}
不仅是按键,GPIO输入模式与输出模式一样,均广泛应用于各种需要读取外部电路电平的场景。有关GPIO输入的函数如下:
/*** @brief Read the specified input port pin.* @param GPIOx where x can be (A..G) to select the GPIO peripheral for STM32G4xx family* @param GPIO_Pin specifies the port bit to read.* This parameter can be any combination of GPIO_PIN_x where x can be (0..15).* @retval The input port pin value.*/
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
然而,这种方法在实际中并不常用。可以想象,除了判断按键是否按下以外,一个嵌入式系统一定还有其他的许多任务需要执行。倘若在主循环中一直判断按键是否被按下,则会占用CPU,效率低下,同时也可能因为在执行其他任务,响应不及时。因此,我们更常用的是采用中断的方式来判断按键是否被按下。
先来普及一下什么是中断。我们知道,CPU是按照顺序依次执行主函数中的指令的。中断是指打断CPU当前正在执行的指令,跳到中断程序中执行,随后跳会主函数原来的位置继续执行原指令。例如在生活中,我们的主程序是写代码,这时电话铃响,我们不得不停下手中的工作,进入中断——接电话,接完电话后又继续回到写代码的工作中。正如打断我们的有可能是电话铃声,有可能是门铃声,也有可能是短信铃声等等,打断CPU的中断方式也是多种多样的,如GPIO外部中断、定时器中断、定时器捕获中断等等……
考虑到比赛中对按键的判断涉及到长短按,我们在考虑程序执行效率而采用中断的同时,要考虑判断长短按的方法,这就涉及到按键按下的时间问题。在单片机中,与时间有关的问题,都是通过定时器来实现的。因此,下面我们来介绍定时器中断。
先来介绍一下与定时器有关的概念。在单片机中,有一个晶振(石英晶体振荡器),它通常决定了单片机的时钟频率。通过对时钟的分频,可以得到许许多多的时钟源。不同的硬件通过采用不同的时钟源,再对其进行分频,就得到了独属于这个硬件自己的时钟频率,定时器亦是如此。
参照官方例程(LCD的例程),我们按如下步骤配置时钟树:
(1)开启外部高速时钟
(2)勾选HSE,将时钟频率设置为80MHz后按回车
(3)所得到的定时器频率即可以在上图右侧圆圈处查看
这样我们就得到了时钟频率为80MHz的定时器。
下面我们来开启定时器中断。我们设置TIM4如下:
其中,定时器频率按照如下公式计算:(具体原理请自行搜索)
f0为时钟频率80MHz,Prescaler为预分频系数,Counter Period为计数周期。这样我们就把TIM4定时器的频率设置为了100Hz,即周期为0.01s。最后,只需要打开中断开关,就完成了定时器中断的配置。
在Cube中设置好后,想要使用定时器中断,还要在主函数初始化时开启定时器中断(在此处是开启TIM4的定时器中断)
HAL_TIM_Base_Start_IT(&htim4); //开启TIM4的基本(Base)功能(定时)中断(IT(InTerrupt))
然后编写定时器中断函数(注意:函数名和形参均是固定的,不能修改!!!可参照下图寻找):
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) //定时(Period)中断(Elapsed)回调(Callback)函数,回调即从主程序中调到中断程序中
{if (htim->Instance == TIM4) //如果是TIM4定时器触发的中断{//B1按下,点亮LD1和LD2if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET){LED_On(LD1|LD2);}//B2按下,点亮LD3和LD4if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET){LED_On(LD3|LD4);}//B3按下,点亮LD5和LD6if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_RESET){LED_On(LD5|LD6);}//B4按下,点亮LD7和LD8if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){LED_On(LD7|LD8);}}
}
这样,每当时间过去了0.01s,CPU就会进入定时中断回调函数中,运行我们预先写好的中断程序(在此处是读取按键端口的电平,随后执行相应任务),即定时按键扫描,而不是一直循环扫描按键是否按下,这样就为CPU节省下了大量的时间,大大提高了程序的运行效率。