AVR單片機教程——走向高層

本文隸屬於AVR單片機教程系列。html

 

在系列教程的最後一篇中,我將向你推薦3個能夠深造的方向:RTOS、C++、事件驅動。掌握這些技術能夠幫助你更快、更好地開發更大的項目。git

本文涉及到許多概念性的內容,若是你有不一樣意見,歡迎討論。編程

關於高層

這一篇教程叫做「走向高層」。什麼是高層?函數

我認爲,若是寥寥幾行代碼就能實現一個複雜功能,或者一行代碼能夠對應到幾百句彙編,那麼你就站在高層。高層與底層是相對的概念,沒有絕對的界限。工具

站得高,看得遠,這一樣適用於編程,咱們要走向高層。高層是對底層的封裝,是對現實的抽象,高層相比於底層更加貼近應用。站在高層,你能夠看到不少底層看不到的東西,主要有編程工具和思路。合理利用工具,能夠簡化代碼,下降工做量;用合適的思路編程,更能夠事半功倍。學習

可是,掌握高層並不意味着忽視甚至鄙視底層,高層創建在底層基礎之上。其一,有些高層出現的詭異現象能夠追溯到底層,這樣的debug任務只有通曉底層與高層的開發者才能勝任;其二,爲了讓高層實現複雜功能的同時得到可接受的運行效率,底層必須設計地更加精緻,這就對底層提出了更高的要求。測試

相信你通過一期和二期的教程,已經至關熟悉AVR編程的底層了。跟我一塊兒走上高層吧!ui

RTOS

實時操做系統(RTOS)是一類操做系統。帶有操做系統的計算機系統相比不帶有的,最顯著的特色是支持多任務。咱們以前寫的程序,在監控按鍵的同時,開了一個定時器中斷用於數碼管動態掃描,兩個任務同時進行,是多任務嗎?不徹底是。監控按鍵與動態掃描兩個任務只有一個能夠佔據main函數,另外一個必須放在中斷裏,中斷裏的任務不能執行太長時間,不然就會干擾main函數的運行。而操做系統中的任務調度器能夠給每一個任務分配必定的運行時間,CPU一會執行這個,一會執行那個,每一個任務都好像獨佔了CPU連續執行同樣。操作系統

RTOS與其餘操做系統的主要區別在於任務調度器的設計。在RTOS中,全部任務都有優先級,優先級高的被調度器保證優先執行,以得到最短的響應時間。在與現實世界打交道的嵌入式系統中,這樣的功能每每是必要的。debug

操做系統一般須要中檔的硬件,8位的AVR稍差了一點,主頻和存儲容量達不到一些操做系統的要求,不過仍是有可選項的。咱們來試着在開發板上運行FreeRTOS。FreeRTOS是一個免費的、爲單片機設計的RTOS,是目前嵌入式市場佔有率第二的操做系統,僅次於Linux。

首先去官網下載代碼。下載的是一個.zip壓縮包,找到FreeRTOS文件夾,目錄下DemoSource中的部分代碼是須要使用的。做爲一個跨平臺的系統,大多數代碼平臺無關,只存一份,其餘平臺相關的代碼,每一個平臺都有獨立的實現,源碼是demo都是如此,這使得代碼組織有些複雜,你能夠參考官方文檔

