單片機 嵌入式

由淺入深說單片機:看大神們總結的經驗

2019-06-27
299次瀏覽

單片機該怎么學


你會運用單片機嗎?我想你一定學過,但不一定會運用。因為學習單片機比學習其他學科需要付出更多的努力和代價,不僅要學習理論知識還要練習實際操作,而且主要是在實際操作中才能真正學到單片機技術。此外,學習單片機還需要投入一定的學習成本,隨著你學習知識的擴展成本還會增加。


學習單片機的動機不外乎有四種:一是為興趣愛好而學,二是為專業而學;三是為飯碗而學;四是在工作中被逼而學。不管是哪種動機,因主修專業的不同以及電子基礎的深淺不同,對于不同的人可能采用不同的學習方法,根據筆者的親身學習經驗和教授徒弟學習的感受,提出筆者的學習方法和步驟。


第一步:基礎理論知識學習


基礎理論知識包括模擬電路、數字電路和C語言知識。模擬電路和數字電路屬于抽象學科,要把它學好還得費點精神。在你學習單片機之前,覺得模擬電路和數字電路基礎不好的話,不要急著學習單片機,應該先回顧所學過的模擬電路和數字電路知識,為學習單片機加強基礎。


否則,你的單片機學習之路不僅會很艱難和漫長,還可能半途而廢。筆者始終認為,扎實的電子技術基礎是學好單片機的關鍵,直接影響單片機學習入門的快慢。


有些同學覺得單片機很難,越學越復雜,最后學不下去了。有的同學看書時似乎明白了,可是動起手來卻一塌糊涂,究其原因就是電子技術基礎沒有打好,首先被表面知識給困惑了。


單片機屬于數字電路,其概念、術語、硬件結構和原理都源自數字電路,如果數字電路基礎扎實,對復雜的單片機硬件結構和原理就能容易理解,就能輕松地邁開學習的第一步,自信心也會樹立起來。


相反,基礎不好,這個看不懂那個也弄不明白,越學問題越多,越學越沒有信心。如果你覺得單片機很難,那就應該先放下單片機教材,去重溫數字電路,搞清楚觸發器、寄存器、門電路、COMS電路、時序邏輯和時序圖、進制轉換等理論知識。理解了這些知識之后再去看看單片機的結構和原理,我想你會大徹大悟,信心倍增。


模擬電路是電子技術最基礎的學科,她讓你知道什么是電阻、電容、電感、二極管、三極管、場效應管、放大器等等以及它們的工作原理和在電路中的作用,這是學習電子技術必須掌握的基礎知識。


一般是先學習模擬電路再去學習數字電路。扎實的模擬電路基礎不僅讓你容易看懂別人設計的電路,而且讓你的設計的電路更可靠,提高產品質量。


C語言知識并不難,沒有任何編程基礎的人都可以學,在我看來,初中生、高中生、中專生、大學生都能學會。當然,數學基礎好、邏輯思維好的人學起來相對輕松一些。


C語言需要掌握的知識就那么3個條件判斷語句、3個循環語句、3個跳轉語句和1個開關語句。別小看這10個語句,用他們組合形成的邏輯要多復雜有多復雜。學習時要一條語句一條語句的學,學一條活用一條,全部學過用過這些關鍵語句后,相信你的C基礎建立了。


當基礎打好以后,你會感覺到單片機不再難學了,而且越學越起勁。當單片機乖乖的依照你的邏輯思維和算法去執行指令,實現預期控制效果的時候,成就感會讓你信心十足、夜以續日、廢寢忘食的投入到單片機的世界里。可以這么說,扎實的電子技術基礎和C語言基礎能增強學習單片機信心,較快掌握單片機技術。


第二步:單片機實踐


這是真正學習單片機的過程,既讓人興奮又讓人疲憊,既讓人無奈又讓人不服,既讓人孤獨又讓人充實,既讓人氣憤又讓人欣慰,既有失落感又有成就感。


其中的酸甜苦辣只有學過的人深有體會。思想上要有刻苦學習的決心,硬件上要有一套完整的學習開發工具,軟件上要注重理論和實踐相結合。


1.有刻苦學習的決心


首先,明確學習目的。先認真回答兩個問題:我學單片機來做什么?需要多長時間把它學會?這是你學單片機的動力。沒有動力,我想你堅持不了多久。


其次,端正學習心態。單片機學習過程是枯燥乏味、孤獨寂寞的過程。要知道,學習知識沒有捷徑,只有循序漸進,腳踏實地,一步一個腳印,才能學到真功夫。


再次,要多動腦勤動手。單片機的學習具有很強的實踐性,是一門很注重實際動手操作的技術學科。不動手實踐你是學不會單片機的。最后,虛心交流。在單片機學習過程中每個人都會遇到無數不能解決的問題,需要你向有經驗的過來人虛心求教,否則,一味的自己埋頭摸索會走許多彎路,浪費很多時間。


