26.3.15 ZD-EP52 [LED/KEY] 点天灯!!!!

终于开始点灯了,,。。


1 点亮一个LED灯

在点亮一个LED灯之前肯定要创建一个项目工程,具体流程参见26.3.7 ZD-EP27 [Keil新建工程-HAL库版本] 新建HAL库版本工程流程*基于寄存器版本的增删,这里直接给出HAL库工程框架:

在新建完工程框架后,开始进行后续步骤:

1.1 添加板级驱动文件

在工程文件夹的Drivers文件夹下添加一个BSP(Board Support Package)文件夹用于存放相关外设的配置文件,再在BSP文件夹下新建一个LED文件夹。

回到Keil,我们打开工程文件,新建两个文件,分别命名为lcd.c lcd.h,并保存在LED文件夹下,至此我们开始正式配置LED外设。

1.2 配置LED灯

我们首先在.h头文件中做出基本的头文件定义:

#ifndef __LED_H
#define __LED_H

#include "./SYSTEM/sys/sys.h"

/*在这里输入相关宏定义或函数声明*/

#endif

再来到.c文件开始编写LED的初始化代码。

我们先引用头文件led.h,定义一个函数led_init(),类型及形参均为void。

在正式展示代码之前,我们先明确一下我们要做什么:

  • 使能时钟,通过__HAL_RCC_GPIOx_CLK_ENABLE()来实现。
  • 初始化GPIO引脚,通过定义GPIO_InitTypeDef结构体和HAL_GPIO_Init()函数来实现。
    • 定义GPIO_InitTypeDef结构体。
  • 复位LED灯:通过HAL_GPIO_WritePin()来实现。
  • 翻转LED灯:通过HAL_GPIO_TogglePin()来实现。
  • 查阅LED相关原理图:如下。可以得知两个LED分别挂载在PF9和PF10引脚上。此外我们还可以得知两个LED灯接的是3.3V的电压,因此要使LED能处于灭状态,我们输出的电压也要为高电平状态,因此这里的输出模式应使用推挽输出。
探索者LED原理图

知道了这些,我们就知道首先要定义些东西:

#ifndef __LED_H
#define __LED_H

#include "./SYSTEM/sys/sys.h"

//LED时钟使能
#define LED_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOF_CLK_ENABLE(); }while(0)

//LEDO引脚定义
#define LED0_GPIO_PORT GPIOF
#define LED0_GPIO_PIN GPIO_PIN_9

//LED1引脚定义
#define LED1_GPIO_PORT GPIOF
#define LED1_GPIO_PIN GPIO_PIN_10

//LED 端口操作定义
#define LED0(x) do{ x ? \
                    HAL_GPIO_WritePin(LED0_GPIO_PORT,LED0_GPIO_PIN,GPIO_PIN_SET):\
                    HAL_GPIO_WritePin(LED0_GPIO_PORT,LED0_GPIO_PIN,GPIO_PIN_RESET);\
                    }while(0)
#define LED1(x) do{ x ? \
                    HAL_GPIO_WritePin(LED1_GPIO_PORT,LED1_GPIO_PIN,GPIO_PIN_SET):\
                    HAL_GPIO_WritePin(LED1_GPIO_PORT,LED1_GPIO_PIN,GPIO_PIN_RESET);\
                    }while(0)

//LED电平翻转定义
#define LED0_Toggle() do{ HAL_GPIO_TogglePin(LED0_GPIO_PORT, LED0_GPIO_PIN);}while(0)
#define LED1_Toggle() do{ HAL_GPIO_TogglePin(LED1_GPIO_PORT, LED1_GPIO_PIN);}while(0)

void led_init(void);

#endif

led.h定义内容

首先定义使能函数LED_GPIO_CLK_ENABLE(),通过单次的do{}while(0)语块调用__HAL_RCC_GPIOF_CLK_ENABLE()函数来实现,其中的GPIOF即为探索者LED所在端口。

其次对LED0和LED1的相关端口、引脚进行定义,以便于后续的输入。

再次,对LED的端口操作进行宏定义,这一步极大的方便了我们对LED的操作,分析函数,其是一个在do{}while(0)语块下的一个判断语句,当操作数x为1时输出的是HAL_GPIO_WritePin(LED0_GPIO_PORT,LED0_GPIO_PIN,GPIO_PIN_SET)语句,即将LED0对应的引脚设置为高电平(GPIO_PIN_SET),反之则为低电平(GPIO_PIN_RESET)。

