自制嵌入式操作系統 DAY1

遙想當年剛學習操作系統的時候,很難理解教科書中關於線程/進程的描述。原因還是在於操作系統書上的內容太過抽象,對於一個沒有看過內核代碼的初學者來說,很難理解各種數據結構的調度。後來自己也買了一些造輪子的書,照着好幾本書也造了幾個玩具操作系統,有X86,有ARM的。經過實踐之後回頭再去看操作系統的書,才恍然大悟操作系統書中所寫的知識點。

看了許多操作系統實踐類的書籍後,有些書只是淺嘗輒止,試圖用300頁將通用操作系統各個模塊都講了一遍,這一類書幫助讀者理解操作系統還是有限;而有些書寫的確實很不錯,內容詳實,然而動輒上千頁,讓讀者望而生畏,但是讀完並且照着書寫完一個玩具OS的話,絕對對OS的理解有很大幫助。這裏推薦鄭剛老師寫的《操作系統真相還原》,本人覺得這本書非常好,深入淺出。那我爲何還要寫這篇博客呢?我覺得操作系統內核最核心,且初學者最難理解的部分莫過於進程/線程(在RTOS中稱爲任務),所以本文試圖寫一個只有不到1000多行代碼的RTOS來幫助讀者理解操作系統核心部分。一般小型RTOS中並沒有虛擬內存管理,文件系統,設備管理等模塊,這樣減小讀者的負擔,更好理解操作系統的核心部分(進程/線程/任務),在這之後再去學習其他的模塊必然事半功倍。所以本文僅僅作爲一篇入門讀物,若是能幫助各位看官進入操作系統的大門,也算是功德無量。當然在下才疏學淺,難免有錯誤的地方,大神發現的話請指出。

話不多說,直接進入正題。

預備知識

雖然本文旨在一篇入門的教程,但希望讀者具有以下的預備知識,否則讀起來會有諸多不順。

源碼GIT

https://github.com/JiaminMa/write_rtos_in_3days.git

環境搭建

本文使用qemu虛擬機來仿真arm cortex m3的芯片,QEMU可以自己編譯,也可以下載,我已經編譯好一份QEMU,各位看官可以直接clone該git然後使用tools裏面的qemu即可。編譯器使用的是GNU的arm-none-eabi-gcc,這個可以使用sudo apt-get install gcc-arm-none-eabi
下載到。哦對了,我的linux用的是ubuntu16 64位,希望各位看官可以用相同版本的ubuntu,否則可能會有一些環境的問題,概不負責。以下乃環境搭建參考步驟:

  1. git clone https://github.com/JiaminMa/write_rtos_in_3days.git
  2. vim ~/.bashrc
  3. export PATH=$PATH:/mnt/e/write_rtos_in_3days/tools, 這一步每個人的配置不一樣,要把write_rtos_in_3days/tools設置爲讀者自己的tools的目錄
  4. source ~/.bashrc
  5. sudo apt-get install gcc-arm-none-eabi

1 QEMU ARM CORTEX M3入門

qemu-system-arm對於CORTEX M的芯片官方只支持了Stellaris LM3S6965EVB和Stellaris LM3S811EVB,本文使用了LM3S6965EVB作爲開發平臺。非官方的有STM32等其他CM3/4的芯片及開發板,但這裏選用官方的支持更穩定一些。我在doc目錄下放了LM3S6965的芯片手冊,感興趣的讀者可以自己看,實際上本文在寫嵌入式操作系統中,除了UART並沒有使用到LM3S6965的外設,大部分代碼都是針對ARM CM3內核的操作,所以並不需要對LM3S6965EVB很清楚。

打印Hello World

沒錯,本章就是要在qemu平臺上打印最喜聞樂見的Hello world。本節的完整代碼在01_hello_world中。

異常向量表

當CM3內核響應了一個發生的異常後,對應的異常服務例程(ESR)就會執行。爲了決定ESR的入口地址,CM3使用了“向量表查表機制”。這裏使用一張向量表。向量表其實是一個WORD(32位整數)數組,每個下標對應一種異常,該下標元素的值則是該ESR的入口地址。向量表在地址空間中的位置是可以設置的,通過NVIC中的一個重定位寄存器來指出向量表的地址。在復位後,該寄存器的值爲0。因此,在地址0處必須包含一張向量表,用於初始時的異常分配。

異常類型 表項地址偏移量 異常向量
0 0x00 MSP初始值
1 0x04 復位函數入口
2 0x08 NMI
3 0x0C Hard Fault
4 0x10 MemManage Fault
5 0x14 總線Fault
6 0x18 用法Fault
7-10 0x1c-0x28 保留
11 0x2c SVC
12 0x30 調試監視器
13 0x34 保留
14 0x38 PendSV
15 0x3c SysTick
16 0x40 IRQ #0
17 0x44 IRQ #1
18-255 0x48-0x3ff IRQ#2-#239

舉個例子,如果發生了異常11(SVC),則NVIC會計算出偏移移量是11x4=0x2C,然後從那裏取出服務例程的入口地址並跳入。要注意的是這裏有個另類:0號類型並不是什麼入口地址,而是給出了復位後MSP的初值。 Cortex M3權威指南P43 3.5向量表>

本文中,int_vector.c中包含了異常向量表,源代碼如下。我們將MSP(主棧)的值設爲0x2000c000,程序入口爲main,NMI中斷和HardFault中斷分別爲自己處理函數,其他異常以及中斷暫時全部使用IntDefaultHandler。


static void NmiSR(void){
    while(1);
}

static void FaultISR(void){
    while(1);
}

static void IntDefaultHandler(void){
    while(1);
}