2.有一套完整的學習開發工具


學習單片機是需要成本的。必須有一臺電腦、一塊單片機開發板(如果開發板不能直接下載程序代碼的話還得需要一個編程器)、一套視頻教程、一本單片機教材和一本C語言教材。


電腦是用來編寫和編譯程序,并將程序代碼下載到單片機上;開發板用來運行單片機程序,驗證實際效果;視頻教程就是手把手教你單片機開發環境的使用、單片機編程和調試。


對于單片機初學者來說,視頻教程必須看,要不然,哪怕把教材看了幾遍,還是不知道如何下手,尤其是院校里的單片機教材,學了之后,面對真正的單片機時可能還是束手無策;單片機教材和C語言教材是理論學習資料,備忘備查。不要為了節約成本不用開發板而光用Protur軟件仿真調試,這和紙上談兵沒什么區別。


3. 要注重理論和實踐相結合


單片機C語言編程理論知識并不深奧,光看書不動手也能明白。但在實際編程的時候就沒那么簡單了。一個程序的形成不僅需要有C語言知識,更多需要融入你個人的編程思路和算法。


編程思路和算法決定一個程序的優劣,是單片機編程的大問題,只有在實際動手編寫的時候才會有深切的感悟。一個程序能否按照你的意愿正常運行就要看你的思路和算法是否正確、合理。


如果程序不正常則要反復調試(檢查、修改思路和算法),直到成功。這個過程耗時、費腦、疲精神,意志不堅強者往往被絆倒在這里半途而廢。


學習編寫程序應該按照以下過程學習,效果會更好。看到例程題目先試著構思自己的編程思路,然后再看教材或視頻教程里的代碼,研究人家的編程思路,注意與自己思路的差異;接下來就照搬人家的思路親自動手編寫這個程序,領會其中每一條語句的作用;對有疑問的地方試著按照自己的思路修改程序,比較程序運行效果,領會其中的奧妙。


每一個例程都堅持按照這個過程學習,你很快會找到編程的感覺,取其精華去其糟粕,久而久之會形成你獨特的編程思想。當然,剛開始,看別人的程序源代碼就像看天書一樣,只要硬著頭皮看,看到不懂的關鍵字和語句就翻書查閱、對照。只要能堅持下來,學習收獲會事半功倍。


在實踐過程中不僅要學會別人的例程,還要在別人的程序上改進和拓展,讓程序產生更強大的功能。同時,還要懂得通過查閱芯片數據手冊(DATASHEET)里有關芯片命令和數據的讀寫時序來核對別人例程的可靠性,如果你覺得例程不可靠就把它修改過來,成為是你自己的程序。


不僅如此,自己應該經常找些項目來做,以鞏固所學的知識和積累更多的經驗。


第三步:單片機硬件設計


當編寫自己的程序信手拈來、閱讀別人的程序能夠發現問題的時候,說明你的單片機編程水平相當不錯了。接下來就應該研究的硬件了。硬件設計包括電路原理設計和PCB板設計。學習做硬件要比學習做軟件麻煩,成本更高,周期更長。


但是,學習單片機的最終目的是做產品開發----軟件和硬件相結合形成完整的控制系統。所以,做硬件也是學習單片機技術的一個必學內容。


電路原理設計涉及到各種芯片的應用,而這些芯片外圍電路的設計、典型應用電路和與單片機的連接等在芯片數據手冊(DATASHEET)都能找到答案,前提是要看得懂全英文的數據手冊。


否則,照搬別人的設計永遠落在別人的后面,你做的產品就沒有創意。電子技術領域的第一手資料(DATASHEET)都是英文,從第一手資料里你所獲得的知識可能是在教科書、網絡文檔和課外讀物等所沒有的知識。


雖然有些資料也都是在DATASHEET的基礎上撰寫的,但內容不全面,甚至存在翻譯上的遺漏和錯誤。當然,閱讀DATASHEET需要具備一定的英文閱讀能力,這也是阻礙單片機學習者晉級的絆腳石。良好的英文閱讀能力能讓你在單片機技術知識的海洋里自由遨游。


做PCB板就比較簡單了。只要懂得使用Protel軟件或 AltimDesigner軟件就沒問題了。但要想做的板子布局美觀、布線合理還得費一番功夫了。


嫻熟的單片機C語言編程、會使用Protel軟件或 AltimDesigner軟件設計PCB板和具備一定的英文閱讀能力,你就是一個遇強則強的單片機高手了。


單片機經驗分享


經驗之一:用“軟件陷阱+程序口令”對付PC指針的彈飛

