【STM32H7教程】第19章 STM32H7的GPIO應用之按鍵FIFO


完整教程下載地址:http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980

第19章       STM32H7的GPIO應用之按鍵FIFO

本章教程為大家介紹STM32H7的GPIO應用之按鍵FIFO,這個方案已經在實際項目中千錘百煉,比較實用。

19.1 初學者重要提示

19.2 按鍵硬件檢測

19.3 按鍵FIFO的驅動設計

19.4 按鍵板級支持包(bsp_key.c)

19.5 按鍵FIFO驅動移植和使用

19.6 實驗例程設計框架

19.7 實驗例程說明(MDK)

19.8 實驗例程說明(IAR)

19.9 總計

 

 

19.1 初學者重要提示

  1.  學習本章節前,務必保證已經學習了第15,16和17章。
  2.  按鍵FIFO驅動擴展和移植更簡單,組合鍵也更好用。支持按下、彈起、長按和組合鍵。

19.2 按鍵硬件設計

V7開發板有三個獨立按鍵和一個五向搖桿,下面是三個獨立按鍵的原理圖:

 

注意,K1(S1)、K2(S2)和K3(S3)按鍵的上拉電阻是接在5V電壓上,因為這三個按鍵被復用為PS/2鍵盤鼠標接口,而PS/2是需要5V供電的(注,V5和V6開發板做了PS/2復用,而V7沒有使用,這里只是為了兼容之前的板子)。實際測試,K1、K2、K3按鍵和PS/2鍵盤是可以同時工作的。

下面是五向搖桿的原理圖:

 

通過這個硬件設計,有如下兩個知識點為大家做介紹:

19.2.1 硬件設計

按鍵和CPU之間串聯的電阻起保護作用。按鍵肯定是存在機械抖動的,開發板上面的硬件沒有做硬件濾波處理,即使設計了硬件濾波電路,軟件上還是需要進行濾波。

  1.   保護GPIO,避免軟件錯誤將IO設置為輸出,如果設置為低電平還好,如果設置輸出的是高電平,按鍵按下會直接跟GND(低電平)連接,從而損壞MCU。
  2.   保護電阻也起到按鍵隔離作用,這些GPIO可以直接用於其它實驗。

19.2.2 GPIO內部結構分析按鍵

詳細的GPIO模式介紹,請參考第15章的15.3小節,本章僅介紹輸入模式。下面我們通過一張圖來簡單介紹GPIO的結構。

 

紅色的線條是GPIO輸入通道的信號流向,作為按鍵檢測IO,這些需要配置為浮空輸入。按鍵已經做了5V上拉,因此GPIO內部的上下拉電阻都選擇關閉狀態。

19.3 按鍵FIFO的驅動設計

bsp_key按鍵驅動程序用於掃描獨立按鍵,具有軟件濾波機制,采用FIFO機制保存鍵值。可以檢測如下事件:

  •   按鍵按下。
  •   按鍵彈起。
  •   長按鍵。
  •   長按時自動連發。

我們將按鍵驅動分為兩個部分來介紹,一部分是FIFO的實現,一部分是按鍵檢測的實現。

 

bsp_key.c 文件包含按鍵檢測和按鍵FIFO的實現代碼。

bsp.c 文件會調用bsp_InitKey()初始化函數。

bsp.c 文件會調用bsp_KeyScan按鍵掃描函數。

bsp_timer.c 中的Systick中斷服務程序調用 bsp_RunPer10ms。

中斷程序和主程序通過FIFO接口函數進行信息傳遞。

函數調用關系圖:

 

19.3.1 按鍵FIFO的原理

FIFO是First Input First Output的縮寫,先入先出隊列。我們這里以5個字節的FIFO空間進行說明。Write變量表示寫位置,Read變量表示讀位置。初始狀態時,Read = Write = 0。

 

我們依次按下按鍵K1,K2,那么FIFO中的數據變為:

 

如果Write!= Read,則我們認為有新的按鍵事件。

我們通過函數bsp_GetKey讀取一個按鍵值進行處理后,Read變量變為1。Write變量不變。

 

我們繼續通過函數bsp_GetKey讀取3個按鍵值進行處理后,Read變量變為4。此時Read = Write = 4。兩個變量已經相等,表示已經沒有新的按鍵事件需要處理。

有一點要特別的注意,如果FIFO空間寫滿了,Write會被重新賦值為0,也就是重新從第一個字節空間填數據進去,如果這個地址空間的數據還沒有被及時讀取出來,那么會被后來的數據覆蓋掉,這點要引起大家的注意。我們的驅動程序開辟了10個字節的FIFO緩沖區,對於一般的應用足夠了。

 

設計按鍵FIFO主要有三個方面的好處:

  •   可靠地記錄每一個按鍵事件,避免遺漏按鍵事件。特別是需要實現按鍵的按下、長按、自動連發、彈起等事件時。
  •   讀取按鍵的函數可以設計為非阻塞的,不需要等待按鍵抖動濾波處理完畢。
  •   按鍵FIFO程序在嘀嗒定時器中定期的執行檢測,不需要在主程序中一直做檢測,這樣可以有效地降低系統資源消耗。

19.3.2 按鍵FIFO的實現

在bsp_key.h 中定了結構體類型KEY_FIFO_T。這只是類型聲明,並沒有分配變量空間。

#define KEY_FIFO_SIZE    10
typedef struct
{
    uint8_t Buf[KEY_FIFO_SIZE];        /* 鍵值緩沖區 */
    uint8_t Read;                    /* 緩沖區讀指針1 */
    uint8_t Write;                 /* 緩沖區寫指針 */
    uint8_t Read2;                 /* 緩沖區讀指針2 */
}KEY_FIFO_T;

在bsp_key.c 中定義s_tKey結構變量, 此時編譯器會分配一組變量空間。

static KEY_FIFO_T s_tKey;        /* 按鍵FIFO變量,結構體 */

一般情況下,只需要一個寫指針Write和一個讀指針Read。在某些情況下,可能有兩個任務都需要訪問按鍵緩沖區,為了避免鍵值被其中一個任務取空,我們添加了第2個讀指針Read2。出廠程序在bsp_Idle()函數中實現的按K1K2組合鍵截屏的功能就使用的第2個讀指針。

當檢測到按鍵事件發生后,可以調用 bsp_PutKey函數將鍵值壓入FIFO。下面的代碼是函數的實現:

/*
*********************************************************************************************************
*    函 數 名: bsp_PutKey
*    功能說明: 將1個鍵值壓入按鍵FIFO緩沖區。可用於模擬一個按鍵。
*    形    參: _KeyCode : 按鍵代碼
*    返 回 值: 無
*********************************************************************************************************
*/
void bsp_PutKey(uint8_t _KeyCode)
{
    s_tKey.Buf[s_tKey.Write] = _KeyCode;

    if (++s_tKey.Write  >= KEY_FIFO_SIZE)
    {
        s_tKey.Write = 0;
    }
}

