擊上方“果果小師弟”,選擇“置頂/星標公眾號”

摘要:在看別人單片機程序時,你也許是崩潰的,因為全局變量滿天飛,不知道哪個在哪用了,哪個表示什么,而且編寫極其不規范。自己寫單片機程序時,也許你也是崩潰的。總感覺重新開啟一個項目,之前的寫過相似的代碼也無法使用,得重新敲,代碼重用度不高,編程效率低下,代碼無法積累。而且感覺寫這個代碼沒有思想,沒有靈魂,沒有框架,只是一個一個功能代碼的堆砌,很空泛。
那么這個時候,你也許應該在單片機中引入面向對象的思想了,使代碼更規范。
int?main?(void)
{
?while(1)
?{
??sing();
??dance();
??play();
?}
}
函數sing執行的時間比較長的話,函數dance就不能很快的被執行。任何一個函數死掉的話就會影響整個系統。
在使用 51、AVR、STM32 單片機裸機的時候一般都是在main函數里面用while(1)做一個大循環來完成所有的處理,即應用程序是一個無限的循環,循環中調用相應的函數完成所需的處理。有時候我們也需要中斷中完成一些處理。相對于多任務系統而言,這個就是單任務系統,也稱作前后臺系統,中斷服務函數作為前臺程序,大循環while(1)作為后臺程序。

對應的編程代碼大概是這樣的:
void?EXTI_IRQHandler()
{
????flag?=?1;
}
int?main?(void)
{
????while(1)
????{
????????if?(flag?=?1)
????????{
????????????do_something();
????????????flag?=?0;
????????}
????}
}
有什么問題?
前后臺系統的實時性差,前后臺系統各個任務(應用程序)都是排隊等著輪流執行,不管你這個程序現在有多緊急,沒輪到你就只能等著!相當于所有任務(應用程序)的優先級都是一樣的。但是前后臺系統簡單啊,資源消耗也少啊!在稍微大一點的嵌入式應用中前后臺系統就明顯力不從心了。
void?first_task()
{
????while?(1)
????{
????????if(has_data())
????????????put_data();
????}
}
void?second_task()
{
????while?(1)
????{
????????if(get_data())
????????????do_something();
????}
}
int?main(void)
{
????create_task(first_task);
????create_task(second_task);
????start_scheduler();
}
多任務系統會把一個大問題“分而治之”,把大任務劃分成很多個小問題,逐步的把小任務解決掉,大任務也就隨之解決了,這些任務是并發處理的。注意,并不是說同一時刻一起執行很多個任務,而是由于每個任務執行的時間很短,導致看起來像是同一時刻執行了很多個任務一樣。
以按鍵為例,點亮一個小燈!