然后还有一个对Toggle翻转函数的宏定义,与端口操作类似,这里不做过多解释。

最后对初始化函数进行一个声明。

定义完后我们就可以开始进行初始化函数的定义了:

有一点需要注意的是,这里在头文件进行的宏定义目的是为了更好的输入和移植,实际上在初始化函数中可以直接使用宏定义前的内容,不过会更复杂。
#include "./BSP/LED/led.h"


void led_init(void)
{
    //初始化结构体定义
    GPIO_InitTypeDef GPIO_LED;
    
    //时钟使能
    LED_GPIO_CLK_ENABLE();
    
    //结构体成员赋值
    GPIO_LED.Pin = LED0_GPIO_PIN;
    GPIO_LED.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_LED.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(LED0_GPIO_PORT, &GPIO_LED);
    
    GPIO_LED.Pin = LED1_GPIO_PIN;
    HAL_GPIO_Init(LED1_GPIO_PORT, &GPIO_LED);
    
    LED0(1);
    LED1(1);
}

led.c初始化内容

可以看到在初始化前我们定义了一个结构体,该结构体储存的是GPIO的相关初始化配置,因此我们需要提前定义以便于后续赋值。

随后进行了时钟使能的操作。

再来就开始对结构体的成员赋值了,我们共有两个LED灯,这里先对LED0即PF9进行初始化配置,于是我们给结构体下的Pin成员赋值为LED0对应的Pin值。

接下来我们对结构体下的Mode进行赋值,Mode即该GPIO引脚的工作模式,这里我们用作输出,且模式为推挽(Push Pull)(因为开漏的话无法输出高电平),赋值为GPIO_MODE_OUTPUT_PP。

最后我们对速度(Speed成员)进行赋值,驱动LED灯需要的速度并不大,因此选择GPIO_SPEED_FREQ_LOW的低速来驱动即可。

此处的相关赋值内容可通过F12 ​Go to Definition功能调整到结构体定义处,在每个成员的旁边都会有注释 @ref 来指向该成员的赋值内容,此时再通过ctrl+F进行全文件查找即可找到对应的赋值内容及其描述。

随后我们就可以对LED0对应引脚进行GPIO_Init()操作了,需要注意的是,该函数的第二个形参为指针类型,因此需要在定义的结构体名字前加上&表示指向其地址。

再者我们将成员Pin改为LED1对应Pin值即可直接对该引脚进行相同的初始化操作。

最最后为确保LED默认处于灭状态,我们使用LED0(1)和LED1(1)操作来设置LED默认处于灭状态。这里的(1)指的是引脚输出高电平,因为这两个LED接的是高电平,因此要输出高电平才能让LED灯处于灭状态。

1.3 点亮LED灯

在完成配置后,我们可以得到一下功能函数:

  • led_init():LED灯初始化函数
  • LED0(x):LED0控制函数,其中x为1时LED灭,为0时LED亮。
  • LED1(x):LED1控制函数,其中x为1时LED灭,为0时LED亮。
  • LED0_Toggle():翻转LED0的电平,即翻转LED0的状态。
  • LED1_Toggle():翻转LED1的电平,即翻转LED1的状态。

于是通过以下代码即可实现LED灯亮灭操作:

##include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"              /* 包含LED头文件 */

int main(void)
{
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
    delay_init(168);                    /* 延时初始化 */
    led_init();                         /* 初始化LED */

    while(1)
    {
        LED0(0);                        /* 点亮LED0 */
        delay_ms(200);                  /* 延时200ms */
        LED0(1);                        /* 熄灭LED0 */
        LED1(0);                        /* 点亮LED1 */
        delay_ms(200);                  /* 延时200ms */
        LED1(1);                        /* 熄灭LED1 */
    }
}

LED跑马灯实验main.c内容

恭喜我也恭喜你终于点亮了一个,不对两个小灯!


2 通过按钮控制LED灯

在探索者开发板上还有一块地方摆着四个按钮,那么我们要怎么通过这四个按钮来控制LED灯的亮灭呢?

2.1 添加板级驱动文件

同led.c和led.h的操作一样,我们在BSP目录下新建一个KEY文件夹,并在Keil中新建两个文件分别命名为key.c和key.h放在KEY目录下。然后再开始对KEY外设进行配置。

2.2 配置KEY按钮