這個bsp_PutKey函數除了被按鍵檢測函數調用外,還可以被其他底層驅動調用。比如紅外遙控器的按鍵檢測,也共用了同一個按鍵FIFO。遙控器的按鍵代碼和主板實體按鍵的鍵值統一編碼,保持鍵值唯一即可實現兩套按鍵同時控制程序的功能。

應用程序讀取FIFO中的鍵值,是通過bsp_GetKey函數和bsp_GetKey2函數實現的。我們來看下這兩個函數的實現:

/*
*********************************************************************************************************
*    函 數 名: bsp_GetKey
*    功能說明: 從按鍵FIFO緩沖區讀取一個鍵值。
*    形    參:  無
*    返 回 值: 按鍵代碼
*********************************************************************************************************
*/
uint8_t bsp_GetKey(void)
{
    uint8_t ret;

    if (s_tKey.Read == s_tKey.Write)
    {
        return KEY_NONE;
    }
    else
    {
        ret = s_tKey.Buf[s_tKey.Read];

        if (++s_tKey.Read >= KEY_FIFO_SIZE)
        {
            s_tKey.Read = 0;
        }
        return ret;
    }
}

/*
*********************************************************************************************************
*    函 數 名: bsp_GetKey2
*    功能說明: 從按鍵FIFO緩沖區讀取一個鍵值。獨立的讀指針。
*    形    參:  無
*    返 回 值: 按鍵代碼
*********************************************************************************************************
*/
uint8_t bsp_GetKey2(void)
{
    uint8_t ret;

    if (s_tKey.Read2 == s_tKey.Write)
    {
        return KEY_NONE;
    }
    else
    {
        ret = s_tKey.Buf[s_tKey.Read2];

        if (++s_tKey.Read2 >= KEY_FIFO_SIZE)
        {
            s_tKey.Read2 = 0;
        }
        return ret;
    }
}

返回值KEY_NONE = 0, 表示按鍵緩沖區為空,所有的按鍵時間已經處理完畢。按鍵的鍵值定義在 bsp_key.h文件,下面是具體內容:

typedef enum
{
    KEY_NONE = 0,            /* 0 表示按鍵事件 */

    KEY_1_DOWN,            /* 1鍵按下 */
    KEY_1_UP,                /* 1鍵彈起 */
    KEY_1_LONG,            /* 1鍵長按 */

    KEY_2_DOWN,            /* 2鍵按下 */
    KEY_2_UP,                /* 2鍵彈起 */
    KEY_2_LONG,            /* 2鍵長按 */

    KEY_3_DOWN,            /* 3鍵按下 */
    KEY_3_UP,                /* 3鍵彈起 */
    KEY_3_LONG,            /* 3鍵長按 */

    KEY_4_DOWN,            /* 4鍵按下 */
    KEY_4_UP,                /* 4鍵彈起 */
    KEY_4_LONG,            /* 4鍵長按 */

    KEY_5_DOWN,            /* 5鍵按下 */
    KEY_5_UP,                /* 5鍵彈起 */
    KEY_5_LONG,            /* 5鍵長按 */

    KEY_6_DOWN,            /* 6鍵按下 */
    KEY_6_UP,                /* 6鍵彈起 */
    KEY_6_LONG,            /* 6鍵長按 */

    KEY_7_DOWN,            /* 7鍵按下 */
    KEY_7_UP,                /* 7鍵彈起 */
    KEY_7_LONG,            /* 7鍵長按 */

    KEY_8_DOWN,            /* 8鍵按下 */
    KEY_8_UP,                /* 8鍵彈起 */
    KEY_8_LONG,            /* 8鍵長按 */

    /* 組合鍵 */
    KEY_9_DOWN,            /* 9鍵按下 */
    KEY_9_UP,                /* 9鍵彈起 */
    KEY_9_LONG,            /* 9鍵長按 */

    KEY_10_DOWN,            /* 10鍵按下 */
    KEY_10_UP,            /* 10鍵彈起 */
    KEY_10_LONG,            /* 10鍵長按 */
}KEY_ENUM;

必須按次序定義每個鍵的按下、彈起和長按事件,即每個按鍵對象(組合鍵也算1個)占用3個數值。我們推薦使用枚舉enum, 不用#define的原因:

  •   便於新增鍵值,方便調整順序。
  •   使用{ } 將一組相關的定義封裝起來便於理解。
  •   編譯器可幫我們避免鍵值重復。

我們來看紅外遙控器的鍵值定義,在bsp_ir_decode.h文件。因為遙控器按鍵和主板按鍵共用同一個FIFO,因此在這里我們先貼出這段定義代碼,讓大家有個初步印象。

/* 定義紅外遙控器按鍵代碼, 和bsp_key.h 的物理按鍵代碼統一編碼 */
typedef enum
{
    IR_KEY_STRAT     = 0x80,
    IR_KEY_POWER     = IR_KEY_STRAT + 0x45,
    IR_KEY_MENU     = IR_KEY_STRAT + 0x47, 
    IR_KEY_TEST     = IR_KEY_STRAT + 0x44,
    IR_KEY_UP     = IR_KEY_STRAT + 0x40,
    IR_KEY_RETURN    = IR_KEY_STRAT + 0x43,
    IR_KEY_LEFT    = IR_KEY_STRAT + 0x07,
    IR_KEY_OK        = IR_KEY_STRAT + 0x15,
    IR_KEY_RIGHT    = IR_KEY_STRAT + 0x09,
    IR_KEY_0        = IR_KEY_STRAT + 0x16,
    IR_KEY_DOWN    = IR_KEY_STRAT + 0x19,
    IR_KEY_C        = IR_KEY_STRAT + 0x0D,
    IR_KEY_1        = IR_KEY_STRAT + 0x0C,
    IR_KEY_2        = IR_KEY_STRAT + 0x18,
    IR_KEY_3        = IR_KEY_STRAT + 0x5E,
    IR_KEY_4        = IR_KEY_STRAT + 0x08,
    IR_KEY_5        = IR_KEY_STRAT + 0x1C,
    IR_KEY_6        = IR_KEY_STRAT + 0x5A,
    IR_KEY_7        = IR_KEY_STRAT + 0x42,
    IR_KEY_8        = IR_KEY_STRAT + 0x52,
    IR_KEY_9        = IR_KEY_STRAT + 0x4A,    
}IR_KEY_E;

我們下面來看一段簡單的應用。這個應用的功能是:主板K1鍵控制LED1指示燈;遙控器的POWER鍵和MENU鍵控制LED2指示燈。

#include "bsp.h"