int?mian(void)
{
????while?(1)
????{
????????if(HAL_GPIO_ReadPin(GPIOA,?GPIO_PIN_3)?==?GPIO_PIN_SET)
????????{
????????????printf("按鍵按下\r\n");
????????}
????}
}
首先我們把每一個按鍵都看成一個對象,既然是對象就肯定有屬性和行為,比如我們定義一個學生,那么這個學生有什么屬性呢?
肯定有姓名、年齡、身高、體重對吧,這些是一些基本的屬性,我們可以用一些單獨的變量來定義它,比如:
typedef?struct
{
?uint8_t??*name;?//姓名(變量)
?uint8_t??age;???//年齡(變量)
??uint8_t??height;//身高(變量)
??uint8_t??weight;//體重(變量)
}?student_t;
但是一個學生還有很多行為對吧,它會唱歌、跳舞、打籃球、也會關注果果小師弟的公眾號對吧,于是我們就可以這樣定義:
typedef?struct
{
?uint8_t??*name;??//姓名(變量)
?uint8_t??age;????//年齡(變量)
??uint8_t??height;?//身高(變量)
??uint8_t??weight;?//體重(變量)
?void?(*Sing_song)(void);?//會唱歌(函數指針)
?void?(*Dance_latin)(void);?//會跳舞(函數指針)
??void?(*Wechat_zhiguoxin)(void);?//會關注果果的公眾號(函數指針)
}?student_t;
好了,這里我們提到了函數指針,所以就來說一說函數指針。
函數指針,顧名思義它就是一個指針,只不過它是一個函數指針,所以指向的是一個函數。類比一般的變量指針,指針變量,實質上是一個變量,只不過這個變量存放的是一個地址,在32位單片機中,任何類型的指針變量都存放的是一個大小為4字節的地址。
重要的話說三遍!牢記在心!!!為什要記住函數指針,因為在單片機面向對象編程中,結構體的成員不是變量就是函數指針這兩種類型。變量就不用說了,函數指針理解就好。
其實函數指針可以類比一般的變量,看下面:
int???a;?=?>?void?Sing_song(void);
int?*?p;?=?>?void?(*zhiguoxin)(void);
p=&a;???=?>?zhiguoxin?=?&Sing_song;
a,右邊定義函數Sing_song;int指針,右邊定義函數指針;那么函數指針怎么用呢?我們還是以單片機為例,把按鍵類比為一個對象,這個按鍵有按鍵標志位,有長按或者短按,按鍵還有行為:按鍵初始化、按鍵循環檢測等。
所以我們創建下面這樣一個結構體,當然這個結構體不一定僅僅有這些變量和函數,這完全取決于你自己的定義,你想怎么定義就怎么定義,你甚至可以定義按鍵的顏色都。
typedef?struct
{
?uint8_t??KEY_Flag;??//標志位(變量)
?uint8_t??Click;//按下(變量)
?void?(*KEY_Init)(void);?//按鍵初始化(函數指針)
?void?(*KEY_Detect)(void);?//按鍵檢測(函數指針)
}?KEY_t;
現在已經定義了KEY_t這種類型的結構體,處理器還沒有分配給這個結構體內存,因為我們只是聲明這樣一個類型,而類型是不占用內存的,只有我們定義對應的結構體類型的變量時才會在占用內存空間。
那么怎么定義一個結構體類型的變量呢?
KEY_t???KEY1;
然后就要初始化結構體的成員變量了。
KEY_t??KEY1?=?{0,0,KEY_init,KEY_detect};??
這里要注意了現在結構體有四個成員,前兩個普通的變量,我們初始化為0,還有兩個函數指針,我們是不是要把我們想寫得函數的函數名字放在這里啊。
那么聰明的你肯定知道還要定義KEY_init();和KEY_detect();這兩個函數。這兩個函數可以這樣寫。
static?void?KEY_init()
{
??GPIO_InitTypeDef?GPIO_InitStruct;
??GPIO_InitStruct.Pin?=?GPIO_PIN_3;
??GPIO_InitStruct.Mode?=?GPIO_MODE_OUTPUT_PP;
??GPIO_InitStruct.Pull?=?GPIO_NOPULL;
??GPIO_InitStruct.Speed?=?GPIO_SPEED_FREQ_LOW;
??HAL_GPIO_Init(GPIOA,?&GPIO_InitStruct);
}
static?void?KEY_detect()?
{
?uint8_t?i?=?0;?
?if(KEY1.KEY_Flag?==?1)
?{
??HAL_Delay(100);
??if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_3)?==?GPIO_PIN_SET)
??{
????printf("按鍵按下\r\n");
??}
??KEY1.KEY_Flag?=?0;
?}
}
好了具體函數中的代碼我就不需要解釋了。這樣一個按鍵的對象我們就定義好了,這個按鍵我們賦予了"他"生命,有屬性(變量)有行為(函數)。
這樣我們在主函數就可以這樣的調用,來實現相應的功能了。按鍵使用了中斷,這里并沒有講解。
void?main(void)
{
??KEY1.KEY_Init();//初始化按鍵
??while(1)?
???{??
??KEY1.KEY_Detect();//按鍵檢測
?}
}
如果理解了這些,那么面向對象的精髓你基本已經掌握了,接下來就是不斷地去練習和實踐了。
我們知道,現有的編程范式主要是:面向過程編程、面向對象編程、函數式編程。
對于流程清晰的簡單程序,一般只有一條流程主線,很容易被劃分成順序執行的幾個步驟,面向對象編程和面向過程編程沒有太大差別,并且面向過程編程常常比面向對象編程更加直觀高效。
但當我們面對一個大型的復雜程序,由于其錯綜復雜的流程和交互關系,很難將其簡單地拆分成一條主線串成的簡單步驟,而通常表現為一個網狀關系結構。這個時候,面向過程編程的這種流程化和線性化的思維方式就會顯得比較吃力,而面向對象編程的優勢就比較明顯了。
面向對象編程風格的代碼更容易復用、擴展和維護、更高級、更人性化、更適合大規模復雜程序的開發。在Linux中就是用的面向對象編程,里面有很多的結構體、指針、鏈表等等。如果還沒有接觸到面向對象編程只能說明你做的東西還不夠復雜。
在單片機舉一個例子,一塊開發板可能會適配不同的屏幕:

