軟體工程結構2----代碼層級

這次想和大家聊聊筆者對代碼層級的理解,只是分享罷了,談不上正確與錯誤,若是有益處那是最好了。

1、代碼層級

筆者在先前的文章中反覆的提到封裝的概念,封裝代碼封裝了一個設備實體,例如,定時器封裝代碼封裝了所有定時器設備相關的功能。因為有了封裝,使得設備的驅動封裝代碼成為了一個通用的工具。

在通用計算機領域,Linux/Windows操作系統提供了豐富多元的通用工具,例如在屏幕上顯示數字「1」,編程者不需要去思考如何實現顯示數字「1」的,只需要調用底層開發者提供的封裝代碼介面即可。

筆者曾經跟隨著這樣的思想,封裝過一個printf函數,在TFT液晶顯示屏+單片機系統中顯示任意的字元。正是因為這樣的經歷,讓我更加深入的思考「代碼層級」的概念,因為我先前編寫的printf函數,只能應用於特定的TFT屏幕+特定單片機,依賴性非常強。如果我更換了屏幕設備,那麼printf代碼就完全不能用了。

那麼如何解決通用性問題呢?我想就是將代碼分級。

筆者思考下,代碼分為三個層級

分別是

① CSP(Chip Support Program)

② DSP (device Support Program)

③ 演算法層

在工程文件中,我會建立這三個文件,用來管理代碼。

2、依次說明層級

① CSP(Chip Support Program)

筆者將這一層級命名為CSP,這一層級的代碼與主控晶元密切相關,單片機型號眾多,但總體功能都是換湯不換藥。GPIO、TIMER、UART這三個是單片機幾乎必有的。不同的單片機寄存器地址,寄存器使用方法都是不一樣的。但是最終要做的事卻是一樣,那麼我們為什麼不用一個無關底層的函數名來模糊這樣的硬體區別呢?

以GPIO類代碼為例,我統一定義了改變電平的函數

set_gpio(uint8_t port_id ,uint8_t pin_id, bool_t status);

這個函數名在所有我手邊的單片機CSP中都是統一的。

例如,我手邊有51單片機 和MSP430單片機,那麼勢必我有兩套CSP代碼集

51的CSP代碼集中有下述代碼

void set_gpio(uint8_t port_id , uint8_t pin_id,bool_t status){

Port_enum_t port_enum = (Port_enum_t )port_id;

switch(port_enum){

case P1_PORT: if (status == bool_true){

P1 |= 0x01L << pin_id;

}else{

P1 &= ~(0x01L << pin_id);

}

case P2_PORT: if (status == bool_true){

P2 |= 0x01L << pin_id;

}else{

P2 &= ~(0x01L << pin_id);

}

}

}

MSP430的CSP代碼集中

代碼如下

void set_gpio(uint8_t port_id, uint8_t pin_id,bool_t status){

port_MSP430_enum_t port_enum = (port_MSP430_enum_t )port_id;

switch(port_enum){

case P1_PORT_430: if (status == bool_true){

P1OUT|= 0x01L << pin_id;

}else{

P1OUT&= ~(0x01L << pin_id);

}

case P2_PORT_430: if (status == bool_true){

P2OUT |= 0x01L << pin_id;

}else{

P2OUT &= ~(0x01L << pin_id);

}

}

}

仔細觀察MSP430和51的CSP程序集中 set_gpio的方法十分類似,也就是寄存器物理地址定義不一樣而已,物理地址定義在廠家文件中可以找到。51記得是<reg52.h>

,MSP430的話是<msp430f4xx.h>。還有就是編寫代碼的方式有些許不同。

但是仔細思考,這樣的區別根本不影響 後級使用,因為後級使用者只是關心你這樣的操作有沒有做完,不關心你是怎樣做完的!

於是,如果我們統一了CSP的命名規則。在後級代碼大量的調用 set_gpio函數,並且現在要換主控晶元的情況下,我們只需要完全調換CSP代碼集,就可以完全兼容後級代碼了!

CSP層級的意義在於更換主控晶元的時候增強軟體兼容性。

方法是:

1 統一CSP程序集的函數命名

2 使用成熟的封裝思想,封裝特定單片機的CSP代碼集

筆者手邊就有MSP430 51單片機和STM32的CSP程序集。這就意味著,我運行在MSP430單片機環境下的工程代碼,可以非常快速的運行在STM32 、51單片機環境下。