int main(void)
{
    uint8_t ucKeyCode;
            
    bsp_Init();

    IRD_StartWork();    /* 啟動紅外解碼 */

    while(1)
    {
        bsp_Idle();
        
        /* 處理按鍵事件 */
        ucKeyCode = bsp_GetKey();
        if (ucKeyCode > 0)
        {
            /* 有鍵按下 */
            switch (ucKeyCode)
            {
                case KEY_DOWN_K1:        /* K1鍵按下 */
                    bsp_LedOn(1);        /* 點亮LED1 */
                    break;

                case KEY_UP_K1:        /* K1鍵彈起 */
                    bsp_LedOff(1);    /* 熄滅LED1 */
                    break;                    

                case IR_KEY_POWER:        /* 遙控器POWER鍵按下 */
                    bsp_LedOn(1);        /* 點亮LED2 */
                    break;

                case IR_KEY_MENU:        /* 遙控器MENU鍵按下 */
                    bsp_LedOff(1);    /* 熄滅LED2 */
                    break;                    

                case MSG_485_RX:        /* 通信程序的發來的消息 */
                    /* 執行通信程序的指令 */
                    break;

                default:
                    break;
            }
        }
    }
}

看到這里,想必你已經意識到bsp_PutKey函數的強大之處了,可以將不相關的硬件輸入設備統一為一個相同的接口函數。

在上面的應用程序中,我們特意添加了一段紅色的代碼來解說更高級的用法。485通信程序收到有效的命令后通過 bsp_PutKey(MSG_485_RX)函數可以通知APP應用程序進行進一步加工處理(比如顯示接收成功)。這是一種非常好的任務間信息傳遞方式,它不會破壞程序結構。不必新增全局變量來做這種事情,你只需要添加一個鍵值代碼。

對於簡單的程序,可以借用按鍵FIFO來進行少量的信息傳遞。對於復雜的應用,我們推薦使用bsp_msg專門來做這種任務間的通信。因為bsp_msg除了傳遞消息代碼外,還可以傳遞參數結構。

19.3.3 按鍵檢測程序分析

在bsp_key.h 中定了結構體類型KEY_T。

#define KEY_COUNT    10               /* 按鍵個數, 8個獨立建 + 2個組合鍵 */

typedef struct
{
    /* 下面是一個函數指針,指向判斷按鍵手否按下的函數 */
    uint8_t (*IsKeyDownFunc)(void); /* 按鍵按下的判斷函數,1表示按下 */

    uint8_t  Count;        /* 濾波器計數器 */
    uint16_t LongCount;    /* 長按計數器 */
    uint16_t LongTime;        /* 按鍵按下持續時間, 0表示不檢測長按 */
    uint8_t  State;        /* 按鍵當前狀態(按下還是彈起) */
    uint8_t  RepeatSpeed;    /* 連續按鍵周期 */
    uint8_t  RepeatCount;    /* 連續按鍵計數器 */
}KEY_T;

在bsp_key.c 中定義s_tBtn結構體數組變量。

static KEY_T s_tBtn[KEY_COUNT];
static KEY_FIFO_T s_tKey;        /* 按鍵FIFO變量,結構體 */

每個按鍵對象都分配一個結構體變量,這些結構體變量以數組的形式存在將便於我們簡化程序代碼行數。

使用函數指針IsKeyDownFunc可以將每個按鍵的檢測以及組合鍵的檢測代碼進行統一管理。

因為函數指針必須先賦值,才能被作為函數執行。因此在定時掃描按鍵之前,必須先執行一段初始化函數來設置每個按鍵的函數指針和參數。這個函數是 void bsp_InitKey(void)。它由bsp_Init()調用。

/*
*********************************************************************************************************
*    函 數 名: bsp_InitKey
*    功能說明: 初始化按鍵. 該函數被 bsp_Init() 調用。
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
void bsp_InitKey(void)
{
    bsp_InitKeyVar();        /* 初始化按鍵變量 */
    bsp_InitKeyHard();        /* 初始化按鍵硬件 */
}

下面是bsp_InitKeyVar函數的定義:

/*
*********************************************************************************************************
*    函 數 名: bsp_InitKeyVar
*    功能說明: 初始化按鍵變量
*    形    參:  無
*    返 回 值: 無
*********************************************************************************************************
*/
static void bsp_InitKeyVar(void)
{
    uint8_t i;

    /* 對按鍵FIFO讀寫指針清零 */
    s_tKey.Read = 0;
    s_tKey.Write = 0;
    s_tKey.Read2 = 0;

    /* 給每個按鍵結構體成員變量賦一組缺省值 */
    for (i = 0; i < KEY_COUNT; i++)
    {
        s_tBtn[i].LongTime = KEY_LONG_TIME;            /* 長按時間 0 表示不檢測長按鍵事件 */
        s_tBtn[i].Count = KEY_FILTER_TIME / 2;        /* 計數器設置為濾波時間的一半 */
        s_tBtn[i].State = 0;                        /* 按鍵缺省狀態,0為未按下 */
        s_tBtn[i].RepeatSpeed = 0;                    /* 按鍵連發的速度,0表示不支持連發 */
        s_tBtn[i].RepeatCount = 0;                    /* 連發計數器 */
    }

    /* 如果需要單獨更改某個按鍵的參數,可以在此單獨重新賦值 */
    
    /* 搖桿上下左右,支持長按1秒后,自動連發 */
    bsp_SetKeyParam(KID_JOY_U, 100, 6);
    bsp_SetKeyParam(KID_JOY_D, 100, 6);
    bsp_SetKeyParam(KID_JOY_L, 100, 6);
    bsp_SetKeyParam(KID_JOY_R, 100, 6);
}

注意一下 Count 這個成員變量,沒有設置為0。為了避免主板上電的瞬間,檢測到一個無效的按鍵按下或彈起事件。我們將這個濾波計數器的初值設置為正常值的1/2。bsp_key.h中定義了濾波時間和長按時間。

/*
    按鍵濾波時間50ms, 單位10ms。
    只有連續檢測到50ms狀態不變才認為有效,包括彈起和按下兩種事件
    即使按鍵電路不做硬件濾波,該濾波機制也可以保證可靠地檢測到按鍵事件
*/
#define KEY_FILTER_TIME   5
#define KEY_LONG_TIME     100            /* 單位10ms, 持續1秒,認為長按事件 */

uint8_t KeyPinActive(uint8_t _id)(會調用函數KeyPinActive判斷狀態)函數就是最底層的GPIO輸入狀態判斷函數。

