dede網(wǎng)站地圖文章變量合肥網(wǎng)站seo公司
本教程基于韋東山百問網(wǎng)出的 DShanMCU-RA6M5開發(fā)板 進行編寫,需要的同學可以在這里獲取: https://item.taobao.com/item.htm?id=728461040949
配套資料獲取:https://renesas-docs.100ask.net
瑞薩MCU零基礎(chǔ)入門系列教程匯總: https://blog.csdn.net/qq_35181236/article/details/132779862
第1章 單片機程序的設(shè)計模式
本章目標
- 理解裸機程序設(shè)計模式
- 了解多任務(wù)系統(tǒng)中程序設(shè)計的不同
1.1 裸機程序設(shè)計模式
裸機程序的設(shè)計模式可以分為:輪詢、前后臺、定時器驅(qū)動、基于狀態(tài)機。前面三種方法都無法解決一個問題:假設(shè)有 A、B 兩個都很耗時的函數(shù),無法降低它們相互之間的影響。第 4 種方法可以解決這個問題,但是實踐起來有難度。
假設(shè)一位職場媽媽需要同時解決 2 個問題:給小孩喂飯、回復工作信息,場景如圖所示,
后面將會演示各類模式下如何寫程序:
1.1.1 輪詢模式
示例代碼如下:
// 經(jīng)典單片機程序: 輪詢void main(){while (1){喂一口飯();回一個信息();}}
在 main 函數(shù)中是一個 while 循環(huán),里面依次調(diào)用 2 個函數(shù),這兩個函數(shù)相互之間有影響:如果“喂一口飯”太花時間,就會導致遲遲無法“回一個信息”;如果“回一個信息”太花時間,就會導致遲遲無法“喂下一口飯”。
使用輪詢模式編寫程序看起來很簡單,但是要求 while 循環(huán)里調(diào)用到的函數(shù)要執(zhí)行得非常快,在復雜場景里反而增加了編程難度。
1.1.2 前后臺
所謂“前后臺”就是使用中斷程序。假設(shè)收到同事發(fā)來的信息時,電腦會發(fā)出“滴”的一聲,這時候媽媽才需要去回復信息。示例程序如下:
// 前后臺程序void main(){while (1){// 后臺程序喂一口飯();}}// 前臺程序
void 滴_中斷()
{回一個信息();
}
- main 函數(shù)里 while 循環(huán)里的代碼是后臺程序,平時都是 while 循環(huán)在運行;
- 當同事發(fā)來信息,電腦發(fā)出“滴”的一聲,觸發(fā)了中斷。媽媽暫停喂飯,去執(zhí)行“滴_中斷”給同事回復信息;
在這個場景里,給同事回復信息非常及時:即使正在喂飯也會暫停下來去回復信息。“喂一口飯”無法影響到“回一個信息”。但是,如果“回一個信息”太花時間,就會導致 “喂一口飯”遲遲無法執(zhí)行。
繼續(xù)改進,假設(shè)小孩吞下飯菜后會發(fā)出“啊”的一聲,媽媽聽到后才會喂下一口飯。喂飯、回復信息都是使用中斷函數(shù)來處理。示例程序如下:
// 前后臺程序
void main()
{while (1){// 后臺程序}
}// 前臺程序
void 滴_中斷()
{回一個信息();
}// 前臺程序
void 啊_中斷()
{喂一口飯();
}
main 函數(shù)中的 while 循環(huán)是空的,程序的運行靠中斷來驅(qū)使。如果電腦聲音“滴”、小孩聲音“啊”不會同時、相近發(fā)出,那么“回一個信息”、“喂一口飯”相互之間沒有影響。在不能滿足這個前提的情況下,比如“滴”、“啊”同時響起,先“回一個信息”時就會耽誤“喂一口飯”,這種場景下程序遭遇到了輪詢模式的缺點:函數(shù)相互之間有影響。
1.1.3 定時器驅(qū)動
定時器驅(qū)動模式,是前后臺模式的一種,可以按照不用的頻率執(zhí)行各種函數(shù)。比如需要每 2 分鐘給小孩喂一口飯,需要每 5 分鐘給同事回復信息。那么就可以啟動一個定時器,讓它每 1 分鐘產(chǎn)生一次中斷,讓中斷函數(shù)在合適的時間調(diào)用對應(yīng)函數(shù)。示例代碼如下:
// 前后臺程序: 定時器驅(qū)動
void main()
{while (1){// 后臺程序}
}// 前臺程序: 每 1 分鐘觸發(fā)一次中斷
void 定時器_中斷()
{static int cnt = 0;cnt++;if (cnt % 2 == 0){喂一口飯();}else if (cnt % 5 == 0){回一個信息();}
}
main 函數(shù)中的 while 循環(huán)是空的,程序的運行靠定時器中斷來驅(qū)使。
- 定時器中斷每 1 分鐘發(fā)生一次,在中斷函數(shù)里讓 cnt 變量累加(代碼第 14 行)。
- 第 15 行:進行求模運算,如果對 2 取模為 0,就“喂一口飯”。這相當于每發(fā)生 2 次中斷就“喂一口飯”。
- 第 19 行:進行求模運算,如果對 5 取模為 0,就“回一個信息”。這相當于每發(fā)生 5 次
中斷就“回一個信息”。
這種模式適合調(diào)用周期性的函數(shù),并且每一個函數(shù)執(zhí)行的時間不能超過一個定時器周期。如果“喂一口飯”很花時間,比如長達 10 分鐘,那么就會耽誤“回一個信息”;反過來也是一樣的,如果“回一個信息”很花時間也會影響到“喂一口飯”;這種場景下程序遭遇到了輪詢模式的缺點:函數(shù)相互之間有影響。
1.1.4 基于狀態(tài)機
當“喂一口飯”、“回一個信息”都需要花很長的時間,無論使用前面的哪種設(shè)計模式,都會退化到輪詢模式的缺點:函數(shù)相互之間有影響??梢允褂脿顟B(tài)機來解決這個缺點,示例代碼如下:
// 狀態(tài)機
void main()
{while (1){喂一口飯();回一個信息();}
}
在 main 函數(shù)里,還是使用輪詢模式依次調(diào)用 2 個函數(shù)。
關(guān)鍵在于這 2 個函數(shù)的內(nèi)部實現(xiàn):使用狀態(tài)機,每次只執(zhí)行一個狀態(tài)的代碼,減少每次執(zhí)行的時間,代碼如下:
void 喂一口飯(void)
{static int state = 0;switch (state){case 0:{/* 舀飯 *//* 進入下一個狀態(tài) */state++;break;}case 1:{/* 喂飯 *//* 進入下一個狀態(tài) */state++;break;}case 2:{/* 舀菜 *//* 進入下一個狀態(tài) */state++;break;}case 2:{/* 喂菜 *//* 恢復到初始狀態(tài) */state = 0;break;}}
}void 回一個信息(void)
{static int state = 0;switch (state){case 0:{/* 查看信息 *//* 進入下一個狀態(tài) */state++;break;}case 1:{/* 打字 *//* 進入下一個狀態(tài) */state++;break;}case 2:{/* 發(fā)送 *//* 恢復到初始狀態(tài) */state = 0;break;}}
}
以“喂一口飯”為例,函數(shù)內(nèi)部拆分為 4 個狀態(tài):舀飯、喂飯、舀菜、喂菜。每次執(zhí)行“喂一口飯”函數(shù)時,都只會執(zhí)行其中的某一狀態(tài)對應(yīng)的代碼。以前執(zhí)行一次“喂一口飯”函數(shù)可能需要 4 秒鐘,現(xiàn)在可能只需要 1 秒鐘,就降低了對后面“回一個信息”的影響。
同樣的,“回一個信息”函數(shù)內(nèi)部也被拆分為 3 個狀態(tài):查看信息、打字、發(fā)送。每次執(zhí)行這個函數(shù)時,都只是執(zhí)行其中一小部分代碼,降低了對“喂一口飯”的影響。
使用狀態(tài)機模式,可以解決裸機程序的難題:假設(shè)有 A、B 兩個都很耗時的函數(shù),怎樣降低它們相互之間的影響。但是很多場景里,函數(shù) A、B 并不容易拆分為多個狀態(tài),并且這些狀態(tài)執(zhí)行的時間并不好控制。所以這并不是最優(yōu)的解決方法,需要使用多任務(wù)系統(tǒng)。
1.2 多任務(wù)系統(tǒng)
1.2.1 多任務(wù)模式
對于裸機程序,無論使用哪種模式進行精心的設(shè)計,在最差的情況下都無法解決這個問題:假設(shè)有 A、B 兩個都很耗時的函數(shù),無法降低它們相互之間的影響。使用狀態(tài)機模式時,如果函數(shù)拆分得不好,也會導致這個問題。本質(zhì)原因是:函數(shù)是輪流執(zhí)行的。假設(shè)“喂一口飯”需要 t1~t5 這 5 段時間,“回一個信息需要”ta~te 這 5 段時間,輪流執(zhí)行時:先執(zhí)行完 t1~t5,再執(zhí)行 ta~te,如下圖所示:
對于職場媽媽,她怎么解決這個問題呢?她是一個眼明手快的人,可以一心多用,她這
樣做:
- 左手拿勺子,給小孩喂飯
- 右手敲鍵盤,回復同事
- 兩不耽誤,小孩“以為”媽媽在專心喂飯,同事“以為”她在專心聊天
- 但是腦子只有一個啊,雖然說“一心多用”,但是誰能同時思考兩件事?
- 只是她反應(yīng)快,上一秒鐘在考慮夾哪個菜給小孩,下一秒鐘考慮給同事回復什么信息
- 本質(zhì)是:交叉執(zhí)行,t1~t5 和 ta~te 交叉執(zhí)行,如下圖所示:
基于多任務(wù)系統(tǒng)編寫程序時,示例代碼如下:
// RTOS 程序
喂飯任務(wù)()
{while (1){喂一口飯();}
}回信息任務(wù)()
{while (1){回一個信息();}
}void main()
{// 創(chuàng)建 2 個任務(wù)create_task(喂飯任務(wù));create_task(回信息任務(wù));// 啟動調(diào)度器start_scheduler();
}
- 第 21、22 行,創(chuàng)建 2 個任務(wù);
- 第 25 行,啟動調(diào)度器;
- 之后,這 2 個任務(wù)就會交叉執(zhí)行了;
基于多任務(wù)系統(tǒng)編寫程序時,反而更簡單了:
- 上面第 2~8 行是“喂飯任務(wù)”的代碼;
- 第 10~16 行是“回信息任務(wù)”的代碼,編寫它們時甚至都不需要考慮它和其他函數(shù)的相互影響。就好像有 2 個單板:一個只運行“喂飯任務(wù)”這個函數(shù)、另一個只運行“回信息任務(wù)”這個函數(shù)。
多任務(wù)系統(tǒng)會依次給這些任務(wù)分配時間:你執(zhí)行一會,我執(zhí)行一會,如此循環(huán)。只要切換的間隔足夠短,用戶會“感覺這些任務(wù)在同時運行”。如下圖所示:
1.2.2 互斥操作
多任務(wù)系統(tǒng)中,多個任務(wù)可能會“同時”訪問某些資源,需要增加保護措施以防止混亂。比如任務(wù) A、B 都要使用串口,能否使用一個全局變量讓它們獨占地、互斥地使用串口?示例代碼如下:
// RTOS 程序
int g_canuse = 1;
void uart_print(char *str)
{if (g_canuse){g_canuse = 0;printf(str);g_canuse = 1;}
}task_A()
{while (1){uart_print("0123456789\n");}}task_B()
{while (1){uart_print("abcdefghij");}
}void main()
{// 創(chuàng)建 2 個任務(wù)create_task(task_A);create_task(task_B);// 啟動調(diào)度器start_scheduler();
}
程序的意圖是:task_A 打印“0123456789”,task_B 打印“abcdefghij”。在 task_A 或task_B 打印的過程中,另一個任務(wù)不能打印,以避免數(shù)字、字母混雜在一起,比如避免打印這樣的字符:“012abc”。
第 6 行使用全局變量 g_canuse 實現(xiàn)互斥打印,它等于 1 時表示“可以打印”。在進行實際打印之前,先把 g_canuse 設(shè)置為 0,目的是防止別的任務(wù)也來打印。
這個程序大部分時間是沒問題的,但是只要它運行的時間足夠長,就會出現(xiàn)數(shù)字、字母混雜的情況。下圖把 uart_print 函數(shù)標記為①~④個步驟:
void uart_print(char *str)
{if (g_canuse) ①{g_canuse = 0; ② printf(str); ③g_canuse = 1; ④}
}
如果 task_A 執(zhí)行完①,進入 if 語句里面執(zhí)行②之前被切換為 task_B:在這一瞬間,g_canuse 還是 1。
task_B 執(zhí)行①時也會成功進入 if 語句,假設(shè)它執(zhí)行到③,在 printf 打印完部分字符比如“abc”后又再次被切換為 task_A。
task_A 繼續(xù)從上次被暫停的地方繼續(xù)執(zhí)行,即從②那里繼續(xù)執(zhí)行,成功打印出“0123456789”。這時在串口上可以看到打印的結(jié)果為:“abc0123456789”。
是不是“①判斷”、“②清零”間隔太遠了,uart_print 函數(shù)改進成如下的代碼呢?
void uart_print(char *str)
{g_canuse--; ① 減一 if (g_canuse == 0) ② 判斷{printf(str); ③ 打印}g_canuse++; ④ 加一
}
即使改進為上述代碼,仍然可能產(chǎn)生兩個任務(wù)同時使用串口的情況。因為“①減一”這個操作會分為 3 個步驟:a.從內(nèi)存讀取變量的值放入寄存器里,b.修改寄存器的值讓它減一,c.把寄存器的值寫到內(nèi)存上的變量上去。
如果task_A執(zhí)行完步驟a、b,還沒來得及把新值寫到內(nèi)存的變量里,就被切換為task_B:在這一瞬間,g_canuse 還是 1。
task_B 執(zhí)行①②時也會成功進入 if 語句,假設(shè)它執(zhí)行到③,在 printf 打印完部分字符比如“abc”后又再次被切換為 task_A。
task_A 繼續(xù)從上次被暫停的地方繼續(xù)執(zhí)行,即從步驟 c 那里繼續(xù)執(zhí)行,成功打印出“0123456789”。這時在串口上可以看到打印的結(jié)果為:“abc0123456789”。
從上面的例子可以看到,基于多任務(wù)系統(tǒng)編寫程序時,訪問公用的資源的時候要考慮“互斥操作”。任何一種多任務(wù)系統(tǒng)都會提供相應(yīng)的函數(shù)。
1.2.3 同步操作
如果任務(wù)之間有依賴關(guān)系,比如任務(wù) A 執(zhí)行了某個操作之后,需要任務(wù) B 進行后續(xù)的處理。如果代碼如下編寫的話,任務(wù) B 大部分時間做的都是無用功。
// RTOS 程序
int flag = 0;void task_A()
{while (1){// 做某些復雜的事情// 完成后把 flag 設(shè)置為 1flag = 1;}
}void task_B()
{while (1){if (flag){// 做后續(xù)的操作}}
}void main()
{// 創(chuàng)建 2 個任務(wù)create_task(task_A);create_task(task_B);// 啟動調(diào)度器start_scheduler();
}
上述代碼中,在任務(wù) A 沒有設(shè)置 flag 為 1 之前,任務(wù) B 的代碼都只是去判斷 flag。而任務(wù) A、B 的函數(shù)是依次輪流運行的,假設(shè)系統(tǒng)運行了 100 秒,其中任務(wù) A 總共運行了 50秒,任務(wù) B 總共運行了 50 秒,任務(wù) A 在努力處理復雜的運算,任務(wù) B 僅僅是浪費 CPU 資源。
如果可以讓任務(wù) B 阻塞,即讓任務(wù) B 不參與調(diào)度,那么任務(wù) A 就可以獨占 CPU 資源加快處理復雜的事情。當任務(wù) A 處理完事情后,再喚醒任務(wù) B。示例代碼如下:
//RTOS 程序
void task_A()
{while (1){// 做某些復雜的事情// 釋放信號量,會喚醒任務(wù) B;}
}void task_B()
{while (1){// 等待信號量, 會讓任務(wù) B 阻塞// 做后續(xù)的操作}
}void main()
{// 創(chuàng)建 2 個任務(wù)create_task(task_A);create_task(task_B);// 啟動調(diào)度器start_scheduler();
}
- 第 15 行:任務(wù) B 運行時,等待信號量,不成功時就會阻塞,不在參與任務(wù)調(diào)度。
- 第 7 行: 任務(wù) A 處理完復雜的事情后,釋放信號量會喚醒任務(wù) B。
- 第 16 行:任務(wù) B 被喚醒后,從這里繼續(xù)運行。
在這個過程中,任務(wù) A 處理復雜事情的時候可以獨占 CPU 資源,加快處理速度。