__attribute__ ((section(".isr_vector")))void (*g_pfnVectors[])(void) =
{
    0x2000c000,                             // StackPtr, set in RestetISR
    main,                                    // The reset handler
    NmiSR,                                  // The NMI handler
    FaultISR,                               // The hard fault handler
    IntDefaultHandler,                      // The MPU fault handler
    IntDefaultHandler,                      // The bus fault handler
    IntDefaultHandler,                      // The usage fault handler
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    IntDefaultHandler,                      // SVCall handler
    IntDefaultHandler,                      // Debug monitor handler
    0,                                      // Reserved
    IntDefaultHandler,                      // The PendSV handler
    IntDefaultHandler,                      // The SysTick handler
    IntDefaultHandler,                      // GPIO Port A
    IntDefaultHandler,                      // GPIO Port B
    IntDefaultHandler,                      // GPIO Port C
    IntDefaultHandler,                      // GPIO Port D
    IntDefaultHandler,                      // GPIO Port E
    IntDefaultHandler,                      // UART0 Rx and Tx
    IntDefaultHandler,                      // UART1 Rx and Tx
    IntDefaultHandler,                      // SSI0 Rx and Tx
    IntDefaultHandler,                      // I2C0 Master and Slave
    IntDefaultHandler,                      // PWM Fault
    IntDefaultHandler,                      // PWM Generator 0
    IntDefaultHandler,                      // PWM Generator 1
    IntDefaultHandler,                      // PWM Generator 2
    IntDefaultHandler,                      // Quadrature Encoder 0
    IntDefaultHandler,                      // ADC Sequence 0
    IntDefaultHandler,                      // ADC Sequence 1
    IntDefaultHandler,                      // ADC Sequence 2
    IntDefaultHandler,                      // ADC Sequence 3
    IntDefaultHandler,                      // Watchdog timer
    IntDefaultHandler,                      // Timer 0 subtimer A
    IntDefaultHandler,                      // Timer 0 subtimer B
    IntDefaultHandler,                      // Timer 1 subtimer A
    IntDefaultHandler,                      // Timer 1 subtimer B
    IntDefaultHandler,                      // Timer 2 subtimer A
    IntDefaultHandler,                      // Timer 2 subtimer B
    IntDefaultHandler,                      // Analog Comparator 0
    IntDefaultHandler,                      // Analog Comparator 1
    IntDefaultHandler,                      // Analog Comparator 2
    IntDefaultHandler,                      // System Control (PLL, OSC, BO)
    IntDefaultHandler,                      // FLASH Control
    IntDefaultHandler,                      // GPIO Port F
    IntDefaultHandler,                      // GPIO Port G
    IntDefaultHandler,                      // GPIO Port H
    IntDefaultHandler,                      // UART2 Rx and Tx
    IntDefaultHandler,                      // SSI1 Rx and Tx
    IntDefaultHandler,                      // Timer 3 subtimer A
    IntDefaultHandler,                      // Timer 3 subtimer B
    IntDefaultHandler,                      // I2C1 Master and Slave
    IntDefaultHandler,                      // Quadrature Encoder 1
    IntDefaultHandler,                      // CAN0
    IntDefaultHandler,                      // CAN1
    IntDefaultHandler,                      // CAN2
    IntDefaultHandler,                      // Ethernet
    IntDefaultHandler,                      // Hibernate
    IntDefaultHandler,                      // USB0
    IntDefaultHandler,                      // PWM Generator 3
    IntDefaultHandler,                      // uDMA Software Transfer
    IntDefaultHandler,                      // uDMA Error
    IntDefaultHandler,                      // ADC1 Sequence 0
    IntDefaultHandler,                      // ADC1 Sequence 1
    IntDefaultHandler,                      // ADC1 Sequence 2
    IntDefaultHandler,                      // ADC1 Sequence 3
    IntDefaultHandler,                      // I2S0
    IntDefaultHandler,                      // External Bus Interface 0
    IntDefaultHandler,                      // GPIO Port J
    IntDefaultHandler,                      // GPIO Port K
    IntDefaultHandler,                      // GPIO Port L
    IntDefaultHandler,                      // SSI2 Rx and Tx
    IntDefaultHandler,                      // SSI3 Rx and Tx
    IntDefaultHandler,                      // UART3 Rx and Tx
    IntDefaultHandler,                      // UART4 Rx and Tx
    IntDefaultHandler,                      // UART5 Rx and Tx
    IntDefaultHandler,                      // UART6 Rx and Tx
    IntDefaultHandler,                      // UART7 Rx and Tx
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    IntDefaultHandler,                      // I2C2 Master and Slave
    IntDefaultHandler,                      // I2C3 Master and Slave
    IntDefaultHandler,                      // Timer 4 subtimer A
    IntDefaultHandler,                      // Timer 4 subtimer B
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    0,                                      // Reserved
    IntDefaultHandler,                      // Timer 5 subtimer A
    IntDefaultHandler,                      // Timer 5 subtimer B
    IntDefaultHandler,                      // Wide Timer 0 subtimer A
    IntDefaultHandler,                      // Wide Timer 0 subtimer B
    IntDefaultHandler,                      // Wide Timer 1 subtimer A
    IntDefaultHandler,                      // Wide Timer 1 subtimer B
    IntDefaultHandler,                      // Wide Timer 2 subtimer A
    IntDefaultHandler,                      // Wide Timer 2 subtimer B
    IntDefaultHandler,                      // Wide Timer 3 subtimer A
    IntDefaultHandler,                      // Wide Timer 3 subtimer B
    IntDefaultHandler,                      // Wide Timer 4 subtimer A
    IntDefaultHandler,                      // Wide Timer 4 subtimer B
    IntDefaultHandler,                      // Wide Timer 5 subtimer A
    IntDefaultHandler,                      // Wide Timer 5 subtimer B
    IntDefaultHandler,                      // FPU
    IntDefaultHandler,                      // PECI 0
    IntDefaultHandler,                      // LPC 0
    IntDefaultHandler,                      // I2C4 Master and Slave
    IntDefaultHandler,                      // I2C5 Master and Slave
    IntDefaultHandler,                      // GPIO Port M
    IntDefaultHandler,                      // GPIO Port N
    IntDefaultHandler,                      // Quadrature Encoder 2
    IntDefaultHandler,                      // Fan 0
    0,                                      // Reserved
    IntDefaultHandler,                      // GPIO Port P (Summary or P0)
    IntDefaultHandler,                      // GPIO Port P1
    IntDefaultHandler,                      // GPIO Port P2
    IntDefaultHandler,                      // GPIO Port P3
    IntDefaultHandler,                      // GPIO Port P4
    IntDefaultHandler,                      // GPIO Port P5
    IntDefaultHandler,                      // GPIO Port P6
    IntDefaultHandler,                      // GPIO Port P7
    IntDefaultHandler,                      // GPIO Port Q (Summary or Q0)
    IntDefaultHandler,                      // GPIO Port Q1
    IntDefaultHandler,                      // GPIO Port Q2
    IntDefaultHandler,                      // GPIO Port Q3
    IntDefaultHandler,                      // GPIO Port Q4
    IntDefaultHandler,                      // GPIO Port Q5
    IntDefaultHandler,                      // GPIO Port Q6
    IntDefaultHandler,                      // GPIO Port Q7
    IntDefaultHandler,                      // GPIO Port R
    IntDefaultHandler,                      // GPIO Port S
    IntDefaultHandler,                      // PWM 1 Generator 0
    IntDefaultHandler,                      // PWM 1 Generator 1
    IntDefaultHandler,                      // PWM 1 Generator 2
    IntDefaultHandler,                      // PWM 1 Generator 3
    IntDefaultHandler                       // PWM 1 Fault
};