官方提供了ATmega323單片機的demo,爲了在開發板上運行,須要作一些修改。demo基於WinAVR平臺,它與Atmel Studio同樣,都是基於avr-gcc的。若是你有WinAVR的話,直接用makefile就能夠編譯;Atmel Studio雖然也提供了make,但有些微區別,無法直接用makefile,所以咱們本身創建項目來編譯。

  1. 新建項目,而後在Solution Explorer中建3個文件夾:sourceportdemo

  2. 拷貝一些文件到這些目錄下:

    • source\Source\include\全部文件、\Source\下的tasks.cqueue.clist.ccroutine.c

    • port\Source\portable\GCC\ATmega323\全部文件和\Source\portable\MemMang`下的heap_1.c

    • demo\Demo\Common\include\全部文件、\Demo\Common\Minimal\下的crflash.cinteger.cPollQ.ccomtest.c\Demo\AVR_ATMega323_WinAVR\makefile之外的全部文件,再把ParTest.cserial.c拎出來,main.c拎到外面。

    我是怎麼知道的呢?我參考了官方文檔和makefile文件。

  3. 在Solution Explorer中Add Existing Item,在項目屬性->Toolchain->AVR/GNU C Compiler->Directories中添加這三個目錄。

  4. 修改代碼,使之適用於咱們的開發板:

    修改的理由有如下幾種:

    • ATmega323和ATmega324的寄存器略有不一樣;

    • WinAVR和Atmel Studio提供的工具鏈中的一些定義方式不一樣;

    • 硬件配置與鏈接不一樣。

    因此須要作如下修改:

    • port.c中:TIMSK改成TIMSK1SIG_OUTPUT_COMPARE1A改成TIMER1_COMPA_vect;54行改成0x02

    • FreeRTOSConfig.h中:48行改成25000000

    • serial.c中:UDRUCSRBUCSRCUBRRLUBRRH分別改成UDR0UCSR0BUCSR0CUBRR0LUBRR0H;67行改成0x00;188行改成ISR(USART0_RX_vect);207行改成ISR(USART0_UDRE_vect)

    • comtest.c中:71行改成4;72行改成2

    • ParTest.c中:DDRB改成DDRCPORTB改成PORTC;49行改成0x00;50行改成3;72和99行把uxLED改成(4 + uxLED);76行把ifelse的大括號中的語句對調;

    • main.c中:刪除81和84行;111行改成0;117行改成3;127行改成2;153行返回類型改成int

不出意外的話,如今代碼能夠經過編譯了(我這裏有3個warning)。下載到單片機上,鏈接TXRX,你會發現紅燈和黃燈分別以300ms和400ms爲週期閃爍,綠燈和串口黃燈一塊兒閃爍,藍燈不亮。

實際上,程序建立了1個整數計算、2個串口收發、2個隊列收發、2個寄存器測試、1個錯誤檢查和1個空閒共9個任務,以及2個LED閃爍協程。每過一毫秒,定時器產生一次中斷,任務調度器暫停當前任務,換一個任務開始運行。爲了理解這個過程,咱們先介紹上下文這個概念。

一個任務在執行的過程當中,須要一些臨時變量,它們有的保存在棧上(棧是內存中的一塊區域,寄存器SP指向棧頂),有的在寄存器中;此外,條件分支語句還要用到寄存器SREG中的位,這些位在以前的語句中被置位或清零;還有記錄當前程序執行到哪的程序計數器。這些一塊兒構成了任務執行的上下文:寄存器r0r31SREGSPPC。不一樣任務的上下文是不共享的,但它們卻要佔用相同的位置,爲此,在切換任務時須要把前一個上下文保存起來,並恢復要切換到的任務的上下文,這個過程稱爲上下文切換,而後才能繼續這個任務。

咱們來結合代碼分析一下這個過程。

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_CONTEXTxTaskIncrementTickvTaskSwitchContext(可能不調用)和portRESTORE_CONTEXT,執行彙編語句ret;最後執行reti

在介紹中斷的時候,咱們提到過編譯器添加的額外代碼,把用到的寄存器都push進棧。可是,編譯器只會保護該中斷用到的寄存器,而上下文包括全部寄存器,須要手動地編寫代碼,那麼也就無需編譯器添加多餘的代碼了。函數TIMER1_COMPA_vect被添加attributenaked,表示無需添加任何代碼,把用戶編寫的原本來本地編進去就夠了。

進入中斷時,PC被push進棧(這是硬件作的),PC內容變爲TIMER1_COMPA_vect的地址,隨後開始執行,PC再次push進棧(沒有在圖片中表示出來),開始執行portSAVE_CONTEXT保存上下文。因爲它是宏,就沒有PC進棧的過程。

而後,r0SREGr1r31依次進棧,上下文的內容保存完成,其位置還須要另存。SP指向棧頂,表明着上下文的位置,它被複制到pxCurrentTCB所指的位置中。pxCurrentTCB其實是結構體TCB_t指針,該結構體保存着當前執行的任務的信息,前兩個字節保存棧指針。這樣,上下文就保存完成了。

xTaskIncrementTick把軟件計數器加1,並檢查是否須要任務切換。爲了講解,咱們假定它須要,那麼vTaskSwitchContext就會被調用,pxCurrentTCB指向另外一個TCB_t變量,那裏保存着另外一個任務的上下文,咱們要恢復它。

恢復過程是,先用pxCurrentTCB取出SP,再按相反的順序出棧,上下文中就只剩PC沒有恢復了(retvPortYieldFromTick的調用抵消,一塊兒忽略)。最後執行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有很大幫助,這對你的嵌入式道路是有益無害的。

C++

未完待續……

相關文章
相關標籤/搜索