微控制器怎麼用回撥函數在不同檔案之間傳遞資料

2021-09-27 10:00:46

大家好,我是無際。

今天繼續來聊下回撥函數。

之前寫過一篇受到了廣大老鐵們的認可。

最近有幾個新學員被回撥函數搞得有點懵逼。

不理解為什麼要搞這種繞來繞去、指標指來指去的函數。

先寫篇文章預熱一下,晚上再直播跟大家互動講解和答疑。

其實並不是我想把簡單的東西複雜化,而是如果你想寫出好的程式碼架構,回撥函數是必不可少的。

如果你去看那些大神寫的程式,你會發現他們都是這樣做的,比如說藍芽協定棧、實時作業系統、STM32韌體庫等等。

每個人寫得風格可能不一樣,但是本質是一樣的。

我們先來理解一下回撥函數的作用。

函數我一般喜歡分為輸出型和輸入型(個人理解)。

輸出型:

就是我們主動去呼叫的控制函數,比如說控制LED燈去亮和滅,控制蜂鳴器響和不響,控制LCD顯示,控制繼電器吸合和斷開。

簡單來說,就是我們知道什麼時候該去呼叫這些函數,比如說滿足某些條件的時候,我們就會主動去呼叫這些函數。

這種函數,就是輸出型函數。

輸入型:

輸入型函數一般是用在不同.c檔案/不同層(硬體層、應用層)之間傳遞訊號和資料的,比如說按鍵檢測、串列埠資料。

我們不知道什麼時候按鍵會被按下、什麼時候串列埠會有資料過來對吧?

當然,我們可以寫一個帶返回值的函數,然後定時去檢測,比如說定時10ms去掃描一下按鍵。

Unsigned char ScanKey()

{

//按鍵檢測程式…
}

然後我們在主程式用:

while(1)

{

Unsigned char key;

If(10ms時間到)

{

Key = ScanKey();
}

       if(Key == 有效按鍵值)

       {

              //執行按鍵功能程式

}

}

這樣不斷地去掃描按鍵,檢測按鍵是否被按下。

這種方式當然也是可以的,只是不夠專業,不夠好。

因為這個我需要一直在while迴圈裡判斷Key的值,然後根據Key的值來判斷有沒有按鍵按下,在一定程度上,造成了cpu資源的浪費。

而且有些應用場景,這種方式不好實現,比如說串列埠資料,你不能一直在while迴圈裡判斷是否有新的串列埠資料過來吧?

那我們理想的一種狀態是什麼?

就是如果有按鍵按下了,或者有新的資料來了,再通知我。

這種通知方式一般叫事件觸發,就是觸發了按鍵這個事件,我才去處理。

所以,這個時候回撥函數就能很好地解決這種需求。

我們還是拿按鍵來舉例。

前面我說每個人寫回撥函數的風格可能都不一樣,STM32韌體庫的那些中斷處理常式基本都是回撥函數,但是跟我的編寫風格還是有些差異。

我們在寫回撥函數的時候,需要以下幾步:

第一步:

自定義一個函數指標型別,型別名稱是KeyEvent_CallBack_t。

typedef void (*KeyEvent_CallBack_t)(KEY_VALUE_TYPEDEF keys);

還有這個一般是要自定義在標頭檔案,因為別的.c檔案也會用到。

這是一個無返回值的形參是KEY_VALUE_TYPEDEF列舉型別函數指標型別

一般這個形參keys就是我們最終要通過回撥函數傳遞到別的.c檔案的訊號/資料,如果是按鍵檢測的話也就是按鍵值,是哪個按鍵按下的。

我們來看下KEY_VALUE_TYPEDEF這個列舉都有哪些值?

typedef enum

{

       KEY_IDLE_VAL,

       KEY1_CLICK,

       KEY1_CLICK_RELEASE,

       KEY1_LONG_PRESS,

       KEY1_LONG_PRESS_CONTINUOUS,

       KEY1_LONG_PRESS_RELEASE,           //5

       KEY2_CLICK,                                                   //6

       KEY2_CLICK_RELEASE,

       KEY2_LONG_PRESS,

       KEY2_LONG_PRESS_CONTINUOUS,

       KEY2_LONG_PRESS_RELEASE,

       KEY3_CLICK,                                            //11

       KEY3_CLICK_RELEASE,

       KEY3_LONG_PRESS,

       KEY3_LONG_PRESS_CONTINUOUS,

       KEY3_LONG_PRESS_RELEASE,

       KEY4_CLICK,                                     //16

       KEY4_CLICK_RELEASE,

       KEY4_LONG_PRESS,

       KEY4_LONG_PRESS_CONTINUOUS,

       KEY4_LONG_PRESS_RELEASE,

       KEY5_CLICK,                                     //21

       KEY5_CLICK_RELEASE,

       KEY5_LONG_PRESS,

       KEY5_LONG_PRESS_CONTINUOUS,

       KEY5_LONG_PRESS_RELEASE,

       KEY6_CLICK,                                     //26

       KEY6_CLICK_RELEASE,

       KEY6_LONG_PRESS,

       KEY6_LONG_PRESS_CONTINUOUS,

       KEY6_LONG_PRESS_RELEASE,

}KEY_VALUE_TYPEDEF;

