《 嵌入式系統設計與實踐》一一1.2 嵌入式系統開發

1.2 嵌入式系統開發
嵌入式系統是特殊的,因此也給開發者帶來一些特殊的挑戰。許多嵌入式軟件工程師開發了工具箱來處理各種約束。在我們開始構建自己的系統之前,先來看看開發一個嵌入式系統會有哪些困難。在熟悉了嵌入式系統開發會如何受到限制之後,我們再開始討論一些設計原則並藉此指導我們找到更好的解決方案。
1.2.1 調試
如果在計算機上運行調試軟件,就可以在這臺計算機上編譯和調試。系統有足夠的資源在運行程序的同時調試程序。事實上,硬件根本不知道是在調試程序,因爲這是由軟件完成的。
嵌入式系統就不是這樣了。除了需要交叉編譯器外,還需要一個交叉調試器。這個調試器運行在計算機上,通過特殊的處理器接口和目標處理器通信(見圖1-1)。這個接口是專門用來在處理器工作時對它進行偵聽的。這個接口通常稱爲JTAG(發音「Jay-tag」),而不管有沒有真正地實現這個廣泛應用的標準。
image

圖1-1:計算機和目標處理器
處理器必須通過擴展某些資源以支持這個調試接口,允許調試器在運行時掛起它,並提供調試信息。支持調試操作增加了處理器的成本。爲了節省成本,一些處理器只支持一個受限的功能子集。比如,增加一個斷點會讓處理器修改機器代碼以停在斷點處。但是,如果代碼是執行在閃存(或者任何其他的只讀存儲器)中,那麼處理器將會設置一個內部寄存器(硬件斷點),並在每個執行週期時將其與運行地址比較,如果相等則停止程序運行。這樣可以改變代碼的時序,在調試(或者沒有調試)時出現一些奇怪的問題。內部寄存器也消耗資源,因此常常只有極其有限的硬件斷點可用(大多數情況下,只有兩個)。
總之,處理器支持調試,但與純軟件開發相比,就沒有我們習以爲常的那麼多調試功能。
在計算機與目標系統之間通信的設備通常叫做仿真器、在線仿真器(ICE)或者JTAG適配器。這些都可能指同一個東西(不怎麼合適),或者三個不同的設備。仿真器是爲特定的處理器(或者處理器系列)設計的,因此不要以爲在一個項目中使用的仿真器可以在另一個項目中正常使用。仿真器會增加成本,尤其是當有多個仿真器或者有一個比較大的團隊在開發系統時。
爲了避免購買仿真器或者處理器的限制,許多嵌入式系統都通過其他一些手段實現調試,如使用printf或者一些輕量級日誌向一個沒有使用的通信接口輸出。這些方法非常有用,但也會改變系統的時序,導致一些問題只有在關掉調試輸出之後才能得到解決。
嵌入式系統的軟件開發有些棘手,因爲需要平衡系統的要求和硬件的約束。現在,在待辦事項列表里加上一項:在不那麼友好的硬件環境中,讓軟件具備比較好的可調試性。
1.2.2 更多挑戰
嵌入式系統是爲了完成特定的任務,所以會去掉所有與完成任務不相關的資源。這裏的資源包括:
內存(RAM)
代碼空間(ROM或閃存)
處理器週期或者速度
耗電量(電池壽命)
處理器外設
從某種程度上說,這些是可以互換的。比如,以代碼空間換CPU週期,在寫代碼時可以讓部分代碼佔據更多的空間這樣就可以運行得更快。或者可以降低處理器的運行速度以減少耗電量。如果沒有特定的外圍設備接口,則可以利用輸入/輸出線和處理器週期在軟件裏模擬這個接口。但是,即使再怎麼權衡,以上各種資源依然是非常有限的。資源限制所帶來的挑戰是所有挑戰中最明顯的。
另一類挑戰來自硬件。交叉調試帶來的額外壓力是令人沮喪的。在電路板調試的過程中,對於一個缺陷是由硬件還是軟件造成通常是不確定的,這讓問題變得更難以解決。與計算機不同,在嵌入式系統中,我們編寫的軟件可能對硬件造成實際的破壞。因此,大多數情況下,需要了解硬件以及硬件能做什麼。雖然,這些知識可能在設計另外一個系統的時候毫無用處,但是你必須面對挑戰,快速學習。
開發和測試完成了之後,就進入系統生產製造階段。這是大多數純軟件開發工程師不曾考慮過的事情。構建一個系統,並且以比較合理的成本去生產它,這是軟件工程師和硬件工程師都該銘記於心的一個目標。對可製造性的支持,是確保系統可以以較高的精度重複製造的一種方法。
製造完成之後,產品就進入市場了。對消費類產品來說,同時意味着千家萬戶會「享受」產品中的缺陷。對於醫療、航空或者其他關鍵產品,這些缺陷將是災難性的。(這就是爲什麼現在要做這麼多研究工作的原因)。對於科學研究和監控設備,應用現場可能是那些裝備難以收回(或者需要巨大的風險和代價才能收回,比如在火山口的裝置),因此這些裝置最好能正常運行。系統在從我們手中誕生之後會帶來什麼樣的生活,這也是設計軟件時必須面對的一個挑戰。
在對所有這些問題了然於心,並且在設計系統的時候有了確定的方法解決這些問題之後,還有一個最大的挑戰,這對所有的工程師來說都很常見的挑戰:變更。不僅僅產品目標會變更,項目的需求也會在整個項目週期內變更。最初,可能只是想試驗一些新的想法,去做一些嘗試。隨着對產品目標的認識逐漸加深以及對硬件越來越瞭解,我們就會開始設計更多的機制讓軟件變得可調試、健壯和靈活。在資源受限的環境裏,需要決定在開發時間,內存、代碼空間以及處理器週期這幾個方面能提供哪些基礎設施。通常,最初的設計並不是在你開發完成後所得到的那個,並且開發似乎永無止境。
不幸的是,爲了特定的應用目的設計出來的嵌入式系統有一個副作用:當應用發生變化時,系統可能難以支持變更。設計開發嵌入式系統並不僅僅是關於嚴格的限制和系統的最終完成,這裏的挑戰是要找出這些約束中哪些會在產品開發的後期產生問題。因此,需要能夠預測可能導致變更的原因,設計足夠靈活的軟件來適應可能發生的應用程序變化。
1.2.3 解決問題的原則嵌入式系統就像個智力拼圖,每一小部分都相互鎖在一起(只能以一種方式)。有時候,雖然可以使用蠻力將各個部分拼在一起,但結果卻可能和盒子上的圖像相差甚遠。我們應該摒棄這樣的觀點,即在項目結束時將最終的結果作爲唯一發布的代碼版本。事實上,智力拼圖有個時間維度揭示了其整個生命週期的不同變化:概念設計、原型化、電路板調試、系統調試、測試、發佈、維護,如此循環往復。靈活性並不僅僅指代碼現在能做什麼,而且指在其整個生命週期裏面能做什麼。我們的目標是要做到足夠靈活,這樣才能在滿足產品目標的同時能夠很好地處理資源約束和其他一些嵌入式系統內在的設計挑戰。我們可以應用軟件設計上的很多優秀的設計原則來讓系統變得更加靈活。通過使用模塊,我們將功能分離在子系統裏,並隱藏各個子系統的數據。使用封裝,我們設計子系統之間的接口,以使各個子系統相互獨立。一旦我們擁有了鬆耦合的多個子系統(或者對象),就可以在修改軟件的某一部分時相信這個修改不會影響其他部分。這樣我們就可以分拆我們的系統,然後在需要的時候按照不同的方式再把它們組裝起來。知道在哪裏將一個系統分解爲各個部分需要更多的實踐。一個比較好的原則是考慮哪些部分會獨立地發生變化。在嵌入式系統裏,應用這一原則需要我們考慮各個不同的物理對象。比如,如果傳感器X需要通過通道通信Y通信,那麼這兩個獨立的對象就是兩個候選子系統(也是兩個代碼模塊)。我們把子系統分解爲對象之後,就可以對這些對象進行測試。我很幸運,曾經在一些項目中有非常優秀的質量保證(QA)團隊。在其他的一些項目中,不曾有過任何人在我的代碼和那些將要使用我的系統的人們之間承擔QA的角色。我發現在軟件正式發佈之前捕獲的缺陷就像禮物一樣。錯誤發現的越早,解決這些錯誤的成本越低,對大家越有好處。當然,不必等着別人給我們送禮物。測試和質量向來是相互關聯的。寫測試代碼對系統進行測試可以讓系統質量更高,給代碼提供一些文檔,別人會認爲我們開發出來的軟件卓爾不羣。對代碼文檔化是另一個減少缺陷的方法。但如下這樣註釋代碼,讓人很難理解詳細的程度。i++; // increment the index不需要這樣做,其實這樣的代碼行很少需要註釋。寫註釋的目的是爲了像你一樣的開發人員,在一年之後再看你寫的這些代碼,那個時候的你可能正忙於其他事情並且忘記了當初你怎麼想出這個創造性的解決方案,你可能甚至已經忘記了你寫過這些代碼。因此,請在代碼裏留下些痕跡以幫助你自己找回記憶(文件和函數頭)。總的說來,假設讀者和你具有相同的心智和背景,只需寫清楚這段代碼做了什麼,而不是如何做。最後,在資源有限的系統開發過程中,我們常常會有儘早和儘可能多地去優化代碼的想法。抑制住這個慾望。實現所有的功能、讓系統運行、完成測試,然後再回來按照要求讓代碼更小或者運行得更快。時間是有限而寶貴的,因此在各個子系統能夠運行後再來專注於那些最消耗資源的部分,看看能否得到更好的結果。爲了運行速度去優化一個很少運行的函數不會帶來任何好處,反而會減少花費在那些運行頻率非常高的函數上的時間。有一點可以肯定的是,處理系統的資源約束需要一些優化。但在調優之前請務必搞清楚系統的資源消耗情況。「我們應該忘記小的性能提升,在97%的情況下,不成熟的優化是萬惡之源」——Donald Knuth