分享到plurk 分享到twitter 分享到facebook

版本 29b7320acc74e68c72b8aef0051b4dd980aca7f6

rtenv-plus

HackPad 共筆<https://embedded2014.hackpad.com/Rtenv-plus-0VHEYKBpr3D>_

組員

楊震 / <Omar002<https://github.com/Omar002/rtenv-plus/>_>

丁士宸 / <Stanley Ding<https://github.com/StanleyDing/rtenv>_>

程政罡 / <marktwtn<https://github.com/marktwtn/rtenv-plus>_>

李昆憶 / <LanKuDot<https://github.com/LanKuDot/rtenv-plus>_>

鄭聖文 / <Shengwen<https://github.com/shengwen1997/>_>

作業系統架構

Context Switch

Kernel Mode 與 User Mode 間的轉換

kernel mode — activate() —> user mode — syscall or interrupt —> exception — exception handler —> kernel mode

.. image:: /embdded/rtenv-plus/kernel_mode_vs_user_mode.PNG 注:在下方 fork原理 以 fork 為例,可以知道轉換細節。

activate

  • 功能:從 kernel mode 轉換成 user mode

.. code-block:: c

activate:
 41     /* save kernel state */
 42     mrs ip, psr
 43     push {r4, r5, r6, r7, r8, r9, r10, r11, ip, lr}
 44     
 45     ldmia r0!, {r4, r5, r6, r7, r8, r9, r10, r11, lr}
 46     ldmia r0!, {r7}
 47
 48     /* switch to process stack pointer */
 49     msr psp, r0
 50     mov r0, #3
 51     msr control, r0
 52     
 53     /* program breaking code
 54     pop {r4, r5, r6, r7, r8, r9, r10, r11, lr}
 55     pop {r7}
 53     */
 54     bx lr
  • 指令介紹:mrs Rd, PSRmsr PSR, Rd[#]_:Rd 為 general-purpose registers,PSR 可以為 psr、cpsr、apsr、msp、psp 等。 mrs Rd, PSR 可以將 PSR 的值寫到 Rd,而 msr PSR, Rd 則是將 Rd 值寫到 PSR 裡。

  • 運作:

    • L42, 43:將 psr(program status register) 的值保存到 ip (r12) 裡,然後一同 push 到 main stack 裡。

    • L45:將 user_thread_stack 的 register 依序 pop 到 r4~r11 及lr,也是為何 user_thread_stack 的前9個 register 設計為 r4~r10、fp、_lr。

    • L46:再將 user_thread_stack_r7 pop 到 r7

    • L49:將 r0 所帶的值寫入到 psp (process stack pointer),注意呼叫 activate 所放的參數就是該 task 之 task_control_block 中 stack 的 address。

    • L50, 51:將 control register 的值設為 3,藉此可以將 stack pointer 轉為指向 process stack (使 sp 值為 psp)。所以藉由 sp 可以存取其 stack 的內容。

    • 所以除了 r0~r3 及 ip、sp、pc、cpsr 之外,都被換成 user-mode 的 register 了。

task

rtenv-plus 中的 task 以 task_control_block 來呈現其資訊。

.. code-block:: c

struct task_control_block {
    struct user_thread_stack *stack; // 指到記憶體中 stack 的位置
    int pid; //記錄目前 task 的 pid
    int status; //記錄目前 task 的狀態
    int priority; //記錄目前 task 的優先度

    struct list list; 
};

// 其中 status 共有 5 種狀態被定義
#define TASK_READY      0
#define TASK_WAIT_READ  1
#define TASK_WAIT_WRITE 2
#define TASK_WAIT_INTR  3
#define TASK_WAIT_TIME  4

// 其中 priority 預設為 20,最低的 priority 為39

GDB macro 利用 macro 可以快速查看目前的 task 是哪一個,並顯示出目前所有 task 的狀態以及優先度

::

define print_status
    if($arg0 == 0)
        printf "TASK_READY     "
    end
    if($arg0 == 1)
        printf "TASK_WAIT_READ "
    end
    if($arg0 == 2)
        printf "TASK_WAIT_WRITE"
    end
    if($arg0 == 3)
        printf "TASK_WAIT_INTR "
    end
    if($arg0 == 4)
        printf "TASK_WAIT_TIME "
    end
end

define ps
    printf "pid    status            priority\n"

    set $t = tasks
    set $i = 0
    while $i != task_count
        if ($i == current_task)
            printf "*%2d    ", $t[$i].pid
        else
            printf "%3d    ", $t[$i].pid
        end
        print_status $t[$i].status
        printf "%11d\n", $t[$i].priority
        set $i = $i + 1
    end
end

.. image:: /embdded/rtenv-plus/gdb_macro.png

init_task

  • 功能:將系統初始函式 first() 的位址放置到 process stack 的 lr 位置。藉由 activate 置換 process state 上來,可讓程式執行 first()

  • 運作:

.. code-block:: c

/* 傳入的參數為:欲執行 first() 的 task 的 stack位址 以及 first() 的位址 */
unsigned int *init_task(unsigned int *stack, void (*start)())
{
    /* 由於 stack 的設計為 full descendent stack,
     * 所以 stack pointer 一開始必須指向最高位址。
     * 觀察 user_thread_stack 的設計:r4 是最低位址,處在 stack 的底部
     * 而預期將 first() 的位址存到 _lr 中,所以必須 push 9個 word
     */
    stack += STACK_SIZE - 9;
    /* 利用 pointer arithmetic,可以將 first() 的位址存到 _lr 中:
     * user_thread_stack -> |r4 |r5 |r6 |r7 |r8 |r9 |r10|fp |_lr|...
     *             stack -> |[0]|[1]|[2]|[3]|[4]|[5]|[6]|[7]|[8]|...
     */
    stack[8] = (unsigned int)start;
    /* 回傳新的 sp 給該 task */
    return stack;
}

How to fork

第一次進到 while loop

::

[ File: main.elf ]
while (1) { 
    tasks[current_task].stack = activate(tasks[current_task].stack);
333c:   f240 72a8   movw    r2, #1960   ; 0x7a8
...
3354:   681b        ldr r3, [r3, #0]
3356:   4618        mov r0, r3
3358:   f00e f8e2   bl  11520 <activate>    // 由此進入 activate,所以 LR 存的值是 0x335d
335c:   f240 72a8   movw    r2, #1960   ; 0x7a8
3360:   f2c2 0200   movt    r2, #8192   ; 0x2000
...
  • 進到 activate() 後,藉由 pop user state 到 register,將預先存好的 first() 的位址存到 LR 中。而原本的 LR 被 push 到 main stack 中,存有離開 activate() 後繼續執行的指令位址。Core registers 及 stacks 的狀態變化如下圖:

    .. image:: /embedded/activate.PNG

    利用 bx lr 使得程式轉往執行 first()

  • 進到 first() 後,程式執行第一行的 fork()

    ::

    [ 程式執行 fork() ] void first() { 2f84: b580 push {r7, lr} 2f86: af00 add r7, sp, #0 if (!fork()) setpriority(0, 0), pathserver(); 2f88: f00e fb22 bl 115d0 // 由此進入 fork(),LR存的值為 0x2f8d 2f8c: 4603 mov r3, r0 2f8e: 2b00 cmp r3, #0

  • 在 syscall (這裡是fork) 中,會觸發 svc exception,程式轉往執行 SVC_Handler()

    .. image:: /embedded/SVC_Handler.PNG

    • 左圖:同時 processor 會將 xPSR、PC、LR、R12、R3、R2、R1、R0[#]_依序 push 到目前的 stack 中 ( process stack ),被 push 到 process stack 的資訊中含有離開 fork() 後繼續執行的指令位址。
    • 中圖:將目前的 state 存到 process stack。原本的 LR 含有 exception return ( 0xfffffffd ) 的資訊,也會一併被 push 到 process stack 中儲存。
    • 右圖:將 kernel state 從 main stack 中 pop 出來,此時 LR 擁有的位址為離開 activate() 後要執行的指令位址(之前在進入 activate() 時所存的 )。所以離開 SVC_Handler() 後,程式會轉往執行 main() 的 while loop,也就是 kernel mode。
  • 目前的 process stack 狀態,會發現與 user_thread_stack 的設計一致,以及不同時期被 push 進去的 R7:

    .. image:: /embedded/user_thread_stack.PNG

    • 在離開 activate() 函式會回傳新的 process stack pointer 給 TCB 的 stack pointer,也就是取得 R0 的值,所以 TCB 的 stack pointer 會得到正確的 process stack 的正確位址。

進行fork

  • 開始執行 kernel mode 後,藉由 tasks[current_task].stack->r7,可以取得在 fork() 傳入的值。因此 kernel 判定要執行 fork 動作,將母 task 的 stack 內容複製到子 task 的 stack 中,但是母 task 的 r0 存的是目前產生的 task 數量,而子 task 則是 0。

母task

  • 進到 activate() 後,再次將 kernel state 與 user state 作交換。此時,LR 含有 EXC_RETURN[#]_的值 0xfffffffd,則當 processor 執行 bx lr 時必須進行 exception return。

    .. image:: /embedded/activate_again.PNG

  • Exception return:當 LR 值為 EXC_RETURN 之一: 0xfffffffd 時,

    1. processor 會轉回 thread mode
    2. 從 process stack 取回 exception 時所 push 進去的 registers
    3. 使用 PSP 為當下的 SP
  • 也就是說,進行 exception return 後,PC 會擁有之前 exception 發生的的下一行指令位址,至於 LR 則為離開 fork() 而回到 first() 繼續執行的位址。

    .. image:: /embedded/activate_again_exc_return.PNG

    ::

    [ fork() ] .global fork fork: push {r7} 115d0: b480 push {r7} mov r7, #0x1 115d2: f04f 0701 mov.w r7, #1 svc 0 115d6: df00 svc 0 // <- Exception 在這裡發生,所以 PC 被存的值為 0x115d8 nop 115d8: bf00 nop // <- 藉由 exception return 使的程式回到這裡繼續執行 pop {r7} 115da: bc80 pop {r7} bx lr 115dc: 4770 bx lr // <- 回到 first 繼續執行

  • 由於在 kernel mode 中,已經將 fork() 所應回傳的值放到 process stack 的 r0 中,藉由 exception return 將這個值 pop 到 R0。則當程式離開 fork() 時,會回傳 task_count (R0)。 因此,母 task 在 if(!fork()) 為 false,則繼續執行下一個 fork()。

    .. code-block:: c

    void first() { if (!fork()) setpriority(0, 0), pathserver(); // 子 task 的 if 判定為 true,於是執行 pathserver() if (!fork()) setpriority(0, 0), romdev_driver(); // 母 task 的 if 判定為 false,於是繼續執行下一行 …

子task

  • activate() 與 exception return 的行為與母 task 相同,只是在 if(!fork()) 判定中為true (因為回傳值為 0),所以就會進行 if 下的函式,於是新的 task 就開始運行了。

list

  • 實作檔案:list.c

  • 類型:cyclic double linked list

    .. image:: /embedded/rtenv-plus/double_linked_list.PNG

  • 函式:

    • list_init:初始化 node,prev 及 next 都指向自己。
    • list_empty:如果 list 的 next 還是指向自己,代表 list 為空。
    • list_remove:將指定的 node 從 linked list 中移除。
    • list_unshift:將 new 從原本的 linked list 中移除,再將其 push 到 list 的 next。
    • list_push:將 new 從原本的 linked list 中移除,再將其 push 到 list 的 prev。
    • list_shift:將 list 的 next 從 linked list 中 pop 出來。
  • Macro:

    • list_entry:取得該 list node 所屬的 structure 或 union variable 的位址。

    .. code-block:: c

    #define list_entry(list, type, member)
    (container_of((list), type, member)) #define container_of(ptr, type, member)
    ((type )(((void )ptr) - offsetof(type, member)))

    Macro offsetof( type, member ) 會以 bytes 的形式回傳指定 member 在 type 指定的 structure 或 union 的位置。如:

    .. code-block:: c

    struct foo { int a; int b; };

    則 offsetof( foo, a ) 會回傳 0,offsetof( foo, b ) 會回傳 4。看到 TCB 的設計:

    .. code-block:: c

    struct task_control_block { struct user_thread_stack *stack; int pid; int status; int priority;

      struct list list;

    };

    使用 task = list_entry(curr, struct task_control_block, list); 傳入 list node 的位址減去 list node 在 structure 中的 byte 位置,就會得到該 structure 第一個元素的起始位址,同時也是該 structure 變數的位址。

    • list_for_each:以 list 為起點,搜尋所有 node。
    • list_for_each_safe:以 list 為起點,但考慮到有些因為呼叫如 list_shift 之類的 function 會把 list node pop 出來,導致搜尋的連結中斷,list_for_each_safe 讓剩下的 node 可以繼續被搜尋。

ready_list

rtenv-plus maintain 一個 global 的 list 陣列 ready_list,為排程的資料結構,ready_list 的每一個元素都對應到不同 priority。

.. code-block:: c

struct list ready_list[PRIORITY_LIMIT + 1];

.. image:: /embedded/rtenv-plus/ready_list_array.png

使用 list_push 將 task push 到對應優先權的 ready_list 中,多個 task 之優先權可能相同

.. image:: /embedded/rtenv-plus/ready_list_element.png

判別 ready_list 中的 element 是否為 empty,則觀察有沒有 task 被 push 到該 element 中

.. image:: /embedded/rtenv-plus/ready_list_empty.png

當 rtenv-plus 分配給 task 的執行時間到了,且正在執行的 task 之優先權所對應到的 ready_list 元素,最前端為正在執行的 task, 則將該 task 放到 ready_list 元素的最末端。

.. code-block:: c

task = &tasks[current_task];
if (timeup && ready_list[task->priority].next == &task->list)
    list_push(&ready_list[task->priority], &tasks[current_task].list);

每次 rtenv-plus 在選擇下一個要執行的 task,會從 ready_list 中尋找優先權最大且最前端的 task。

.. code-block:: c

for (i = 0; list_empty(&ready_list[i]); i++); // 尋找非空的 ready_list。

list = ready_list[i].next; // 將 list 指到 ready_list[i].next 所指的 `struct list`,這個 list 會指到某一個 struct task 裡頭的 struct list。
task = list_entry(list, struct task_control_block, list); // 將 task 指到上面講的 struct task
current_task = task->pid; // 記錄這個 task 的 pid

.. code-block:: c

struct task_control_block {
    struct user_thread_stack *stack;
    int pid;
    int status;
    int priority;

    struct list list;
};

Event Monitoring

  • 實作檔案:event_monitor.c
  • 功能:負責接受來自 event sources 所發出的 event occurrences,並將這些 pending 轉給 handlers 來處理 event。[#]_
  • 特性:
    • Event Collection:kernel 擁有一個 event monitor 來收集 occurrences 並轉送給 handlers。
    • Event Sensor:透過 event_monitor 中的 events 來幫 handler “裝上 Sensor”。而每個 event sensor 都有 pending 用以暫存發出 occurrence 的 task。
  • 函式:
    • event_monitor_init:初始化 event_monitor
    • event_monitor_find_free:找尋有無尚未被使用的 sensor
    • event_monitor_register:為 handler 裝上 sendor
    • event_monitor_block:將發出 event occurrence 的 task 從 ready_list 取出放到對應 event 的 pending list 裡,使 task 等待 handler 的處理
    • event_monitor_release:標記有 occurrence pending
    • event_monitor_serve:檢查 pending,並讓 handler 處理 pending。處理完後將 task 從 pending list 取出到 ready list 裡

System Call

  • 功能:將 syscall 代碼存入 R7,觸發 SVC exception,將代碼存入 Processor stack,並轉換成 Kernel Mode 後,在 main() 中處理 system call。

File System

在 rtenv-plus 中,與檔案系統有關的 task 有三個:

  • pathserver():檔案系統的最上層,負責管理已經註冊( register,或暫存 )的檔案路徑及 mount point。
  • romfs_server():向 pathserver() 註冊 ROMFS_TYPE,處理對於擁有 ROMFS_TYPE 的 mount point 的請求。
  • romdev_driver():檔案系統的底層,取得檔案的資訊與資料。

初始化

  • romfs_server():向 pathserver() 註冊 fs type - ROMFS_TYPE,使得 pathserver() 處理向註冊為 ROMFS_TYPE 的 mount point 請求時,可以透過 romfs_server() 來處理。
  • romdev_driver():向 pathserver() 註冊所管理的檔案路徑 ROMDEV_PATH,並設置該 path 所屬的 file descriptor,以作為取得檔案資訊之用。
  • first() 中的 mount("/dev/rom0", "/", ROMFS_TYPE, 0):設置掛載點,對於尋找 / 目錄下的檔案,會往 /dev/rom0 來尋找,並透過 romfs_server() 以及 romdev_driver()

Pipe 的 Read 與 Write

serialout()rs232_xmit_msg_task() 為例。/dev/tty0/out 為 fifo pipe,而 rs232_xmit_msg_task() 寫入資料到 pipe 中,serialout() 從這個 pipe 讀出資料。

.. image:: /embedded/rtenv-plus/Read_and_write.png

以 write 的部分為例,在 FILE_ACCESS_ACCEPT 的 case 中會送出 read_event pending。執行完 write 的工作後,kernel 會繼續執行 event_monitor_serve(),檢查有沒有 event pending,此時,剛剛送出的 read_event 就會被處理,檢查有沒有在 read 的時候被 push 到 read_event list 的 task。如果有,則執行 read_event 的 handler - pipe_read_release()pipe_read_release() 會重新呼叫 file_read(),讓 task 再次嘗試讀取 pipe,流程判斷如上圖,但是不同的 case 有不同的處理方式:

  • FILE_ACCESS_ACCEPT:值為1,代表讀取成功。event_monitor_serve() 會將該 task 從 read_event 的 list 中 pop 出來,並重新 push 回 ready_list 裡,task 的狀態也被改為 TASK_READY。
  • FILE_ACCESS_BLOCK:值為0,代表仍舊未達指定讀取量,則會被繼續 BLOCK 住。
  • FILE_ACCESS_ERROR:不會發生。因為在第一次嘗試讀取時,如果發生 FILE_ACCESS_ERROR,該 task 不會被 push 到 read_event 的 list 中。

至於 read,則反之。下圖為從輸入到輸出所牽涉的 task 以及使用的 pipe,其中箭頭的起點方執行 write,終點方為 read:

.. image:: /embedded/rtenv-plus/Tasks_with_pipe.PNG

移植過程

  1. 將舊的函式庫移除
  1. 註解硬體相關的函式庫
  2. 嘗試編譯
  3. 找出相依的 function

.. image:: /embedded/rtenv-plus/comment_header.png .. image:: /embedded/rtenv-plus/dependency.png 2. 加入新的函式庫 (他有的我不能少) a. 在檔案中加入標頭檔 b. 修改 Makefile 中的 include path

.. image:: /embedded/rtenv-plus/makefile_port.png 3. 確認硬體是能動的 a. 跑範例程式 b. 將範例程式丟入作業系統看是否能正常運作 c. 移除範例程式,使用原先硬體 d. 修改系統參數 UART2 -> UART1 e. 嘗試 make。 f. 把不過的部份暫時拿掉,例如 romfs

  1. 程式死了
  1. gdb 下去追
  2. 修掉架構相關的 bug
  3. 修改架構所需的常數

.. image:: /embedded/rtenv-plus/CPUCLOCK.png

安裝與測試

  1. 下載 rtenv-plus 程式碼

https://github.com/StanleyDing/rtenv-plus

https://github.com/Omar002/rtenv-plus

  1. 安裝 st-link

http://github.com/texane/stlink.git

  1. 安裝 screen

sudo apt-get install screen

  1. 把線接好

  2. 燒錄 rtenv-plus

  1. 進入 rtenv-plus 資料夾
  2. make flash
  1. 開啟 screen

screen /dev/ttyUSB0 115200 8n1

  1. 按下板子上的 reset 按鈕

硬體驅動原理

  • USB OTG</embedded/OTG>_

效能表現

參考資料

.. [#] mrs指令<http://infocenter.arm.com/help/topic/com.arm.doc.dui0489i/Cihjcedb.html>_ 、 msr指令<http://infocenter.arm.com/help/topic/com.arm.doc.dui0489i/Cihibbbh.html>_

.. [#] Cortex-M3 Exception Entry<http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0552a/Babefdjc.html>_

.. [#] Cortex-M3 Exception Return<http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0552a/Babefdjc.html>_

.. [#] WikiPedia: Event Monitoring<http://en.wikipedia.org/wiki/Event_monitoring>_