• 正文
    • 一、面向?qū)ο缶幊蹋∣OP)
    • 二、測試驅(qū)動(dòng)開發(fā)(TDD)
    • 三、防御性編程
    • 四、敏捷開發(fā)
    • 五、瀑布模型
  • 相關(guān)推薦
申請入駐 產(chǎn)業(yè)圖譜

嵌入式開發(fā)中常用的軟件工程方法有哪些?

03/24 15:40
614
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點(diǎn)資訊討論

大家好,我是雜燴君。

嵌入式開發(fā)里,有哪些常用的軟件工程方法呢?

一、面向?qū)ο缶幊蹋∣OP)

盡管 C 語言并非面向?qū)ο缶幊陶Z言,但借助一些編程技巧,也能實(shí)現(xiàn)面向?qū)ο缶幊蹋∣OP)的核心特性,如封裝、繼承和多態(tài)。

1. 封裝

封裝是把數(shù)據(jù)和操作數(shù)據(jù)的函數(shù)捆綁在一起,對外部隱藏內(nèi)部實(shí)現(xiàn)細(xì)節(jié)。在嵌入式 C 語言中,可通過結(jié)構(gòu)體和函數(shù)指針來實(shí)現(xiàn)封裝。

#include<stdio.h>

// 定義一個(gè)LED結(jié)構(gòu)體
typedefstruct?{
? ??int?pin;
? ??void?(*turnOn)(struct LED*);
? ??void?(*turnOff)(struct LED*);
} LED;

// 實(shí)現(xiàn)LED開啟函數(shù)
voidledTurnOn(LED* led){
? ??printf("LED on pin %d is turned on.n", led->pin);
}

// 實(shí)現(xiàn)LED關(guān)閉函數(shù)
voidledTurnOff(LED* led){
? ??printf("LED on pin %d is turned off.n", led->pin);
}

// 初始化LED
voidledInit(LED* led,?int?pin){
? ? led->pin = pin;
? ? led->turnOn = ledTurnOn;
? ? led->turnOff = ledTurnOff;
}

intmain(void){
? ? LED myLed;
? ? ledInit(&myLed,?13);
? ? myLed.turnOn(&myLed);
? ? myLed.turnOff(&myLed);
? ??return0;
}

LED結(jié)構(gòu)體封裝了pin數(shù)據(jù)和turnOnturnOff函數(shù)指針。ledInit函數(shù)用于初始化LED結(jié)構(gòu)體,把具體的函數(shù)賦值給函數(shù)指針。外部代碼僅能通過這些函數(shù)指針來操作LED,而無需了解內(nèi)部實(shí)現(xiàn)細(xì)節(jié)。

2. 繼承

繼承是指一個(gè)對象直接使用另一對象的屬性和方法。在嵌入式 C 語言中,可通過結(jié)構(gòu)體嵌套實(shí)現(xiàn)繼承。

#include<stdio.h>

// 定義一個(gè)基類結(jié)構(gòu)體
typedefstruct?{
? ??int?id;
? ??void?(*printInfo)(struct Base*);
} Base;

// 實(shí)現(xiàn)基類的打印信息函數(shù)
voidbasePrintInfo(Base* base){
? ??printf("Base ID: %dn", base->id);
}

// 定義一個(gè)派生類結(jié)構(gòu)體
typedefstruct?{
? ? Base base;?// 繼承Base結(jié)構(gòu)體
? ??char* name;
} Derived;

// 實(shí)現(xiàn)派生類的打印信息函數(shù)
voidderivedPrintInfo(Derived* derived){
? ? basePrintInfo(&derived->base);
? ??printf("Derived Name: %sn", derived->name);
}

// 初始化基類
voidbaseInit(Base* base,?int?id){
? ? base->id = id;
? ? base->printInfo = basePrintInfo;
}

// 初始化派生類
voidderivedInit(Derived* derived,?int?id,?char* name){
? ? baseInit(&derived->base, id);
? ? derived->name = name;
? ? derived->base.printInfo = (void?(*)(Base*))derivedPrintInfo;
}

