天天看點

#夏日挑戰賽# 使用LiteOS Studio揭秘LiteOS在STM32上如何運作

「本文正在參加星光計劃3.0–夏日挑戰賽」

本文基于LiteOS一站式開發工具LiteOS Studio,通過單步調試,來動态分析LiteOS的啟動流程,給開發者一個更直覺的展示。

了解LiteOS系統,我們可以先從它的啟動流程開始。不同的晶片和編譯工具,其啟動流程可能會有一些差異,本文基于碼雲 LiteOS開源站點 master分支12月的代碼,以STM32F769IDISCOVERY(ARM Cortex M7)開發闆和GCC編譯工具為例,使用LiteOS Studio的單步調試,動态分析LiteOS的啟動流程。

1、LiteOS Studio環境準備

在開始前,需要準備好LiteOS Studio環境,包含LiteOS Studio安裝、建立工程、編譯、燒錄,掌握LiteOS Studio如何調測等等,可以參考官網文檔站點https://liteos.gitee.io/liteos_studio/#/project_stm32。

  • 如何搭建LiteOS Studio開發環境 請參考搭建Windows開發環境
  • 如何建立STM32F769IDISCOVERY的LiteOS工程  請參考 建立工程
  • 如何編譯,燒錄、調測,請分别參考 編譯配置-編譯代碼,燒錄配置-燒錄,調試器-執行調試

注意,如果開發闆使用的是闆載ST-LINK仿真器,需要刷為JLINK。請參考 st-link仿真器單步調測。

另外,執行單步調測,預設停止在main()函數。LiteOS作業系統的啟動是從main函數開始的。而ARM Cortex-M晶片從上電到執行main函數,中間經過了Reset_Handler等函數。LiteOS系統重新開機、複位等都是從Reset_Handler函數開始執行的。在LiteOS Studio工程找到檔案.vscode\launch.json,把其中的postLaunchCommands屬性下面的"b main"改為"b Reset_Handler"。如下圖:

#夏日挑戰賽# 使用LiteOS Studio揭秘LiteOS在STM32上如何運作

重新開始調測,系統會暫停在Reset_Handler函數處。如下圖:

#夏日挑戰賽# 使用LiteOS Studio揭秘LiteOS在STM32上如何運作

2、los_startup_gcc.S啟動引導檔案介紹

當對STM32F769IDISCOVERY開發闆進行上電操作或者複位操作時,該開發闆會從異常向量表中擷取Reset_Handler函數的位址并執行該函數。彙編檔案targets\STM32F769IDISCOVERY\los_startup_gcc.S定義了該函數。

los_startup_gcc.S是啟動引導檔案,從Reset_Handler開始到執行main函數,主要工作就是準備C代碼的運作環境,具體包括:

  • 設定棧指針SP,對應語句 ldr sp, =_estack
  • 初始化中斷向量,對應函數LoopCopyVectorInit
  • 初始化data段,對應函數LoopCopyDataInit
  • 初始化bss段,對應函數LoopFillZerobss
  • 初始化系統時鐘,跳轉到函數SystemInit
  • 跳轉到 C 代碼函數main

代碼如下:

Reset_Handler:
  cpsid i
  ldr   sp, =_estack      /* set stack pointer */

/* Copy the vector_ram segment initializers from flash to SRAM */
  movs  r1, #0
  b  LoopCopyVectorInit

CopyVectorInit:
  ldr   r3, =_si_liteos_vector_data
  ldr   r3, [r3, r1]
  str   r3, [r0, r1]
  adds   r1, r1, #4

LoopCopyVectorInit:
  ldr   r0, =_s_liteos_vector
  ldr   r3, =_e_liteos_vector
  adds   r2, r0, r1
  cmp   r2, r3
  bcc   CopyVectorInit

/* Copy the data segment initializers from flash to SRAM */
  movs  r1, #0
  b  LoopCopyDataInit

CopyDataInit:
  ldr  r3, =_sidata
  ldr  r3, [r3, r1]
  str  r3, [r0, r1]
  adds  r1, r1, #4

LoopCopyDataInit:
  ldr  r0, =_sdata
  ldr  r3, =_edata
  adds  r2, r0, r1
  cmp  r2, r3
  bcc  CopyDataInit
  ldr  r2, =_sbss
  b  LoopFillZerobss
/* Zero fill the bss segment. */
FillZerobss:
  movs  r3, #0
  str  r3, [r2], #4

