本文探討了在嵌入式開發(fā)中設(shè)計(jì)嵌入式軟件架構(gòu)的第四步——接口和組件設(shè)計(jì)。
在上一篇文章中,我們探討了如何將一個系統(tǒng)分解成域和任務(wù),我們已經(jīng)確定了該系統(tǒng)的數(shù)據(jù)資產(chǎn)。你可能還記得,我們創(chuàng)建的上一張圖將我們的應(yīng)用程序分為安全和非安全應(yīng)用程序域、硬件獨(dú)立和依賴域(由抽象層分隔),最后分為任務(wù)。我們的系統(tǒng)分解如圖1所示。
由于blog格式的空間和時間限制,我們建議在描述如何讓任務(wù)進(jìn)行交互的架構(gòu)過程中增加額外的步驟。我們想探索如何完成我們的一項(xiàng)任務(wù),并設(shè)計(jì)我們的界面和組件。讓我們看看如何為電機(jī)任務(wù)定義組件及其接口。
步驟4–接口和組件設(shè)計(jì)
組件和界面設(shè)計(jì)是密切相關(guān)的活動。事實(shí)上,如果我們查看組件的標(biāo)準(zhǔn)定義,我們會發(fā)現(xiàn)以下描述:
軟件組件是一個組合單元,具有合同規(guī)定的接口和明確的上下文依賴關(guān)系。
我們甚至不能在不討論接口的情況下定義組件!接口是軟件開發(fā)人員用來與組件交互的工具。因此,我們開始將我們的運(yùn)動任務(wù)分解為組件是有意義的,然后我們可以為每個組件定義接口。
定義軟件組件
嵌入式開發(fā)人員可以使用許多不同的方法和技術(shù)來識別將堆疊在一起以執(zhí)行任務(wù)提供的期望行為的組件。我對幾乎任何任務(wù)的偏好都是將系統(tǒng)分解成一個分層的軟件圖,從底層驅(qū)動程序開始,一直到應(yīng)用程序代碼。圖2顯示了一個示例,顯示了電機(jī)任務(wù)的這種圖。
這些組件的用途可能是不言自明的,但為了以防萬一,讓我們定義一下每個組件的作用:
pwm_drv — 微控制器上用于脈寬調(diào)制的硬件外設(shè)驅(qū)動器。
Abstraction layer — 打破電機(jī)驅(qū)動器和硬件之間的依賴關(guān)系。這允許我們使用PWM驅(qū)動器來驅(qū)動電機(jī),或者用外部集成電路的驅(qū)動器來代替pwm_drv。
motor_drv — 設(shè)計(jì)用于控制電機(jī)的驅(qū)動器。它沒有硬件依賴性。它唯一的依賴是抽象層。
motor_sm — 跟蹤電機(jī)當(dāng)前狀態(tài)和所需狀態(tài)的狀態(tài)機(jī)。
motor_app — 特定應(yīng)用支持功能。這些功能可能包括收集遙測數(shù)據(jù)、檢查故障等。
motor_task — 保存實(shí)際電機(jī)任務(wù)代碼的組件。該元件依賴于其他電機(jī)元件。電機(jī)任務(wù)可能包括RTOS應(yīng)用程序交互,如信號量、隊(duì)列、互斥和事件邏輯。
總之,這些組件具有接收命令的所有必要行為,該命令改變控制電機(jī)的狀態(tài)機(jī),然后啟動電機(jī)。這種分解很酷的一點(diǎn)是,如果電機(jī)控制鏈的各個方面發(fā)生變化,比如新的驅(qū)動芯片、新的應(yīng)用程序需求等,有了適當(dāng)?shù)慕涌冢?span>嵌入式開發(fā)人員只需要進(jìn)行最小的更改。
定義接口
對于motor任務(wù),我們需要定義兩類接口。首先,有一個任務(wù)接口,它接收來自其他應(yīng)用程序組件的命令,告訴它電機(jī)應(yīng)該做什么,比如MOTOR_ON或MOTOR_OFF。其次,每個組件都有接口。
電機(jī)任務(wù)接口設(shè)計(jì)
在定義與電機(jī)任務(wù)通信的接口時,回顧圖1中所示的數(shù)據(jù)資產(chǎn)是很有幫助的。我們可以看到控制器任務(wù)與電機(jī)任務(wù)交互。為了讓控制器任務(wù)告訴電機(jī)任務(wù)電機(jī)應(yīng)該做什么,需要幾條信息:電機(jī)狀態(tài)、電機(jī)方向、電機(jī)轉(zhuǎn)速。
如果我們希望系統(tǒng)可伸縮以管理多個電機(jī),我們甚至可以包括一個電機(jī)ID。例如,在有些應(yīng)用中,狀態(tài)機(jī)通過消息控制電機(jī),但用戶可以用消息覆蓋狀態(tài)。在這些情況下,我們甚至可能希望包含一個任務(wù)ID,以便電機(jī)任務(wù)知道哪個任務(wù)正在請求電機(jī)控制。
從數(shù)據(jù)接口的角度來看,嵌入式開發(fā)人員可以為數(shù)據(jù)定義一個結(jié)構(gòu),該結(jié)構(gòu)將用于控制和電機(jī)任務(wù)的接口。用C語言編寫如下代碼:
MotorMessage_t結(jié)構(gòu)定義了命令電機(jī)任務(wù)執(zhí)行實(shí)際工作所需的數(shù)據(jù)接口。我們?nèi)绾潍@得機(jī)動任務(wù)的信息取決于項(xiàng)目的需求。例如,使用消息隊(duì)列、數(shù)據(jù)緩沖區(qū)或其他機(jī)制。然而,這些細(xì)節(jié)是由程序員決定的,而不是軟件架構(gòu)師。
定義組件接口
有幾種不同的方法來為我們的組件設(shè)計(jì)界面。首先,我們可以做一個便簽,列出我們認(rèn)為需要的功能。接下來,我們可以使用一些UML圖表工具并利用類圖,即使我們沒有編寫面向?qū)ο蟮拇a。最后,我們可以直接進(jìn)入代碼,開始編寫測試,迫使我們?yōu)榻M件的行為開發(fā)接口。
類圖很棒,因?yàn)樗鼈冊试S嵌入式開發(fā)人員指定屬性、操作和類之間的關(guān)系(模塊和組件也可以)。有幾件事我們應(yīng)該注意。首先,電機(jī)任務(wù)使用電機(jī)應(yīng)用程序、電機(jī)狀態(tài)機(jī)和電機(jī)驅(qū)動器。這里沒有繼承關(guān)系。我們可以看到電機(jī)驅(qū)動器有一個電機(jī)接口。motor應(yīng)用程序使用MotorError_t枚舉,但除此之外,組件是解耦的,不進(jìn)行交互。
接下來,我們定義了我們認(rèn)為的操作是什么,以及組件需要的功能或方法。例如,我們可以看到電機(jī)驅(qū)動器有兩個功能:電機(jī)初始化和電機(jī)命令。電機(jī)命令采用電機(jī)消息類型和所有信息來命令電機(jī),這是通過電機(jī)接口(抽象層)完成的。
當(dāng)我們查看類圖時,我們可以看到電機(jī)任務(wù)與底層組件進(jìn)行交互,并驅(qū)動電機(jī)的最終行為。首先,信息通過MotorQ進(jìn)入電機(jī)任務(wù);然后,電機(jī)任務(wù)運(yùn)行電機(jī)狀態(tài)機(jī)。最后,電機(jī)任務(wù)調(diào)用電機(jī)應(yīng)用程序查找錯誤,并使用狀態(tài)結(jié)果通過電機(jī)驅(qū)動器命令電機(jī)。從一個可能并不明顯的類圖中可以得出很多東西。那么,我們的架構(gòu)是否遺漏了什么?
我們的設(shè)計(jì)缺少了什么?
我們可能有足夠的資源來開始實(shí)現(xiàn)電機(jī)應(yīng)用程序代碼;然而,更多的圖表和時間可以極大地提高清晰度。例如,嵌入式開發(fā)人員可以考慮額外的UML圖,比如序列圖,來顯示控制任務(wù)與電機(jī)任務(wù)交互的時序要求。或是創(chuàng)建一個序列圖,以便開發(fā)人員了解電機(jī)任務(wù)如何與其他組件交互。比如電機(jī)任務(wù)是應(yīng)該在任務(wù)結(jié)束的時候命令電機(jī),還是先做,把抖動降到最低?如果出現(xiàn)錯誤,處理的邏輯是什么?電機(jī)應(yīng)該保持運(yùn)轉(zhuǎn)還是停止運(yùn)轉(zhuǎn)?
你通常會發(fā)現(xiàn),你至少需要三到四個UML圖來完整地指定一項(xiàng)任務(wù),以便開發(fā)人員可以對其進(jìn)行編碼。我們只看了幾個,這意味著還有更多的事情要做。我們可以開始開發(fā)測試,并使用測試驅(qū)動的開發(fā)來進(jìn)一步發(fā)展我們的界面和對系統(tǒng)的理解。這可能是目前的發(fā)展方向。但是,我們知道的足夠多,隨著我們實(shí)現(xiàn)細(xì)節(jié),問題將會出現(xiàn),這將推動對初始架構(gòu)的更改。
軟件架構(gòu)設(shè)計(jì)第4步結(jié)論
正如本文所見,我們可以利用UML圖和組件堆疊圖來識別組件并定義它們的接口。此外,類圖可以成為設(shè)計(jì)師進(jìn)行界面設(shè)計(jì)的優(yōu)秀工具。嵌入式開發(fā)人員必須記住,雖然我們遵循一個簡單的五步過程來開發(fā)我們的架構(gòu),但這些步驟只是一個指南,而不是全部。作為架構(gòu)師,我們經(jīng)常需要更深入。