/*
*********************************************************************************************************
*    函 數 名: KeyPinActive
*    功能說明: 判斷按鍵是否按下
*    形    參: 無
*    返 回 值: 返回值1 表示按下(導通),0表示未按下(釋放)
*********************************************************************************************************
*/
static uint8_t KeyPinActive(uint8_t _id)
{
    uint8_t level;
    
    if ((s_gpio_list[_id].gpio->IDR & s_gpio_list[_id].pin) == 0)
    {
        level = 0;
    }
    else
    {
        level = 1;
    }

    if (level == s_gpio_list[_id].ActiveLevel)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

/*
*********************************************************************************************************
*    函 數 名: IsKeyDownFunc
*    功能說明: 判斷按鍵是否按下。單鍵和組合鍵區分。單鍵事件不允許有其他鍵按下。
*    形    參: 無
*    返 回 值: 返回值1 表示按下(導通),0表示未按下(釋放)
*********************************************************************************************************
*/
static uint8_t IsKeyDownFunc(uint8_t _id)
{
    /* 實體單鍵 */
    if (_id < HARD_KEY_NUM)
    {
        uint8_t i;
        uint8_t count = 0;
        uint8_t save = 255;
        
        /* 判斷有幾個鍵按下 */
        for (i = 0; i < HARD_KEY_NUM; i++)
        {
            if (KeyPinActive(i)) 
            {
                count++;
                save = i;
            }
        }
        
        if (count == 1 && save == _id)
        {
            return 1;    /* 只有1個鍵按下時才有效 */
        }        

        return 0;
    }
    
    /* 組合鍵 K1K2 */
    if (_id == HARD_KEY_NUM + 0)
    {
        if (KeyPinActive(KID_K1) && KeyPinActive(KID_K2))
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }

    /* 組合鍵 K2K3 */
    if (_id == HARD_KEY_NUM + 1)
    {
        if (KeyPinActive(KID_K2) && KeyPinActive(KID_K3))
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }

    return 0;
}

在使用GPIO之前,我們必須對GPIO進行配置,比如打開GPIO時鍾,設置GPIO輸入輸出方向,設置上下拉電阻。下面是配置GPIO的代碼,也就是bsp_InitKeyHard()函數:

/*
*********************************************************************************************************
*    函 數 名: bsp_InitKeyHard
*    功能說明: 配置按鍵對應的GPIO
*    形    參:  無
*    返 回 值: 無
*********************************************************************************************************
*/
static void bsp_InitKeyHard(void)
{    
    GPIO_InitTypeDef gpio_init;
    uint8_t i;

    /* 第1步:打開GPIO時鍾 */
    ALL_KEY_GPIO_CLK_ENABLE();
    
    /* 第2步:配置所有的按鍵GPIO為浮動輸入模式(實際上CPU復位后就是輸入狀態) */
    gpio_init.Mode = GPIO_MODE_INPUT;               /* 設置輸入 */
    gpio_init.Pull = GPIO_NOPULL;                 /* 上下拉電阻不使能 */
    gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH;  /* GPIO速度等級 */
    
    for (i = 0; i < HARD_KEY_NUM; i++)
    {
        gpio_init.Pin = s_gpio_list[i].pin;
        HAL_GPIO_Init(s_gpio_list[i].gpio, &gpio_init);    
    }
}

我們再來看看按鍵是如何執行掃描檢測的。

按鍵掃描函數bsp_KeyScan10ms ()每隔10ms被執行一次。bsp_RunPer10ms函數在systick中斷服務程序中執行。

void bsp_RunPer10ms(void)
{
    bsp_KeyScan10ms();        /* 掃描按鍵 */
}

bsp_KeyScan10ms ()函數的實現如下:

/*
*********************************************************************************************************
*    函 數 名: bsp_KeyScan10ms
*    功能說明: 掃描所有按鍵。非阻塞,被systick中斷周期性的調用,10ms一次
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
void bsp_KeyScan10ms(void)
{
    uint8_t i;

    for (i = 0; i < KEY_COUNT; i++)
    {
        bsp_DetectKey(i);
    }
}

每隔10ms所有的按鍵GPIO均會被掃描檢測一次。bsp_DetectKey函數實現如下:

/*
*********************************************************************************************************
*    函 數 名: bsp_DetectKey
*    功能說明: 檢測一個按鍵。非阻塞狀態,必須被周期性的調用。
*    形    參: IO的id, 從0開始編碼
*    返 回 值: 無
*********************************************************************************************************
*/
static void bsp_DetectKey(uint8_t i)
{
    KEY_T *pBtn;

    pBtn = &s_tBtn[i];
    if (IsKeyDownFunc(i))
    {
        if (pBtn->Count < KEY_FILTER_TIME)
        {
            pBtn->Count = KEY_FILTER_TIME;
        }
        else if(pBtn->Count < 2 * KEY_FILTER_TIME)
        {
            pBtn->Count++;
        }
        else
        {
            if (pBtn->State == 0)
            {
                pBtn->State = 1;

                /* 發送按鈕按下的消息 */
                bsp_PutKey((uint8_t)(3 * i + 1));
            }

            if (pBtn->LongTime > 0)
            {
                if (pBtn->LongCount < pBtn->LongTime)
                {
                    /* 發送按鈕持續按下的消息 */
                    if (++pBtn->LongCount == pBtn->LongTime)
                    {
                        /* 鍵值放入按鍵FIFO */
                        bsp_PutKey((uint8_t)(3 * i + 3));
                    }
                }
                else
                {
                    if (pBtn->RepeatSpeed > 0)
                    {
                        if (++pBtn->RepeatCount >= pBtn->RepeatSpeed)
                        {
                            pBtn->RepeatCount = 0;
                            /* 常按鍵后,每隔RepeatSpeed * 10ms發送1個按鍵 */
                            bsp_PutKey((uint8_t)(3 * i + 1));
                        }
                    }
                }
            }
        }
    }
    else
    {
        if(pBtn->Count > KEY_FILTER_TIME)
        {
            pBtn->Count = KEY_FILTER_TIME;
        }
        else if(pBtn->Count != 0)
        {
            pBtn->Count--;
        }
        else
        {
            if (pBtn->State == 1)
            {
                pBtn->State = 0;

                /* 發送按鈕彈起的消息 */
                bsp_PutKey((uint8_t)(3 * i + 2));
            }
        }

        pBtn->LongCount = 0;
        pBtn->RepeatCount = 0;
    }
}

對於初學者,這個函數看起來比較吃力,我們拆分進行分析。

pBtn = &s_tBtn[i];

讀取相應按鍵的結構體地址,程序里面每個按鍵都有自己的結構體。

static KEY_T s_tBtn[KEY_COUNT];
if (IsKeyDownFunc(i))
{
    這個里面執行的是按鍵按下的處理
}
else
{
    這個里面執行的是按鍵松手的處理或者按鍵沒有按下的處理
}

執行函數IsKeyDownFunc(i)做按鍵狀態判斷。

/*
**********************************************************************************
下面這個if語句主要是用於按鍵濾波前給Count設置一個初值,前面說按鍵初始化的時候
已經設置了Count = KEY_FILTER_TIME/2
**********************************************************************************
*/
if (pBtn->Count < KEY_FILTER_TIME)
{
    pBtn->Count = KEY_FILTER_TIME;
}

/*
**********************************************************************************
這里實現KEY_FILTER_TIME時間長度的延遲
**********************************************************************************
*/
else if(pBtn->Count < 2 * KEY_FILTER_TIME)
{
    pBtn->Count++;
}
/*
**********************************************************************************
這里實現KEY_FILTER_TIME時間長度的延遲
**********************************************************************************
*/
else
{
/*
**********************************************************************************
這個State變量是有其實際意義的,如果按鍵按下了,這里就將其設置為1,如果沒有按下這個
變量的值就會一直是0,這樣設置的目的可以有效的防止一種情況的出現:比如按鍵K1在某個
時刻檢測到了按鍵有按下,那么它就會做進一步的濾波處理,但是在濾波的過程中,這個按鍵
按下的狀態消失了,這個時候就會進入到上面第二步else語句里面,然后再做按鍵松手檢測濾波
,濾波結束后判斷這個State變量,如果前面就沒有檢測到按下,這里就不會記錄按鍵彈起。
**********************************************************************************
*/
    if (pBtn->State == 0)
    {
        pBtn->State = 1;

        /* 發送按鈕按下的消息 */
        bsp_PutKey((uint8_t)(3 * i + 1));
    }

    if (pBtn->LongTime > 0)
    {
        if (pBtn->LongCount < pBtn->LongTime)
        {
            /* 發送按鈕持續按下的消息 */
            if (++pBtn->LongCount == pBtn->LongTime)
            {
                /* 鍵值放入按鍵FIFO */
                bsp_PutKey((uint8_t)(3 * i + 3));
            }
        }
        else
        {
            if (pBtn->RepeatSpeed > 0)
            {
                if (++pBtn->RepeatCount >= pBtn->RepeatSpeed)
                {
                    pBtn->RepeatCount = 0;
                    /* 長按鍵后,每隔10ms發送1個按鍵 */
                    bsp_PutKey((uint8_t)(3 * i + 1));
                }
            }
        }
    }
}

19.3.4 按鍵檢測采用中斷方式還是查詢方式

檢測按鍵有中斷方式和GPIO查詢方式兩種。我們推薦大家用GPIO查詢方式。

從裸機的角度分析

中斷方式:中斷方式可以快速地檢測到按鍵按下,並執行相應的按鍵程序,但實際情況是由於按鍵的機械抖動特性,在程序進入中斷后必須進行濾波處理才能判定是否有效的按鍵事件。如果每個按鍵都是獨立的接一個IO引腳,需要我們給每個IO都設置一個中斷,程序中過多的中斷會影響系統的穩定性。中斷方式跨平台移植困難。

查詢方式:查詢方式有一個最大的缺點就是需要程序定期的去執行查詢,耗費一定的系統資源。實際上耗費不了多大的系統資源,因為這種查詢方式也只是查詢按鍵是否按下,按鍵事件的執行還是在主程序里面實現。

從OS的角度分析

中斷方式:在OS中要盡可能少用中斷方式,因為在RTOS中過多的使用中斷會影響系統的穩定性和可預見性(搶占式調度的OS基本沒有可預見性)。只有比較重要的事件處理需要用中斷的方式。

查詢方式:對於用戶按鍵推薦使用這種查詢方式來實現,現在的OS基本都帶有CPU利用率的功能,這個按鍵FIFO占用的還是很小的,基本都在1%以下。

19.4 按鍵板級支持包(bsp_key.c)

按鍵驅動文件bsp_key.c主要實現了如下幾個API:

  •   KeyPinActive
  •   IsKeyDownFunc
  •   bsp_InitKey
  •   bsp_InitKeyHard
  •   bsp_InitKeyVar
  •   bsp_PutKey
  •   bsp_GetKey
  •   bsp_GetKey2
  •   bsp_GetKeyState
  •   bsp_SetKeyParam
  •   bsp_ClearKey
  •   bsp_DetectKey
  •   bsp_DetectFastIO
  •   bsp_KeyScan10ms
  •   bsp_KeyScan1ms

 

所有這些函數在本章的19.3小節都進行了詳細講解,本小節主要是把需要用戶調用的三個函數做個說明。

19.4.1 函數bsp_InitKeyHard

函數原型:

/*
*********************************************************************************************************
*    函 數 名: bsp_InitKey
*    功能說明: 初始化按鍵. 該函數被 bsp_Init() 調用。
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
void bsp_InitKey(void)
{
    bsp_InitKeyVar();        /* 初始化按鍵變量 */
    bsp_InitKeyHard();        /* 初始化按鍵硬件 */
}