如果是樹莓派,我只要花時間將樹莓派的CSP完成,一樣可以運行已有的任何代碼!

筆者略微了解過JAVA這個語言,JAVA提出了一個虛擬機的概念,JAVA的口號就是「到處運行」,我想最底層的機理一定也是這樣!

通過相同的函數名,不同的實現方法,來實現調換底層運行代碼實體。好似面向對象的語言中「重寫」的概念!

筆者把CSP層級抽象為下述圖像

②DSP(device Support Program)

設備支持代碼,筆者將他理解為獨立於單片機的其他器件支持代碼。在市面上有許多好用的獨立設備,例如EEPROM,或者是高精度ADC,這些設備通過IIC協議進行訪問,至於是什麼單片機去讀寫,他根本不關心,也就是說他是一個獨立於主控系統的部分,是無關主控的設備。

於是,就可以獨立將這些代碼分立出來,把輸出/輸入留出來。

以EEPROM為例

假設我們已經完成了下述代碼

void init_eeporm();

void access_eeporm(w_r w_or_r , uint8_t * buf , uint16_t buf_len);

那麼對外部文件可見的就是上述這兩個功能函數,其他函數用static隱藏起來。我們稱它為輸出代碼。

那麼輸入代碼呢?

很顯然是GPIO代碼,因為軟體IIC協議是需要GPIO來模擬的。

PS:筆者強烈不推薦硬體IIC,個人觀點。

於是使用#define來實現輸入代碼的接入

#define eeporm_set_port(uint8_t port_id , uint8_t pin_id,bool_t status)

set_gpio(uint8_t port_id, uint8_t pin_id,bool_t status)

#define eeprom_read_port(uint8_t port_id , uint8_t pin_id)

read_gpio(uint8_t port_id , uint8_t pin_id)

於是DSP代碼集實體就會使用CSP提供的基礎驅動代碼,從而實現自己的功能輸出。

也就是EEPROM代碼集會使用eeporm_set_port函數來實現電平轉換,至於怎麼轉換,那就是底層實體代碼的任務了。

從DSP代碼開始,程序代碼就有極強的可移植性了,甚至可以拿通用計算機來驅動EEPROM,只要給我一個改變電平/讀取電平的方法。

筆者將DSP抽象為下述圖像

③演算法層

任何一個軟體工程都會有一個最終的功能輸出,例如鬧鐘,那麼他一定要實現時間到,發出蜂鳴器報警的功能。那麼筆者將實現最終功能的代碼實體稱為演算法層代碼。這一層十分靈活,可以完全放開自己的想像而不要思考如何實現。

就如同「吃雞」,絕地大逃殺的軟體開發團隊一樣,沒人會思考開槍後聲音是咋出來的,更沒人考慮顯示屏是咋顯示出人來的。考慮的更多是美工、遊戲性的問題。

筆者將絕地大逃殺的軟體開發團隊所寫的軟體代碼,都會列入演算法層的級別。

演算法層會反覆調用底層資源(CSP),也會調用DSP資源。

他更多的意義在於計算,會有大量的浮點數運算,這些浮點運算數據源大多數來源於DSP層感測器代碼。

筆者舉個例子吧,最一般的CSP-DSP-演算法層應用---溫度報警系統

CSP就用51單片機,51的CSP程序集調入

DSP肯定要有DS18B20了,所以DS18B20.c/.h作為DSP調入

演算法層,計算當前溫度是否超過報警值,其中一定要通過DSP獲得當前溫度值,通過DSP來驅動蜂鳴器吧~

加上演算法層,筆者最終總結的圖像是這樣的。

3、筆者思考

手電筒,實現一個照明的作用,設計手電筒的人,不會去固定電池用哪一種,哪一個廠家的,而是會提出一個電池的要求,必須輸出5V電就可以了。至於你用電池供電,還是開關電源供電,隨便你了。

就好像DSP層,我只是希望你能提供給我改變電平的能力和方法,至於你是誰,你怎麼做到的,我不關心。

我在想,正是這樣的黑盒子,這樣的封裝,這樣的分工,才創造了我們這樣繁榮的物質文明。

不在乎過程,僅在乎結果是否做成。

雖然有些不想承認,但是社會就是這樣吧~

唯有努力變強,才能做到不隨風而動,主掌自己的人生和命運吧!

若是有益處請點贊關注喲~ :)


推薦閱讀:

TAG:軟體體系結構 | 單片機 | C編程語言 |