main函數

CM3內核從異常向量表中取出MSP,然後設置MSP後就跳到reset向量中,在這裏是main函數,其啓動過程如下圖所示。main函數的實現在main.c中,源代碼如下,非常簡單,往串口數據寄存器中寫數據打印Hello World,然後就while(1)循環。由於這是QEMU虛擬機,所以並不需要對串口進行初始化等操作,直接往DR寄存器裏寫數據即可打印出字符,在真實的硬件這麼做是不行的,必須初始化串口的時鐘已經相應的寄存器來配置其工作模式。

main.c

#include <stdint.h>
volatile uint32_t * const UART0DR = (uint32_t *)0x4000C000;

void send_str(char *s)
{
    while(*s != '\0') {
        *UART0DR = *s++;
    }
}

void main()
{
    send_str("hello world\n");
    while(1);
}

存儲分佈

CM3的存儲器映射是相對固定的,具體可以參看《CORTEX_M3 權威指南》84頁的圖5.1。本文中的存儲分佈如下表所示,0x0-0x40000爲只讀存儲,即FLASH,0x20000000-0x20040000爲SRAM區。FLASH和SRAM分別是256K。

內存地址 存儲區域
0x0-0x400 異常向量表
0x400-0x40000 代碼段,只讀數據段
0x20000000-0x20004000 數據段,bss段
0x20004000-0x20008000 進程堆棧段(PSP)
0x20008000-0x2000c000 主棧段(MSP)

具體實現參看鏈接文件rtos.ld,鏈接文件在後面的文章不會改動,所以只需要記住即可。

rtos.ld

MEMORY
{
    FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 256K
    SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 256K
}

SECTIONS
{
    .text :
    {
        _text = .;
        KEEP(*(.isr_vector))
        *(.text*)
        *(.rodata*)
        _etext = .;
    } > FLASH

    /DISCARD/ :
    {
        *(.ARM.exidx*)
        *(.gnu.linkonce.armexidx.*)
    }

    .data : AT(ADDR(.text) + SIZEOF(.text))
    {
        _data = .;
        *(vtable)
        *(.data*)
        _edata = .;
    } > SRAM

    .bss :
    {
        _bss = .;
        *(.bss*)
        *(COMMON)
        _ebss = .;
    } > SRAM

    . = ALIGN(32);          
    _p_stack_bottom = .;
    . = . + 0x4000;
    _p_stack_top = 0x20008000;
    . = . + 0x4000;         
    _stack_top = 0x2000c000; 
}

Makefile

Makefile 指定了編譯器,編譯選項以及編譯命令等,在後續章節中,只需要objs := 即可,當加入一個新的源文件只需要在obj後面添加相應的.o即可。比如新建了test.c,那麼改成objs := int_vector.o main.o test.o即可。這裏不解釋Makefile的原理,如果有不熟悉的讀者請自行學習Makefile的規則,網上關於Makefile的好教程有許多。
Makefile

TOOL_CHAIN = arm-none-eabi-
CC = ${TOOL_CHAIN}gcc
AS = ${TOOL_CHAIN}as
LD = ${TOOL_CHAIN}ld
OBJCOPY = ${TOOL_CHAIN}objcopy
OBJDUMP = $(TOOL_CHAIN)objdump

CFLAGS      := -Wall -g -fno-builtin -gdwarf-2 -gstrict-dwarf -mcpu=cortex-m3 -mthumb -nostartfiles  --specs=nosys.specs -std=c11 \
                -O0 -Iinclude
LDFLAGS     := -g

objs := int_vector.o main.o

rtos.bin: $(objs)
    ${LD} -T rtos.ld -o rtos.elf $^
    ${OBJCOPY} -O binary -S rtos.elf $@
    ${OBJDUMP} -D -m arm rtos.elf > rtos.dis

run: $(objs)
    ${LD} -T rtos.ld -o rtos.elf $^
    ${OBJCOPY} -O binary -S rtos.elf rtos.bin
    ${OBJDUMP} -D -m arm rtos.elf > rtos.dis
    qemu-system-arm -M lm3s6965evb --kernel rtos.bin -nographic

debug: $(objs)
    ${LD} -T rtos.ld -o rtos.elf $^
    ${OBJCOPY} -O binary -S rtos.elf rtos.bin
    ${OBJDUMP} -D -m arm rtos.elf > rtos.dis
    qemu-system-arm -M lm3s6965evb --kernel rtos.bin -nographic -s -S

%.o:%.c
    ${CC} $(CFLAGS) -c -o $@ $<

%.o:%.s
    ${CC} $(CFLAGS) -c -o $@ $<

clean:
    rm -rf *.o *.elf *.bin *.dis

