??作者:一只大喵咪1201
??專欄:《智能家居項目》
??格言:你只管努力,剩下的交給時間!
在這個專欄中,本喵要實現(xiàn)一個智能家居的小項目,先基于HAL庫實現(xiàn)裸機(jī)版本,之后再實現(xiàn)一個RTOS版本,為了無縫實現(xiàn)從裸機(jī)到RTOS的移植以及維護(hù),本喵會使用面向?qū)ο蟮乃枷?,將整個項目分層來實現(xiàn),構(gòu)建一種編程架構(gòu)。
本項目重點:
- 設(shè)計出優(yōu)秀的程序框架:容易擴(kuò)展、容易維護(hù)。
- 具體:
- 把項目拆分為各個子系統(tǒng)。
- 使用面向?qū)ο蟮乃枷耄炎酉到y(tǒng)抽象為結(jié)構(gòu)體。
- 編寫函數(shù)時,有一定的封裝細(xì)節(jié),看函數(shù)名就知道怎么用,不需要深入函數(shù)內(nèi)部看它的實現(xiàn)。
??項目簡介
如上圖,使用百問網(wǎng)的STM32F103ZET6
開發(fā)板,實現(xiàn):
- 開發(fā)板啟動后,自動連接家里的路由器,在OLED上顯示出IP。
- 手機(jī)上啟動微信小程序,輸入開發(fā)板OLED上顯示的IP,連接開發(fā)板。
- 在微信小程序里,點擊圖標(biāo)控制開發(fā)板的LED、風(fēng)扇。
如上圖所示,在程序設(shè)計過程中,分為幾個層次:
- 第1層:軟件系統(tǒng),就是整個系統(tǒng)、整個程序。
- 第2層:分解為子系統(tǒng),比如我們可以拆分為:輸入子系統(tǒng)、顯示子系統(tǒng)、業(yè)務(wù)系統(tǒng)。
- 第3層:分解為類,在C語言里沒有類,可以使用結(jié)構(gòu)體來描述子系統(tǒng)。
- 第4層:分解成子程序,實現(xiàn)那些結(jié)構(gòu)體中的屬性和方法(結(jié)構(gòu)體中有函數(shù)指針)。
如上圖所示,在本項目中,可以分為6個子系統(tǒng):
- 設(shè)備子系統(tǒng):比如實現(xiàn)LED控制、風(fēng)扇控制。
- 顯示子系統(tǒng):在OLED上顯示信息。
- 輸入子系統(tǒng):可以接收按鍵數(shù)據(jù)、網(wǎng)絡(luò)數(shù)據(jù)。
- 網(wǎng)絡(luò)子系統(tǒng):負(fù)責(zé)網(wǎng)絡(luò)連接、數(shù)據(jù)收發(fā)。
- 字體子系統(tǒng):獲得字符的字庫。
- 業(yè)務(wù)子系統(tǒng):起綜合作用,根據(jù)輸入值(網(wǎng)絡(luò)數(shù)據(jù)),控制設(shè)備。
其中業(yè)務(wù)子系統(tǒng)包含其余5個子系統(tǒng),可以看作是上層,并且同樣也可以看作一個子系統(tǒng)。
??輸入子系統(tǒng)(按鍵)
首先來實現(xiàn)輸入子系統(tǒng),它可以接收來自按鍵,網(wǎng)絡(luò),標(biāo)準(zhǔn)輸入等設(shè)備的數(shù)據(jù),然后供上層業(yè)務(wù)子系統(tǒng)去使用。整個輸入子系統(tǒng)劃分為五個層次實現(xiàn),這里本喵僅實現(xiàn)按鍵一個輸入設(shè)備。
?應(yīng)用層
- 對于傳遞的"數(shù)據(jù)數(shù)據(jù)",我們把它稱為"輸入事件"。
如上圖,在input_system.h
輸入子系統(tǒng)頭文件中定義輸入事件結(jié)構(gòu)體,用來描述發(fā)生的輸入事件,無論是按鍵輸入還是網(wǎng)絡(luò)以及標(biāo)準(zhǔn)輸入,都會創(chuàng)建一個這樣的結(jié)構(gòu)體對象,但是INPUT_EVENT_TYPE
不同,只有根據(jù)該成員變量的值才可以確定發(fā)生了哪種輸入,通過其他成員變量可以獲取到需要的事件屬性,比如發(fā)生事件,按鍵編號,以及字符串?dāng)?shù)據(jù)等等。
輸入事件類型有多種,在這個項目中并不會用到觸摸屏輸入,本喵這樣寫是為了表明拓展維護(hù)的方便性,在輸入子系統(tǒng)層面,需要增加輸入事件類型,以及描述輸入事件的結(jié)構(gòu)體InputEvent
中增加觸摸屏觸摸的位置。
接下來就是輸入事件的來源了,從框圖中看到有按鍵輸入,網(wǎng)絡(luò)輸入,標(biāo)準(zhǔn)輸入,以后甚至可以擴(kuò)展更多的輸入來源,這些輸入來源產(chǎn)生輸入事件。
- "輸入事件"由"輸入設(shè)備"產(chǎn)生。
如上圖,在input_system.h
輸入子系統(tǒng)頭文件中定義輸入設(shè)備結(jié)構(gòu)體,用來描述輸入設(shè)備,每一個設(shè)備都會創(chuàng)建一個這樣的結(jié)構(gòu)體對象,其中包含設(shè)備的名稱,獲取輸入事件,初始化設(shè)備,去初始化設(shè)備等方法,以及下一個設(shè)備節(jié)點的指針。
每一個設(shè)備都自帶獲取輸入事件的方法,也就是獲取InputEvent
對象的函數(shù),站在輸入子系統(tǒng)的層面,它并不關(guān)心該方法是如何實現(xiàn)的,只在需要獲取輸入數(shù)據(jù)的時候直接調(diào)取該方法即可。
包括初始化和去初始化也是設(shè)備自帶的方法,上層只需要直接調(diào)用即可,至于去初始化是在不需要某個設(shè)備的時候,將其配置恢復(fù)到初始化狀態(tài),從系統(tǒng)中抹除該設(shè)備。
為了管理多個設(shè)備,本喵將其放在一個鏈表中,所以還有一個pNext
指針指向下一個設(shè)備節(jié)點。
- 在輸入子系統(tǒng)層面,并不關(guān)心獲取輸入事件函數(shù)是如何實現(xiàn)的,而且該函數(shù)的實現(xiàn)涉及到了硬件底層,所以并不在子系統(tǒng)層面實現(xiàn)。
如上圖,在input_system.c
源文件中,創(chuàng)建一個全局鏈表,用來讓輸入子系統(tǒng)管理輸入設(shè)備,并且實現(xiàn)注冊輸入設(shè)備,增加輸入設(shè)備,初始化所有輸入設(shè)備等函數(shù)。
注冊輸入設(shè)備的本質(zhì)就是將新增加的輸入設(shè)備節(jié)點插入到鏈表中,讓輸入子系統(tǒng)能夠通過操作鏈表來維護(hù)使用輸入設(shè)備。
增加輸入設(shè)備也是輸入子系統(tǒng)要處理的事情,在增加輸入設(shè)備函數(shù)中再調(diào)用增加具體輸入設(shè)備的函數(shù),需要增加多少輸入設(shè)備,就將對應(yīng)設(shè)備的增加函數(shù)放進(jìn)去。
初始化所有輸入設(shè)備的時候,只需要變量鏈表中的設(shè)備節(jié)點,調(diào)用每個設(shè)備節(jié)點自帶的初始化化函數(shù)即可。
- 這三個函數(shù)要在
input_system.h
中聲明。
無論是一個輸入設(shè)備還是多個輸入設(shè)備,所產(chǎn)生的數(shù)據(jù)并不只一個,但是使用者只有輸入子系統(tǒng)一個,為了防止數(shù)據(jù)丟失,所以這些數(shù)據(jù)也需要維護(hù)起來,這里使用環(huán)緩沖區(qū)列來維護(hù),主要有輸入事件產(chǎn)生,就將相應(yīng)的InputEvent
對象放入環(huán)形緩沖區(qū)中,子系統(tǒng)只需要從環(huán)形緩沖區(qū)讀取數(shù)據(jù)就可以,不用關(guān)心數(shù)據(jù)是怎么來的。
如上圖所示,環(huán)形緩沖區(qū)本質(zhì)上也是一個數(shù)組,就拿寫來說,當(dāng)這個數(shù)組被寫滿后ring_buffer[7] = data
,就通過取模運(yùn)算pW = (7 + 1) % 8 = 0
重新從數(shù)組的起始位置開始寫數(shù)據(jù),讀也是類似的道理。
- pR是向環(huán)形緩沖區(qū)讀數(shù)據(jù)時的下標(biāo)。
- pW是向環(huán)形緩沖區(qū)寫數(shù)據(jù)時的下標(biāo)。
通過pR
是否等于pW
來判斷環(huán)形緩沖區(qū)中是否有數(shù)據(jù),沒有數(shù)據(jù)就相等,有數(shù)據(jù)就不相等,同樣通過pW
是否等于pR
來判斷環(huán)形緩沖區(qū)是否寫滿數(shù)據(jù),相等就寫滿了,不相等就沒寫滿。
如上圖所示,定義環(huán)形緩沖區(qū)結(jié)構(gòu)體,通過維護(hù)pW
和pR
來維護(hù)環(huán)狀,以及從存放輸入事件的buffer
中讀寫事件。
輸入子系統(tǒng)還需要提供讀寫數(shù)據(jù)的方法:
如上圖所示,創(chuàng)建一個全局的環(huán)形緩沖區(qū)對象,由于是靜態(tài)全局變量,且沒有初始化,所以編譯器會用0去初始化,并放在未初始化數(shù)據(jù)段,讀寫事件都是在操作這個全局的環(huán)形緩沖區(qū)。
此時,輸入子系統(tǒng)已經(jīng)具有了上圖所示結(jié)構(gòu)以及對應(yīng)的操作方法,輸入子系統(tǒng)的層就完成了,到目前位置絲毫沒有提及到和STM32F103ZE
開發(fā)板有關(guān)的內(nèi)容,連一句相關(guān)的代碼也沒有,實現(xiàn)了應(yīng)用層和硬件的解耦。
?設(shè)備層
此時輸入子系統(tǒng)中的上層部分已經(jīng)完成了,還需要處理輸入子系統(tǒng)設(shè)備層,這里本喵僅實現(xiàn)按鍵輸入設(shè)備:
如上圖所示,在gpio_key.h
中定義了兩個按鍵的編號,之后直接使用即可。
如上圖所示,在gpio_key.c
中實例化出一個按鍵對象,并進(jìn)行初始化,賦值設(shè)備名,初始化函數(shù)等,還要提供一個增加按鍵設(shè)備的函數(shù)AddInputDeviceGPIOKey
供應(yīng)用層在初始化所有設(shè)備時候調(diào)用。
- 對于裸機(jī)程序,事件獲取方法不用注冊到設(shè)備隊列中,而是在后面中斷函數(shù)中調(diào)用。
此時,已經(jīng)實現(xiàn)了按鍵的設(shè)備層,包括按鍵設(shè)備的實例化,按鍵設(shè)備的初始化方法,以及增加按鍵設(shè)備的方法。
? 內(nèi)核層抽象層
本喵想讓這個系統(tǒng)支持多個系統(tǒng),包括裸機(jī),F(xiàn)reeRTOS,RT-Thread,甚至是Linux,這里將裸機(jī)也看作是一種內(nèi)核。
不同內(nèi)核下的數(shù)據(jù)來源:
- 裸機(jī):數(shù)據(jù)來自中斷,在中斷中解析數(shù)據(jù)并放入環(huán)形緩沖區(qū)。
- RTOS:創(chuàng)建任務(wù),在任務(wù)中解析數(shù)據(jù)并放入環(huán)形緩沖區(qū)。
內(nèi)核抽象層中,根據(jù)不同的內(nèi)核對按鍵進(jìn)行初始化,本喵這里僅實現(xiàn)裸機(jī)的按鍵初始化:
如上圖,初始化按鍵的時候,調(diào)用KAL_GPIOKeyInit
,在函數(shù)內(nèi)部再調(diào)用不同內(nèi)核對按鍵的初始化函數(shù),對于裸機(jī)則調(diào)用芯片層的CAL_GPIOKinit
函數(shù)進(jìn)行初始化,如果是RTOS,則僅需要將該函數(shù)改成對應(yīng)的初始化函數(shù)即可。
- 設(shè)備抽象層調(diào)用的是該層的
KAL_GPIOKeyInit
,根本不關(guān)心具體的實現(xiàn)邏輯。
在描述輸入事件的結(jié)構(gòu)體InputEvent
中有一個time
成員變量用來記錄事件發(fā)生的事件,而這個時間在不同的內(nèi)核中表現(xiàn)方式不同,所以在內(nèi)核抽象層需要實現(xiàn)獲取時間的函數(shù)。
如上圖,在使用的時候,直接調(diào)用內(nèi)核抽象層的KAL_GetTime
獲取時間即可,在該函數(shù)內(nèi)部,根據(jù)具體的獲取方式調(diào)用對應(yīng)的函數(shù)。
如本喵使用的STM32F103ZET6
是通過滴答定時器來獲取時間的,需要獲取芯片中寄存器的值,所以要調(diào)用CAL_GetTime
從芯片獲取時間。
對于Linux,它在系統(tǒng)內(nèi)部會記錄著時間,此時就可以直接返回時間,不用再向下調(diào)用。
此時,內(nèi)核抽象層也實現(xiàn)了,設(shè)備層會調(diào)用內(nèi)核抽象層的初始化函數(shù)。
?芯片抽象層
項目的最終實現(xiàn)需要依托具體的芯片,本喵用的STM32F103ZET6
是支持HAL
庫的,但是也有一些芯片并沒有HAL
庫,需要用它自己的庫來操作,所以在這一層要實現(xiàn)對不同類型芯片的支持。
如上圖,在芯片抽象層會調(diào)用CAL_GPIOKeyInit
來初始化按鍵,在函數(shù)內(nèi)部根據(jù)不同的芯片再調(diào)用它對應(yīng)的初始化函數(shù),如ST芯片就調(diào)用KEY_GPIO_ReInit
。
同樣,不同芯片獲取時間的方式也不同,這里也要實現(xiàn)針對不同芯片獲取時間的方式:
如上圖所示,從芯片寄存器中獲取時間的時候,對于ST芯片,調(diào)用HAL_GetTick
獲取即可,對于其他芯片,放入對應(yīng)的獲取方式即可。
此時芯片抽象層也實現(xiàn)了,內(nèi)核抽象層會調(diào)用該層的CAL_GPIOKeyInit
初始化按鍵。
?硬件操作
本喵使用的是STM32F103ZET6
芯片,使用CubeMX
和HAL
庫進(jìn)行按鍵初始化,在初始化的時候,要在中斷函數(shù)中進(jìn)行輸入數(shù)據(jù)的讀取,并放入環(huán)形緩沖區(qū)中。
如上圖,在driver_key.h
中進(jìn)行一些芯片的資源定義,方便后面使用。
如上圖,使用HAL
庫對按鍵進(jìn)行初始化,在按鍵中斷函數(shù)中處理輸入事件InputEvent
并且放入到環(huán)形隊列中
此時,具體芯片的硬件配置也設(shè)置好了,輸入子系統(tǒng)中按鍵設(shè)備就完全寫好了。
如上圖,現(xiàn)在整個代碼結(jié)構(gòu)是這樣,其中智能家居項目部分全部放在了smartdevice
文件夾中,包含輸入子系統(tǒng)的應(yīng)用層,設(shè)備抽象層,內(nèi)核抽象層,芯片抽象層。
其余部分是通過CubeMX
進(jìn)行的基本外設(shè)配置,整個輸入子系統(tǒng)中,只有在硬件操作的時候會用到這里的配置,其余四層都是獨立的,不存在耦合。
??按鍵單元測試
?串口
為了觀察按鍵按下后的現(xiàn)象,使用串口將發(fā)生的輸入事件InputEvent
打印出來,此時串口配置并不屬于我們實現(xiàn)的輸入子系統(tǒng),只是一個調(diào)試工具,直接使用HAL
庫配置就可以。
如上圖所示是串口的頭文件,只包含串口的使能和失能函數(shù)聲明。
如上圖所示是串口的具體配置函數(shù),這里同樣需要一個環(huán)形緩沖區(qū),這里本喵就不展示它的實現(xiàn)了,后面本喵會放源碼。
在調(diào)用EnableDebugIRQ
打開串口后,在向串口發(fā)送數(shù)據(jù)的時候,直接調(diào)用printf
即可,因為printf
底層會調(diào)用fputc
函數(shù),所以需要在這里將fput
重定向,使得printf
符合我們的要求。
在fputc
中,先將發(fā)送完成標(biāo)志清0,然后調(diào)用HAL
庫的中斷發(fā)送函數(shù)發(fā)送一個字節(jié),當(dāng)發(fā)送完成標(biāo)志位為0時就一直等待,說明沒有發(fā)送完成。這個字節(jié)發(fā)送完成以后,會進(jìn)入串口的發(fā)送中斷回調(diào)函數(shù),在中斷函數(shù)中將發(fā)送標(biāo)志位置1,讓fputc
退出循環(huán)等待。printf
發(fā)送多個字節(jié)就調(diào)用多次fputc
。
在獲取串口發(fā)送來的數(shù)據(jù)時,直接調(diào)用scanf
即可,因為scanf
底層會調(diào)用fgetc
函數(shù),所以也需要重定向fgetc
函數(shù),使得scanf
符合我們的要求。
當(dāng)串口上有數(shù)據(jù)到來時,會發(fā)生串口中斷,通過判斷SR
寄存器的第五位確定是接收到了數(shù)據(jù),并且將接收到的數(shù)據(jù)放入到環(huán)形緩沖區(qū)中。fgetc
直接從環(huán)形緩沖區(qū)中讀取數(shù)據(jù)。
- 為了像在PC端一樣使用標(biāo)準(zhǔn)庫中的
printf
和scanf
,必須重新實現(xiàn)fputc
和fgetc
函數(shù),讓終端變成串口,符合我們的要求。
?測試
為了看我們設(shè)計的輸入子系統(tǒng)是否正確,需要專門寫一個單元測試函數(shù)來測試一下:
如上圖所示,將按鍵設(shè)備添加到輸入子系統(tǒng)中,然后進(jìn)行初始化,在while(1)
循環(huán)中讀取輸入事件,并通過串口打印輸入事件的信息。
在main
函數(shù)中調(diào)用該測試函數(shù),通過串口調(diào)試助手查看打印信息:
如上圖,將板子的串口和電腦連在一起后,通過串口調(diào)試助手可以看到,當(dāng)按鍵1或者按鍵2按下后,會打印出發(fā)生的事件信息,包括事件類型,發(fā)生事件,按鍵編號,以及按鍵值,說明設(shè)計的輸入子系統(tǒng)是成功的。
??源碼
這部分代碼是在OLED代碼的基礎(chǔ)上寫的,包含源碼以及串口調(diào)試工具,需要的小伙伴自取傳送門。文章來源:http://www.zghlxwxcb.cn/news/detail-718357.html
??總結(jié)
這篇文章實現(xiàn)了智能家居項目中輸入子系統(tǒng)中的按鍵設(shè)備,最重要的是介紹的代碼框架和編程思想,之后的項目部分都會按照這個思路來擴(kuò)展維護(hù)。文章來源地址http://www.zghlxwxcb.cn/news/detail-718357.html
到了這里,關(guān)于【智能家居項目】裸機(jī)版本——項目介紹 | 輸入子系統(tǒng)(按鍵) | 單元測試的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!