LoopFillZerobss:
  ldr  r3, = _ebss
  cmp  r2, r3
  bcc  FillZerobss

/* Call the clock system initialization function.*/
  bl  SystemInit
/* Call static constructors */
/*    bl __libc_init_array */
/* Call the application's entry point.*/
  bl  main
  bx  lr
           

Data段存放的是已經初始化的全局變量,需要從Flash中擷取這些資料到RAM中。而bss段存放的是沒有初始化的全局變量,是以Flash中并沒有bss段的變量值,是以啟動引導檔案隻是對RAM中的.bss段進行清零操作。

los_startup_gcc.S啟動引導檔案中使用的_estack 、_si_liteos_vector_data、_s_liteos_vector、_e_liteos_vector、_sidata、_sdata 、_edata、_sbss、_ebss,這些符号都定義在targets\STM32F769IDISCOVERY\liteos.ld連結腳本中。

連結腳本根據應用需要,設定堆棧大小和棧位址,并控制每個段的存放位置。對于中斷向量和data段,既要放到Flash中,也需要放到RAM中,并通過連結腳本的AT關鍵字把Flash的位址設定為load位址。

注:連結腳本中的相關代碼可以通路https://gitee.com/LiteOS/LiteOS/blob/master/targets/STM32F769IDISCOVERY/liteos.ld檢視。

los_startup_gcc.S啟動引導檔案中除了定義Reset_Handler函數,還定義了其他中斷異常處理函數Default_Handler,并為Default_Handler的每個異常處理程式提供弱别名。所謂弱别名,即具有相同名稱的任何函數都将覆寫此處的函數。這樣做可以防止使用者使能了中斷卻沒有設定中斷處理程式時造成的崩潰。Default_Handler函數隻是進入一個無限循環以保留系統狀态供調試器檢查。

3、los_startup_gcc.S啟動引導檔案動态運作

現在我們來單步調測運作los_startup_gcc.S,啟動調測後,系統會暫停在Reset_Handler函數的第一行代碼cpsid i,此語句用來關中斷,執行前後,觀察寄存器primask值的變化,會發現由0變為1。繼續執行語句" ldr sp, =_estack",同樣觀察寄存器,寄存器sp的值變化了。如下圖:

#夏日挑戰賽# 使用LiteOS Studio揭秘LiteOS在STM32上如何運作

繼續運作單步調測,觀察如何調用LoopCopyVectorInit和CopyVectorInit,實作把中斷向量從Flash複制到RAM的。在調測過程中,寄存器的數值可能是10進制進行展示的,如果想檢視其他進制展示的數值,可以在調測界面的螢幕視窗輸入$寄存器名稱+進制代碼來切換進制檢視,如$r0,x來檢視r0寄存器的16進制。詳細的進制代碼如下:

  • x hexadecimal
  • d signed decimal
  • u unsigned decimal
  • o octal
  • t binary
  • a address

    進制切換如圖所示:

    #夏日挑戰賽# 使用LiteOS Studio揭秘LiteOS在STM32上如何運作

由于循環次數較多,如果想跨過中斷向量的複制,繼續下面的代碼,可以設定斷點,然後F5繼續調測到斷點處。如下圖,我們在118行設定了斷點,繼續執行會完成向量表的複制,去執行資料段data的初始化。

#夏日挑戰賽# 使用LiteOS Studio揭秘LiteOS在STM32上如何運作

以此類推,通過Studio邊調測、邊分析啟動過程的後續代碼。當執行到語句“bl main”,再按F11跳入繼續執行時,就會跳轉到C代碼的main函數。下文繼續分析main函數。

4、main函數介紹

LiteOS的main函數定義在targets\STM32F769IDISCOVERY\Src\main.c。main函數主要負責LiteOS的初始化工作。代碼如下:

INT32 main(VOID)
{
    HardwareInit();

    PRINT_RELEASE("\n********Hello Huawei LiteOS********\n"
                  "\nLiteOS Kernel Version : %s\n"
                  "build data : %s %s\n\n"
                  "**********************************\n",
                  HW_LITEOS_KERNEL_VERSION_STRING, __DATE__, __TIME__);

    UINT32 ret = OsMain();
    if (ret != LOS_OK) {
        return LOS_NOK;
    }

    OsStart();

    return 0;
}
           

硬體初始化函數HardwareInit()和主要晶片相關,這裡不做詳細介紹。下面介紹LiteOS核心的初始化,代碼如下:

LITE_OS_SEC_TEXT_INIT UINT32 OsMain(VOID)
{
    UINT32 ret;

#ifdef LOSCFG_EXC_INTERACTION
    ret = OsMemExcInteractionInit((UINTPTR)&__bss_end);
    if (ret != LOS_OK) {
        return ret;
    }
#endif
    /* 初始化動态記憶體池 */
    ret = OsMemSystemInit((UINTPTR)&__bss_end + g_excInteractMemSize);
    if (ret != LOS_OK) {
        return ret;
    }
    /*
     * 配置最大支援的任務個數、信号量個數、互斥鎖個數、
     * 隊列個數以及軟體定時器個數,設定g_sysClock和
     * g_tickPerSecond全局變量
     */
    OsRegister();

#ifdef LOSCFG_SHELL_LK
    OsLkLoggerInit(NULL);
#endif

#ifdef LOSCFG_SHELL_DMESG
    ret = OsDmesgInit();
    if (ret != LOS_OK) {
        return ret;
    }
#endif
/*
 * 初始化硬中斷,此後LiteOS就會接管系統的中斷,
 * 使用中斷前需要先注冊中斷并使能
 */
    OsHwiInit();
    /*
     * 設定中斷向量的中斷處理函數,包括
     * Hard Fault硬體故障中斷、
     * Non Maskable Interrupt不可屏蔽中斷(NMI)、
     * Memory Management記憶體管理中斷、
     * Bus Fault 總線故障中斷、
     * Usage Fault使用故障中斷、
     * SVCall利用SVC指令調用系統服務的中斷
     */
    ArchExcInit();

    ret = OsTickInit(GET_SYS_CLOCK(), LOSCFG_BASE_CORE_TICK_PER_SECOND);
    if (ret != LOS_OK) {
        return ret;
    }

#ifdef LOSCFG_PLATFORM_UART_WITHOUT_VFS
    uart_init();
#ifdef LOSCFG_SHELL
    extern int uart_hwiCreate(void); /* HuaWeiChange */
    uart_hwiCreate();
#endif /* LOSCFG_SHELL */
#endif /* LOSCFG_PLATFORM_UART_WITHOUT_VFS */
    /*
     * 初始化任務連結清單包括任務的排序連結清單,
     * 初始化優先級消息隊列連結清單(用于管理不同優先級任務)
     */
    ret = OsTaskInit();
    if (ret != LOS_OK) {
        PRINT_ERR("OsTaskInit error\n");
        return ret;
    }

#ifdef LOSCFG_KERNEL_TRACE
    ret = LOS_TraceInit(NULL, LOS_TRACE_BUFFER_SIZE);
    if (ret != LOS_OK) {
        PRINT_ERR("LOS_TraceInit error\n");
        return ret;
    }
#endif
/*
 * 初始化任務螢幕
 */
#ifdef LOSCFG_BASE_CORE_TSK_MONITOR
    OsTaskMonInit();
#endif
/*
 * OsIpcInit包括初始化消息隊列連結清單、互斥鎖連結清單和信号量連結清單
 */
    ret = OsIpcInit();
    if (ret != LOS_OK) {
        return ret;
    }

    /*
     * CPUP should be inited before first task creation which depends on the semaphore
     * when LOSCFG_KERNEL_SMP_TASK_SYNC is enabled. So don't change this init sequence
     * if not necessary. The sequence should be like this:
     * 1. OsIpcInit
     * 2. OsCpupInit -> has first task creation
     * 3. other inits have task creation
     */
#ifdef LOSCFG_KERNEL_CPUP
    ret = OsCpupInit();
    if (ret != LOS_OK) {
        PRINT_ERR("OsCpupInit error\n");
        return ret;
    }
#endif
/*
 * OsSwtmrInit對軟體定時器和其在percpu上的排序連結清單進行初始化,
 * 并初始化定期器處理函數的記憶體池,同時還會建立軟體定時器
 * 的消息隊列和定時器任務
 */
#ifdef LOSCFG_BASE_CORE_SWTMR
    ret = OsSwtmrInit();
    if (ret != LOS_OK) {
        return ret;
    }
#endif

#ifdef LOSCFG_KERNEL_SMP
    (VOID)OsMpInit();
#endif

#ifdef LOSCFG_KERNEL_DYNLOAD
    ret = OsDynloadInit();
    if (ret != LOS_OK) {
        return ret;
    }
#endif

#if defined(LOSCFG_HW_RANDOM_ENABLE) || defined (LOSCFG_DRIVERS_RANDOM)
    random_alg_context.ra_init_alg(NULL);
    run_harvester_iterate(NULL);
#endif
/* 建立空閑任務 */
    ret = OsIdleTaskCreate();
    if (ret != LOS_OK) {
        return ret;
    }

#ifdef LOSCFG_KERNEL_RUNSTOP
    ret = OsWowWriteFlashTaskCreate();
    if (ret != LOS_OK) {
        return ret;
    }
#endif

#ifdef LOSCFG_DRIVERS_BASE
    ret = OsDriverBaseInit();
    if (ret != LOS_OK) {
        return ret;
    }
#ifdef LOSCFG_COMPAT_LINUX
    (VOID)do_initCalls(LEVEL_ARCH);
#endif
#endif

#ifdef LOSCFG_KERNEL_PERF
    ret = LOS_PerfInit(NULL, LOS_PERF_BUFFER_SIZE);
    if (ret != LOS_OK) {
        return ret;
    }
#endif
/*
 * LOSCFG_PLATFORM_OSAPPINIT宏預設已經在.config、menuconfig.h中定義。
 * OsAppInit建立了一個名為“app_Task”的任務,該任務處理函數為
 * app_init,任務優先級為10;
 * OsTestInit建立了一個名為“IT_TST_IN”的任務,該任務處理函數為
 * TestTaskEntry,任務優先級為25。該函數暫時沒有開源。
 */
#ifdef LOSCFG_PLATFORM_OSAPPINIT
    ret = osAppInit();
#else /* LOSCFG_TEST */
    ret = OsTestInit();
#endif
    if (ret != LOS_OK) {
        return ret;
    }

    return LOS_OK;
}
           