头文件及初始化函数(key_init())的书写这里不过多赘述,首先我们依旧明确我们要做的内容:

  • 使能时钟,通过__HAL_RCC_GPIOx_CLK_ENABLE()来实现。
  • 初始化GPIO引脚,通过定义GPIO_InitTypeDef结构体和HAL_GPIO_Init()函数来实现。
    • 定义GPIO_InitTypeDef结构体。
  • 读取KEY状态:通过HAL_GPIO_ReadPin()函数来实现。
  • 返回KEY状态:通过定义一个key_scan()函数来实现。
  • 查阅KEY相关原理图:如下。可以得知四个KEY分别挂载在PA0,PE4,PE3,PE2引脚上。此外我们还可以得知KEY_UP灯接的是3.3V的电压,因此要使该按钮能正确传导信号,需要将引脚的下拉电阻打开,即处于输入下拉的状态,此时引脚默认处于低电平,按下按钮时使引脚变为高电平;而KEY0/1/2则相反,它们接的是地,要使按钮能正确传导信号,需要将引脚的上拉电阻打开,即处于输入上拉模式,此时引脚默认处于高电平,按下按钮使引脚变为低电平。
探索者KEY外设原理图

知道这些东西我们就可以做出如下定义:

#ifndef __KEY_H
#define __KEY_H

#include "./SYSTEM/sys/sys.h"

//KEY时钟使能
#define KEY_D_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOE_CLK_ENABLE();}while(0)
#define KEY_U_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOA_CLK_ENABLE();}while(0)

//KEY0引脚定义
#define KEY0_GPIO_PORT GPIOE
#define KEY0_GPIO_PIN GPIO_PIN_4

//KEY1引脚定义
#define KEY1_GPIO_PORT GPIOE
#define KEY1_GPIO_PIN GPIO_PIN_3

//KEY2引脚定义
#define KEY2_GPIO_PORT GPIOE
#define KEY2_GPIO_PIN GPIO_PIN_2

//KEY_UP引脚定义
#define KEY_UP_GPIO_PORT GPIOA
#define KEY_UP_GPIO_PIN GPIO_PIN_0

//KEY状态读取
#define KEY0 HAL_GPIO_ReadPin(KEY0_GPIO_PORT,KEY0_GPIO_PIN)
#define KEY1 HAL_GPIO_ReadPin(KEY1_GPIO_PORT,KEY1_GPIO_PIN)
#define KEY2 HAL_GPIO_ReadPin(KEY2_GPIO_PORT,KEY2_GPIO_PIN)
#define KEY_UP HAL_GPIO_ReadPin(KEY_UP_GPIO_PORT,KEY_UP_GPIO_PIN)

//KEY状态描述
#define KEY0_PRES 1
#define KEY1_PRES 2
#define KEY2_PRES 3
#define KEY_UP_PRES 4

void key_init(void);
uint8_t key_scan(uint8_t mode);

#endif

key.h定义内容

关于使能和引脚定义这里不做过多赘述,我们来看状态读取这一栏,直接定义KEYx为读取到的引脚状态,这里的ReadPin的形参即为端口号和引脚号,输出内容要结合原理图来看,对应KEY0/1/2来说,当输出值为0时,说明对应按钮按下,而对于KEY_UP来说,当输出值为1时才对应按钮按下。我们再对按钮按下的状态进行一个定义,便于后续通过switch()函数来对应每一个按钮的操作状态。

最后声明了key_init()和key_scan()两个函数,关于init函数,我们这边简单看一下即可,操作与led_init()类似:

//KEY初始化配置
void key_init(void)
{
    //KEY初始化结构体
    GPIO_InitTypeDef GPIO_KEY;
    
    //KEY时钟使能
    KEY_D_GPIO_CLK_ENABLE();
    KEY_U_GPIO_CLK_ENABLE();
    
    //KEY结构体成员赋值
    GPIO_KEY.Mode = GPIO_MODE_INPUT;
    GPIO_KEY.Pull = GPIO_PULLUP;
    
    GPIO_KEY.Pin = KEY0_GPIO_PIN;
    HAL_GPIO_Init(KEY0_GPIO_PORT, &GPIO_KEY);
    
    GPIO_KEY.Pin = KEY1_GPIO_PIN;
    HAL_GPIO_Init(KEY1_GPIO_PORT, &GPIO_KEY);
    
    GPIO_KEY.Pin = KEY2_GPIO_PIN;
    HAL_GPIO_Init(KEY2_GPIO_PORT, &GPIO_KEY);
    
    GPIO_KEY.Pull = GPIO_PULLDOWN;
    GPIO_KEY.Pin = KEY_UP_GPIO_PIN;
    HAL_GPIO_Init(KEY_UP_GPIO_PORT, &GPIO_KEY);
}

