在過去Cortex-M單晶片開發經驗中,使用過新唐、Renesas、STM32和TI,搭配使用過的開發IDE有IAR、e2studio、STM32CUBE、keilc、和CCS,主要focus在算法/功能的開發,從來沒認真探討過這些MCU的底層基礎建設(HAL、BSP、bootloader、RTOS)以及IDE背後是如何對MCU進行Compile and fLASH(Cross Compile),並且透過Uart在電腦端進行real-time debug,所以這次想透過STM32加上Ubuntu的開源環境,對整個嵌入式系統編譯過程進行學習。
如此未來在開發嵌入式系統時就不會被該產品的IDE綁定,能使用各種opensource的editor如vim、vscode、atom等,此外能對RTOS的系統有更深入的了解。
另外開發經驗中都是以bare-metal的方式,簡單以幾個interrupt priority設定完成整個專案所需功能,但當整個系統越來越複雜non-OS的開發方式肯定是不夠的,考慮到模組化開發需要有分時多工(TDM)的程式架構,所以這次也會透過STM32所提供的Motor Control(MC) SDK中的FreeRTSO範例對MCU底層基礎建設和RTOS概念進行一個完整的學習,並且以RTOS的角度去分析stm32的MCSDK。
我的開發環境
在x86電腦中使用gcc提供的arm compiler進行cross compile,compile後會產生16進位檔.hex,再透過openocd燒錄置mcu的memory中。
在單晶片開發時,大部分都採用燒錄後執行程式,並且透過UART吐出數據的方式來debug。不過隨著系統的複雜度上升,這樣的開發方式逐漸會遇到瓶頸。也因此,比較大的晶片例如32位元的,通常會內建JTAG/SWD/SWIM等燒錄介面,搭配OpenOCD+GDB進行debug(簡單來說就是能夠單步執行並且隨時能知道記憶體內容),整體系統架構如下圖所示:
from Linaro
VSCODE C/C++ CMAKE CMAKE TOOLS Cortex-Debug
!
STM32CUBE
直接官網下載,我們需要他來產生相應板子的MAKEFILE。
OpenOCD燒錄工具
OpenOCD is a free-software tool mainly used for on-chip debugging, in-system programming and boundary-scan testing.
gcc-arm-none-eabi compiler
直接到官網Arm GNU下載,並利用增加環境變數(.bashrc)的方式進行安裝。
STM32Cube產生Makefile
vscode的環境設置參考一下YT(不然main.c會有error)
vscode環境設置
compile toggle led project 生成.hex檔案
OpenOCD與STlink連線
接下來進行燒錄與reset後程式成功燒進mcu
測試後發現arm gdb工具在ubuntu 22.04 有bug 目前無法使用gdb工具
The type of an operating system is defined by how the scheduler decides which program to run when. For example, the scheduler used in a multi user operating system (such as Unix) will ensure each user gets a fair amount of the processing time. As another example, the scheduler in a desk top operating system (such as Windows) will try and ensure the computer remains responsive to its user.
The scheduler in a Real Time Operating System (RTOS) is designed to provide a predictable (normally described as deterministic) execution pattern. This is particularly of interest to embedded systems as embedded systems often have real time requirements. A real time requirements is one that specifies that the embedded system must respond to a certain event within a strictly defined time (the deadline). A guarantee to meet real time requirements can only be made if the behaviour of the operating system's scheduler can be predicted (and is therefore deterministic).
Traditional real time schedulers, such as the scheduler used in FreeRTOS, achieve determinism by allowing the user to assign a priority to each thread of execution. The scheduler then uses the priority to know which thread of execution to run next. In FreeRTOS, a thread of execution is called a task
From freeRTOS官網的介紹
接下來以Motor Control為例子介紹何謂RTOS
,基本上RTOS系統可分為Hardware-independent的Thread(Task) & interrupt,跟Hardware-dependent的Kernel,若以Motor Control來講其包括的Task和Kernel舉例如下。 在freeRTOS中稱Thread為
Task
。
from 工研院 嵌入式即時作業系統於運動控制領域之運用
TASK
Background interrupt
其中核心的中斷只有ADC中斷,會進行FOC運算,更新Motor每個周期所需之PWM,其餘的外部中斷都非常簡單。
KERNEL
在STM32中把RTOS & Motor control LIB稱作
MiddleWares
,用詞似乎不準確。
以下是本人對ROTS的理解,RTOS的Real time其注重的是核心background interrupt
的即時性,例如freeRTOS中會設定讓Kernel的SystemTick
中斷的priority為最低,也就是在FreeRTOS中Kernel的時脈xPortSysTickHandler
優先權,所以表示任何在FreeRTOS kernel上執行的軟體Task,都不會打擾核心硬體中斷,例如 : motor current control loop(更新PWM)。
只有在要進行一些核心的程式碼操作時,例如:scheduling、queue等操作時,會強制MASK所有的interrupt,防止在使用FreeRTOS API的情況下,Kernel被打擾造成kernel crush,如此在獲得freeRTOS所提供的好處的同時,還能確保控制系統中重要的中斷(current loop),能夠即時的更新PWM命令去控制馬達。
另外較不重要的外部中斷,例如 : I2C、UART、Button I/O這類的中斷通常非常簡單,會瞬間執行完畢,其目的只是要透過中斷去呼叫使用者針對這個I/O所要做的Task,取代Polling。
The priorities between Task and Interrupt
Interrupt priorities are purely a hardware thing. Task priorities are purely a software thing. Tasks have the ability to mask out interrupts, but if interrupts are not masked they will always interrupt a task – even if it is the lowest priority interrupt that is interrupting the highest priority task.
但在freeRTOS的官方文件中也有提到,在使用freeRTOS時對於整理程式的效率不一定會提高,雖然freeRTOS能減少許多非必要的運算(CPU IDLE),但也增加了RTOS tick跟Contex switch的負擔,所以運算效率還是要取決於實際應用,但將程式模塊化方便閱讀、維護與擴充這點確實是freeRTOS的優點。
而如果是在更複雜的機器人控制系統或是無人機系統中,RTOS的使用就是必要的,可以想像機器人控制系統中,會將需要即時處理的FOC運算放在中斷(或直接以ASIC硬體實現),而各種位置、運動軌跡、姿態和視覺的計算則以TASK的方式進行分時多工(TDM),不只模組化方便後須維護,且在各Task之間還提供了插隊機制,可將重要的任務(例如:硬體保護任務)指定較高優先權,例如﹔知名的無人機專案Pixhawk中使用的Nuttx。
Jiffies
Jiffies is a unit of time used in computing to represent the duration of a single tick of the system timer interrupt. It's commonly used in the Linux kernel to keep track of the elapsed time since the system was booted. The length of a jiffy can vary depending on the system configuration and can be expressed in either clock cycles or in nanoseconds. When we talk about freertos in Stm32 MCU, freertos using sysmtem tick under the help of HAL to determine the Jiffies.
CONTEXT SWITCH
A context switch is the process of storing the state of a running task (also known as a "process") and loading the state of a different task. This allows multiple tasks to share a single CPU, as the CPU appears to be executing only one task at a time. In a context switch, the operating system saves the current state of the task that was running, including its register values and program counter, and then restores the state of the task that is to resume execution. Context switches can occur due to various reasons, such as the scheduling of a higher-priority task, a task yielding voluntarily, or the completion of a task. Context switches can have an overhead in terms of performance and should be minimized as much as possible for efficient system operation.
POSIX STANDARDS
POSIX (Portable Operating System Interface) is a family of standards specified by the IEEE for maintaining compatibility between operating systems. The POSIX standards define a common interface for operating system services such as file and process management, user authentication, and inter-process communication, among others.
POSIX is designed to be a portable interface, meaning that software written to comply with the POSIX standards can be run on any operating system that implements those standards. This allows developers to write software that can run on multiple platforms with minimal changes, increasing compatibility and portability.
POSIX is widely used in Unix-like operating systems, as well as in some real-time operating systems. It is also used as a basis for many programming APIs and is supported by many programming languages.
在freeRTOS中好像也能夠設定POSIX標準
透過成大師生整理的NCKU wiki、freertos的官網以及Real-time Operating Systems Book 2來學習freertos。
FreeRTOS 是一個相對其他作業系統而言較小的作業系統。最小化的 FreeRTOS核心僅包括 3 個 .c 文件(tasks.c、queue.c、list.c)和少數標頭檔,總共不到9000 行程式碼,還包括了註解和空行。一個典型的編譯後binary(二進位碼)小於 10 KB。
FreeRTOS 的程式碼可以分為三個主要區塊:任務、通訊和硬體界面。
在不同硬體裝置上,通訊埠設定上也不同,定義在portmacro.h
標頭檔內,有兩種特殊資料型態portTickType以及portBASE_TYPE。另外FreeRTOS明確的定義變數名稱以及資料型態,不會有unsigned以及signed搞混使用的情形發生。
TASK的狀態
Blocked vs Suspended
blocked 是說如果有個 task 將要等待某個目前無法取得的資源(mutex lock or waiting semaphore),則會被設為 blocked 狀態(這是被動的),OS 會呼叫 blocking API 來設定 task 進入 blocked queue。而suspended 與 blocked 的差異在於,suspended 是 task 主動呼叫 API 來要求讓自己進入暫停狀態的,每一種狀態 FreeRTOS 都會給予一個 list 儲存(除了 running)。
一個典型的Task如下
Blocked
Task 的 blocked 狀態通常是 task 進入了一個需要等待某事件發生的狀態,這個事件通常是執行時間到了(例如 systick interrupt)或是同步處理的回應,如果像一開始的 ATaskFunciton() 中使用 while(1){} 這樣的無限迴圈來作等待事件,會占用 CPU 運算資源,也就是 task 實際上是在 running,但又沒做任何事情,占用著資源只為了等待 event,所以比較好的作法是改用 vTaskDelay(),當 task 呼叫了 vTaskDelay(),task 會進入 blocked 狀態,就可以讓出 CPU 資源了。
使用 infinite loop 的執行時序圖
使用 vTaskDelay() 的執行時序圖
vTaskDelay():這個函式的參數如果直接給數值,是 ticks,例如 vTaskDelay(250) 是暫停 250 個 ticks 的意思,由於每個 CPU 的一個 tick 時間長度不同,FreeRTOS 提供了 portTICK_RATE_MS 這個巨集常數,可以幫我們轉換 ticks 數為毫秒 (ms),也就是說 vTaskDelay( 250/portTICK_RATE_MS ) 這個寫法,就是讓 task 暫停 250 毫秒(ms)的意思 (v8.2.1 改名為 portTICK_PERIOD_MS)
Suspended
如果一個 task 會有一段時間不會執行,那就可以進入 suspend 狀態。
例如有個 task 叫做 taskPrint,只做 print 資料,而有好幾個 operation 負責做運算,若運算要很久,則可以把 taskPrint 先丟入 suspend 狀態中,直到所有運算皆完成後,再喚醒 taskPrint 進入 ready 狀態,最後將資料 print 出來。
FreeRTOS 使用 ready list 去管理準備好要執行的 tasks,而 ready list 的資料儲存方式如下圖。
這邊就想到之前上過Jserv大的Linux核心設計線上課程,其中提到所謂嵌入型的list
概念。只要在自定義的結構中加入 struct list_head,就可以搭配 Linux 中一系列的 linked list 操作。這部分的詳細實作可以參考Jserv的教材 lab0-c。細節關於嵌入型list可參考你所不知道的C語言linked list篇。
task.c
task.h
負責所有關於建立、排程和維護任務的繁重工作
taskSELECT_HIGHEST_PRIORITY_TASK
OS 會在進行 context switch 時選出下一個欲執行的 task。
下面是在 ready list 中依照優先權選取執行目標的程式部分,FreeRTOS 的優先權最小爲 0,數字越大則優先權越高。
一個ReadyTaskList中的每個索引(pxIndex)各自指向了一串 task listt,所以listGET_OWNER_OF_NEXT_ENTRY 就是在某個ReadyTaskList索引中去取得其中 task list 裡某個 task 的 TCB
configASSERT()程式開階段所使用的DEBUG工具,當輸入為false會關閉中斷功能進入無限迴圈。
xTaskCreate()
建立TASK的函數 xTaskCreate
WHY using dynamic alloca for TCB?
在進行pvPortMalloc時候,會先進行vTaskSuspendAll(); ,藉由不發生context swiitch的swap out動作,配置記憶體空間,等到配置完成再呼叫xTaskResumeAll()。pvPortMalloc在heap.c
裡面定義,基本上就是做記憶體配置,根據各不同port去實作pvPortMalloc。
爲了便於排程,創造新 task 時,stack 中除了該有的資料外,還要加上『空的』 register 資料(第一次執行時理論上 register 不會有資料),讓新 task 就像是被 context switch 時選的 task 一樣,依照前述變數的命名規則,下面是實作方式
在 TCB 完成初始化後,要把該 TCB 接上其他相關的 list,這個過程中必須暫時停止 interrupt 功能,以免在 list 還沒設定好前就被中斷設定(例如 systick)。
而 ARM Cortex-M4 處理器在 task 遇到中斷時,會將 register 的內容 push 進該 task 的 stack 的頂端,待下次執行時再 pop 出去,以下是在 port.c 裡的實作
利用組語進行實際記憶體的操作 但why用組語?
暫時停止 interrupt 功能的實作,是將 CPU 中控制 interrupt 權限的暫存器(basepri)內容設爲最高,此時將沒有任何 interrupt 可以被呼叫,該呼叫的函數名稱為 ulPortSetInterruptMask()藉此 mask 遮罩掉所有的 interrupt (所有優先權低於configMAX_SYSCALL_INTERRUPT_PRIORITY 的 task 將無法被執行)
使用FreeRTOS,main中的架構如下
當使用 vTaskCreate() 將 task 被建立出來以後,需要使用 vTaskStartScheduler() 來啟動排程器決定讓哪個 task 開始執行,當 vTaskStartScheduler() 被呼叫時,會先建立一個 idle task,這個 task 是為了確保 CPU 在任一時間至少有一個 task 可以執行 (取代直接切換回 kernel task) 而在 vTaskStartScheduler() 被呼叫時自動建立的 user task,idle task 的 priority 為 0 (lowest),目的是為了確保當有其他 user task 進入 ready list 時可以馬上被執行。
最後利用xPortStartScheduler()來啟動timer,並開始第一個任務xPortStartScheduler()的呼叫與硬體特性有關,所以此function會定義在HAL,以提供不同硬體架構實現。
pxTaskToDelete: 利用handle去識別出哪一個task。 這種可能性存在於如果在 loop 中發生執行錯誤 (fail),則需要跳出迴圈並終止(自己)執行,此時就需要使用 vTaskDelete 來刪除自己,發生錯誤的例子:
handle目前還不確定用途,TaskDelete實際用法待補充。
在scheduler啟動之後FreeRTOS主要靠三個中斷,去進行任務的切換,執行CONTEXT SWITCH,在FreeRTOS中會將原本系統的中斷Systick_Handler、SVC_Handler和PendSV_Handler重新指定給FreeRTOS用,至於這三個在系統中有甚麼作用可參考。
//待整理
在ST MCSDK中TASK在建立的時候會被稱作Thread,並且利用CMSIS-RTOS API進行封裝如下,其中instance
就是pvParameters
的數量,這邊我們設定為0,osThreadCreate
第二參數為NULL
Generic RTOS Interface (CMSIS standards)