函數描述:

此函數主要用於按鍵的初始化。

使用舉例:

底層驅動初始化直接在bsp.c文件的函數bsp_Init里面調用即可。

19.4.2 函數bsp_GetKey

函數原型:

/*
*********************************************************************************************************
*    函 數 名: bsp_GetKey
*    功能說明: 從按鍵FIFO緩沖區讀取一個鍵值。
*    形    參: 無
*    返 回 值: 按鍵代碼
*********************************************************************************************************
*/
uint8_t bsp_GetKey(void)
{
    uint8_t ret;

    if (s_tKey.Read == s_tKey.Write)
    {
        return KEY_NONE;
    }
    else
    {
        ret = s_tKey.Buf[s_tKey.Read];

        if (++s_tKey.Read >= KEY_FIFO_SIZE)
        {
            s_tKey.Read = 0;
        }
        return ret;
    }
}

函數描述:

此函數用於從FIFO中讀取鍵值。

使用舉例:

調用此函數前,務必優先調用函數bsp_InitKey進行初始化。

/*
*********************************************************************************************************
*    函 數 名: main
*    功能說明: c程序入口
*    形    參: 無
*    返 回 值: 錯誤代碼(無需處理)
*********************************************************************************************************
*/
int main(void)
{
    uint8_t ucKeyCode;        /* 按鍵代碼 */
    
    bsp_Init();        /* 硬件初始化 */
    
    /* 進入主程序循環體 */
    while (1)
    {        
        /* 按鍵濾波和檢測由后台systick中斷服務程序實現,我們只需要調用bsp_GetKey讀取鍵值即可。 */
        ucKeyCode = bsp_GetKey();    /* 讀取鍵值, 無鍵按下時返回 KEY_NONE = 0 */
        if (ucKeyCode != KEY_NONE)
        {
            switch (ucKeyCode)
            {
                case KEY_DOWN_K1:            /* K1鍵按下 */
                    printf("K1鍵按下\r\n");
                    break;

                case KEY_DOWN_K2:            /* K2鍵按下 */
                    printf("K2鍵按下\r\n");
                    break;

                default:
                    /* 其它的鍵值不處理 */
                    break;
            }
        }
    }
}

19.4.3 函數bsp_KeyScan10ms

函數原型:

/*
*********************************************************************************************************
*    函 數 名: bsp_KeyScan10ms
*    功能說明: 掃描所有按鍵。非阻塞,被systick中斷周期性的調用,10ms一次
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
void bsp_KeyScan10ms(void)
{
    uint8_t i;

    for (i = 0; i < KEY_COUNT; i++)
    {
        bsp_DetectKey(i);
    }
}

函數描述:

此函數是按鍵的主處理函數,用於檢測和存儲按下、松手、長按等狀態。

使用舉例:

調用此函數前,務必優先調用函數bsp_InitKey進行初始化。

另外,此函數需要周期性調用,每10ms調用一次。

  •   如果是裸機使用,將此函數放在bsp.c文件的bsp_RunPer10ms函數里面即可,這個函數是由滴答定時器調用的,也就是說,大家要使用按鍵,定時器的初始化函數bsp_InitTimer一定要調用。
  •   如果是RTOS使用,需要開啟一個10ms為周期的任務調用函數bsp_KeyScan10ms。

19.5 按鍵FIFO驅動移植和使用

按鍵移植步驟如下:

  •   第1步:復制bsp_key.c和bsp_key.c到自己的工程。
  •   第2步:根據自己使用的獨立按鍵個數和組合鍵個數,修改幾個地方。
#define HARD_KEY_NUM    8                 /* 實體按鍵個數 */
#define KEY_COUNT   (HARD_KEY_NUM + 2)   /* 8個獨立建 + 2個組合按鍵 */
  •   第3步:根據使用的引腳時鍾,修改下面函數:
/* 使能GPIO時鍾 */
#define ALL_KEY_GPIO_CLK_ENABLE() {    \
        __HAL_RCC_GPIOB_CLK_ENABLE();    \
        __HAL_RCC_GPIOC_CLK_ENABLE();    \
        __HAL_RCC_GPIOG_CLK_ENABLE();    \
        __HAL_RCC_GPIOH_CLK_ENABLE();    \
        __HAL_RCC_GPIOI_CLK_ENABLE();    \
    };
  •   第4步:根據使用的具體引腳,修改如下函數,第3列參數低電平表示按下或者高電平表示按下:
/* GPIO和PIN定義 */
static const X_GPIO_T s_gpio_list[HARD_KEY_NUM] = {
    {GPIOI, GPIO_PIN_8, 0},        /* K1 */
    {GPIOC, GPIO_PIN_13, 0},    /* K2 */
    {GPIOH, GPIO_PIN_4, 0},        /* K3 */
    {GPIOG, GPIO_PIN_2, 0},        /* JOY_U */    
    {GPIOB, GPIO_PIN_0, 0},        /* JOY_D */
    {GPIOG, GPIO_PIN_3, 0},        /* JOY_L */    
    {GPIOG, GPIO_PIN_7, 0},        /* JOY_R */    
    {GPIOI, GPIO_PIN_11, 0},    /* JOY_OK */
};
  •   第5步:根據使用的組合鍵個數,在函數IsKeyDownFunc里面添加相應個數的函數:
    /* 組合鍵 K1K2 */
    if (_id == HARD_KEY_NUM + 0)
    {
        if (KeyPinActive(KID_K1) && KeyPinActive(KID_K2))
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }

第2行ID表示HARD_KEY_NUM + 0的組合鍵,HARD_KEY_NUM + 1表示下一個組合鍵,以此類推。

另外就是,函數KeyPinActive的參數是表示檢測哪兩個按鍵,設置0的時候表示第4步里面的第1組按鍵,設置為1表示第2組按鍵,以此類推。

  •   第6步:主要用到HAL庫的GPIO驅動文件,簡單省事些可以添加所有HAL庫.C源文件進來。
  •   第7步:移植完整,應用方法看本章節配套例子即可。

特別注意,別忘了每10ms調用一次按鍵檢測函數bsp_KeyScan10ms。

19.6 實驗例程設計框架

通過程序設計框架,讓大家先對配套例程有一個全面的認識,然后再理解細節,本次實驗例程的設計框架如下:

 

 1、 第1階段,上電啟動階段:

這部分在第14章進行了詳細說明。

 2、 第2階段,進入main函數:

  •   第1部分,硬件初始化,主要是MPU、Cache、HAL庫、系統時鍾、滴答定時器、按鍵等。
  •   第2部分,應用程序設計部分,實現了一個按鍵應用。
  •   第3部分,按鍵掃描程序每10ms在滴答定時中斷執行一次。

19.7 實驗例程說明(MDK)

配套例子:

V7-002_按鍵檢測(軟件濾波,FIFO機制)

實驗目的:

  1. 學習按鍵的按下,彈起,長按和組合鍵的實現。

實驗內容:

  1. 啟動一個自動重裝軟件定時器,每100ms翻轉一次LED2。

實驗操作:

  1. 3個獨立按鍵和5向搖桿按下時均有串口消息打印。
  2. 5向搖桿的左鍵和右鍵長按時,會有連發的串口消息。
  3. 獨立按鍵K1和K2按鍵按下,串口打印消息。

上電后串口打印的信息:

波特率 115200,數據位 8,奇偶校驗位無,停止位 1

 

程序設計:

  系統棧大小分配:

 

  RAM空間用的DTCM:

 

  硬件外設初始化

硬件外設的初始化是在 bsp.c 文件實現:

/*
*********************************************************************************************************
*    函 數 名: bsp_Init
*    功能說明: 初始化所有的硬件設備。該函數配置CPU寄存器和外設的寄存器並初始化一些全局變量。只需要調用一次
*    形    參:無
*    返 回 值: 無
*********************************************************************************************************
*/
void bsp_Init(void)
{
    /* 配置MPU */
    MPU_Config();
    
    /* 使能L1 Cache */
    CPU_CACHE_Enable();

    /* 
       STM32H7xx HAL 庫初始化,此時系統用的還是H7自帶的64MHz,HSI時鍾:
       - 調用函數HAL_InitTick,初始化滴答時鍾中斷1ms。
       - 設置NVIV優先級分組為4。
     */
    HAL_Init();

    /* 
       配置系統時鍾到400MHz
       - 切換使用HSE。
       - 此函數會更新全局變量SystemCoreClock,並重新配置HAL_InitTick。
    */
    SystemClock_Config();

    /* 
       Event Recorder:
       - 可用於代碼執行時間測量,MDK5.25及其以上版本才支持,IAR不支持。
       - 默認不開啟,如果要使能此選項,務必看V7開發板用戶手冊第xx章
    */    
#if Enable_EventRecorder == 1  
    /* 初始化EventRecorder並開啟 */
    EventRecorderInitialize(EventRecordAll, 1U);
    EventRecorderStart();
#endif
    
    bsp_InitKey();        /* 按鍵初始化,要放在滴答定時器之前,因為按鈕檢測是通過滴答定時器掃描 */
    bsp_InitTimer();      /* 初始化滴答定時器 */
    bsp_InitUart();    /* 初始化串口 */
    bsp_InitExtIO();    /* 初始化FMC總線74HC574擴展IO. 必須在 bsp_InitLed()前執行 */    
    bsp_InitLed();        /* 初始化LED */    
}

 

  MPU配置和Cache配置:

數據Cache和指令Cache都開啟。配置了AXI SRAM區(本例子未用到AXI SRAM)和FMC的擴展IO區。