key_init()函数内容

唯一要注意的是,KEY0/1/2的Pull成员均为GPIO_PULLUP即上拉模式,而KEY_UP的Pull成员为GPIO_PULLDOWN即下拉模式,此外输入模式中没有对速度的要求,因此无需定义Speed成员。

下面我们重点来看key_scan()函数。

在介绍该函数之前,我们先来思考一下按下按钮的理想状态是什么?实际状态又是什么?

独立按钮抖动波形图

我们可以看到,理想状态下我们按下按钮就能立马做出回应,这种状态下我们能够直接对按钮状态进行读取,然而实际情况下,按钮总是伴随着抖动,按下按钮时只有等待一段时间后才能达到稳定闭合状态,因此在实际应用中,我们需要进行“消抖”处理,那“消抖”要怎么做呢?很简单,等,我们在读取按钮状态时先等上很小一段时间(10ms),因此要求key.c中引用delay.h头文件,这个时候读取到的按钮状态基本上都是稳定的,于是我们可以建立一个最基本的key_scan()(针对KEY0按钮)函数:

uint8_t key_scan()
{
    if(KEY0 == 0)
    {
        delay_ms(10);            //消抖
        if(KEY0 == 0)
        {
            while(KEY0 == 0);    //只有按钮松开时才返回操作
            retuen 1;            //成功返回值1
        }
    }
    return 0;                    //失败返回值0
}

基本key_scan()函数实现

关于这个函数的检测逻辑,我们先把它带入到main.c中:

//相关头文件及初始化定义已省略

while(1)
{
    if(key_scan())
    {
        LED0_Toggle();
    }
    else
    {
        delay_ms(10);
    }

}

基本key_scan()函数使用范例

该函数实现的功能就是通过KEY0来控制LED0,下面我们开始整个过程的分析。

无输入状态下整个程序一直处于延时10ms的while(1)循环中,当你按下按钮超过了ReadPin的检测阈值(即按下按钮但未达到稳定闭合状态)且正好进入到下一个while循环时,key_scan()函数的第一个if条件达成,开始进入下一语句进行消抖即等待处理,10ms后抖动波状态已结束进入稳定闭合状态,此时进入key_scan()的第二个if判断,若读取到的KEY0状态仍处于按下状态(应该没人能做到在10ms内松开这个按钮吧),则正式进入返回值处理语段,先是一个while(KEY0 == 0);的循环,即当你一直按下这个按钮时他就一直处于循环状态无法返回值,只有当你松开按钮时才返回1,这样处理的目的是消除松开时的抖动,不过该方法有点草率,不过总而言之key_scan()函数返回了1,于是我们回到main.c,if条件达成进入下一语句,即翻转LED0状态,至此通过KEY0控制LED0的目的就此达成。

我们不难看到这个基本模型的局限性:只对一个按钮生效,且LED不能及时接收到KEY的状态反馈,于是我们(其实是正点原子)将key_scan()函数晋升为了如下的体系健全的函数:

uint8_t key_scan(uint8_t mode)
{
    static uint8_t key_up = 1;  /* 按键按松开标志 */
    uint8_t keyval = 0;

    if (mode) key_up = 1;       /* 支持连按 */

    if (key_up && (KEY0 == 0 || KEY1 == 0 || KEY2 == 0 || KEY_UP == 1))  /* 按键松开标志为1, 且有任意一个按键按下了 */
    {
        delay_ms(10);           /* 去抖动 */
        key_up = 0;

        if (KEY0 == 0)  keyval = KEY0_PRES;

        if (KEY1 == 0)  keyval = KEY1_PRES;

        if (KEY2 == 0)  keyval = KEY2_PRES;

        if (KEY_UP == 1) keyval = KEY_UP_PRES;
    }
    else if (KEY0 == 1 && KEY1 == 1 && KEY2 == 1 && KEY_UP == 0)         /* 没有任何按键按下, 标记按键松开 */
    {
        key_up = 1;
    }

    return keyval;              /* 返回键值 */
}

完整key_scan()函数定义

首先补充一点,该函数返回值仅为1,0,因此函数返回值使用uint8_t的八位类型即可。

这个完整的key_scan()函数首先定义了一个形参mode,用于选择是连按模式还是单点模式,具体作用我们后续再说。