執行/調試

好了,終於把所有的源文件,鏈接文件和Makefile搞定了,運行一把。可以看到以下打印,那麼說明執行正確。

如果需要調試的話,執行make debug,然後在另外一個窗口使用arm-linux-gdb調試,如下圖所示

CM3進階

本節代碼在02_cm3文件夾下

異常向量表改動

在完成了hello world後,我們可以實現CM3更多的功能了。我們要把常用的CM3的操作實現一把。首先改寫int_vector.c。因爲在進入c函數之前需要做一些棧的操作,所以講reset handler從main換成reset_handler, reset_handler在cm3_s.s中實現。還有就是將會實現sys_tick的中斷服務函數。這裏有細心的哥們會問爲什麼reset_handler + 1。原因是對於CM3的thumb code指令集地址最低位必須爲1,而reset_handler定義在彙編.S文件中,引入到C文件裏編譯器並沒有自動+1,所以這裏手動+1。而main是定義在c文件中,所以它已經自動將最低位+1了。
main.c

main,                                    // The reset handler
...
IntDefaultHandler,                      // The SysTick handler

改爲

((unsigned int)reset_handler + 1),      // The reset handler
...
systick_handler,                      // The SysTick handler

reset_handler

reset_handler的實現很簡單,將CM3運行時的棧切換成PSP,然後設置PSP的值,我習慣除了中斷處理程序使用MSP,其他代碼都用PSP。切換棧寄存器的動作很簡單,就是修改CONTROL寄存器的第1位,即可,CONTROL寄存器定義如下圖。_p_stack_top定義在rtos.ld中,其值是0x20008000。最後就是跳轉到main來執行c代碼。對於PSP和MSP是什麼的朋友可能需要去看看CM3權威指南了哦。

cm3_s.s

.text                                   
.code 16                                
.global main                            
.global reset_handler                   
.global _p_stack_top                    
.global get_psp                         
.global get_msp                         
.global get_control_reg                 

reset_handler:                          

    /*Set the stack as process stack*/     
    /* tmp = CONTROL
     * tmp |= 2
     * CONTROL = tmp 
     * /           
    mrs r0, CONTROL                  
    mov r1, #2                          
    orr r0, r1                    
    msr CONTROL, r0                     

    ldr r0, =_p_stack_top               
    mov sp, r0                          

    ldr r0, =main                       
    blx r0                              
    b .                                 

main函數改動

main函數主要完成以下兩點:
- 清0 BSS段
BSS段裏存放的是未初始化的全局變量以及靜態變量,內存在真實的物理硬件上上電後是隨機值,所以需要對BSS段中的數據清0,以免發生不測。當然在虛擬機上,未曾使用的內存應該是0,但爲了規範起見,還是將bss清0。
- 使能systick
systick是CM3的內核組件,其初始化的代碼在cm3.c中實現,在下個小節講解,本小節只講解main函數的改變。systick_handler是systick的中斷服務程序,在main.c中實現,每當systick中斷髮生時,就會進入到systick_handler中執行相關代碼,在這裏只是打印一句話。

main.c

#include "os_stdio.h"
#include <stdint.h>
#include "cm3.h"

extern uint32_t _bss;
extern uint32_t _ebss;

static inline void clear_bss(void)
{
    uint8_t *start = (uint8_t *)_bss;
    while ((uint32_t)start < _ebss) {
        *start = 0;
        start++;
    }
}

void systick_handler(void)
{
    DEBUG("systick_handler\n");
}

int main()
{

    systick_t *systick_p = (systick_t *)SYSTICK_BASE;
    clear_bss();

    DEBUG("Hello RTOS\n");
    DEBUG("psp:0x%x\n", get_psp());
    DEBUG("msp:0x%x\n", get_msp());

    init_systick();
    while(1) {
    }
    return 0;
}

Systick使能

SysTick定時器被捆綁在NVIC中,用於產生SysTick異常(異常號:15)。在以前,操作系統還有所有使用了時基的系統,都必須一個硬件定時器來產生需要的“滴答”中斷,作爲整個系統的時基。滴答中斷對操作系統尤其重要。例如,操作系統可以爲多個任務許以不同數目的時間片,確保沒有一個任務能霸佔系統;或者把每個定時器週期的某個時間範圍賜予特定的任務等,還有操作系統提供的各種定時功能,都與這個滴答定時器有關。因此,需要一個定時器來產生週期性的中斷,而且最好還讓用戶程序不能隨意訪問它的寄存器,以維持操作系統“心跳”的節律。
Cortex-M3處理器內部包含了一個簡單的定時器。因爲所有的CM3芯片都帶有這個定時器,軟件在不同 CM3器件間的移植工作就得以化簡。該定時器的時鐘源可以是內部時鐘(FCLK,CM3上的自由運行時鐘),或者是外部時鐘( CM3處理器上的STCLK信號)。不過,STCLK的具體來源則由芯片設計者決定,因此不同產品之間的時鐘頻率可能會大不相同。因此,需要檢視芯片的器件手冊來決定選擇什麼作爲時鐘源。
SysTick定時器能產生中斷,CM3爲它專門開出一個異常類型,並且在向量表中有它的一席之地。它使操作系統和其它系統軟件在CM3器件間的移植變得簡單多了,因爲在所有CM3產品間,SysTick的處理方式都是相同的。 選自CORTEX_M3權威指南 P137
有4個寄存器控制SysTick定時器,如下表所示:

SysTick控制及狀態寄存器(地址:0xE000_E010)

位段 名稱 類型 復位值 描述
16 COUNTFLAG R 0 如果在上次讀取本寄存器後,SysTick已經計到了0,則該位爲1。如果讀取該位,該位將自動清零
2 CLKSOURCE R/W 0 0=外部時鐘源(STCLK)
1=內核時鐘(FCLK)
1 TICKINT R/W 0 1=SysTick倒數計數到0時產生SysTick異常請求
0=數到0時無動作
0 ENABLE R/W 0 SysTick定時器的使能位

SysTick重裝載數值寄存器(地址:0xE000_E014)

位段 名稱 類型 復位值 描述
23:0 RELOAD R/W 0 讀取時返回當前倒計數的值,寫它則使之清零,同時還會清除在SysTick控制及狀態寄存器中的COUNTFLAG標誌

SysTick校準數值寄存器(地址:0xE000_E01C)

位段 名稱 類型 復位值 描述
23:0 CURRENT R/Wc 0 讀取時返回當前倒計數的值,寫它則使之清零,同時還會清除在SysTick控制及狀態寄存器中的COUNTFLAG標誌

SysTick校準數值寄存器(地址:0xE000_E01C)

位段 名稱 類型 復位值 描述
31 NOREF R - 1=沒有外部參考時鐘(STCLK不可用)
0=外部參考時鐘可用
30 SKEW R - 1=校準值不是準確的10ms
0=校準值是準確的10ms
23:0 TENMS R/W 0 在10ms的間隔中倒計數的格數。芯片設計者應該通過Cortex-M3的輸入信號提供該數值。若該值讀回零,則表示無法使用校準功能

在本節中,使用SystemClock作爲systick的時鐘,設置爲1s發生一次systick中斷,所以將reload寄存器設置爲12M,最後是將systick的中斷優先級設置爲最低。調用這個函數之後,就能使能systick了,systick在後面的RTOS實現中扮演着關鍵的角色。

cm3.h

#ifndef CM3_H
#define CM3_H
#include <stdint.h>

#define SCS_BASE            (0xE000E000)                 /*System Control Space Base Address */
#define SYSTICK_BASE        (SCS_BASE +  0x0010)         /*SysTick Base Address*/
#define SCB_BASE            (SCS_BASE +  0x0D00)
#define HSI_CLK             12000000UL
#define SYSTICK_PRIO_REG    (0xE000ED23)