當CPU受到外界干擾,有時PC指針會飛到另一段程序中,或跳到空白段去。其實,如果PC指針飛到空白段去,倒也好處理。只要在空白段設立軟件陷阱(攔截指令),將程序攔截到初始化段或程序錯誤處理段。但是,如果PC指針飛到另一段程序中去了,系統如何辦?小匠在這里推薦一種方法——程序口令,思路如下:

1、首先,程序必須模塊化。每個模塊(子程序)執行一個功能。每個模塊只有一個出口(RET)。

2、設立一個模塊(子程序)ID寄存器。

3、為每個子程序配置一個唯一的ID號碼。

4、每當子程序執行完畢,要返回(RET)之前,

先將本子程序的ID號送入 ID寄存器。

5、返回到上級程序后,先判斷ID寄存器中的ID號。

如果正確,則繼續執行;如果不正確,則表示PC指針有可能已經跳錯了,子程序沒有按預計的出口返回,這時將程序攔截到初始化段或程序錯誤處理段。

這種方法,如同在程序中設立了若干個崗哨,每次調用子程序返回后,都要對口令(ID號),驗明正身后再放行。再配合軟件陷阱,基本上可以將大多數PC指針彈飛的現象檢測到。到了程序錯誤處理段,要殺要剮(冷啟動還是熱啟動)就由您了。

僅以一條代碼來揭示程序飛跑的本質!750102H ;MOV 01H,#02H ,如當前PC不是指向75H,而是指向01H或02H,那么51內的指令譯碼器將把她們忠實地翻譯成AJMP X01H 或 LJMP XXXXH 而XX01H XXXXH又是什么呢?天知道!這樣惡性飛跑下去那還不死定!改革一下:

CLR A ;0C4H

INC A ;04H

MOV R1,A ;0F9H

INC A ;04H

MOV @R1,A ;86H

每一字節代碼都不能在生成跳轉和循環,且都是單字節指令!往那跑去?跑出去了都要自己回來!“在家”千日好!“跳出”事事難嘛!這樣只要平時習慣了用累加器和寄存器把數倒一倒,把那些危險代碼都給倒掉,這樣雖說給PC的“足”上多加了兩字節的“包”可它不好“跑”啊!“足包”====跑!有朋友會問:要是PC抓做02H--LJMP 又有抓做了老鼻子遠的XXH,再抓做隔壁的YYH不就沒用了嗎?提這樣的問題只有ZENYIN這種鉆牛角得才會提!PC那一位最活躍啊?PC0啊!要“扯拐”顯然發生在她身上,至于那PC15同志啊,睡得更死豬一樣,雷爆(強干擾)來了都打不醒?此外如果干擾都強到了PC高位都出錯的地步!關電!關電!不干了!“不是我們不行而是敵人太強大”!反過來要是敵人在你的專政下,只是偶爾出來搗搗亂,但一出來就沖到屁西(PC)高層,就要問問是不是你的王國根基(硬件)有問題了?而非出在意識形態(軟件)上!硬件為本!軟件為標!標本兼治鑄就堅強體魄,方能百毒不侵!

經驗之二、不要輕信軟件狗

關于軟件狗的討論,論壇上多矣。匠人也曾經查閱過許多關于軟件狗的文章。有些大師確實提出了一些比較有技巧性的方法。但是,匠人的忠告是:不要輕信軟件狗!其實,軟件狗相當于軟件的一種自律行為。一般的思路都是通過設立一個計數器,在計時中斷中對其+1,在主程序的適當地方對其清零。如果程序失控了,清零指令未被執行,但中斷造常發生,則計數器溢出(狗狗叫了)。但是這里有個問題:萬一干擾導致中斷被屏蔽了,那軟件狗就永遠不會叫了!——針對這種可能,有人提出在主程序中反復刷新中斷使能標志,保證不讓中斷被屏蔽。——但萬一程序飛到某個死循環中去了,不再執行“刷新中斷使能標志”這一功能了,還是有可能把狗狗活活餓死。

所以,匠人的觀點是:看門狗必須擁有獨立的計數器。(即硬件看門狗)好在現在好多芯片都提供了內部WDT。這種狗都是自帶計數器的。即使干擾導致程序失控,WDT還是會造常計數直到溢出。當然,匠人也沒有要將軟件狗一棍子全部打死的意思。畢竟不管是軟狗還是硬狗,逮到耗子就是好狗嘛(狗拿耗子——多管閑事?)。如果哪位訓狗專家確實養過一條能看門的好軟件狗,請牽出來讓大伙瞧瞧。

經驗之三、話說RAM冗余技術

所謂的RAM冗余,就是:

1、將重要的數據信息備份2份(或以上)并存放在RAM中不同的區域(指地址不相連)。

2、當平時對這些數據進行修改時,同時也更新備份。

3、當干擾發生并被攔截到“程序錯誤處理段”中時,

