|
- - 前言 -
- 時至今日,依舊看到很多小伙伴們放著單片機里的定時器不用,動輒delay1s(); delay500ms();雖然簡單粗暴,但是其實是很不妥當的。
- 還有很多需要按鍵的程序,動不動就“while(k1==0);”的等按鍵松開,這樣的代碼只是“為了滿足某個功能“而設計的代碼,而非”為了保證產品的質量“而設計的代碼。
- 我們應該時刻考慮“如果這傻逼用戶不按我的標準操作來搞我的產品,我該怎么寫程序?”,而不是”只要客戶乖乖聽我的,這樣做是沒問題的,就行了;出了問題都是他亂搞,跟我有個毛關系”,后面的思維是典型的只顧自己爽的思維,我們應當想方設法的提高產品的可靠度。
- OK,扯遠了,現在只是想提出一些設計的思路。
- part1 . 系統的時基 & 依托于時基去設計函數
- 系統的時鐘基線 -- 關鍵詞,是“時鐘基線”。
- 我們需要設置一個定時器,這個定時器會告訴單片機:“好的,現在已經過了1ms了(或者100ms?這都沒關系)”單片機一拍腦袋:“啊,已經到了1ms了么,那我就把某一件事情做一下吧”。
- 大致像這樣:
-
51hei.png (16.78 KB, 下載次數: 189)
下載附件
主函數與定時器
2024-4-1 20:31 上傳
- 這個程序的意思就是,讓單片機每隔1ms就do_sth()一次(定時器的初始化函數省略了沒畫);
- 這樣寫還可以讓中斷函數非常的簡潔,幾乎一進去就出來,基本不會影響到被中斷打斷的函數的工作;
- 以傳遞關鍵參數的形式,大循環里只要查一查這個flag1ms值,知道什么時間要做什么事情就對了。
- 按鍵函數舉例 -- 上圖中這個do_sth()可以是任意函數,以按鍵函數keyPress()為例,可以這樣寫:
- void keyPress(){
- static unsigned int key_press_time = 0; // ……請記得標為靜態變量
- if(K1==0){
- if(++key_press_time <=0 ) --key_press_time;//計量按鍵時間,并避免數據溢出
- if(key_press_time==3000){
- //在此寫下按鍵長按3s時要做的事情;
- }
- }else{
- if(20<=key_press_time && key_press_time < 3000){
- //大于20ms小于3s,視為短按,在此寫下寫短按的處理代碼
- }
- key_press_time=0;
- }
- }
復制代碼
- 這里有很多可以研究的地方:
- ①“if(++key_press_time <=0 ) --key_press_time;”這一句,看起來用“++key_press_time”就能搞定,但是,誰也不能保證某些用戶真的不會按按鍵超過65秒的啊;萬一他真的按了65576ms;單片機還就真的以為用戶“短按”了一次呢(65576-65536=40ms,屬于短按范疇),這下那個短按程序段也會被執行一次;現在這樣寫,哪怕你按100年也沒關系了,反正我的單片機就每隔1ms進來看一次,K1這個按鈕你想按多久就按多久,掉在我的范圍內我就處理你,超出我的范圍我就無視你。
- ②“if(key_press_time==3000){ ; }”這里的3000只是隨便設置的的一個時間(3s),如果你需要做按鍵長短按功能,這里就是你的長按程序所放的位置;你也可以不用3000;用2000,50000都沒事,別超過65534就行(提問:為什么這里又是65534呢?);
- ③“if(20<=key_press_time && key_press_time < 3000)”這里,前面的>=20是個消抖的設計,客戶再強也是人類,再強的人類也不可能1秒按一個按鍵超過20次;也就是不可能短于50ms的時間;這里用了20ms相當于兼容 男上加男的快男 也來按按鍵了。后面<3000是不能和長按的時間沖突,因為3s我們已經人為的設置成長按時間節點了。
- 好,現在知道相關的程序該怎么寫了,我們回到剛剛討論的時鐘基線處。
- 多個時鐘基線的方案 -- 剛剛的例子中我們只展示了1ms的時基,對于單片機來說,1ms已經很慢了,但對于人和某些設備來說來說還是快的不行,比如一些本就需要10ms間隔的通信設置,需要隔100ms才能刷一次的顯示,需要50ms才能重新采的某些傳感器等等,這些東西放進1ms的do_sth()里面肯定是不行的(其實也行,但不方便管理來著)——怎么辦呢?
- ——好辦,放進10ms 或50ms 或100ms 的do_sth() 里就OK了。
- ——但是我們只有1個定時器啊,怎么弄幾個時基呢?
- ——1個定時器就夠了,其他的時基都基于那個1ms的時基來就好。我們已經有了flag1ms,當然可以弄幾個flag10ms乃至flag1000ms,只要你需要,就能設置。
- 如下圖,我們通過參數控制參數的形式,將flag1ms擴展為多個時基,使得程序漸漸的趨于模塊化,精準化。
-
51hei1.png (33.52 KB, 下載次數: 214)
下載附件
增大時基的設置
2024-4-1 20:31 上傳
- 這里有個要注意的地方,就是函數從宏觀上來看,確實是“每隔1ms就do_sth();每隔10ms就do_sth1();每隔100ms就do_sth2()"。
- 但是,從微觀上看,單片機是沒法在同一時刻做2件事情的!所以,每到10ms的時候,單片機會”先把1ms的事情做完再做10ms的事“;每到100ms的時候,單片機會”先把1ms的事情做完再做10ms的事,再做100ms的事“。
- 現在,我們的系統已經趨于成型了,不過要注意一點,這些做事情的子函數里萬萬不得有delay函數了(放幾個nop倒是無傷大雅),好不容易劃分好了時鐘基線,你在1ms的時基里塞上幾個delay2ms是要鬧怎樣。
- 還有一個問題,那就是子函數不要拖泥帶水,寫得越簡潔越好,趕緊把事情做完,把控制權還給單片機,由它來決定接下來要做什么。
- 函數應該放在哪個時基里? -- 這也是關鍵的地方,這要求我們要對自己的程序要有清楚的把握。以及一定的產品思維。
- 總的原則還是沒變:所有的函數都要寫得簡潔干凈,不要有任何模塊的delay()加起來超過200us!
- 一般來說,按鍵按一次不會超過50ms,放到flag10ms或者flag20ms的時間段里是沒什么關系的(注意,這時候函數里的3000表示的就不是3s了,而是30s或者60s!這是遵循乘法原則的!);
- LCD顯示之類的,控制某個LED之類的,100ms一次就行了,人眼視覺暫留之類的看不出問題的。
- 一般:檢測,通信這類的子程序都能放到flag10ms或者flag20ms里。輸出,顯示這類的放flag100ms就OK了。
- flag1ms里面應該放什么呢?我的意見是盡量什么也別放,空著都OK,因為其實沒什么東西需要刷新得這么快的。
- 如果有特別需要關照的部分,比如說步進電機的驅動啥的,請放到另一個定時器中斷里(單片機有倆定時器的,不用白不用),按你需要的來設置。
- 定時器的中斷觸發時間建議不要低于0.5ms,不然進中斷就太頻繁了,誰也不會希望自己正看著小電影的時候,爸媽過來拍你的房門吧?
- —— 2019年6月10日更新 ——
- Part2.將代碼模塊化,降低耦合度
- - 降低代碼間的耦合度 -
- 通俗點說,耦合度就是表示兩個東西“你中有我,我中有你”的程度,代碼間的耦合度越高,你修改起來就越費勁,有時候看到代碼黏糊糊的像一大坨蜜糖,難免會讓人不知從何處下口。
- 還是用個例子來說明一下吧——《電風扇系統設計1》
- 假設我們要寫個電風扇的控制程序 —— 按鍵方面:風扇有開關鍵,強風弱風切換,擺頭控制3個按鍵(注意,這里的按鍵是那種大開大合的開關按鍵,不是那種輕觸按鍵); 顯示方面:每個按鍵旁邊各有1個小led燈,共3個燈; 輸出方面:有個控制風扇擺頭的負載,有個控制風扇轉不轉的負載,有個控制風扇轉得快或者慢的負載。
- 或許有人三下兩下就畫了個這樣的流程圖(一般風扇沒開的時候是不能擺頭的,這里就先不管了):
-
51hei2.jpg (41.99 KB, 下載次數: 193)
下載附件
2024-4-1 20:31 上傳
- 然后就隨手寫出了這樣的代碼:
-
51hei3.jpg (19.83 KB, 下載次數: 193)
下載附件
2024-4-1 20:33 上傳
- 老實說,這沒啥問題,因為我們的題目要求是那么的簡單,簡單的要求那就簡單的實現吧。雖然這里的按鍵、顯示、負載的程序代碼像蜜糖一樣黏糊糊的,但是代碼也才十幾行,誰會在意呢?也大致畫一下如上代碼的結構吧:
-
51hei4.jpg (8.39 KB, 下載次數: 177)
下載附件
2024-4-1 20:33 上傳
- 但是,別忘了2點!①是客戶的需求隨時都會改動;②是這么簡單的項目還用不到你做。所以,我們稍微,加點難度上來看看?
- 《電風扇系統設計2》
- 在《風扇1》的基礎上,①風扇沒開時不能擺頭;②風扇沒開時,為了與沒電作區分,要讓3個LED燈同時以1s為周期閃爍;③開風扇后若是5小時內按鍵都沒有變化過,則自動關掉風扇(避免久開,當然此后LED也要閃爍),若想重開風扇必須把“風扇開關按鍵”先關掉再打開才行。
- 說句實話,先前的那份代碼也到此為止了,如果有人還想在原來的代碼上改出滿足現在的需求的代碼,我只能祝他好運。雖然也不是不能改,但是若是想用你那delay500ms來滿足我的閃爍要求;或者硬是嵌入一些繞來繞去的東西在里面,那肯定沒有從頭再來要省事。
- 那么——現在該怎么做呢?
- 首先,把粘連的模塊分開,按鍵歸按鍵,顯示歸顯示,輸出歸輸出。就像這樣子:
51hei5.jpg (9.87 KB, 下載次數: 211)
下載附件
2024-4-1 20:35 上傳
- 這樣的話,大家各做各的事情,互不干擾,需要共享的信息則通過關鍵參數來傳遞。這里我們設置了3個全局變量:key_on_flag, key_strong_flag, key_sheak_flag; 在keyPress()函數里可以修改這3個變量的值,然后在display()和output()函數里查詢這3個變量的值來控制顯示和輸出。這時候的代碼結構就是這種樣子的了——各個模塊分開,用關鍵參數傳遞信息。
51hei6.jpg (12.31 KB, 下載次數: 192)
下載附件
2024-4-1 20:35 上傳
- 所謂的“模塊化,降低耦合度”大致就是這種感覺,現在你需要在哪里改動就單獨改動哪個部分的,不至于瞻前顧后、束手束腳了。
- 還沒完呢,第二步,把我們的“時基”弄進來(不然上一堂課就白講了),如圖所示:
51hei7.jpg (20.54 KB, 下載次數: 194)
下載附件
2024-4-1 20:35 上傳
- 如此一來,關于那些指定時間的、指定頻率的要求,我們也能輕松的應對了。比如這時候的display()函數,可以這樣寫(這個其實還有缺陷,不過先湊合一下)——
-
51hei8.jpg (12.07 KB, 下載次數: 198)
下載附件
2024-4-1 20:37 上傳
- 相信通過這樣一個例子,大家應該能看到模塊化對你的軟件系統有多大的改善了。模塊間的耦合度降低之后,自有一種九陽神功中的“他強由他強,清風拂山崗。它弱由他弱,明月照大江。”的感覺。你那邊愛怎么變就怎么變,我這邊有必要就改動,沒必要就不改動,將彼此間的影響減到最低。
- 時間有限,模塊化的解說先到這里,后續有時間會將狀態機等知識盡可能的講解給大家。同時還得把這個需求變更后的《電風扇系統設計2》的軟件給寫完。
- ——2019年6月11日 ,今天沒空更貼,暫時先優化一下排版——
- ——2019年6月14日,修復了教程中的keyPress()和display()函數中的兩個靜態變量忘了加static關鍵字的bug——
- ——2019年6月16日,更新,狀態機上篇 ——
- part3.使用狀態機,幫助你管理系統狀態(上篇)
- (本章較為復雜,需要讀者有較好的數電和C語言功底,方能融匯貫通)
- - 狀態機入門(有基礎的可以跳過本小節) -
- 直白的說,狀態機就是若干個“當前狀態 + 觸發條件 = 新狀態( + 附加動作)”的公式。
- 狀態機可以畫成圖的形式(《狀態遷移圖》),也可以做成表格的形式(《狀態分析表》),如果大家還有數字電子技術的功底的話,應該對下圖還不至于太陌生。
-
51hei9.jpg (40.4 KB, 下載次數: 204)
下載附件
2024-4-1 20:37 上傳
- ↑此為某狀態遷移圖舉例(與下表同義)
51hei10.jpg (15.74 KB, 下載次數: 199)
下載附件
2024-4-1 20:37 上傳
- ↑此為某狀態分析表舉例(與上圖同義)
- 如果上述圖表使用公式來表達的話那就是4個公式(取決于圖的箭頭數,或者表的跳轉數),用公式的話,公式的數量不但多,看起來也很麻煩,所以我們一般用《圖》或者《表》來描述你的狀態機。
- 注意,一般的狀態機是包含“動作”的,這里為了教學方便,略過了“動作”(后面會加回來的)。
- 狀態機的使用有助于我們更直接,更便捷的管理我們的系統工作。尤其是在系統比較復雜的時候。
- - 用程序來表現狀態機 -
- 狀態用枚舉量為佳,因為我們應當保證系統的狀態處于可控范圍內,枚舉量是我們工程師自定義的一個量,可以使系統處處受我們控制。
- 觸發條件用bit變量即可,觸發了就是1,沒觸發就是0;當然char之類也可以,但沒必要。
- 有基礎的同學可以使用"位結構體"來優化這些觸發條件的內存,這里不提,請自行百度。
- ok,那現在可以先定義我們的狀態機變量了(以上述圖表為例)。
- typedef enum{
- STATE1,
- STATE2,
- STATE3
- }ENUM_STATE; //定義ENUM_STATE枚舉類型,表狀態
- ENUM_STATE system_state = STATE1; //定義上述枚舉類型的枚舉變量system_state, 初始化為STATE1
- bit test_flag_a, test_flag_b, test_flag_c; //定義3個觸發條件的bit變量。
復制代碼
- 那么,狀態機的程序要怎么寫呢?其實我們觀察《狀態分析表》的時候,有人會喜歡根據當前狀態,分析觸發條件,來決定下一刻的狀態;有人會喜歡從觸發條件開始,看看現在的狀態是否受這種觸發條件影響,而進入新的狀態。
- ——好吧,有兩種觀察方法,就有2種寫法,讀者可以自由選擇自己喜歡的寫法。
- 寫法1(根據當前狀態,看觸發條件是否有效):
- void systemStateCtrl(){
- switch(system_state){
- case STATE1:
- if(test_flag_b) system_state = STATE2;
- break;
- case STATE2:
- if(test_flag_a) system_state = STATE1;
- else if(test_flag_c) system_state = STATE3;//條件a比條件c優先
- break;
- case STATE3:
- if(test_flag_a) system_state = STATE1;
- break;
- default:
- break;
- }
- }
復制代碼
- 寫法2(根據觸發條件,看當前狀態是否需要改變):
- void systemStateCtrl(){
- if(test_flag_a){
- if(system_state==STATE2 || system_state==STATE3)
- system_state = STATE1;
- }
- else if(test_flag_b){
- if(system_state==STATE1)
- system_state = STATE2;
- }
- else if(test_flag_c){
- if(system_state==STATE2)
- system_state = STATE3;
- }
- else{
- ;
- }
- }
復制代碼
- 這2種寫法各有特點,并沒有優劣之分,各位可以自取所需。非要比較的話,我個人認為:第一種寫法"好讀一些",第二種寫法"好寫一些"。
- - 根據狀態的值,控制系統的工作流 -
- “系統的工作”很好理解,就是系統現在在干什么的意思,那么“系統的工作流”是什么意思?
- ——系統的工作流,表示系統在某段時間內的工作流程。
- ——為什么要普及“工作流”這個東西呢?
- ——因為,很多情況下,某個狀態的工作不是寫死的,而是可變的。比如:冰箱在制冷時,制冷器并不是一直開著的,而是一段時間開,一段時間關;洗衣機在洗衣服時,滾筒不是一直開著的,而是先等注水完成之后,正著轉一段時間,然后反著轉一段時間,然后又正著轉……就是有一系列的工作步驟,這些工作的步驟其實就是我們所說的工作流。
- 如果你非要把洗衣機在洗衣服時,滾筒正著轉和反著轉分成2種新的狀態的話……一路走好。
- 總之,為了給狀態下的動作有一定的預留空間(因為天知道,需求會不會發生變化),我們需要給每個狀態都做一套關于此狀態下的動作的設計。
- 這個程序寫起來也很簡單,嵌入位置上,直接塞進狀態機的屁股后面就行。如下:
- /* 狀態機程序 */
- void systemStateCtrl(){
- //你的狀態機程序
- systemStateWork();//把狀態工作程序放這里
- }
- /* 狀態工作程序 */
- void systemStateWork(){ //設計你各個狀態下的工作
- switch(system_state){
- case STATE1:
- do_sth1();break;
- case STATE2:
- do_sth2();break;
- case STATE3:
- do_sth3();break;
- default:
- break;
- }
- }
復制代碼
- - 例程:《電風扇系統設計2》的狀態機初版 -
- 狀態機真的是一個很龐大的知識點啊,好不容易把理論說完了,接下來諸位看看我的實例吧。
- 這個系統設計的需求我就不再重復了,各位往回看一看就能找到,關鍵在于,需求③我們還未處理【③開風扇后若是5小時內按鍵都沒有變化過,則自動關掉風扇(避免久開,當然此后LED也要閃爍),若想重開風扇必須把“風扇開關按鍵”先關掉再打開才行。】。
- 那么開始吧,我們在一開始,會將系統的狀態分成“風扇開”和“風扇關”兩種,直接由風扇的開機鍵控制切換。但是,多了新的需求之后,開機鍵就不好使了——因為有種“風扇關”的狀態,這時候的開機鍵也是按下的!經過一番思索,為了和真正的“風扇關”作區別,我們可以再創造一種新的狀態——“停機”!也就是開機太久了,需要停機休息。停機時候的搖頭鍵,強弱風鍵都無效,只有開機鍵松開,才能讓你退出“停機”,進入"關機"。
- 根據我們的分析,可以畫出系統的狀態遷移圖:
-
51hei11.jpg (40.93 KB, 下載次數: 192)
下載附件
2024-4-1 20:39 上傳
- 同樣的,狀態分析表。
-
51hei12.jpg (17.13 KB, 下載次數: 195)
下載附件
2024-4-1 20:39 上傳
- 相關程序如下
typedef enum{
STATE_OFF,
STATE_ON,
STATE_STOP
}ENUM_STATE; //定義ENUM_STATE枚舉類型
ENUM_STATE system_state = STATE_OFF; //定義枚舉變量system_state, 初始化為STATE_OFF
bit key_on_flag, key_off_flag, work_too_long_flag; //定義3個觸發條件的bit變量(其實用2個就行)
void systemStateCtrl(){
if(key_on_flag){
if(system_state==STATE_ON || system_state==STATE_STOP)
system_state = STATE_OFF;
}
else if(key_off_flag){
if(system_state==STATE_OFF)
system_state = STATE_ON;
}
else if(work_too_long_flag){
if(system_state==STATE_ON)
system_state = STATE_STOP;
}
else{
;
}
systemStateWork();//把狀態工作程序放這里
}
void systemStateWork(){ //設計你各個狀態下的工作
switch( system_state ){
case STATE_OFF:
do_sth1(); //關機時的工作
break;
case STATE_ON:
do_sth2(); //開機時的工作
break;
case STATE_STOP:
do_sth3(); //超時停機時的工作
break;
default:
break;
}
}
- 現在,我們從《電風扇系統設計2》的需求出發,先解析系統的狀態;再作圖表(其實圖和表做一個就行,這里為了解讀需要,都做了出來),將狀態機的大體結構都描述出來;這時候寫各個狀態下的工作程序,就是水到渠成的事情了。
- - 狀態機小結 -
- 狀態機確實是個比較長的教程,所以我將之分成了2部分來解說,第一部分只強調“狀態+條件=新狀態”這一部分,目的是讓大家先對狀態機有個初步的認知;第二部分再來描述“狀態+條件=新狀態+動作”的完整的狀態機。
- 眼尖的貼友應該也發現了,這里為了解說方便,還沒有將“強弱風”,“是否擺頭”等動作做進狀態機里。不過他們其實只是一些“動作”而已,在開始分析系統的狀態時,我們要分清楚系統的“主要動作”和“次要動作”,對于電風扇而言,開和關是主要的動作,擺頭和強弱風只是一些次要的附加動作罷了。
- 關于如何劃分系統的主要狀態?
- ——這也是一個值得拿來長篇大論的話題,但我這里不想深入探討,也無力深入探討。因為“將指定物品分類”,或者“請給蘋果,梨,水果,蔬菜,白菜,豆腐,白板筆這幾個詞進行排序”這類問題本就有無數解法,是很看重解答者的主觀意識的。
- 熟練掌握狀態機的用法,可以使你能夠更準確的把握系統的狀態。
- 本章未完,此外,由于我的個人原因,狀態機的下篇只能保證在今年7月15日前更新,還請各位諒解。屆時會將本教程劃上一個完美的句號,同時各種源碼等也會打包上傳。
|
評分
-
查看全部評分
|