intmain(void){
? ? Derived myDerived;
? ? derivedInit(&myDerived,?1,?"Derived Object");
? ? myDerived.base.printInfo((Base*)&myDerived);
? ??return0;
}

Derived結(jié)構(gòu)體嵌套了Base結(jié)構(gòu)體,從而繼承了Base結(jié)構(gòu)體的屬性和方法。derivedInit函數(shù)在初始化Derived結(jié)構(gòu)體時(shí),會(huì)調(diào)用baseInit函數(shù)初始化基類部分,并且把printInfo函數(shù)指針指向derivedPrintInfo函數(shù)。

3. 多態(tài)

多態(tài)是指不同對象對同一消息做出不同響應(yīng)。在嵌入式 C 語言中,可通過函數(shù)指針實(shí)現(xiàn)多態(tài)。

#include<stdio.h>

// 定義一個(gè)基類結(jié)構(gòu)體
typedefstruct?{
? ??void?(*operation)(struct Base*);
} Base;

// 定義一個(gè)派生類1結(jié)構(gòu)體
typedefstruct?{
? ? Base base;
} Derived1;

// 定義一個(gè)派生類2結(jié)構(gòu)體
typedefstruct?{
? ? Base base;
} Derived2;

// 派生類1的操作函數(shù)
voidderived1Operation(Base* base){
? ??printf("Derived1 operation.n");
}

// 派生類2的操作函數(shù)
voidderived2Operation(Base* base){
? ??printf("Derived2 operation.n");
}

// 初始化派生類1
voidderived1Init(Derived1* derived1){
? ? derived1->base.operation = derived1Operation;
}

// 初始化派生類2
voidderived2Init(Derived2* derived2){
? ? derived2->base.operation = derived2Operation;
}

// 執(zhí)行操作
voidperformOperation(Base* base){
? ? base->operation(base);
}

intmain(void){
? ? Derived1 myDerived1;
? ? Derived2 myDerived2;
? ? derived1Init(&myDerived1);
? ? derived2Init(&myDerived2);

? ? performOperation((Base*)&myDerived1);
? ? performOperation((Base*)&myDerived2);
? ??return0;
}

Base結(jié)構(gòu)體包含一個(gè)函數(shù)指針operationDerived1Derived2結(jié)構(gòu)體繼承了Base結(jié)構(gòu)體,并分別實(shí)現(xiàn)了自己的operation函數(shù)。performOperation函數(shù)接收一個(gè)Base指針,依據(jù)具體對象調(diào)用相應(yīng)的operation函數(shù),從而實(shí)現(xiàn)了多態(tài)。

二、測試驅(qū)動(dòng)開發(fā)(TDD)

測試驅(qū)動(dòng)開發(fā)(Test-Driven Development,TDD是一種軟件開發(fā)方法論,它強(qiáng)調(diào)在編寫實(shí)際功能代碼之前先編寫測試代碼。

TDD 的核心流程遵循 “紅 - 綠 - 重構(gòu)” 循環(huán),下面將詳細(xì)介紹其原理、流程、優(yōu)勢、局限性以及示例。

TDD 基于 “測試先行” 的理念,開發(fā)者首先明確需求并將其轉(zhuǎn)化為具體的測試用例。

由于此時(shí)還未編寫實(shí)現(xiàn)代碼,測試用例必然會(huì)失?。ǔ尸F(xiàn) “紅色” 狀態(tài))。

接著,開發(fā)者編寫最少的代碼使測試用例通過(達(dá)到 “綠色” 狀態(tài))。

最后,對代碼進(jìn)行重構(gòu),在不改變代碼外部行為的前提下優(yōu)化其內(nèi)部結(jié)構(gòu),提高代碼的可讀性、可維護(hù)性和可擴(kuò)展性。

實(shí)踐:使用Unity測試框架。