將數據與備份做比較,采用表決方式(少數服從多數)選出正確(或可能正確?)的那個。

4、備份越多,效果越好。(當然,你得有足夠的存儲空間)。

5、只備份最最原始的數據。中間變量(指那些可以從原始數據重新推導出來的數據)不必備份,

注:

1、這種思路的理論依據,據說是源于一種“概率論”,即一個人被老婆打腫臉的概率是很大的,但如果他捂著臉去上班卻發現全公司每個已婚男人的臉都青了,這種概率是很小的。同理,一個RAM寄存器數據被沖毀的概率是很大的,但地址不相連的多個RAM同時被沖毀的概率是很小的。

2、前兩年,小匠學徒時,用過一次這種方法,但效果不太理想。當時感覺可能是概率論在我這失效了?現在回想起來,可能是備份的時機選的不好。結果將已經沖毀的數據又備份進去了。這樣以來,恢復出來的數據自然也就不對了。

經驗之四、話說指令冗余技術

前面有個朋友問到指令冗余,按匠人的理解,指令冗余,就是動作冗余。舉個例子,你要在某個輸出口上輸出一個高電平去驅動一個外部器件,你如果只送一次“1”,那么,當干擾來臨時,這個“1”就有可能變成“0”了。正確的處理方式是,你定期刷新這個“1”。那么,即使偶然受了干擾,它也能恢復回來。除了I/O口動作的冗余,匠人強烈建議大家在下面各方面也采用這種方法:

1、LCD的顯示。有時,也許你會用一些LCD的專用驅動芯片(如HT1621),這種芯片有個好處,即你只要將顯示數據傳送給它,它就會不斷的自動掃描LCD。但是,你千萬不要以為這樣就沒你啥事了。正確的處理方式是,要記得定期刷新送顯數據(即使顯示內容沒有改變)。對于CPU中自帶LCD DRIVER 的,也要定期刷新LCD RAM。

2、中斷使能標志的設置。不要以為你在程序初始化段將中斷設置好就OK了。應該在主程序中適當的地方定期刷新一下,以免你的中斷被掛起來。

3、其它一些標志字和參數寄存器(包括你自己定義的),也要記得常常刷新。

4、其它一些你認為有必要反復刷新的地方。

經驗之五、10種軟件濾波方法

下面奉獻——匠人嘔心瀝血搜腸刮肚冥思苦想東拼西湊整理出來的10種軟件濾波方法:

1、限幅濾波法(又稱程序判斷濾波法)

A、方法:根據經驗判斷,確定兩次采樣允許的最大偏差值(設為A),每次檢測到新值時判斷:如果本次值與上次值之差<=A,則本次值有效。如果本次值與上次值之差>A,則本次值無效,放棄本次值,用上次值代替本次值

B、優點:能有效克服因偶然因素引起的脈沖干擾。

C、缺點:無法抑制那種周期性的干擾,平滑度差。

2、中位值濾波法

A、方法:連續采樣N次(N取奇數),把N次采樣值按大小排列,取中間值為本次有效值。

B、優點:能有效克服因偶然因素引起的波動干擾,對溫度、液

位的變化緩慢的被測參數有良好的濾波效果。

C、缺點:對流量、速度等快速變化的參數不宜。

3、算術平均濾波法

A、方法:連續取N個采樣值進行算術平均運算。N值較大時:信號平滑度較高,但靈敏度較低;N值較小時:信號平滑度較低,但靈敏度較高。N值的選取:一般流量,N=12;壓力:N=4

B、優點:適用于對一般具有隨機干擾的信號進行濾波,這樣信號的特點是有一個平均值,信號在某一數值范圍附近上下波動。

C、缺點:對于測量速度較慢或要求數據計算速度較快的實時控制不適用,比較浪費RAM。

4、遞推平均濾波法(又稱滑動平均濾波法)

A、方法:把連續取N個采樣值看成一個隊列,隊列的長度固定為N,每次采樣到一個新數據放入隊尾,并扔掉原來隊首的一次數據.(先進先出原則),把隊列中的N個數據進行算術平均運算,就可獲得新的濾波結果。N值的選取:流量,N=12;壓力:N=4;液面,N=4~12;溫度,N=1~4

B、優點:對周期性干擾有良好的抑制作用,平滑度高,適用于高頻振蕩的系統。

C、缺點:靈敏度低,對偶然出現的脈沖性干擾的抑制作用較差,不易消除由于脈沖干擾所引起的采樣值偏差,不適用于脈沖干擾比較嚴重的場合,比較浪費RAM

5、中位值平均濾波法(又稱防脈沖干擾平均濾波法)

A、方法:相當于“中位值濾波法”+“算術平均濾波法”。連續采樣N個數據,去掉一個最大值和一個最小值,然后計算N-2個數據的算術平均值。N值的選取:3~14