那么每一塊板子肯定有不同的代碼適配,在程序中我們可以讀出屏幕的ID,然后通過if判斷來執行不同的指令,就行這樣。

如果使用面向對象編程,那么就可以這樣寫代碼。
typedef?struct?lcd{
?uint8_t?type;
?void?(*LCD_Init)(void)
}lcd_t,?*plcd_t;
int?Read_id()
{
?/*?0:?LCDA
??*?1:?LCDB
??*/
?return?0;?
}
int?Get_Lcd_Type(void)
{
?return?Read_id();
}
void?LCDA_Init(void)//屏幕A初始化
{
????LCD_WR_REG(0xCF);??
????LCD_WR_DATA(0x00);?
????LCD_WR_DATA(0xC1);?
????LCD_WR_DATA(0X30);?
}
void?LCDB_Init(void)//屏幕B初始化
{
????LCD_WR_REG(0X11);
????delay_ms(20);
????LCD_WR_REG(0XD0);
????LCD_WR_DATA(0X07);?
}
lcd_t?openedv_com_lcds[]?=?{
?{0,?LCDA_Init},
?{1,?LCDB_Init},
};
plcd_t?get_lcd(void)//獲取到屏幕類型
{
?int?type?=?Get_Lcd_Type();
?return?&openedv_com_lcds[type];
}
int?main(void?)
{
?plcd_t?lcd;?
?lcd?=?get_lcd();//獲取到屏幕類型
?lcd->?LCD_Init();//初始化對應屏幕
?while?(1)
??{}?
}
這里只是偽代碼處理辦法,原理就和上面所講的一樣,在結構體中使用變量和函數。
到這里你應該掌握了面向對象得單片機編程方法,一起來試驗幾個例子:
typedef?struct
{?
?void?(*LED_ON)(uint8_t?LED_Num);?????//打開
?void?(*LED_OFF)(uint8_t?LED_Num);????//關閉
?void?(*LED_Flip)(uint8_t?LED_Num);???//翻轉
}?LED_t;
typedef?struct
{
?uint8_t??KEY_Flag;????????//標志位(變量)
?uint8_t??Click;???????????//按下(變量)
?void?(*KEY_Init)(void);???//按鍵初始化(函數指針)
?void?(*KEY_Detect)(void);?//按鍵檢測(函數指針)
}?KEY_t;
typedef?struct
{
?uint8_t?Status;??????//狀態
?void?(*ON)(void);?????//打開
?void?(*OFF)(void);????//關閉
}?BEEP_t;
typedef?struct
{
?USART_TypeDef?*uart;/*?STM32內部串口設備指針?*/
?uint8_t?*pTxBuf;???/*?發送緩沖區?*/
?uint8_t?*pRxBuf;???/*?接收緩沖區?*/
?
?uint16_t?usTxBufSize;??/*?發送緩沖區大小?*/
?uint16_t?usRxBufSize;??/*?接收緩沖區大小?*/
?
??uint16_t?usTxWrite;?/*?發送緩沖區寫指針?*/
??uint16_t?usTxRead;??/*?發送緩沖區讀指針?*/
??uint16_t?usTxCount;?/*?等待發送的數據個數?*/
??uint16_t?usRxWrite;?/*?接收緩沖區寫指針?*/
??uint16_t?usRxRead;??/*?接收緩沖區讀指針?*/
??uint16_t?usRxCount;?/*?還未讀取的新數據個數?*/
??
??void?(*RS485_Set_SendMode)(void);??//RS-485接口設置為發送模式
??void?(*RS485_Set_RecMode)(void);???//RS-485接口設置為接收模式
}UART_T;

推薦閱讀
01 |加入嵌入式交流群 |
02 |嵌入式資源獲取 |
03 |STM32中斷優先級詳解 |
04 |STM32下載程序新思路--使用串口下載程序 |