typedef struct systick_tag {
    volatile uint32_t ctrl;
    volatile uint32_t load;
    volatile uint32_t val;
    volatile uint32_t calrb;
}systick_t;

extern void init_systick(void);
#endif /*CM3_H*/

cm3.c

#include "cm3.h"

void init_systick()
{
    systick_t *systick_p = (systick_t *)SYSTICK_BASE;
    uint8_t *sys_prio_p = (uint8_t *)SYSTICK_PRIO_REG;
    /*Set systick as lowest prio*/
    *sys_prio_p = 0xf0;
    /*set systick 1s*/
    systick_p->load = (HSI_CLK & 0xffffffUL) - 1;
    systick_p->val = 0;
    /*Enable interrupt, System clock source, Enable Systick*/    
    systick_p->ctrl = 0x7;
}

printk/DEBUG打印實現

有了串口打印之後,實現printf(k)/DEBUG函數就很簡單了,打印函數實現在os_stdio.c中。關於如何實現printf的文章網上有很多,這裏就不展開了,讀者有興趣可以去參考其他文章。本文重點還是放在RTOS的實現上。DEBUG是一個宏,只有在DEBUG_SUPPORT定義的情況下才會實現打印

os_stdio.h

#define DEBUG_SUPPORT
#ifdef DEBUG_SUPPORT
#define DEBUG printk
#else
#define DEBUG no_printk
#endif /*DEBUG*/

好了,大功告成,執行make run,可以看到sys_tick一秒打印一次如下圖。

2 RTOS初探:任務切換

在上述簡單講了CM3的啓動以及systick組件後,終於可以上硬菜了。好了,本節主要探討兩個問題:
1. 任務是怎麼切換的?
2. 任務是什麼切換的?

本節代碼位於03_rtos_basic下。

任務是怎麼切換的?

任務定義及任務接口定義

task.h定義了任務的數據結構task_t, 以及任務的接口,task_init, task_sched, task_switch, task_run_first。
在當前代碼下,定義了一個任務表g_task_table,該表現在只存放兩個任務的指針,然後定義了g_current_task用來指向當前任務,g_next_task指向下一個準備運行的任務。
任務控制塊task_t中現在只包含一個值,就是當前任務棧的指針。任務與任務之間不共享棧空間,這點在操作系統的書上都有寫,其實你可以把任務當做是通用OS中的內核線程,它們共享全局數據區,但都擁有自己的棧空間。獨立的棧空間對於主要用於保存任務執行的上下文以及局部變量。

圖 雙任務結構

include/task.h

#ifndef TASK_H
#define TASK_H

#include <stdint.h>

typedef uint32_t task_stack_t;
/*Task Control block*/
typedef struct task_tag {

    task_stack_t *stack;
}task_t;

extern task_t *g_current_task;
extern task_t *g_next_task;
extern task_t *g_task_table[2];

extern void task_init (task_t * task, void (*entry)(void *), void *param, uint32_t * stack);
extern void task_sched(void);
extern void task_switch(void);
extern void task_run_first(void);

#endif /*TASK_H*/

任務切換過程

先來談一談任務間切換的過程,兩個任務切換過程原理很簡單,分爲兩部分:
- 保存當前任務的寄存器
本文中使用CM3的PendSV來實現了任務切換的功能。CM3處理異常/中斷時,硬件會把R0-R3,R12,LR,PC, XPSR自動壓棧。然後由PendSV的中斷服務程序(後面簡稱PendSV ISR)手動把R4-R11寄存器壓入任務棧中,這樣就完成了任務上下文的保存。
- 恢復下一個任務的寄存器(包含PC),當恢復PC時就能跳轉到任務被打斷的地方繼續執行。
恢復過程正好與保存過程相反,PendSV ISR會先手動地將R4-R11恢復到CM3中,然後在PendSV ISR退出時,CM3硬件會自動將R0-R3,R12,LR,XPSR恢復到CM3的寄存器中。
如下圖所示,便是任務切換的過程:(注:圖中任務恢復的<—-SP慢了一拍,看官注意下就好了,不想重畫動態圖了,圖層太多了)