B、優點:融合了兩種濾波法的優點,對于偶然出現的脈沖性干擾,可消除由于脈沖干擾所引起的采樣值偏差。

C、缺點:測量速度較慢,和算術平均濾波法一樣,比較浪費RAM。

6、限幅平均濾波法

A、方法:相當于“限幅濾波法”+“遞推平均濾波法”,每次采樣到的新數據先進行限幅處理,再送入隊列進行遞推平均濾波處理 。

B、優點:融合了兩種濾波法的優點,對于偶然出現的脈沖性干擾,可消除由于脈沖干擾所引起的采樣值偏差。

C、缺點:比較浪費RAM。

7、一階滯后濾波法

A、方法:取a=0~1,本次濾波結

果=(1-a)*本次采樣值+a*上次濾波結果。

B、優點:對周期性干擾具有良好的抑制作用,適用于波動頻率較高的場合。

C、缺點: 相位滯后,靈敏度低,滯后程度取決于a值大小,不能消除濾波頻率高于采樣頻率的1/2的干擾信號。

8、加權遞推平均濾波法

A、方法:是對遞推平均濾波法的改進,即不同時刻的數據加以不同的權。通常是,越接近現時刻的數據,權取得越大。給予新采樣值的權系數越大,則靈敏度越高,但信號平滑度越低。

B、優點:適用于有較大純滯后時間常數的對象和采樣周期較短的系統。

C、缺點:對于純滯后時間常數較小,采樣周期較長,變化緩慢的信號不能迅速反應系統當前所受干擾的嚴重程度,濾波效果差。

9、消抖濾波法

A、方法:設置一個濾波計數器將每次采樣值與當前有效值比較:如果采樣值=當前有效值,則計數器清零如果采樣值<>當前有效值,則計數器+1,并判斷計數器是否>=上限N(溢出),如果計數器溢出,則將本次值替換當前有效值,并清計數器。

B、優點:對于變化緩慢的被測參數有較好的濾波效果,可避免在臨界值附近控制器的反復開/關跳動或顯示器上數值抖動。

C、缺點:對于快速變化的參數不宜,如果在計數器溢出的那一次采樣到的值恰好是干擾值,則會將干擾值當作有效值導入系統。

10、限幅消抖濾波法

A、方法:相當于“限幅濾波法”+“消抖濾波法” 先限幅,后消抖。

B、優點: 繼承了“限幅”和“消抖”的優點改進了“消抖濾波法”中的某些缺陷,避免將干擾值導入系統。

C、缺點:對于快速變化的參數不宜。

IIR 數字濾波器

A. 方法:確定信號帶寬, 濾之。 Y(n) = a1*Y(n-1) + a2*Y(n-2) + . + ak*Y(n-k) + b0*X(n) + b1*X(n-1) + b2*X(n-2) + . + bk*X(n-k)。

B. 優點:高通,低通,帶通,帶阻任意。設計簡單(用matlab)

C. 缺點:運算量大。



對于單片機程序來說,大家都不陌生,但是真正使用架構,考慮架構的恐怕并不多,隨著程序開發的不斷增多,本人覺得架構是非常必要的。前不就發帖與大家一起討論了一下怎樣架構你的單片機程序,發現真正使用架構的并不都,而且這類書籍基本沒有。

????

資深工程師談單片機應用程序架構


本人經過摸索實驗并總結,大致應用程序的架構有三種:


1. 簡單的前后臺順序執行程序,這類寫法是大多數人使用的方法,不需用思考程序的具體架構,直接通過執行順序編寫應用程序即可。


2. 時間片輪詢法,此方法是介于順序執行與操作系統之間的一種方法。


3. 操作系統,此法應該是應用程序編寫的最高境界。


下面就分別談談這三種方法的利弊和適應范圍等。


1

順序執行法:


這種方法,這應用程序比較簡單,實時性,并行性要求不太高的情況下是不錯的方法,程序設計簡單,思路比較清晰。但是當應用程序比較復雜的時候,如果沒有一個完整的流程圖,恐怕別人很難看懂程序的運行狀態,而且隨著程序功能的增加,編寫應用程序的工程師的大腦也開始混亂。即不利于升級維護,也不利于代碼優化。本人寫個幾個比較復雜一點的應用程序,剛開始就是使用此法,最終雖然能夠實現功能,但是自己的思維一直處于混亂狀態。導致程序一直不能讓自己滿意。


這種方法大多數人都會采用,而且我們接受的教育也基本都是使用此法。對于我們這些基本沒有學習過數據結構,程序架構的單片機工程師來說,無疑很難在應用程序的設計上有一個很大的提高,也導致了不同工程師編寫的應用程序很難相互利于和學習。


本人建議,如果喜歡使用此法的網友,如果編寫比較復雜的應用程序,一定要先理清頭腦,設計好完整的流程圖再編寫程序,否則后果很嚴重。當然應該程序本身很簡單,此法還是一個非常必須的選擇。


下面就寫一個順序執行的程序模型,方面和下面兩種方法對比:


  1. /**************************************************************************************
    * FunctionName?? : main()
    * Description??? : 主函數
    * EntryParameter : None
    * ReturnValue??? : None
    **************************************************************************************/
    int main(void)?
    {?
    ??? uint8 keyValue;

  2. ??? InitSys();????????????????? // 初始化

  3. ??? while (1)
    ?? ?{
    ??????? TaskDisplayClock();
    ?????? ?keyValue = TaskKeySan();
    ??????? switch (keyValue)
    ?????? {
    ??????????? case x: TaskDispStatus(); break;
    ??????????? ...
    ??????????? default: break;
    ??????? }
    ??? }
    }


2

時間片輪詢法


時間片輪詢法,在很多書籍中有提到,而且有很多時候都是與操作系統一起出現,也就是說很多時候是操作系統中使用了這一方法。不過我們這里要說的這個時間片輪詢法并不是掛在操作系統下,而是在前后臺程序中使用此法。也是本貼要詳細說明和介紹的方法。


對于時間片輪詢法,雖然有不少書籍都有介紹,但大多說得并不系統,只是提提概念而已。下面本人將詳細介紹本人模式,并參考別人的代碼建立的一個時間片輪詢架構程序的方法,我想將給初學者有一定的借鑒性。


記得在前不久本人發帖《1個定時器多處復用的問題》,由于時間的問題,并沒有詳細說明怎樣實現1個定時器多處復用。在這里我們先介紹一下定時器的復用功能。


使用1個定時器,可以是任意的定時器,這里不做特殊說明,下面假設有3個任務,那么我們應該做如下工作:


1. 初始化定時器,這里假設定時器的定時中斷為1ms(當然你可以改成10ms,這個和操作系統一樣,中斷過于頻繁效率就低,中斷太長,實時性差)。


2. 定義一個數值:


  1. #define TASK_NUM?? (3)??????????????????//? 這里定義的任務數為3,表示有三個任務會使用此定時器定時。

  2. uint16 TaskCount[TASK_NUM]?;?????????? //? 這里為三個任務定義三個變量來存放定時值

  3. uint8? TaskMark[TASK_NUM];?????????????//? 同樣對應三個標志位,為0表示時間沒到,為1表示定時時間到。


3. 在定時器中斷服務函數中添加:


  1. /**************************************************************************************
    * FunctionName : TimerInterrupt()
    * Description : 定時中斷服務函數
    * EntryParameter : None
    * ReturnValue : None
    **************************************************************************************/
    void TimerInterrupt(void)
    {
    ??? uint8 i;

    ??? for (i=0; i<TASKS_NUM; i++)?
    ??? {
    ??????? if (TaskCount[i])?
    ??????? {
    ????????????? TaskCount[i]--;?
    ????????????? if (TaskCount[i] == 0)?
    ????????????? {
    ??????????????????? TaskMark[i] = 0x01;?
    ????????????? }
    ??????? }
    ?? }
    }


代碼解釋:定時中斷服務函數,在中斷中逐個判斷,如果定時值為0了,表示沒有使用此定時器或此定時器已經完成定時,不著處理。否則定時器減一,知道為零時,相應標志位值1,表示此任務的定時值到了。


4. 在我們的應用程序中,在需要的應用定時的地方添加如下代碼,下面就以任務1為例:


  1. TaskCount[0] = 20;???????// 延時20ms

  2. TaskMark[0]??= 0x00;???? // 啟動此任務的定時器


到此我們只需要在任務中判斷TaskMark[0]?是否為0x01即可。其他任務添加相同,至此一個定時器的復用問題就實現了。用需要的朋友可以試試,效果不錯哦。


通過上面對1個定時器的復用我們可以看出,在等待一個定時的到來的同時我們可以循環判斷標志位,同時也可以去執行其他函數。


循環判斷標志位:


那么我們可以想想,如果循環判斷標志位,是不是就和上面介紹的順序執行程序是一樣的呢?一個大循環,只是這個延時比普通的for循環精確一些,可以實現精確延時。


執行其他函數:


那么如果我們在一個函數延時的時候去執行其他函數,充分利用CPU時間,是不是和操作系統有些類似了呢?但是操作系統的任務管理和切換是非常復雜的。下面我們就將利用此方法架構一直新的應用程序。


時間片輪詢法的架構:


1.設計一個結構體:


  1. // 任務結構
    typedef struct _TASK_COMPONENTS
    {
    ??? uint8 Run;???????????????? // 程序運行標記:0-不運行,1運行
    ??? uint8 Timer; ???????????? // 計時器
    ??? uint8 ItvTime;??????????????// 任務運行間隔時間
    ??? void (*TaskHook)(void);????// 要運行的任務函數
    } TASK_COMPONENTS;???????// 任務定義


這個結構體的設計非常重要,一個用4個參數,注釋說的非常詳細,這里不在描述。


2. 任務運行標志出來,此函數就相當于中斷服務函數,需要在定時器的中斷服務函數中調用此函數,這里獨立出來,并于移植和理解。


  1. /**************************************************************************************
    * FunctionName?? : TaskRemarks()
    * Description??? : 任務標志處理
    * EntryParameter : None
    * ReturnValue??? : None
    **************************************************************************************/
    void TaskRemarks(void)
    {
    ??? uint8 i;

  2. ??? for (i=0; i<TASKS_MAX; i++)????????? // 逐個任務時間處理
    ??? {
    ??????? ?if (TaskComps[i].Timer)????????? // 時間不為0
    ??????? {
    ??????????? TaskComps[i].Timer--;???????? // 減去一個節拍
    ??????????? if (TaskComps[i].Timer == 0)?????? // 時間減完了
    ?????????? ?{
    ???????????????? TaskComps[i].Timer = TaskComps[i].ItvTime;?????? // 恢復計時器值,從新下一次
    ???????????????? TaskComps[i].Run = 1;?????????? // 任務可以運行
    ??????????? }
    ??????? }
    ?? }
    }


大家認真對比一下次函數,和上面定時復用的函數是不是一樣的呢?


3. 任務處理


  1. /**************************************************************************************
    * FunctionName?? : TaskProcess()
    * Description??? : 任務處理
    * EntryParameter : None
    * ReturnValue??? : None
    **************************************************************************************/
    void TaskProcess(void)
    {
    ??? uint8 i;

  2. ??? for (i=0; i<TASKS_MAX; i++)?????????? // 逐個任務時間處理
    ??? {
    ???????? if (TaskComps[i].Run)?????????? // 時間不為0
    ??????? {
    ???????????? TaskComps[i].TaskHook();???????? // 運行任務
    ???????????? TaskComps[i].Run = 0;????????? // 標志清0
    ??????? }
    ??? }???
    }


此函數就是判斷什么時候該執行那一個任務了,實現任務的管理操作,應用者只需要在main()函數中調用此函數就可以了,并不需要去分別調用和處理任務函數。


到此,一個時間片輪詢應用程序的架構就建好了,大家看看是不是非常簡單呢?此架構只需要兩個函數,一個結構體,為了應用方面下面將再建立一個枚舉型變量。


下面我就就說說怎樣應用吧,假設我們有三個任務:時鐘顯示,按鍵掃描,和工作狀態顯示。


1. 定義一個上面定義的那種結構體變量


  1. /**************************************************************************************
    * Variable definition????????????????????????????
    **************************************************************************************/
    static TASK_COMPONENTS TaskComps[] =?
    {
    ??? {0, 60, 60, TaskDisplayClock},??????????? // 顯示時鐘
    ??? {0, 20, 20, TaskKeySan},?????????????? // 按鍵掃描
    ??? {0, 30, 30, TaskDispStatus},??????????? // 顯示工作狀態

  2. ???? // 這里添加你的任務。。。。

  3. };

復制代碼


在定義變量時,我們已經初始化了值,這些值的初始化,非常重要,跟具體的執行時間優先級等都有關系,這個需要自己掌握。


①大概意思是,我們有三個任務,沒1s執行以下時鐘顯示,因為我們的時鐘最小單位是1s,所以在秒變化后才顯示一次就夠了。


②由于按鍵在按下時會參數抖動,而我們知道一般按鍵的抖動大概是20ms,那么我們在順序執行的函數中一般是延伸20ms,而這里我們每20ms掃描一次,是非常不錯的出來,即達到了消抖的目的,也不會漏掉按鍵輸入。


③為了能夠顯示按鍵后的其他提示和工作界面,我們這里設計每30ms顯示一次,如果你覺得反應慢了,你可以讓這些值小一點。后面的名稱是對應的函數名,你必須在應用程序中編寫這函數名稱和這三個一樣的任務。


2. 任務列表


  1. // 任務清單
    typedef enum _TASK_LIST
    {
    ??? TAST_DISP_CLOCK,??????????? // 顯示時鐘
    ??? TAST_KEY_SAN,???????????? // 按鍵掃描
    ??? TASK_DISP_WS,???????????? // 工作狀態顯示
    ?????// 這里添加你的任務。。。。
    ???? TASKS_MAX?????????????????????????????????????????? // 總的可供分配的定時任務數目
    } TASK_LIST;

復制代碼