我们首先定义了一个静态变量key_up按钮松开标志且默认值为1,为什么用静态变量呢,我们先看后续操作,当key_up值为1且有按钮按下时,我们的key_up变为0,此时我们如果保持按钮按下,程序进入到下一个循环时key_up值仍为0(静态变量特性,保存上一次的变量变化),此时就不会再进入到按下循环当中,而是判断按钮是否松开,如果松开就重置key_up为1,这样就可以避免函数一直在进行相同操作,同时也做到了基本函数中消除松开抖动的作用,而且能够避免基本实现函数中的循环,做到按下按钮实时反馈。

接着我们定义了键值变量keyval用于储存输出的键值。

再接下来就是判断按钮是否按下的操作了,如果按下则先进行消抖,再改变key_up的值,之后再来判断具体是哪个按钮按下从而返回不同的值。

若无按钮按下则重置key_up的值为1,最后返回键值。

还有一段代码我们没有提到:if (mode) key_up = 1; /* 支持连按 */这就是连按模式的关键,我们前面都是围绕mode == 0的情况下来说的,如果mode值为1,就会除非这一条代码,即无论之前的key_up值是多少,我都赋值为1,这样做能干嘛呢?我们先来模拟一遍。

我们先假定main函数中有这样的语句:while(1){key_scan(1);}即不停对键值进行检测,当我们按下按钮时,key_scan的if被第一次触发,返回对应键值,如果此时按钮仍处于按下状态,进入下一循环我们发现他还能返回一次键值,如此往复他能一直返回这个键值,且每次间隔10ms,这不跟我们前面说的避免函数一直在进行相同操作背道而驰吗?

下面我们来看一个具体实例:

2.3 通过KEY来控制LED灯

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/KEY/key.h"

int main(void)
{
    HAL_Init(); /* 初始化HAL库 */
    sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
    delay_init(168); /* 延时初始化 */
    led_init(); /* 初始化LED */
    key_init(); /* 初始化按键 */

    while(1)
    {
        uint8_t key = key_scan(0);
        
        if(key)
        {
            switch(key)
            {
                case KEY0_PRES:
                {
                    uint8_t count = 0;
                    while(key && count < 50)
                    {
                        count++;
                        key = key_scan(1);
                    }
                    if(count >= 50)
                    {
                        LED0_Toggle();
                        LED1_Toggle();
                    }
                    break;
                }
                case KEY1_PRES:
                    while(key)
                    {
                        LED0_Toggle();
                        delay_ms(200);
                        LED1_Toggle();
                        delay_ms(200);
                        key = key_scan(1);
                    }
                    break;
                case KEY2_PRES:
                    LED0_Toggle();
                    break;
                case KEY_UP_PRES:
                    LED1_Toggle();
                    break;
                default:
                    break;
            }
        }
        else
        {
            delay_ms(10);
        }
    }
}

完整key_scan()函数在main.c的具体实例

可以看到我们在这里通过switch函数实现了不同按钮的不同操作,这里的KEY2和KEY_UP不做过多解释,作用同基本实现函数一致,不过在这里实现了按钮的实时反馈,我们重点看到KEY0和KEY1的两个应用场景:

最开始我们定义了一个变量key用于读取key_scan(0)的输出值,此后若要进行连按模式操作时每一次都要给key赋值为key_scan(1)的输出值,由此来区分单点和连按模式不同的按钮。

  • KEY0:首先定义一个变量count用于计数,再进行一个while条件循环,条件为key输出值为1且count小于50,而循环体内就进行两个操作:count++和重新赋值key,这个循环会直到count赋值到50结束,即等待了50*10=500ms,循环结束后再进行一次判断,如果count>=50就执行命令,当然这一句也可以没有,循环结束后直接执行命令即可,不过也可以通过判断来区分长按,短按和点击,进而执行三种不同的操作。
  • KEY1:该按键按下后直接进行一个条件循环,条件就是key的值始终为1,而在循环体内处执行的命令外还会重新给key赋值从而进行下一次判断,由此就实现了长按执行循环操作的目的。

当然连按模式的用处不至于此,抓住不断输出值且间隔10ms的特点可以做到许多事情。

然而虽然称其为完整key_scan()函数,其仍存在问题:两个按键同时响应时只会输出一个按键内容,当然这些可以通过不断优化代码来实现。


3 结语

mak说想看我怎么写的博客,于是我把结语给他看了。

欢迎订阅 Subscribe to 沿江路右转Turn Right at Yanjiang Rd.

期待您的精彩评论 Looking forward to your wonderful comments.
[email protected]
订阅 Subscribe
赣ICP备2026002696号