好,那我們先看一下任務切換的源代碼。任務切換這一段代碼必須使用匯編來寫,所以將pendsv ISR放在cm3_s.s中實現。 代碼很簡單,首先判斷PSP是否爲0,如果是0的話說明是第一個任務啓動,那麼就不存在任務保存一說,所以第54行就跳轉到恢復任務的代碼,後續會看到第一個任務啓動與其它任務切換稍有不同,會先設置PSP爲0,當然也可以使用一個全局變量來標誌是否是第一個任務啓動,純屬個人喜好。

第61-64行就是將R0-R11保存到當前任務的棧空間中,然後將SP的值賦給任務控制塊中的task_t.stack。這個就完成了整個任務的保存。

第69-73行是將g_next_task指向的任務賦值給g_current_task,然後從g_current_task中取出任務的棧指針。

第75-76行是將任務棧中所保存的R0-R11恢復到CM3的寄存器中。

第78行設置PSP爲當前SP值,79行就直接切換到PSP去運行,需要注意的是,此時此刻的LR寄存器並不是返回地址,而是一個特殊的含義:
在出入ISR的時候,LR的值將得到重新的詮釋,這種特殊的值稱爲“EXC_RETURN”,在異常進入時由系統計算並賦給LR,並在異常返回時使用它。EXC_RETURN的二進制值除了最低4位外全爲1,而其最低4位則有另外的含義(後面講到,見表9.3和表9.4)

位段 含義
31:4 EXC_RETURN標識:必須全爲1
3 0=返回後進入Handler模式
1=返回後進入線程模式
2 0=從主堆棧中做出棧操作,返回後使用MSP,
1=從進程堆棧中做出棧操作,返回後使用PSP
1 保留,必須爲0
0 0=返回ARM狀態。
1=返回Thumb狀態。在CM3中必須爲1

當執行完80行bx lr之後,硬件會自動恢復棧中的值到R0-R3,R12,LR,PC, XPSR。完成任務的切換 摘自《Cortex M3權威指南》

cm3_s.s

 51 pendsv_handler:
 52     /*CM3 will push the r0-r3, r12, r14, r15, xpsr by hardware*/
 53     mrs     r0, psp
 54     cbz     r0, pendsv_handler_nosave
 55
 56     /* g_current_task->psp-- = r11;
 57      * ...
 58      * g_current_task->psp-- = r4;
 59      * g_current_task->stack = psp;
 60      */
 61     stmdb   r0!, {r4-r11}
 62     ldr     r1, =g_current_task
 63     ldr     r1, [r1]
 64     str     r0, [r1]
 65
 66 pendsv_handler_nosave:
 67
 68     /* *g_current_task = *g_next_task */
 69     ldr     r0, =g_current_task
 70     ldr     r1, =g_next_task
 71     ldr     r2, [r1]
 72     str     r2, [r0]
 73
 74     /*r0 = g_current_task->stack*/
 75     ldr     r0, [r2]
 76     ldmia   r0!, {r4-r11}
 77
 78     msr     psp, r0
 79     orr     lr, lr, #0x04   /*Swtich to PSP*/
 80     bx      lr

順帶就把觸發任務切換(即觸發PendSV)的函數講了吧,task_run_first是在啓動第一個任務的時候調用的,而task_switch是在已經有任務的情況下才會調用。所以task_run_first只會被調用一次,而後面的切換全都使用task_switch。兩者唯一的區別在於task_run_first會設置PSP爲0,緣由在上面已經講過,PendSV會根據PSP是否爲0判斷是不是第一次啓動任務。然後往NVIC_INT_CTRL這個寄存器裏觸發PendSV異常即可進行PendSV ISR完成任務切換

cm3.h

 11 #define NVIC_INT_CTRL       0xE000ED04
 12 #define NVIC_PENDSVSET      0x10000000
 13 #define NVIC_SYSPRI2        0xE000ED22
 14 #define NVIC_PENDSV_PRI     0x000000FF

task.c

 43 void task_switch()
 44 {
 45     MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;
 46 }
 47
 48 void task_run_first()
 49 {
 50     DEBUG("%s\n", __func__);
 51     set_psp(0);
 52     MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI;
 53     MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;
 54 }

NVIC_INT_CTRL寄存器片段

任務初始化

在瞭解了任務切換的過程後,就知道去初始化任務了,首先任務需要一段自己棧空間,因此傳入參數stack,然後任務有自己的函數入口地址,因此需要傳入entry,entry需要param作爲函數參數調用,然後每個任務對應一個task_t控制塊。即使是沒有運行過的任務,也需要經過任務切換(PendSV)的招待,也就是將任務棧中的上下文恢復到寄存器中。所以目前爲止,任務初始化就是將相應的寄存器初始值手動PUSH到任務棧中。PC保存的是任務的入口函數,那麼當下一次任務切換時,就能切換到entry函數裏面執行。然後把param參數傳入到entry裏,因爲R0是函數調用的第一個參數,所以需要把param壓棧到R0的位置,最後將棧指針保存到task_t.stack中。

task.c

void task_init (task_t * task, void (*entry)(void *), void *param, uint32_t * stack) 
{                                                                                    
    DEBUG("%s\n", __func__);                                                         
    *(--stack) = (uint32_t) (1 << 24);          //XPSR, Thumb Mode                   
    *(--stack) = (uint32_t) entry;              //PC                                 
    *(--stack) = (uint32_t) 0x14;               //LR                                 
    *(--stack) = (uint32_t) 0x12;               //R12                                
    *(--stack) = (uint32_t) 0x3;                //R3                                 
    *(--stack) = (uint32_t) 0x2;                //R2                                 
    *(--stack) = (uint32_t) 0x1;                //R1                                 
    *(--stack) = (uint32_t) param;              //R0                                 
    *(--stack) = (uint32_t) 0x11;               //R11                                
    *(--stack) = (uint32_t) 0x10;               //R10                                
    *(--stack) = (uint32_t) 0x9;                //R9                                 
    *(--stack) = (uint32_t) 0x8;                //R8                                 
    *(--stack) = (uint32_t) 0x7;                //R7                                 
    *(--stack) = (uint32_t) 0x6;                //R6                                 
    *(--stack) = (uint32_t) 0x5;                //R5                                 
    *(--stack) = (uint32_t) 0x4;                //R4                                 

    task->stack = stack;                                                             
}                                                                                    