完成核心的初始化後,調用OsStart()開始任務排程,自此LiteOS開始正常工作。OsStart函數的代碼如下:

LITE_OS_SEC_TEXT_INIT VOID OsStart(VOID)
{
    LosTaskCB *taskCB = NULL;
 /* 擷取目前執行任務的CPU ID,STM32F769是單核晶片,cpuid為0 */
    UINT32 cpuid = ArchCurrCpuid();
    /*
     * 配置Tick中斷向量,其中斷處理函數為OsTickHandler。
     * 初始化System Tick Timer及其中斷,并啟動此Timer。
     * 計數器會産生周期性中斷
     */ 
    OsTickStart();

    LOS_SpinLock(&g_taskSpin);
    /* 擷取最高優先級任務隊列中的第一個任務,賦給taskCB */
    taskCB = OsGetTopTask();

#ifdef LOSCFG_KERNEL_SMP
    /*
     * attention: current cpu needs to be set, in case first task deletion
     * may fail because this flag mismatch with the real current cpu.
     */
    taskCB->currCpu = (UINT16)cpuid;
#endif
    /* 設定32位的排程flag,第CPU ID位設定為1 */
    OS_SCHEDULER_SET(cpuid);

    PRINTK("cpu %u entering scheduler\n", cpuid);
    /*
     * 排程g_runTask即taskCB任務,OsStartToRun函數
     * 定義在los_dispatch.S彙編檔案中
     */
    OsStartToRun(taskCB);
}
           

5、main函數動态運作

現在我們來單步調測運作main.c源代碼,LiteOS Studio在調測時,可以同步展示目前運作的源代碼行,及對應的反彙編檔案行,如下圖:

#夏日挑戰賽# 使用LiteOS Studio揭秘LiteOS在STM32上如何運作

在調測過程中,變量的數值可能是10進制進行展示的,如果想檢視其他進制展示的數值,可以在調測界面的螢幕視窗輸入變量名稱名稱+進制代碼來切換進制檢視,如memStart,x來檢視變量memStart的16進制。如圖:

#夏日挑戰賽# 使用LiteOS Studio揭秘LiteOS在STM32上如何運作

小結

本期分享使用LiteOS Studio檢視LiteOS啟動過程,同時展示了使用LiteOS Studio調測的技巧,大家可以繼續邊調測、邊分析後續的代碼,會看到LiteOS整個啟動流程:從闆子複位上電開始,調用彙編代碼Reset_Handler進入啟動引導檔案,完成C代碼運作環境的準備工作、最後跳轉到main函數。在main函數中完成硬體初始化和LiteOS核心的初始化,并通過彙編跳轉到執行第一個最高優先級的任務指令的位址上,進而開始LiteOS的運作。

繼續閱讀