/*
*********************************************************************************************************
*    函 數 名: MPU_Config
*    功能說明: 配置MPU
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
static void MPU_Config( void )
{
    MPU_Region_InitTypeDef MPU_InitStruct;

    /* 禁止 MPU */
    HAL_MPU_Disable();

    /* 配置AXI SRAM的MPU屬性為Write back, Read allocate,Write allocate */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x24000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_512KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL1;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    
    
    /* 配置FMC擴展IO的MPU屬性為Device或者Strongly Ordered */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x60000000;
    MPU_InitStruct.Size             = ARM_MPU_REGION_SIZE_64KB;    
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;    
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER1;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;
    
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    /*使能 MPU */
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

/*
*********************************************************************************************************
*    函 數 名: CPU_CACHE_Enable
*    功能說明: 使能L1 Cache
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
static void CPU_CACHE_Enable(void)
{
    /* 使能 I-Cache */
    SCB_EnableICache();

    /* 使能 D-Cache */
    SCB_EnableDCache();
}

 

  每10ms調用一次按鍵檢測:

按鍵檢測是在滴答定時器中斷里面實現,每10ms執行一次檢測。

/*
*********************************************************************************************************
*    函 數 名: bsp_RunPer10ms
*    功能說明: 該函數每隔10ms被Systick中斷調用1次。詳見 bsp_timer.c的定時中斷服務程序。一些處理時間要求
*              不嚴格的任務可以放在此函數。比如:按鍵掃描、蜂鳴器鳴叫控制等。
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
void bsp_RunPer10ms(void)
{
    bsp_KeyScan10ms();
}

 

  主功能:

主功能的實現主要分為兩部分:

  •  啟動一個自動重裝軟件定時器,每100ms翻轉一次LED2
  •  按鍵消息的讀取,檢測到按下后,做串口打印。
/*
*********************************************************************************************************
*    函 數 名: main
*    功能說明: c程序入口
*    形    參: 無
*    返 回 值: 錯誤代碼(無需處理)
*********************************************************************************************************
*/
int main(void)
{
    uint8_t ucKeyCode;        /* 按鍵代碼 */
    
    bsp_Init();        /* 硬件初始化 */
    
    PrintfLogo();    /* 打印例程名稱和版本等信息 */
    PrintfHelp();    /* 打印操作提示 */


    bsp_StartAutoTimer(0, 100); /* 啟動1個100ms的自動重裝的定時器 */
    
    /* 進入主程序循環體 */
    while (1)
    {
        bsp_Idle();        /* 這個函數在bsp.c文件。用戶可以修改這個函數實現CPU休眠和喂狗 */

        /* 判斷定時器超時時間 */
        if (bsp_CheckTimer(0))    
        {
            /* 每隔100ms 進來一次 */  
            bsp_LedToggle(2);            
        }
        
        /* 按鍵濾波和檢測由后台systick中斷服務程序實現,我們只需要調用bsp_GetKey讀取鍵值即可。 */
        ucKeyCode = bsp_GetKey();    /* 讀取鍵值, 無鍵按下時返回 KEY_NONE = 0 */
        if (ucKeyCode != KEY_NONE)
        {
            switch (ucKeyCode)
            {
                case KEY_DOWN_K1:            /* K1鍵按下 */
                    printf("K1鍵按下\r\n");
                    break;

                case KEY_UP_K1:                /* K1鍵彈起 */
                    printf("K1鍵彈起\r\n");
                    break;

                case KEY_DOWN_K2:            /* K2鍵按下 */
                    printf("K2鍵按下\r\n");
                    break;

                case KEY_UP_K2:                /* K2鍵彈起 */
                    printf("K2鍵彈起\r\n");
                    break;

                case KEY_DOWN_K3:            /* K3鍵按下 */
                    printf("K3鍵按下\r\n");
                    break;

                case KEY_UP_K3:                /* K3鍵彈起 */
                    printf("K3鍵彈起\r\n");
                    break;

                case JOY_DOWN_U:            /* 搖桿UP鍵按下 */
                    printf("搖桿上鍵按下\r\n");
                    break;

                case JOY_DOWN_D:            /* 搖桿DOWN鍵按下 */
                    printf("搖桿下鍵按下\r\n");
                    break;

                case JOY_DOWN_L:            /* 搖桿LEFT鍵按下 */
                    printf("搖桿左鍵按下\r\n");
                    break;
                
                case JOY_LONG_L:            /* 搖桿LEFT鍵長按 */
                    printf("搖桿左鍵長按\r\n");
                    break;

                case JOY_DOWN_R:            /* 搖桿RIGHT鍵按下 */
                    printf("搖桿右鍵按下\r\n");
                    break;
                
                case JOY_LONG_R:            /* 搖桿RIGHT鍵長按 */
                    printf("搖桿右鍵長按\r\n");
                    break;

                case JOY_DOWN_OK:            /* 搖桿OK鍵按下 */
                    printf("搖桿OK鍵按下\r\n");
                    break;

                case JOY_UP_OK:            /* 搖桿OK鍵彈起 */
                    printf("搖桿OK鍵彈起\r\n");
                    break;
                    
                case SYS_DOWN_K1K2:            /* 搖桿OK鍵彈起 */
                    printf("K1和K2組合鍵按下\r\n");
                    break;

                default:
                    /* 其它的鍵值不處理 */
                    break;
            }
        
        }
    }
}

 

19.8 實驗例程說明(IAR)

配套例子:

V7-002_按鍵檢測(軟件濾波,FIFO機制)

實驗目的:

  1. 學習按鍵的按下,彈起,長按和組合鍵的實現。

實驗內容:

  1. 啟動一個自動重裝軟件定時器,每100ms翻轉一次LED2。

實驗操作:

  1. 3個獨立按鍵和5向搖桿按下時均有串口消息打印。
  2. 5向搖桿的左鍵和右鍵長按時,會有連發的串口消息。
  3. 獨立按鍵K1和K2按鍵按下,串口打印消息。

上電后串口打印的信息:

波特率 115200,數據位 8,奇偶校驗位無,停止位 1

 

程序設計:

  系統棧大小分配:

 

  RAM空間用的DTCM:

 

  硬件外設初始化

硬件外設的初始化是在 bsp.c 文件實現:

/*
*********************************************************************************************************
*    函 數 名: bsp_Init
*    功能說明: 初始化所有的硬件設備。該函數配置CPU寄存器和外設的寄存器並初始化一些全局變量。只需要調用一次
*    形    參:無
*    返 回 值: 無
*********************************************************************************************************
*/
void bsp_Init(void)
{
    /* 配置MPU */
    MPU_Config();
    
    /* 使能L1 Cache */
    CPU_CACHE_Enable();

    /* 
       STM32H7xx HAL 庫初始化,此時系統用的還是H7自帶的64MHz,HSI時鍾:
       - 調用函數HAL_InitTick,初始化滴答時鍾中斷1ms。
       - 設置NVIV優先級分組為4。
     */
    HAL_Init();

    /* 
       配置系統時鍾到400MHz
       - 切換使用HSE。
       - 此函數會更新全局變量SystemCoreClock,並重新配置HAL_InitTick。
    */
    SystemClock_Config();

    /* 
       Event Recorder:
       - 可用於代碼執行時間測量,MDK5.25及其以上版本才支持,IAR不支持。
       - 默認不開啟,如果要使能此選項,務必看V7開發板用戶手冊第xx章
    */    
#if Enable_EventRecorder == 1  
    /* 初始化EventRecorder並開啟 */
    EventRecorderInitialize(EventRecordAll, 1U);
    EventRecorderStart();
#endif
    
    bsp_InitKey();        /* 按鍵初始化,要放在滴答定時器之前,因為按鈕檢測是通過滴答定時器掃描 */
    bsp_InitTimer();      /* 初始化滴答定時器 */
    bsp_InitUart();    /* 初始化串口 */
    bsp_InitExtIO();    /* 初始化FMC總線74HC574擴展IO. 必須在 bsp_InitLed()前執行 */    
    bsp_InitLed();        /* 初始化LED */    
}

 

  MPU配置和Cache配置:

數據Cache和指令Cache都開啟。配置了AXI SRAM區(本例子未用到AXI SRAM)和FMC的擴展IO區。

/*
*********************************************************************************************************
*    函 數 名: MPU_Config
*    功能說明: 配置MPU
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
static void MPU_Config( void )
{
    MPU_Region_InitTypeDef MPU_InitStruct;

    /* 禁止 MPU */
    HAL_MPU_Disable();

    /* 配置AXI SRAM的MPU屬性為Write back, Read allocate,Write allocate */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x24000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_512KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL1;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    
    
    /* 配置FMC擴展IO的MPU屬性為Device或者Strongly Ordered */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x60000000;
    MPU_InitStruct.Size             = ARM_MPU_REGION_SIZE_64KB;    
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;    
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER1;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;
    
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    /*使能 MPU */
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

/*
*********************************************************************************************************
*    函 數 名: CPU_CACHE_Enable
*    功能說明: 使能L1 Cache
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
static void CPU_CACHE_Enable(void)
{
    /* 使能 I-Cache */
    SCB_EnableICache();

    /* 使能 D-Cache */
    SCB_EnableDCache();
}

 

  每10ms調用一次按鍵檢測:

按鍵檢測是在滴答定時器中斷里面實現,每10ms執行一次檢測。

/*
*********************************************************************************************************
*    函 數 名: bsp_RunPer10ms
*    功能說明: 該函數每隔10ms被Systick中斷調用1次。詳見 bsp_timer.c的定時中斷服務程序。一些處理時間要求
*              不嚴格的任務可以放在此函數。比如:按鍵掃描、蜂鳴器鳴叫控制等。
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
void bsp_RunPer10ms(void)
{
    bsp_KeyScan10ms();
}

 

 主功能:

主功能的實現主要分為兩部分:

  •  啟動一個自動重裝軟件定時器,每100ms翻轉一次LED2
  •  按鍵消息的讀取,檢測到按下后,做串口打印。
/*
*********************************************************************************************************
*    函 數 名: main
*    功能說明: c程序入口
*    形    參: 無
*    返 回 值: 錯誤代碼(無需處理)
*********************************************************************************************************
*/
int main(void)
{
    uint8_t ucKeyCode;        /* 按鍵代碼 */
    
    bsp_Init();        /* 硬件初始化 */
    
    PrintfLogo();    /* 打印例程名稱和版本等信息 */
    PrintfHelp();    /* 打印操作提示 */


    bsp_StartAutoTimer(0, 100); /* 啟動1個100ms的自動重裝的定時器 */
    
    /* 進入主程序循環體 */
    while (1)
    {
        bsp_Idle();        /* 這個函數在bsp.c文件。用戶可以修改這個函數實現CPU休眠和喂狗 */

        /* 判斷定時器超時時間 */
        if (bsp_CheckTimer(0))    
        {
            /* 每隔100ms 進來一次 */  
            bsp_LedToggle(2);            
        }
        
        /* 按鍵濾波和檢測由后台systick中斷服務程序實現,我們只需要調用bsp_GetKey讀取鍵值即可。 */
        ucKeyCode = bsp_GetKey();    /* 讀取鍵值, 無鍵按下時返回 KEY_NONE = 0 */
        if (ucKeyCode != KEY_NONE)
        {
            switch (ucKeyCode)
            {
                case KEY_DOWN_K1:            /* K1鍵按下 */
                    printf("K1鍵按下\r\n");
                    break;

                case KEY_UP_K1:                /* K1鍵彈起 */
                    printf("K1鍵彈起\r\n");
                    break;

                case KEY_DOWN_K2:            /* K2鍵按下 */
                    printf("K2鍵按下\r\n");
                    break;

                case KEY_UP_K2:                /* K2鍵彈起 */
                    printf("K2鍵彈起\r\n");
                    break;

                case KEY_DOWN_K3:            /* K3鍵按下 */
                    printf("K3鍵按下\r\n");
                    break;

                case KEY_UP_K3:                /* K3鍵彈起 */
                    printf("K3鍵彈起\r\n");
                    break;

                case JOY_DOWN_U:            /* 搖桿UP鍵按下 */
                    printf("搖桿上鍵按下\r\n");
                    break;

                case JOY_DOWN_D:            /* 搖桿DOWN鍵按下 */
                    printf("搖桿下鍵按下\r\n");
                    break;

                case JOY_DOWN_L:            /* 搖桿LEFT鍵按下 */
                    printf("搖桿左鍵按下\r\n");
                    break;
                
                case JOY_LONG_L:            /* 搖桿LEFT鍵長按 */
                    printf("搖桿左鍵長按\r\n");
                    break;

                case JOY_DOWN_R:            /* 搖桿RIGHT鍵按下 */
                    printf("搖桿右鍵按下\r\n");
                    break;
                
                case JOY_LONG_R:            /* 搖桿RIGHT鍵長按 */
                    printf("搖桿右鍵長按\r\n");
                    break;

                case JOY_DOWN_OK:            /* 搖桿OK鍵按下 */
                    printf("搖桿OK鍵按下\r\n");
                    break;

                case JOY_UP_OK:            /* 搖桿OK鍵彈起 */
                    printf("搖桿OK鍵彈起\r\n");
                    break;
                    
                case SYS_DOWN_K1K2:            /* 搖桿OK鍵彈起 */
                    printf("K1和K2組合鍵按下\r\n");
                    break;

                default:
                    /* 其它的鍵值不處理 */
                    break;
            }
        
        }
    }
}

 

19.9 總結

這個方案在實際項目中已經經過千錘百煉,大家可以放心使用。建議熟練掌握其用法。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM