本文隸屬於AVR單片機教程系列。html
在系列教程的最後一篇中,我將向你推薦3個能夠深造的方向:RTOS、C++、事件驅動。掌握這些技術能夠幫助你更快、更好地開發更大的項目。git
本文涉及到許多概念性的內容,若是你有不一樣意見,歡迎討論。編程
這一篇教程叫做「走向高層」。什麼是高層?函數
我認爲,若是寥寥幾行代碼就能實現一個複雜功能,或者一行代碼能夠對應到幾百句彙編,那麼你就站在高層。高層與底層是相對的概念,沒有絕對的界限。工具
站得高,看得遠,這一樣適用於編程,咱們要走向高層。高層是對底層的封裝,是對現實的抽象,高層相比於底層更加貼近應用。站在高層,你能夠看到不少底層看不到的東西,主要有編程工具和思路。合理利用工具,能夠簡化代碼,下降工做量;用合適的思路編程,更能夠事半功倍。學習
可是,掌握高層並不意味着忽視甚至鄙視底層,高層創建在底層基礎之上。其一,有些高層出現的詭異現象能夠追溯到底層,這樣的debug任務只有通曉底層與高層的開發者才能勝任;其二,爲了讓高層實現複雜功能的同時得到可接受的運行效率,底層必須設計地更加精緻,這就對底層提出了更高的要求。測試
相信你通過一期和二期的教程,已經至關熟悉AVR編程的底層了。跟我一塊兒走上高層吧!ui
實時操做系統(RTOS)是一類操做系統。帶有操做系統的計算機系統相比不帶有的,最顯著的特色是支持多任務。咱們以前寫的程序,在監控按鍵的同時,開了一個定時器中斷用於數碼管動態掃描,兩個任務同時進行,是多任務嗎?不徹底是。監控按鍵與動態掃描兩個任務只有一個能夠佔據main
函數,另外一個必須放在中斷裏,中斷裏的任務不能執行太長時間,不然就會干擾main
函數的運行。而操做系統中的任務調度器能夠給每一個任務分配必定的運行時間,CPU一會執行這個,一會執行那個,每一個任務都好像獨佔了CPU連續執行同樣。操作系統
RTOS與其餘操做系統的主要區別在於任務調度器的設計。在RTOS中,全部任務都有優先級,優先級高的被調度器保證優先執行,以得到最短的響應時間。在與現實世界打交道的嵌入式系統中,這樣的功能每每是必要的。debug
操做系統一般須要中檔的硬件,8位的AVR稍差了一點,主頻和存儲容量達不到一些操做系統的要求,不過仍是有可選項的。咱們來試着在開發板上運行FreeRTOS。FreeRTOS是一個免費的、爲單片機設計的RTOS,是目前嵌入式市場佔有率第二的操做系統,僅次於Linux。
首先去官網下載代碼。下載的是一個.zip
壓縮包,找到FreeRTOS
文件夾,目錄下Demo
和Source
中的部分代碼是須要使用的。做爲一個跨平臺的系統,大多數代碼平臺無關,只存一份,其餘平臺相關的代碼,每一個平臺都有獨立的實現,源碼是demo都是如此,這使得代碼組織有些複雜,你能夠參考官方文檔。
官方提供了ATmega323單片機的demo,爲了在開發板上運行,須要作一些修改。demo基於WinAVR平臺,它與Atmel Studio同樣,都是基於avr-gcc的。若是你有WinAVR的話,直接用makefile
就能夠編譯;Atmel Studio雖然也提供了make
,但有些微區別,無法直接用makefile
,所以咱們本身創建項目來編譯。
新建項目,而後在Solution Explorer中建3個文件夾:source
、port
和demo
。
拷貝一些文件到這些目錄下:
source
:\Source\include\
全部文件、\Source\
下的tasks.c
、queue.c
、list.c
和croutine.c
;
port
:\Source\portable\GCC\ATmega323\
全部文件和\Source\portable\MemMang`下的heap_1.c
;
demo
:\Demo\Common\include\
全部文件、\Demo\Common\Minimal\
下的crflash.c
、integer.c
、PollQ.c
和comtest.c
、\Demo\AVR_ATMega323_WinAVR\
除makefile
之外的全部文件,再把ParTest.c
和serial.c
拎出來,main.c
拎到外面。
我是怎麼知道的呢?我參考了官方文檔和makefile
文件。
在Solution Explorer中Add Existing Item,在項目屬性->Toolchain->AVR/GNU C Compiler->Directories中添加這三個目錄。
修改代碼,使之適用於咱們的開發板:
修改的理由有如下幾種:
ATmega323和ATmega324的寄存器略有不一樣;
WinAVR和Atmel Studio提供的工具鏈中的一些定義方式不一樣;
硬件配置與鏈接不一樣。
因此須要作如下修改:
port.c
中:TIMSK
改成TIMSK1
;SIG_OUTPUT_COMPARE1A
改成TIMER1_COMPA_vect
;54行改成0x02
;
FreeRTOSConfig.h
中:48行改成25000000
;
serial.c
中:UDR
、UCSRB
、UCSRC
、UBRRL
、UBRRH
分別改成UDR0
、UCSR0B
、UCSR0C
、UBRR0L
、UBRR0H
;67行改成0x00
;188行改成ISR(USART0_RX_vect)
;207行改成ISR(USART0_UDRE_vect)
;
comtest.c
中:71行改成4
;72行改成2
;
ParTest.c
中:DDRB
改成DDRC
;PORTB
改成PORTC
;49行改成0x00
;50行改成3
;72和99行把uxLED
改成(4 + uxLED)
;76行把if
和else
的大括號中的語句對調;
main.c
中:刪除81和84行;111行改成0
;117行改成3
;127行改成2
;153行返回類型改成int
。
不出意外的話,如今代碼能夠經過編譯了(我這裏有3個warning)。下載到單片機上,鏈接TX
和RX
,你會發現紅燈和黃燈分別以300ms和400ms爲週期閃爍,綠燈和串口黃燈一塊兒閃爍,藍燈不亮。
實際上,程序建立了1個整數計算、2個串口收發、2個隊列收發、2個寄存器測試、1個錯誤檢查和1個空閒共9個任務,以及2個LED閃爍協程。每過一毫秒,定時器產生一次中斷,任務調度器暫停當前任務,換一個任務開始運行。爲了理解這個過程,咱們先介紹上下文這個概念。
一個任務在執行的過程當中,須要一些臨時變量,它們有的保存在棧上(棧是內存中的一塊區域,寄存器SP
指向棧頂),有的在寄存器中;此外,條件分支語句還要用到寄存器SREG
中的位,這些位在以前的語句中被置位或清零;還有記錄當前程序執行到哪的程序計數器。這些一塊兒構成了任務執行的上下文:寄存器r0
到r31
、SREG
、SP
和PC
。不一樣任務的上下文是不共享的,但它們卻要佔用相同的位置,爲此,在切換任務時須要把前一個上下文保存起來,並恢復要切換到的任務的上下文,這個過程稱爲上下文切換,而後才能繼續這個任務。
咱們來結合代碼分析一下這個過程。
void TIMER1_COMPA_vect( void ) __attribute__ ( ( signal, naked ) ); void TIMER1_COMPA_vect( void ) { vPortYieldFromTick(); asm volatile ( "reti" ); } void vPortYieldFromTick( void ) __attribute__ ( ( naked ) ); void vPortYieldFromTick( void ) { portSAVE_CONTEXT(); if( xTaskIncrementTick() != pdFALSE ) { vTaskSwitchContext(); } portRESTORE_CONTEXT(); asm volatile ( "ret" ); } typedef void TCB_t; extern volatile TCB_t * volatile pxCurrentTCB; #define portSAVE_CONTEXT() \ asm volatile ( "push r0 \n\t" \ "in r0, __SREG__ \n\t" \ "cli \n\t" \ "push r0 \n\t" \ "push r1 \n\t" \ "clr r1 \n\t" \ "push r2 \n\t" \ "push r3 \n\t" \ "push r4 \n\t" \ "push r5 \n\t" \ "push r6 \n\t" \ "push r7 \n\t" \ "push r8 \n\t" \ "push r9 \n\t" \ "push r10 \n\t" \ "push r11 \n\t" \ "push r12 \n\t" \ "push r13 \n\t" \ "push r14 \n\t" \ "push r15 \n\t" \ "push r16 \n\t" \ "push r17 \n\t" \ "push r18 \n\t" \ "push r19 \n\t" \ "push r20 \n\t" \ "push r21 \n\t" \ "push r22 \n\t" \ "push r23 \n\t" \ "push r24 \n\t" \ "push r25 \n\t" \ "push r26 \n\t" \ "push r27 \n\t" \ "push r28 \n\t" \ "push r29 \n\t" \ "push r30 \n\t" \ "push r31 \n\t" \ "lds r26, pxCurrentTCB \n\t" \ "lds r27, pxCurrentTCB + 1 \n\t" \ "in r0, 0x3d \n\t" \ "st x+, r0 \n\t" \ "in r0, 0x3e \n\t" \ "st x+, r0 \n\t" \ ); #define portRESTORE_CONTEXT() \ asm volatile ( "lds r26, pxCurrentTCB \n\t" \ "lds r27, pxCurrentTCB + 1 \n\t" \ "ld r28, x+ \n\t" \ "out __SP_L__, r28 \n\t" \ "ld r29, x+ \n\t" \ "out __SP_H__, r29 \n\t" \ "pop r31 \n\t" \ "pop r30 \n\t" \ "pop r29 \n\t" \ "pop r28 \n\t" \ "pop r27 \n\t" \ "pop r26 \n\t" \ "pop r25 \n\t" \ "pop r24 \n\t" \ "pop r23 \n\t" \ "pop r22 \n\t" \ "pop r21 \n\t" \ "pop r20 \n\t" \ "pop r19 \n\t" \ "pop r18 \n\t" \ "pop r17 \n\t" \ "pop r16 \n\t" \ "pop r15 \n\t" \ "pop r14 \n\t" \ "pop r13 \n\t" \ "pop r12 \n\t" \ "pop r11 \n\t" \ "pop r10 \n\t" \ "pop r9 \n\t" \ "pop r8 \n\t" \ "pop r7 \n\t" \ "pop r6 \n\t" \ "pop r5 \n\t" \ "pop r4 \n\t" \ "pop r3 \n\t" \ "pop r2 \n\t" \ "pop r1 \n\t" \ "pop r0 \n\t" \ "out __SREG__, r0 \n\t" \ "pop r0 \n\t" \ );
在定時器中斷TIMER1_COMPA_vect
中,vPortYieldFromTick
被調用,其中依次調用portSAVE_CONTEXT
、xTaskIncrementTick
、vTaskSwitchContext
(可能不調用)和portRESTORE_CONTEXT
,執行彙編語句ret
;最後執行reti
。
在介紹中斷的時候,咱們提到過編譯器添加的額外代碼,把用到的寄存器都push進棧。可是,編譯器只會保護該中斷用到的寄存器,而上下文包括全部寄存器,須要手動地編寫代碼,那麼也就無需編譯器添加多餘的代碼了。函數TIMER1_COMPA_vect
被添加attributenaked
,表示無需添加任何代碼,把用戶編寫的原本來本地編進去就夠了。
進入中斷時,PC
被push進棧(這是硬件作的),PC
內容變爲TIMER1_COMPA_vect
的地址,隨後開始執行,PC
再次push進棧(沒有在圖片中表示出來),開始執行portSAVE_CONTEXT
保存上下文。因爲它是宏,就沒有PC
進棧的過程。
而後,r0
、SREG
、r1
到r31
依次進棧,上下文的內容保存完成,其位置還須要另存。SP
指向棧頂,表明着上下文的位置,它被複制到pxCurrentTCB
所指的位置中。pxCurrentTCB
其實是結構體TCB_t
指針,該結構體保存着當前執行的任務的信息,前兩個字節保存棧指針。這樣,上下文就保存完成了。
xTaskIncrementTick
把軟件計數器加1,並檢查是否須要任務切換。爲了講解,咱們假定它須要,那麼vTaskSwitchContext
就會被調用,pxCurrentTCB
指向另外一個TCB_t
變量,那裏保存着另外一個任務的上下文,咱們要恢復它。
恢復過程是,先用pxCurrentTCB
取出SP
,再按相反的順序出棧,上下文中就只剩PC
沒有恢復了(ret
和vPortYieldFromTick
的調用抵消,一塊兒忽略)。最後執行reti
,該彙編語句從棧頂取兩個字節放進PC
,並跳轉到其位置繼續執行。此時,PC
的內容就是該任務以前被中斷時執行到的位置,如今從PC
開始繼續執行,也就是繼續執行該任務。上下文切換完成。
在對FreeRTOS稍有了解後,咱們動手寫一個基於FreeRTOS的程序。在學習數碼管的時候,你極可能考慮過,在後臺建立一個任務,執行數碼管的掃描。如今,FreeRTOS給了你這個機會。咱們建立兩個任務,一個每一毫秒顯示數碼管的一位,另外一個每200毫秒更新顯示的數字。
#include <stdlib.h> #include "FreeRTOS.h" #include "task.h" #include "semphr.h" #include <ee2/segment.h> SemaphoreHandle_t mutex; portTASK_FUNCTION(segment_scan, pvParameters) { while (1) { static uint8_t digit = 0; xSemaphoreTake(mutex, 1000); segment_display(digit); xSemaphoreGive(mutex); if (++digit == 2) digit = 0; vTaskDelay(1); } } portTASK_FUNCTION(segment_set, pvParameters) { while (1) { static uint8_t number = 0; xSemaphoreTake(mutex, 1000); segment_dec(number); xSemaphoreGive(mutex); if (++number == 100) number = 0; vTaskDelay(200); } } int main() { segment_init(PIN_8, PIN_9); mutex = xSemaphoreCreateMutex(); xTaskCreate(segment_scan, "scan", configMINIMAL_STACK_SIZE, NULL, 1, NULL); xTaskCreate(segment_set, "set", configMINIMAL_STACK_SIZE, NULL, 2, NULL); vTaskStartScheduler(); return 0; }
兩個任務都須要使用數碼管這一資源。若是一個任務正在調用segment_dec
,還沒返回時,定時器中斷髮生,切換到另外一個任務,其中調用了segment_display
,就會發生衝突。咱們用一個互斥量mutex
來解決。當一個任務調用了xSemaphoreTake
後,在它調用xSemaphoreGive
前,mutex
會進入鎖定狀態,若是另外一個任務試圖調用xSemaphoreTake
,則會阻塞住,切換到另外一個任務。這樣就保證兩個任務不會衝突。資源共享是並行程序要着重處理的問題之一。
FreeRTOS還有不少功能等待你去發掘,RTOS就更多了。最後,咱們來談談RTOS的長處和短處。
RTOS是多任務的,這是對代碼順序執行的編程模型的顛覆,使程序能夠實現更多功能,好比兩個連續的(不調用delay
之類的函數的)任務同時執行。即便是大多數狀況下中斷能夠解決的問題,RTOS的引入也能讓你更快地實現相同功能,這既體如今編程思路的改進,還有現成API可供使用,提升開發效率。若是涉及到程序在平臺間的移植,RTOS能提供的幫助就更多了。
RTOS是事件驅動的,儘管表面上不太看得出來。這也能帶來一些收益,咱們將在本文最後一節進行分析。
然而,RTOS的運行負擔較大,包括時間和空間,好比在AVR平臺上,一次任務調度至少須要100多個指令週期。在應用自己不太複雜的狀況下,這一點尤其嚴重,須要根據應用決定是否使用。我把RTOS安排到了最後一篇,顯然是建議在AVR單片機開發中,儘量不要使用RTOS。
最後,RTOS對我的發展是有好處的。Linux儘管不是RTOS,做爲安裝量最大的操做系統內核,是嵌入式開發者必須精通的。各類RTOS與Linux同樣都是操做系統,無非是調度策略不一樣(Linux也有實時的),不少內容都是相通的。學習RTOS對學習Linux有很大幫助,這對你的嵌入式道路是有益無害的。
未完待續……