那我們看一下應用程序是如何初始化task的。本章的應用只有兩個任務進行來回切換,代碼如下,首先定義了兩個任務task1和task2,然後分別定義了兩個task的棧以及入口函數,在main函數中調用task_init分別對兩個任務進行初始化,然後將任務表的第0個元素指向task1,第1個元素指向task2, 如圖 雙任務結構所示一樣。然後將下一個任務指向g_task_table[0],即task1,調用task_run_first,進行第一次任務切換,也就是啓動第一個任務。
main.c

23 void task1_entry(void *param)    
24 {                                
        ...                           
30 }                                
31                                  
32 void task2_entry(void *param)    
33 {                                
        ...                        
39 }                                
40                                  
41 task_t task1;                    
42 task_t task2;                    
43 task_stack_t task1_stk[1024];    
44 task_stack_t task2_stk[1024];   
45 
46 int main()
47 {
48
49     systick_t *systick_p = (systick_t *)SYSTICK_BASE;
50     clear_bss();
51
52     DEBUG("Hello RTOS\n");
53     DEBUG("psp:0x%x\n", get_psp());
54     DEBUG("msp:0x%x\n", get_msp());
55
56     task_init(&task1, task1_entry, (void *)0x11111111, &task1_stk[1024]);
57     task_init(&task2, task2_entry, (void *)0x22222222, &task2_stk[1024]);
58
59     g_task_table[0] = &task1;
60     g_task_table[1] = &task2;
61     g_next_task = g_task_table[0];
62
63     task_run_first();
64
65     for(;;);
66     return 0;
67 } 

任務是什麼切換? 任務的調度

上述小節回答了任務是怎麼切換的?那麼本小節和下一章將說明任務是什麼切換。在本章中所還未引入systick中斷來處理任務的調度(即什麼時候進行的切換)。爲了給讀者更直觀的印象,本小節將在任務內部進行手動切換任務。首先看一下任務調度的源碼,很簡單。當前任務如果是g_task_table[0],那麼下一個運行的任務就是g_task_table[1],反之一樣,在分配好g_current_task和g_next_task後,調用task_switch進行任務的切換, 即進入PendSV ISR,上一小節已經分析過了PendSV ISR的代碼。

task.c

 32 void task_sched()
 33 {
 34     if (g_current_task == g_task_table[0]) {
 35         g_next_task = g_task_table[1];
 36     } else {
 37         g_next_task = g_task_table[0];
 38     }
 39
 40     task_switch();
 41 }

main.c

18 void delay(uint32_t count)  
19 {                           
20     while(--count > 0);     
21 }                           
22                             
23 void task1_entry(void *param)    
24 {                                
25     for(;;) {                    
26         printk("task1_entry\n"); 
27         delay(65536000);         
28         task_sched();            
29     }                            
30 }                                
31                                  
32 void task2_entry(void *param)    
33 {                                
34     for(;;) {                    
35         printk("task2_entry\n"); 
36         delay(65536000);         
37         task_sched();            
38     }                            
39 }     

看一下任務內部做了什麼?其實很簡單,任務1打印了一句話,然後軟件延時了一段時間,調用task_sched切換到任務2執行,任務2做相同的工作。這樣就實現了連個任務之間來回切換工作,我們可以運行make run,看到運行結果如下所示。

3 任務延時

在上一章中,我們實現了任務切換以及任務的調度。當時我們在任務中用到的延時函數是使用軟件延時來做的,使用這種延時方式來做是有問題的。比如說當task1在執行軟件延時時,task1是獨佔CPU的,這個時候其他的任務是沒辦法使用CPU的。而我們使用操作系統的原因之一就是想讓CPU的利用率足夠高,所以正確的情況應該是當task1調用延遲函數之後,task1應該將CPU使用權交給其他的task。本章就是討論如何實現這樣的任務延時函數。

空閒任務Idle task

在正式開始任務延時的話題前,我們需要先引入空閒任務(idle task)的概念,即所有的任務都暫停的時候,CPU乾點什麼事呢?不可能讓CPU跑飛吧,所以此時引用idle task,讓CPU運行idle task。當其他task被某一種情況喚醒,需要運行的時候,idle task就會交出的CPU的控制權給其他task。
Idle task的定義,初始化等與其他應用task並無差異,直接看代碼。從idle_task_entry中就可以看出空閒任務其實不停地循環,直至被RTOS任務調度函數打斷。空閒與其他的區別是不加入到任務表g_task_table[2]中,它有一個獨立的指針g_idle_task。

task.c

8 static task_t g_idle_task_obj;
9 static task_t *g_idle_task;
10 static task_stack_t g_idle_task_stk[1024];

12 static void idle_task_entry(void *param)
13 {
14     for(;;);
15 }

110 void init_task_module()
111 {
112     task_init(&g_idle_task_obj, idle_task_entry, (void *)0, &g_idle_task_stk[1024]);
113     g_idle_task = &g_idle_task_obj;
114
115 }

任務延時實現

任務延時最理想的實現情況是爲一個任務分配一個硬件定時器,當硬件定時器完成定時後觸發相應的中斷來完成任務的調度。如下圖所示,假設定時之前,當前任務是空閒任務,task1擁有硬件定時器1,task2擁有硬件定時器2,分別計數,當定時器1定時時間到,RTOS將當前任務g_current_task切換到任務1執行。

但這樣存在的問題是,一般的SOC並不具備太多的硬件定時器,所以當任務達到幾十甚至上百個的時候,這種是無法完成的。那就需要軟件的方法來完成任務延時。各位看官應該記得CM3進階章節中的systick定時器,任務延時就使用了這個定時器,我們只使用這一硬件定時器,然後給每一個任務分配一個軟件計數器,當systick發生一次中斷就對task中軟件計數器減1,當某一個任務的軟件計數器到時時,就觸發一次任務調度。如下圖所示:

在理解完使用軟件定時器的原理後,我們直接看代碼,實現在task_t中定義個字段delay_ticks用於軟件計數。然後定義任務延時接口task_delay,其參數是delay_ticks個數,各位看官應該還記得之前systick是1s觸發一次中斷,所以這裏1個delay_tick = 1s。最後定義task_system_tick_handler接口,該接口是被定期器中斷函數調用,這是由於不同的芯片的定時器中斷不同,所以這裏定義一個統一接口讓定時器中斷函數調用,可以看到systick_handler中什麼也沒幹,就是調用task_system_tick_handler。
task.h

  8 typedef struct task_tag {
  9
 10     task_stack_t *stack;
 11     uint32_t delay_ticks;
 12 }task_t;

 22 extern void task_delay(uint32_t ticks);
 23 extern void task_system_tick_handler(void);

cm3.c

 14 void systick_handler(void)
 15 {
 16     /*DEBUG("systick_handler\n");*/
 17     task_system_tick_handler();
 18 }

task_delay接口實現

這個函數非常簡單,僅僅只是對任務表中的delay_ticks進行賦值,然後觸發一次任務調度。因爲一旦有任務調用該接口,就說明當前任務需要延時不需要再佔用CPU,所以需要觸發一次任務調度。
task.c

 92 void task_delay(uint32_t ticks)
 93 {
 94     g_current_task->delay_ticks = ticks;
 95     task_sched();
 96 }

task_system_tick_handler接口實現

這個函數就是遍歷任務表g_task_table,對任務表中的每一個任務的delay_ticks減1,對應於上圖中systick中斷髮生的時候,task1和task2的delay_ticks都會減1操作。前提是確保該task的delay ticks必須大於0才行,delay ticks大於0代表該任務有延時操作。在對所有任務的delay_ticks減1操作後,觸發一次任務調度。
task.c

 98 void task_system_tick_handler(void)
 99 {
100     uint32_t i;
101     for (i = 0; i < 2; i++) {
102         if (g_task_table[i]->delay_ticks > 0) {
103             g_task_table[i]->delay_ticks--;
104         }
105     }
106
107     task_sched();
108 }

任務調度函數task_sched改動

在引用空閒函數以及延時函數之後,需要對調度函數進行一些改造,代碼如下,現在這個函數只是爲了demo任務延時的緩兵之計,後續章節會對該函數進行大改。但在這裏還是理解一下這個函數幹了什麼事。

44-50行處理當前任務是idle task時,分別判斷任務表g_task_table是否有任務已經延時時間到,如果某一個任務延時時間到,那麼將g_next_task指向該任務,然後調用task_switch進行任務切換,如果在任務表中沒有任務延時時間到,那麼就不需要進行任務切換,idle task繼續運行。

53-58行處理當前任務是task1時,如果task2的延時時間到,那麼就切換到task2中執行;如果task1的delay_ticks不爲0,那麼切換到idle task運行,這種情況實際上就是task1調用了task_delay函數觸發的任務調度引起;如果兩種都不是,那就不需要進行任務調度,還是繼續運行task1。

61-68行處理當前任務是task2的情況,其邏輯跟task1一樣,不再重複。

41 void task_sched()
 42 {
 43
 44     if (g_current_task == g_idle_task) {
 45         if (g_task_table[0]->delay_ticks == 0) {
 46             g_next_task = g_task_table[0];
 47         } else if (g_task_table[1]->delay_ticks == 0) {
 48             g_next_task = g_task_table[1];
 49         } else {
 50             goto no_need_sched;
 51         }
 52     } else {
 53         if (g_current_task == g_task_table[0]) {
 54             if (g_task_table[1]->delay_ticks == 0) {
 55                 g_next_task = g_task_table[1];
 56             } else if (g_current_task->delay_ticks != 0) {
 57                 g_next_task = g_idle_task;
 58             } else {
 59                 goto no_need_sched;
 60             }
 61         } else if (g_current_task == g_task_table[1]) {
 62             if (g_task_table[0]->delay_ticks == 0) {
 63                 g_next_task = g_task_table[0];
 64             } else if (g_current_task->delay_ticks != 0) {
 65                 g_next_task = g_idle_task;
 66             } else {
 67                 goto no_need_sched;
 68             }
 69         }
 70     }
 71
 72
 73     task_switch();
 74
 75 no_need_sched:
 76     return;
 77 }

應用代碼測試

首先在main函數要調用init_task_module()來初始化空閒任務idle task。然後將task1和task2中delay(65536000)改爲task_delay。task1 延時一個tick(相當於1s),而task2延時5個tick,最後結果可以看到task1與task2交替執行,但task1打印5句時,task2纔打印一句,這就證明延時函數工作了。
main.c

 18 void task1_entry(void *param)
 19 {
 20     init_systick(1000);
 21     for(;;) {
 22         printk("%s\n", __func__);
 23         task_delay(1);
 24     }
 25 }
 26
 27 void task2_entry(void *param)
 28 {
 29     for(;;) {
 30         printk("%s\n", __func__);
 31         task_delay(5);
 32     }
 33 }

 40 int main()
 41 {
        ...
 56     init_task_module();
 57
 58     task_run_first();
        ...
 61     return 0;
 62 }


雖然從打印上來看,跟之前純軟件延遲差不太多,但其背後的原理是完全不同的。純軟件在延時不釋放CPU,會使其他任務得不到CPU使用權,而調用task_delay接口,當前任務就會釋放CPU使用權,RTOS會進行一次任務調度將CPU使用權交給其他任務。

DAY1總結

總結第一天的如下:
1. 環境搭建
2. QEMU CM3仿真:UART打印,systick,gdb調試
3. RTOS基礎:任務切換/任務調度/任務延時簡單實現(基於雙任務及空閒任務)。

第二天會涉及RTOS的內核核心實現,包括任務掛起/喚醒/刪除,延時隊列,臨界區保護,優先級搶佔調度及時間片調度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章