Unity 是一個(gè)輕量級的測試框架,它使用 C 語言實(shí)現(xiàn), 代碼本身很小 。其代碼中大多數(shù)是宏定義,所以實(shí)際編譯后的代碼會(huì)更小,?比較適合在嵌入式測試應(yīng)用

Unity的使用之前也有簡單分享過:

首先,把Unity源碼目錄下的unity.c、unity.h、unity_internals.h三個(gè)文件復(fù)制至我們的工程目錄下,并把unity.c添加到我們的keil工程中,然后添加文件路徑:

我們打開unity_internals.h文件,發(fā)現(xiàn)其有包含一個(gè)頭文件unity_config.h

這個(gè)文件是配置文件,我們與平臺(tái)相關(guān)的特性放在這個(gè)文件中。而這個(gè)文件Unity源碼中并未提供,所以需要我們自己建立,我這邊新建的unity_config.h文件的內(nèi)容如下:

主要在這里面放了硬件相關(guān)的頭文件包含以及兩個(gè)必要的宏定義。第一個(gè)宏定義用于重定向輸出至串口,第二個(gè)宏定義就是我們的串口初始化。

unity_internals.h中我們發(fā)現(xiàn)unity_config.h文件被條件編譯屏蔽掉了,我們需要定義宏把它打開:

最后在我們的main.c中包含頭文件unity.h即可使用unity測試框架。在unity_internals.h中有很多可修改的配置,比如在不同的平臺(tái)中,整數(shù)的長度是不一樣的,在 Unity 中,允許開發(fā)者設(shè)置整數(shù)的長度。如果沒有設(shè)置, Unity 指定的默認(rèn)值是 32 位。我們的STM32就是32位的,所以我們不需要修改。

下面開始編寫測試用例。 在 Unity 中,每個(gè)測試用例是一個(gè)函數(shù), 該函數(shù)沒有參數(shù)和返回值。下面我們來測試一個(gè)閏年判斷函數(shù):

在測試函數(shù)中用到?TEST_ASSERT_TRUE?和?TEST_ASSERT_FALSE?, ?是 Unity 實(shí)現(xiàn)的兩個(gè)斷言, 用于判斷 布爾型表達(dá)式的值為真或?yàn)榧?。這些測試框架一般都是用斷言來進(jìn)行測試的,包括以上分享的幾個(gè)框架都是如此。本例中只用到了兩個(gè)斷言,在 Unity 中還有很多斷言,如下是部分?jǐn)嘌粤信e:

Unity 默認(rèn)需要實(shí)現(xiàn)用例初始化函數(shù) setUp 和用例清理函數(shù) tearDown,這兩個(gè)函數(shù)均沒有參數(shù)和返回值。 在閏年判斷函數(shù)的測試用例中,由于不需要初始化和清理操作,實(shí)現(xiàn)的兩個(gè)函數(shù)如下:

在編寫了測試用例后, 接下來就可以在 main 函數(shù)中運(yùn)行測試用例。在 Unity 中,使用宏?RUN_TEST?運(yùn)行測試用例,參數(shù)為要運(yùn)行的測試用例的函數(shù)名稱。主函數(shù)如下:

UNITY_BEGIN函數(shù)就是UNITY初始化函數(shù),我們的串口初始化也是在這里面被調(diào)用的:

RUN_TEST函數(shù)用于運(yùn)行我們的測試用例。UNITY_END函數(shù)就是返回我們的測試結(jié)果。最終,運(yùn)行得到如下結(jié)果:

假如,我們把測試閏年的測試函數(shù)里的2000改為2001:

那么輸出結(jié)果就變?yōu)椋?/p>

可以從結(jié)果看出沒有通過的用例相關(guān)的代碼所在行。在進(jìn)行這樣子的測試之前,我們當(dāng)然得要明白我們的功能函數(shù)的功能及其預(yù)期輸出,我們才能去進(jìn)行測試用例的設(shè)計(jì)及進(jìn)行測試。

相關(guān)書籍:《測試驅(qū)動(dòng)的嵌入式C語言開發(fā)》