我們這個專案總共有6個按鍵,每個按鍵需要檢測短按、短按釋放、長按、長按釋放、連續長按5個功能,所以總共有30個不同的列舉值分別來對應不同按鍵的不同功能。

第二步:

自定義了函數指標型別以後,我們就可以通過KeyEvent_CallBack_t這個型別名稱,去定義我們的函數指標變數。

KeyEvent_CallBack_t KeyScanCBS;

那KeyScanCBS就是函數指標,所以它的返回值是void型別,形參是KEY_VALUE_TYPEDEF列舉型別的。

最終就是把這個指標指向別的.c檔案的函數,從而實現不同.c檔案之間的資料傳遞,同時又能保持很好的可移植性(相互獨立,互不干擾)。

那怎麼指向呢?我的方法是重新定義一個函數,專門來為這個指標指向,這樣方便別的.c檔案呼叫,這個函數我稱為註冊函數

比如以下函數:

void hal_KeyScanCBSRegister(KeyEvent_CallBack_t pCBS)

{

       if(KeyScanCBS == 0)

       {

                     KeyScanCBS = pCBS;

       }

}     

這個函數的作用就是把我們前面定義的KeyScanCBS函數指標指向外部的函數地址(也就是要指向那個函數的函數名)。

當然,這個函數不是必須的,只是我的思維和程式碼風格,你也可以不單獨寫這樣的函數,只要用之前把KeyScanCBS指向外部函數就可以了,否則等著程式宕機吧哈哈哈。

第三步:

準備好這幾步以後,我們繼續來說下怎麼去使用它。

我們哪裡要用到按鍵的功能,就在那個.c檔案那裡重寫一個同樣的函數

比如說app.c這個檔案是產品功能程式碼(應用層),我需要在應用層使用按鍵功能。

重寫函數的時候,返回值和形參要跟那個函數指標型別一樣。

如果你忘記了,那我們再來回顧下。

typedef void (*KeyEvent_CallBack_t)(KEY_VALUE_TYPEDEF keys);

無返回值,形參為KEY_VALUE_TYPEDEF型別。

只有這樣,你才能把這個函數的地址賦值給KeyScanCBS這個指標,才能正常傳遞資料。

重寫的這個函數就是通過形參來接收硬體層按鍵值的,如果是串列埠資料,也是同理,只是形參不一樣。

然後,我們在產品功能初始化的函數裡直接呼叫剛剛hal_key.c的註冊函數

把KeyEventHandle這個函數的地址賦值給hal_key.c的KeyScanCBS這個函數指標。

所以,最終KeyScanCBS可以理解成等同於KeyEventHandle函數

我們在hal_key.c檔案裡,看按鍵檢測解析程式,最終就是執行KeyScanCBS把我們keys(按鍵值)傳遞到我們app.c檔案的。

這樣,就能做到以事件去驅動,只有按鍵按下,並且真實有效,我才會呼叫KeyScanCBS,才會把按鍵值傳遞給應用層。

而中間,兩個檔案之間沒有任何全域性變數的依賴,也完全可以獨立,大家可以細品消化一下。

這裡有個細節就是為什麼我函數的形參要用列舉型別。

如果你對接過一些模組(WiFi、藍芽等)二次開發就知道了,模組核心程式碼都是封裝成lib這種庫給你的,你並看不到原始碼。

只能用他們的函數,如果不用列舉,那你不知道形參可以傳入什麼值對吧?

如果用列舉,我把能用的值都列出來給你,並且起好名字,讓你一看就知道是啥意思,這是不是就很方便?

Ok,今天就寫到這裡,大家下去可以做下實驗。

原創不易,儘量用最通俗的語言表達,如果對你有幫助,麻煩安排個三連吧^ ^。