好好看看,我們這里定義這個任務清單的目的其實就是參數TASKS_MAX的值,其他值是沒有具體的意義的,只是為了清晰的表面任務的關系而已。


3. 編寫任務函數


  1. /**************************************************************************************
    * FunctionName?? : TaskDisplayClock()
    * Description??? : 顯示任務

  2. * EntryParameter : None
    * ReturnValue??? : None
    **************************************************************************************/
    void TaskDisplayClock(void)
    {

  3. }

  4. /**************************************************************************************
    * FunctionName?? : TaskKeySan()
    * Description??? : 掃描任務
    * EntryParameter : None
    * ReturnValue??? : None
    **************************************************************************************/
    void TaskKeySan(void)
    {


  5. }

  6. /**************************************************************************************
    * FunctionName?? : TaskDispStatus()
    * Description??? : 工作狀態顯示
    * EntryParameter : None
    * ReturnValue??? : None
    **************************************************************************************/
    void TaskDispStatus(void)
    {


  7. }

  8. // 這里添加其他任務。

復制代碼


現在你就可以根據自己的需要編寫任務了。


4. 主函數


  1. /**************************************************************************************
    * FunctionName?? : main()
    * Description??? : 主函數
    * EntryParameter : None
    * ReturnValue??? : None
    **************************************************************************************/
    int main(void)?
    {?
    ??? InitSys();????????????????? // 初始化

  2. ??? while (1)
    ??? {
    ??????? TaskProcess();???????????? // 任務處理
    ??? }
    }


到此我們的時間片輪詢這個應用程序的架構就完成了,你只需要在我們提示的地方添加你自己的任務函數就可以了。是不是很簡單啊,有沒有點操作系統的感覺在里面?


不防試試把,看看任務之間是不是相互并不干擾?并行運行呢?當然重要的是,還需要,注意任務之間進行數據傳遞時,需要采用全局變量,除此之外還需要注意劃分任務以及任務的執行時間,在編寫任務時,盡量讓任務盡快執行完成。


3

操作系統:


操作系統的本身是一個比較復雜的東西,任務的管理,執行本事并不需要我們去了解。但是光是移植都是一件非常困難的是,雖然有人說過“你如果使用過系統,將不會在去使用前后臺程序”。但是真正能使用操作系統的人并不多,不僅是因為系統的使用本身很復雜,而且還需要購買許可證(ucos也不例外,如果商用的話)。


這里本人并不想過多的介紹操作系統本身,因為不是一兩句話能過說明白的,下面列出UCOS下編寫應該程序的模型。大家可以對比一下,這三種方式下的各自的優缺點。


  1. /**************************************************************************************
    * FunctionName?? : main()
    * Description??? : 主函數
    * EntryParameter : None
    * ReturnValue??? : None
    **************************************************************************************/
    int main(void)?
    {?
    ??? OSInit();??????????????? // 初始化uCOS-II

  2. ??? OSTaskCreate((void (*) (void *)) TaskStart,??????? // 任務指針
    ??????????????? (void?? *) 0,??????????? // 參數
    ??????????????? (OS_STK *) &TaskStartStk[TASK_START_STK_SIZE - 1], // 堆棧指針
    ??????????????? (INT8U?? ) TASK_START_PRIO);??????? // 任務優先級

  3. ??? OSStart();?????????????????????????????? ??????? // 啟動多任務環境
    ????????????????????????????????????????
    ??? return (0);?
    }

復制代碼


  1. /**************************************************************************************
    * FunctionName?? : TaskStart()??????????
    * Description??? : 任務創建,只創建任務,不完成其他工作
    * EntryParameter : None
    * ReturnValue??? : None
    **************************************************************************************/
    void TaskStart(void* p_arg)
    {
    ??? OS_CPU_SysTickInit();?????????????????????????????????????? // Initialize the SysTick.?

  2. #if (OS_TASK_STAT_EN > 0)
    ??? OSStatInit();?????????????????????????????????????????????? // 這東西可以測量CPU使用量?
    #endif

  3. ?OSTaskCreate((void (*) (void *)) TaskLed,?????// 任務1
    ??????????????? (void?? *) 0,?????????????? // 不帶參數
    ??????????????? (OS_STK *) &TaskLedStk[TASK_LED_STK_SIZE - 1],? // 堆棧指針
    ??????????????? (INT8U?? ) TASK_LED_PRIO);???????? // 優先級

  4. ?// Here the task of creating your
    ????????????????
    ??? while (1)
    ??? {
    ??????? OSTimeDlyHMSM(0, 0, 0, 100);
    ??? }
    }

?

不難看出,時間片輪詢法優勢還是比較大的,即由順序執行法的優點,也有操作系統的優點。結構清晰,簡單,非常容易理解。


我要點評

极速十一选五平台