三、防御性編程

防御性編程是一種編程范式,旨在通過預(yù)見代碼中可能出現(xiàn)的錯(cuò)誤、異常輸入或未定義行為,提前設(shè)計(jì)保護(hù)措施,確保程序在非預(yù)期情況下仍能穩(wěn)定運(yùn)行、優(yōu)雅降級或清晰報(bào)錯(cuò),而非崩潰或產(chǎn)生不可控后果。

1. 核心原則

不信任任何輸入:假設(shè)所有輸入(包括函數(shù)參數(shù)、用戶輸入、外部接口數(shù)據(jù)等)都是不可信的,必須經(jīng)過驗(yàn)證。

最小化潛在危害:通過隔離風(fēng)險(xiǎn)代碼限制作用域等方式,避免局部錯(cuò)誤擴(kuò)散至整個(gè)系統(tǒng)。

清晰的錯(cuò)誤反饋:錯(cuò)誤發(fā)生時(shí),提供明確的錯(cuò)誤信息日志,便于調(diào)試定位。

優(yōu)雅降級:無法處理錯(cuò)誤時(shí),讓系統(tǒng)進(jìn)入安全狀態(tài)。

2. 最佳實(shí)踐

(1) 參數(shù)校驗(yàn)

函數(shù)入口參數(shù)合法性檢查:

floatsafe_sqrt(float?x){
? ??if?(x <?0) {
? ? ? ??return?NAN; ?// 或觸發(fā)錯(cuò)誤處理
? ? }
? ??returnsqrt(x);
}
(2)斷言(Assert)

驗(yàn)證 “不可能發(fā)生” 的假設(shè) ,輔助調(diào)試:

#include<assert.h>

voidmemcpy_safe(void* dst,?size_t?dst_size,?constvoid* src,?size_t?src_size)
{
? ? assert(dst !=?NULL?&& src !=?NULL); ?// 指針非空
? ? assert(dst_size >= src_size); ? ? ??// 目標(biāo)空間足夠
? ??// 實(shí)際復(fù)制邏輯
}
(3)錯(cuò)誤碼與錯(cuò)誤處理

函數(shù)通過返回值或輸出參數(shù)傳遞錯(cuò)誤狀態(tài):

enum?error_code {
? ? SUCCESS =?0,
? ? ERR_INVALID_PARAM,
? ? ERR_OUT_OF_MEM
};

enum?error_code?init_device(struct device* dev){
? ??if?(dev ==?NULL)?return?ERR_INVALID_PARAM;
? ??if?(allocate_memory(dev) !=?0)?return?ERR_OUT_OF_MEM;
? ??return?SUCCESS;
}
(4)防御未定義行為

確保整數(shù)除法不溢出:

intdivide_safe(int?a,?int?b){
? ??if?(b ==?0)?return?INT_MAX; ?// 或觸發(fā)錯(cuò)誤
? ??if?(a == INT_MIN && b ==?-1)?return?INT_MAX; ?// 處理-2147483648 / -1溢出
? ??return?a / b;
}

防止空指針解引用:

voidprint_string(constchar* str){
? ??if?(str ==?NULL) {
? ? ? ??printf("(null)n");
? ? ? ??return;
? ? }
? ??printf("%sn", str);
}

3.防御性測試優(yōu)缺點(diǎn)

四、敏捷開發(fā)

敏捷開發(fā)強(qiáng)調(diào)快速迭代、客戶反饋和團(tuán)隊(duì)協(xié)作。在嵌入式開發(fā)中,可將項(xiàng)目拆分成多個(gè)小的迭代周期,每個(gè)周期都有可運(yùn)行的版本。

1.迭代開發(fā)與持續(xù)集成

短周期迭代:將項(xiàng)目劃分為多個(gè)短周期迭代,每個(gè)迭代通常為 1 - 4 周。在每個(gè)迭代中,團(tuán)隊(duì)完成一定數(shù)量的用戶故事,交付可運(yùn)行的產(chǎn)品增量。短周期迭代有助于快速驗(yàn)證需求和設(shè)計(jì),及時(shí)調(diào)整項(xiàng)目方向。

