嵌入式軟件架構設計:建立抽象層
軟件架構這東西,眾說紛紜,各有觀點。什么是軟件架構,我們能在網上找到無數種定義。比如,我們可以這樣定義:軟件架構是軟件系統的基本結構,體現在其組件、組件之間的關系、組件設計與演進的規則,以及體現這些規則的基礎設施。怎么定義一般來說,基本上不重要,我們不是在寫學術書籍,工程人員嘛,只關心軟件架構能解決什么問題。
本文引用地址:http://www.ex-cimer.com/article/202312/453918.htm軟件架構不是制定出來的,而是產品和業務需求所決定的,架構師所做的,只是忠于需求,并合理的表達了需求。軟件架構也從來都不是一成不變的。在產品或者產品線的整個生命周期中,隨著業務和需求的變化,軟件架構不斷發展和變化,以適應新的需要。
軟件架構不是一個簡單的項目問題,而是產品或產品線的技術戰略問題。一個良好設計并推廣的軟件架構,能帶來如下好處。
· 最大限度地減少不必要的返工
· 使嵌入式軟件在宏觀層面建立規劃
· 增強復用性,降低開發成本
· 便于團隊內部的技術培訓
· 使技術積累更加容易
經??吹降囊粋€常見問題是,新手工程師,由于經歷與知識不足,往往看不到項目全貌,很難深刻理解軟件架構,他們往往要經過多年的專業訓練,才能逐漸建立架構意識。
但軟件架構真的只是資深工程師和架構師的專利嗎?這個也不見得。古人作文,講究立意為先。
今天工程師做項目和產品,也應該先立意。這個意,就是指要有高度。工程師入門能從軟件架構的高度出發,看待軟件問題,相信對軟件的理解,會更加深刻一些。因此,總結了軟件架構的六個步驟,供嵌入式工程師參考。
1. 隔離硬件相關代碼,建立抽象層
2. 建立統一的軟件基礎設施
3. 妥善識別和處理產品數據
4. 功能分層與分解
5. 組件及其接口設計
6. 測試、調試與跨平臺開發的支持
需要注意的是,這些并不足以保證嵌入式工程師學會軟件架構。嵌入式軟件架構師,是不可培養的。但至少,嵌入式工程師們,可以了解到什么是正確的努力方向,很多時候,選擇比努力更加重要。
嵌入式軟件架構之一 抽象層與硬件隔離
許多新手乃至老手嵌入式工程師,在未了解軟件架構之前,把應用層功能和硬件相關的代碼,不由自主的攪和在一起寫。這種做法非常普遍。比如下面的代碼:
void modbus_rtu_write_reply(uint8_t add, uint8_t func_code, uint16_t reg, uint16_t data)
{
rs485.buff_tx[0] = add;
rs485.buff_tx[1] = func_code;
rs485.buff_tx[2] = (uint8_t)(reg >> 8);
rs485.buff_tx[3] = (uint8_t)(reg);
rs485.buff_tx[4] = (uint8_t)(data >> 8);
rs485.buff_tx[5] = (uint8_t)(data);
uint16_t crc16 = mb_crc16(rs485.buff_tx, 6);
rs485.buff_tx[6] = (uint8_t)(crc16);
rs485.buff_tx[7] = (uint8_t)(crc16 >> 8);
rs485.tx_total = 8;
rs485.tx_num = 0;
/* Send data from the uart port. The hardware related program. */
LL_USART_ClearFlag_TC(USART1);
LL_USART_EnableIT_TC(USART1);
USART1->DR = rs485.buff_tx[rs485.tx_num ++];
}
上面的這一段代碼,不是一個好例子。從函數LL_USART_ClearFlag_TC
開始的一句,也就意味著,這個Modbus的代碼,和MCU提供出的固件庫耦合在一起寫了。
著名的SOLID原則中,有個依賴倒置原則,高層模塊不應該依賴于底層模塊,它們應該共同依賴于抽象。此處的代碼,顯然違反了這一原則。Modbus作為高層模塊,此處對MCU固件庫的API進行了依賴。
對于這種將硬件相關的代碼與功能耦合在一起的軟件架構,在本文中,我們姑且稱之為“耦合架構”;而我們要追求的,是將隔離硬件相關的軟件架構,我們稱之為“隔離架構”。接下來,我們將詳細對比,耦合架構和隔離架構各自的特征。
耦合架構的問題
雖然從原則上來說,耦合架構是不對的,但萬事皆有因,存在即合理。一般而言,大部分嵌入式軟件工程師,都出自硬件相關的專業(比如電子、自動化等),來自于軟件工程和計算機專業的嵌入式工程師不多(他們都去互聯網行業了),因此從他們的知識結構和習慣思維出發,一般從硬件視角看待嵌入式系統,而不是站在軟件抽象的視角。
但理解歸理解,道理歸道理,既然已經從事嵌入式軟件,哪怕是硬件專業出身的,我也建議他一定拋棄既有思維,學會抽象這一強大的軟件思維工具,否則他的職業天花板將非常低。
耦合架構帶來的問題,也是顯而易見的,那就是,實實在在的難以移植。因為一旦硬件發生變化,比如MCU停產,芯片短缺等等(在當前形勢下太過常見),嵌入式軟件就要大把修改。如果軟件規模較大,嘗試移植耦合架構的代碼到在新MCU上,是一項艱巨的工作,沒人愿意干這事。因此產品開發完成,更新架構并推倒重來,幾乎是不可能。
別說工程師不愿意,你問問老板答應嗎?于是工程師們只能檢查所有代碼,把與硬件交互的每一行代碼改掉,遇到硬件交互方式大不相同的,就更糟心,還要大篇幅的改,邊改邊罵娘。比如上面的代碼,如果換一片芯片,可能要改為以下代碼。
void modbus_rtu_write_reply(uint8_t add, uint8_t func_code, uint16_t reg, uint16_t data)
{
rs485.buff_tx[0] = add;
rs485.buff_tx[1] = func_code;
rs485.buff_tx[2] = (uint8_t)(reg >> 8);
rs485.buff_tx[3] = (uint8_t)(reg);
rs485.buff_tx[4] = (uint8_t)(data >> 8);
rs485.buff_tx[5] = (uint8_t)(data);
uint16_t crc16 = mb_crc16(rs485.buff_tx, 6);
rs485.buff_tx[6] = (uint8_t)(crc16);
rs485.buff_tx[7] = (uint8_t)(crc16 >> 8);
rs485.tx_total = 8;
rs485.tx_num = 0;
/* Send data from the uart port. The hardware related program. */
MCU_NEW_USART_ClearFlag_TC(NEW_USART1);
MCU_NEW_USART_EnableIT_TC(NEW_USART1);
NEW_USART1->DR = rs485.buff_tx[rs485.tx_num ++];
}
其次,耦合架構會導致,在開發環境中(如Windows或者Linux,非目標硬件),很難對應用程序進行單元測試。脫離目標硬件,跨平臺開發嵌入式程序,是提升開發效率的重要措施。
對耦合架構來說,應用程序代碼直接調用硬件,如果要進行完整的測試工作,就要花費大量工作,因為測試程序也要去操作硬件,才能驗證正確與錯誤?;蛘?,需要工程師在硬件上完成手動測試(實際上現在大家就這么干的,哈哈)。
手動測試很繁瑣,往往讓人煩躁,工程師的主觀感受,會影響測試質量。很多時候,為了趕進度,或者規避繁瑣的測試工作,軟件并沒有經過很好的測試,整體系統質量受到影響。另外,手動測試,交付軟件可能需要更長的時間。而自動測試,往往只需要一瞬間,清楚明了。
第三,耦合架構將存在不易擴展的問題。耦合架構,往往是共享數據的,也就是所謂的全局變量滿天飛。隨著軟件系統的擴大,每個新功能的添加,變得更加困難,而且是越來越困難,出現BUG的機會急劇增加。屎山就是這么煉成的。
但需要說明的是,數據問題,不是說隔離了硬件,就能完全解決掉。數據問題,是嵌入式軟件乃至任何軟件的核心問題,它需要在架構六部曲之二和之三中,通過軟件基礎設施的合理構建,和數據機制的合理制定,共同得到解決。
隔離架構如何解決問題?
到這里,我們架構的第一步,呼之欲出,那就是:將軟件架構分離為硬件相關和硬件無關兩個部分。這就要引入抽象層這個概念。何為抽象層?抽象層有很多種,比如硬件抽象層(HAL)、設備抽象層(DAL),操作系統抽象層(OSAL),網絡抽象層,文件系統抽象層,Flash抽象層(RT-Thread里就有這個)等等。
對誰進行抽象,就會建立這個東西的抽象層,無一定之規。本文中的抽象層,特指硬件抽象層,或者設備抽象層,或者二者兼備。具體是誰,取決于產品特性。
在硬件相關代碼和硬件獨立代碼之間創建抽象層,這是軟件移植的要求,實際上也是依賴倒置原則需求。在這里,我們有必要對依賴倒置原則進行強調:高層模塊不應該依賴于底層模塊,它們應該共同依賴于抽象。也就是說,應用層代碼(硬件無關),不應該依賴于硬件相關的代碼(驅動代碼),他們應該依賴于抽象層代碼。
抽象層的創建,將允許將應用代碼從一個微控制器移動到下一個微控制器,或者一套硬件遷移到另一套硬件,應用層代碼不必更換。抽象層打破了硬件依賴關系;換句話說,應用程序根本不必知道,也不必關心,當前運行的是什么硬件,應用程序只需要關心抽象層的API是什么樣的。
新的硬件驅動程序要做的,僅僅是滿足接口的要求而已。這意味著如果我們更改硬件,則只會更改硬件相關的模塊,而不是整個代碼庫。
void modbus_rtu_write_reply(uint8_t add, uint8_t func_code, uint16_t reg, uint16_t data)
{
rs485.buff_tx[0] = add;
rs485.buff_tx[1] = func_code;
rs485.buff_tx[2] = (uint8_t)(reg >> 8);
rs485.buff_tx[3] = (uint8_t)(reg);
rs485.buff_tx[4] = (uint8_t)(data >> 8);
rs485.buff_tx[5] = (uint8_t)(data);
uint16_t crc16 = mb_crc16(rs485.buff_tx, 6);
rs485.buff_tx[6] = (uint8_t)(crc16);
rs485.buff_tx[7] = (uint8_t)(crc16 >> 8);
rs485.tx_total = 8;rs485.tx_num = 0;
/* Send data from the uart port. The hardware related program. */
hal_uart_send(HAL_UART_ID_1, rs485.buff_tx, rs485.tx_total);
}
void hal_uart_send
硬件相關的代碼,應該改為如下的樣子。這尚且算不上真正的抽象層,只是抽象層最簡陋的替代實現方法,實際工程應用中,抽象層還有很多細節需要闡述。
void hal_uart_send(uint8_t uart_id, void *buffer, uint32_t size)
{
/* Start the uart sending process, the remaning data will be send in UART ISR
function. */
MCU_NEW_USART_ClearFlag_TC(NEW_USART1);
MCU_NEW_USART_EnableIT_TC(NEW_USART1);
NEW_USART1->DR = rs485.buff_tx[rs485.tx_num ++];
}
抽象層還可以解決單元測試的許多問題。有了抽象層,我們可以在Windows或者Linux上創建硬件的替身程序(mock),也可以稱為假硬件。我們可以在假硬件上給出輸入數據,并通過檢查假硬件給出的輸出數據會否符合預期,來對軟件進行單元測試。在沒有硬件的情況,也可以對應用層程序進行開發。很多嵌入式程序員覺得不可能,但這時很多大公司開發軟件的方式。
抽象層的建立,還有一個好處。軟件不必等著硬件就緒才開始開發,而在硬件可用之前,就開始專注于開發和交付應用程序。
這樣做的好處是,可以在項目早期就對客戶提供試用服務,并根據客戶反饋進行功能調整。如今,太多的團隊專注于首先準備好硬件,而核心應用程序是事后才想到的。這樣并不利于對嵌入式軟件進行良好的設計和實現。
那么如何建立抽象層呢?抽象層的建立,涉及到幾個關鍵的因素:抽象的程度、抽象的手段以及抽象的對象。這些問題,非常復雜,非三言兩語就能說清。
結論
嵌入式軟件與其他軟件領域都不一樣,因為沒有一個軟件領域,和嵌入式軟件一樣,會和硬件進行直接交互(請注意此處直接二字)。
為了應對可能出現的硬件變化(無論是MCU,PCBA,還是連接PCBA的設備),嵌入式軟件架構師應該將硬件相關的代碼獨立出去,并壓縮在一個最小的范圍內。否則,一旦使用耦合架構,不對硬件相關代碼進行剝離,屎山式的代碼,幾乎是注定的結局。
一個成功的軟件架構,從來不是一蹴而就,通常是通過迭代和演進創建的。這需要技術負責人,或者架構師,主動去推動軟件架構的迭代,不斷推動軟件的優化重構。這就有點像明星的好身材,從來不是天生,都是后天自律的結果。
但在嵌入式領域,無論搞什么產品,搞什么復雜的軟件架構,剝離硬件相關,是第一步,也是最為關鍵的一步。連硬件相關代碼都剝不干凈,軟件架構就猶如浮沙筑高臺,無從談起。
合抱之木,生于毫末,有志于提升技術水平的工程師們,先從隔離硬件開始吧。
評論