? 大家好,今天分享一篇嵌入式軟件架構(gòu)設(shè)計(jì)相關(guān)的文章。
軟件架構(gòu)這東西,眾說紛紜,各有觀點(diǎn)。什么是軟件架構(gòu),我們能在網(wǎng)上找到無數(shù)種定義。
比如,我們可以這樣定義:軟件架構(gòu)是軟件系統(tǒng)的基本結(jié)構(gòu),體現(xiàn)在其組件、組件之間的關(guān)系、組件設(shè)計(jì)與演進(jìn)的規(guī)則,以及體現(xiàn)這些規(guī)則的基礎(chǔ)設(shè)施。怎么定義一般來說,基本上不重要,我們不是在寫學(xué)術(shù)書籍,工程人員嘛,只關(guān)心軟件架構(gòu)能解決什么問題。
軟件架構(gòu)不是制定出來的,而是產(chǎn)品和業(yè)務(wù)需求所決定的,架構(gòu)師所做的,只是忠于需求,并合理的表達(dá)了需求。軟件架構(gòu)也從來都不是一成不變的。在產(chǎn)品或者產(chǎn)品線的整個(gè)生命周期中,隨著業(yè)務(wù)和需求的變化,軟件架構(gòu)不斷發(fā)展和變化,以適應(yīng)新的需要。
軟件架構(gòu),也不是一個(gè)簡單的項(xiàng)目問題,而是產(chǎn)品或產(chǎn)品線的技術(shù)戰(zhàn)略問題。一個(gè)良好設(shè)計(jì)并推廣的軟件架構(gòu),能帶來如下好處。
??最大限度地減少不必要的返工
??使嵌入式軟件在宏觀層面建立規(guī)劃
??增強(qiáng)復(fù)用性,降低開發(fā)成本
??便于團(tuán)隊(duì)內(nèi)部的技術(shù)培訓(xùn)
??使技術(shù)積累更加容易
我經(jīng)常看到的一個(gè)常見問題是,新手工程師,由于經(jīng)歷與知識不足,往往看不到項(xiàng)目全貌,很難深刻理解軟件架構(gòu),他們往往要經(jīng)過多年的專業(yè)訓(xùn)練,才能逐漸建立架構(gòu)意識。
但軟件架構(gòu)真的只是資深工程師和架構(gòu)師的專利嗎?這個(gè)也不見得。古人作文,講究立意為先。
今天工程師做項(xiàng)目和產(chǎn)品,也應(yīng)該先立意。這個(gè)意,就是指要有高度。工程師入門能從軟件架構(gòu)的高度出發(fā),看待軟件問題,相信對軟件的理解,會更加深刻一些。因此,我總結(jié)了軟件架構(gòu)的六個(gè)步驟,供嵌入式工程師參考。
1.?隔離硬件相關(guān)代碼,建立抽象層
2.?建立統(tǒng)一的軟件基礎(chǔ)設(shè)施
3.?妥善識別和處理產(chǎn)品數(shù)據(jù)
4.?功能分層與分解
5.?組件及其接口設(shè)計(jì)
6.?測試、調(diào)試與跨平臺開發(fā)的支持
需要注意的是,看完這六個(gè)步驟,并不足以保證嵌入式工程師學(xué)會軟件架構(gòu)。嵌入式軟件架構(gòu)師,是不可培養(yǎng)的。但至少,嵌入式工程師們,可以了解到什么是正確的努力方向,很多時(shí)候,選擇比努力更加重要。
因此,在未來的幾篇文章中,我們會一起探討一下設(shè)計(jì)嵌入式軟件架構(gòu),可以采取的六個(gè)步驟。
嵌入式軟件架構(gòu)之一 抽象層與硬件隔離
許多新手乃至老手嵌入式工程師,在未了解軟件架構(gòu)之前,把應(yīng)用層功能和硬件相關(guān)的代碼,不由自主的攪和在一起寫。這種做法非常普遍。比如下面的代碼:
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?++]; }
上面的這一段代碼,不是一個(gè)好例子。從函數(shù)LL_USART_ClearFlag_TC開始的一句,也就意味著,這個(gè)Modbus的代碼,和MCU提供出的固件庫耦合在一起寫了。
著名的SOLID原則中,有個(gè)依賴倒置原則,高層模塊不應(yīng)該依賴于底層模塊,它們應(yīng)該共同依賴于抽象。此處的代碼,顯然違反了這一原則。Modbus作為高層模塊,此處對MCU固件庫的API進(jìn)行了依賴。
對于這種將硬件相關(guān)的代碼與功能耦合在一起的軟件架構(gòu),在本文中,我們姑且稱之為“耦合架構(gòu)”;而我們要追求的,是將隔離硬件相關(guān)的軟件架構(gòu),我們稱之為“隔離架構(gòu)”。接下來,我們將詳細(xì)對比,耦合架構(gòu)和隔離架構(gòu)各自的特征。
耦合架構(gòu)的問題
雖然從原則上來說,耦合架構(gòu)是不對的,但我個(gè)人對這種軟件寫法,還是能理解的。為什么?萬事皆有因,存在即合理。一般而言,大部分嵌入式軟件工程師,都出自硬件相關(guān)的專業(yè)(比如電子、自動化等),來自于軟件工程和計(jì)算機(jī)專業(yè)的嵌入式工程師不多(他們都去互聯(lián)網(wǎng)行業(yè)了),因此從他們的知識結(jié)構(gòu)和習(xí)慣思維出發(fā),一般從硬件視角看待嵌入式系統(tǒng),而不是站在軟件抽象的視角。
我個(gè)人也是電子工程專業(yè)畢業(yè)的,對此有感受。但理解歸理解,道理歸道理,既然已經(jīng)從事嵌入式軟件,哪怕是硬件專業(yè)出身的,我也建議他一定拋棄既有思維,學(xué)會抽象這一強(qiáng)大的軟件思維工具,否則他的職業(yè)天花板將非常低。
耦合架構(gòu)帶來的問題,也是顯而易見的,那就是,實(shí)實(shí)在在的難以移植。因?yàn)橐坏┯布l(fā)生變化,比如MCU停產(chǎn),芯片短缺等等(在當(dāng)前形勢下太過常見),嵌入式軟件就要大把修改。如果軟件規(guī)模較大,嘗試移植耦合架構(gòu)的代碼到在新MCU上,是一項(xiàng)艱巨的工作,沒人愿意干這事。因此產(chǎn)品開發(fā)完成,更新架構(gòu)并推倒重來,幾乎是不可能。
別說工程師不愿意,你問問老板答應(yīng)嗎?于是工程師們只能檢查所有代碼,把與硬件交互的每一行代碼改掉,遇到硬件交互方式大不相同的,就更糟心,還要大篇幅的改,邊改邊罵娘。比如上面的代碼,如果換一片芯片,可能要改為以下代碼。
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?++]; }
其次,耦合架構(gòu)會導(dǎo)致,在開發(fā)環(huán)境中(如Windows或者Linux,非目標(biāo)硬件),很難對應(yīng)用程序進(jìn)行單元測試。脫離目標(biāo)硬件,跨平臺開發(fā)嵌入式程序,是提升開發(fā)效率的重要措施。
對耦合架構(gòu)來說,應(yīng)用程序代碼直接調(diào)用硬件,如果要進(jìn)行完整的測試工作,就要花費(fèi)大量工作,因?yàn)闇y試程序也要去操作硬件,才能驗(yàn)證正確與錯(cuò)誤?;蛘撸枰こ處熢谟布贤瓿墒謩訙y試(實(shí)際上現(xiàn)在大家就這么干的,哈哈)。
手動測試很繁瑣,往往讓人煩躁,工程師的主觀感受,會影響測試質(zhì)量。很多時(shí)候,為了趕進(jìn)度,或者規(guī)避繁瑣的測試工作,軟件并沒有經(jīng)過很好的測試,整體系統(tǒng)質(zhì)量受到影響。另外,手動測試,交付軟件可能需要更長的時(shí)間。而自動測試,往往只需要一瞬間,清楚明了。
第三,耦合架構(gòu)將存在不易擴(kuò)展的問題。耦合架構(gòu),往往是共享數(shù)據(jù)的,也就是所謂的全局變量滿天飛。隨著軟件系統(tǒng)的擴(kuò)大,每個(gè)新功能的添加,變得更加困難,而且是越來越困難,出現(xiàn)BUG的機(jī)會急劇增加。屎山就是這么煉成的。
但需要說明的是,數(shù)據(jù)問題,不是說隔離了硬件,就能完全解決掉。數(shù)據(jù)問題,是嵌入式軟件乃至任何軟件的核心問題,它需要在架構(gòu)六部曲之二和之三中,通過軟件基礎(chǔ)設(shè)施的合理構(gòu)建,和數(shù)據(jù)機(jī)制的合理制定,共同得到解決。
隔離架構(gòu)如何解決問題?
到這里,我們架構(gòu)的第一步,呼之欲出,那就是:將軟件架構(gòu)分離為硬件相關(guān)和硬件無關(guān)兩個(gè)部分。這就要引入抽象層這個(gè)概念。何為抽象層?抽象層有很多種,比如硬件抽象層(HAL)、設(shè)備抽象層(DAL),操作系統(tǒng)抽象層(OSAL),網(wǎng)絡(luò)抽象層,文件系統(tǒng)抽象層,F(xiàn)lash抽象層(RT-Thread里就有這個(gè))等等。
對誰進(jìn)行抽象,就會建立這個(gè)東西的抽象層,無一定之規(guī)。本文中的抽象層,特指硬件抽象層,或者設(shè)備抽象層,或者二者兼?zhèn)?。具體是誰,取決于產(chǎn)品特性,可參考后續(xù)文章《嵌入式軟件中的抽象層》。
在硬件相關(guān)代碼和硬件獨(dú)立代碼之間創(chuàng)建抽象層,這是軟件移植的要求,實(shí)際上也是依賴倒置原則需求。在這里,我們有必要對依賴倒置原則進(jìn)行強(qiáng)調(diào):高層模塊不應(yīng)該依賴于底層模塊,它們應(yīng)該共同依賴于抽象。也就是說,應(yīng)用層代碼(硬件無關(guān)),不應(yīng)該依賴于硬件相關(guān)的代碼(驅(qū)動代碼),他們應(yīng)該依賴于抽象層代碼。
抽象層的創(chuàng)建,將允許將應(yīng)用代碼從一個(gè)微控制器移動到下一個(gè)微控制器,或者一套硬件遷移到另一套硬件,應(yīng)用層代碼不必更換。抽象層打破了硬件依賴關(guān)系;換句話說,應(yīng)用程序根本不必知道,也不必關(guān)心,當(dāng)前運(yùn)行的是什么硬件,應(yīng)用程序只需要關(guān)心抽象層的API是什么樣的。
新的硬件驅(qū)動程序要做的,僅僅是滿足接口的要求而已。這意味著如果我們更改硬件,則只會更改硬件相關(guān)的模塊,而不是整個(gè)代碼庫。
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
硬件相關(guān)的代碼,應(yīng)該改為如下的樣子。這尚且算不上真正的抽象層,只是抽象層最簡陋的替代實(shí)現(xiàn)方法,實(shí)際工程應(yīng)用中,抽象層還有很多細(xì)節(jié)需要闡述。限于篇幅,在本文中,我們不進(jìn)行探討,請關(guān)注后續(xù)的《抽象層》系列文章。
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上創(chuàng)建硬件的替身程序(mock),也可以稱為假硬件。我們可以在假硬件上給出輸入數(shù)據(jù),并通過檢查假硬件給出的輸出數(shù)據(jù)會否符合預(yù)期,來對軟件進(jìn)行單元測試。在沒有硬件的情況,也可以對應(yīng)用層程序進(jìn)行開發(fā)。很多嵌入式程序員覺得不可能,但這時(shí)很多大公司開發(fā)軟件的方式。
抽象層的建立,還有一個(gè)好處。軟件不必等著硬件就緒才開始開發(fā),而在硬件可用之前,就開始專注于開發(fā)和交付應(yīng)用程序。
這樣做的好處是,可以在項(xiàng)目早期就對客戶提供試用服務(wù),并根據(jù)客戶反饋進(jìn)行功能調(diào)整。如今,太多的團(tuán)隊(duì)專注于首先準(zhǔn)備好硬件,而核心應(yīng)用程序是事后才想到的。這樣并不利于對嵌入式軟件進(jìn)行良好的設(shè)計(jì)和實(shí)現(xiàn)。
那么如何建立抽象層呢?抽象層的建立,涉及到幾個(gè)關(guān)鍵的因素:抽象的程度、抽象的手段以及抽象的對象。這些問題,非常復(fù)雜,非三言兩語就能說清。
結(jié)論
嵌入式軟件與其他軟件領(lǐng)域都不一樣,因?yàn)闆]有一個(gè)軟件領(lǐng)域,和嵌入式軟件一樣,會和硬件進(jìn)行直接交互(請注意此處直接二字)。
為了應(yīng)對可能出現(xiàn)的硬件變化(無論是MCU,PCBA,還是連接PCBA的設(shè)備),嵌入式軟件架構(gòu)師應(yīng)該將硬件相關(guān)的代碼獨(dú)立出去,并壓縮在一個(gè)最小的范圍內(nèi)。否則,一旦使用耦合架構(gòu),不對硬件相關(guān)代碼進(jìn)行剝離,屎山式的代碼,幾乎是注定的結(jié)局。
一個(gè)成功的軟件架構(gòu),從來不是一蹴而就,通常是通過迭代和演進(jìn)創(chuàng)建的。這需要技術(shù)負(fù)責(zé)人,或者架構(gòu)師,主動去推動軟件架構(gòu)的迭代,不斷推動軟件的優(yōu)化重構(gòu)。這就有點(diǎn)像明星的好身材,從來不是天生,都是后天自律的結(jié)果。
但在嵌入式領(lǐng)域,無論搞什么產(chǎn)品,搞什么復(fù)雜的軟件架構(gòu),剝離硬件相關(guān),是第一步,也是最為關(guān)鍵的一步。連硬件相關(guān)代碼都剝不干凈,軟件架構(gòu)就猶如浮沙筑高臺,無從談起。
合抱之木,生于毫末,有志于提升技術(shù)水平的工程師們,先從隔離硬件開始吧。我在此先預(yù)祝成功!
編輯:黃飛
?
評論
查看更多