持續(xù)集成:建立自動(dòng)化的持續(xù)集成環(huán)境,每次代碼提交后自動(dòng)進(jìn)行編譯、測試和集成。在嵌入式開發(fā)中,還需進(jìn)行硬件集成測試,確保軟件和硬件的兼容性。持續(xù)集成能及時(shí)發(fā)現(xiàn)代碼沖突和缺陷,提高代碼質(zhì)量。

迭代評審與回顧:每個(gè)迭代結(jié)束后,進(jìn)行迭代評審會(huì)議,向客戶和相關(guān)利益者展示迭代成果,收集反饋意見。同時(shí),召開迭代回顧會(huì)議,團(tuán)隊(duì)成員總結(jié)本次迭代的經(jīng)驗(yàn)教訓(xùn),提出改進(jìn)措施,應(yīng)用于下一次迭代。

五、瀑布模型

瀑布模型是一種傳統(tǒng)的軟件開發(fā)模型,按照線性順序依次進(jìn)行需求分析、設(shè)計(jì)、編碼、測試、維護(hù)等階段,如同瀑布流水一樣,每個(gè)階段完成后才進(jìn)入下一階段。

瀑布模型作為一種經(jīng)典的軟件開發(fā)方法,在嵌入式開發(fā)等眾多領(lǐng)域具有顯著的優(yōu)勢。

1.階段明確、順序清晰

瀑布模型的各個(gè)階段界限分明,從需求分析、設(shè)計(jì)、編碼、測試到維護(hù),按照固定的線性順序依次推進(jìn)。

這種清晰的階段劃分使得項(xiàng)目流程易于理解和管理,每個(gè)階段都有明確的輸入和輸出,便于項(xiàng)目團(tuán)隊(duì)成員明確各自的職責(zé)和任務(wù)。

2.強(qiáng)調(diào)文檔化

該模型非常重視文檔的編寫和管理,在每個(gè)階段都會(huì)產(chǎn)生相應(yīng)的文檔,如需求規(guī)格說明書、設(shè)計(jì)文檔、測試報(bào)告等。

這些文檔不僅是項(xiàng)目各個(gè)階段的重要成果,也是項(xiàng)目團(tuán)隊(duì)成員之間溝通的重要工具,同時(shí)還為項(xiàng)目的維護(hù)和升級提供了有力的支持。

3.易于控制和管理

由于瀑布模型的線性順序和明確的階段劃分,項(xiàng)目管理者可以很容易地對項(xiàng)目進(jìn)行監(jiān)控和控制。

每個(gè)階段都有明確的里程碑和交付物,管理者可以根據(jù)這些里程碑來評估項(xiàng)目的進(jìn)度和質(zhì)量,及時(shí)發(fā)現(xiàn)問題并采取相應(yīng)的措施進(jìn)行調(diào)整。

例如,在編碼階段結(jié)束后,可以通過代碼審查和單元測試來評估代碼的質(zhì)量,如果發(fā)現(xiàn)問題,可以及時(shí)反饋給開發(fā)人員進(jìn)行修改。

已上就是本次的分享,如果覺得文章有幫助,麻煩幫忙轉(zhuǎn)發(fā),謝謝!

相關(guān)推薦

登錄即可解鎖
  • 海量技術(shù)文章
  • 設(shè)計(jì)資源下載
  • 產(chǎn)業(yè)鏈客戶資源
  • 寫文章/發(fā)需求
立即登錄

本公眾號專注于嵌入式技術(shù),包括但不限于C/C++、嵌入式、物聯(lián)網(wǎng)、Linux等編程學(xué)習(xí)筆記,同時(shí),公眾號內(nèi)包含大量的學(xué)習(xí)資源。歡迎關(guān)注,一同交流學(xué)習(xí),共同進(jìn)步!