FreeRTOS啟動任務調度器函數解釋

来源:https://www.cnblogs.com/RAM-YAO/p/18349835
-Advertisement-
Play Games

導讀 還是要先看官方手冊. 學過DMA的同志可能比較好理解,一句話, 釋放CPU匯流排 : 如果把應用程式執行的整個過程進行進一步分析,可以看到,當程式訪問 I/O 外設或睡眠時,其實是不需要占用處理器的,於是我們可以把應用程式在不同時間段的執行過程分為兩類,占用處理器執行有效任務的計算階段和不必占用 ...


目錄

FreeRTOS的任務開始運行的前提是調用了啟動調度器函數 vTaskStartScheduler() ,只有調用了該函數任務才會被調度並運行。下麵以FreeRTOS v9.0.0版本的源碼進行分析FreeRTOS任務調度的啟動流程。

vTaskStartScheduler() 函數

void vTaskStartScheduler(void)
{
    BaseType_t xReturn;

/* 靜態方法創建空閑任務 */
#if (configSUPPORT_STATIC_ALLOCATION == 1)
    {
        StaticTask_t *pxIdleTaskTCBBuffer = NULL;
        StackType_t *pxIdleTaskStackBuffer = NULL;
        uint32_t ulIdleTaskStackSize;

        /* 以靜態方式創建任務用戶需自定義空閑任務的記憶體分配函數 */
        vApplicationGetIdleTaskMemory(&pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize);
        xIdleTaskHandle = xTaskCreateStatic(prvIdleTask,
                                            "IDLE",
                                            ulIdleTaskStackSize,
                                            (void *)NULL,
                                            (tskIDLE_PRIORITY | portPRIVILEGE_BIT),
                                            pxIdleTaskStackBuffer,
                                            pxIdleTaskTCBBuffer);

        if (xIdleTaskHandle != NULL)
        {
            xReturn = pdPASS;
        }
        else
        {
            xReturn = pdFAIL;
        }
    }
#else
    { /* 動態方法創建空閑任務 */
        xReturn = xTaskCreate(prvIdleTask,
                              "IDLE", configMINIMAL_STACK_SIZE,
                              (void *)NULL,
                              (tskIDLE_PRIORITY | portPRIVILEGE_BIT),
                              &xIdleTaskHandle);
    }
#endif /* configSUPPORT_STATIC_ALLOCATION */
/* 如果啟用軟體定時器 */
#if (configUSE_TIMERS == 1)
    {
        if (xReturn == pdPASS)
        {
            /* 創建空閑任務成功,且啟用軟體定時器,就創建定時器任務 */
            xReturn = xTimerCreateTimerTask();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
#endif /* configUSE_TIMERS */
    /* 任務創建成功或定時器任務創建成功(如果使能定時器任務創建的話)*/
    if (xReturn == pdPASS)
    {
        /* 關中斷,確保開啟調度器之前或過程中,SysTick 不會產生中斷
           在第一個任務開始運行時,會重新打開中斷 */
        portDISABLE_INTERRUPTS();

#if (configUSE_NEWLIB_REENTRANT == 1)
        {
            _impure_ptr = &(pxCurrentTCB->xNewLib_reent);
        }
#endif /* configUSE_NEWLIB_REENTRANT */
        /* 設置下一個任務的解鎖時間為最大,這樣可以避免在啟動調度器之前不會因為任務解鎖而引起任務調度 */
        xNextTaskUnblockTime = portMAX_DELAY;
        /* 置 xSchedulerRunning 標誌為真,這指示這調度器即將進行運行 */
        xSchedulerRunning = pdTRUE;
        /* 將計數值初始化為0,確保在啟動調度器時開始計數,使所有任務的時鐘節拍一致 */
        xTickCount = (TickType_t)0U;

        /* 如果定義了configGENERATE_RUN_TIME_STATS,
        則必須定義以下巨集來配置用於生成運行時計數器時基的定時器/計數器。
        運行時間統計功能 */
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

        /* 用於完成啟動任務調度器中與硬體架構相關的配置部分,以及啟動第一個任務
        調用這個函數以後,就不會再回來了*/
        if (xPortStartScheduler() != pdFALSE)
        {
            /* 調度器正在運行函數不會返回到這裡執行 */
        }
        else
        {
            /* 只有當任務調用 xTaskEndScheduler() 時才會到達這裡
            如果調度器沒有成功啟動,或者某個任務調用了 xTaskEndScheduler() 函數以結束調度器的運行,
            則程式將會執行到這裡。因此,在這裡放置的代碼可能會處理一些特殊情況或進行清理工作 */
        }
    }
    /* 可能是因為沒有足夠的堆空間來創建空閑任務或定時器任務
    通過斷言來檢查內核是否能成功分配所需的記憶體 */
    else
    {
        configASSERT(xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY);
    }

    /* 防止編譯器警告 */
    (void)xIdleTaskHandle;
}

可以看到該函數主要做了幾件事。

1.創建空閑任務,根據配置以不同方式創建空閑任務,靜態或者動態方式。

2.如果啟動了軟體定時器功能就創建軟體定時器任務,根據配置以靜態還是動態方式進行創建軟體定時器任務。

3.關閉中斷,使用 portDISABLE_INTERRUPTS() 關閉中斷,這種方式只會關閉受FreeRTOS所管理的中斷,主要是為了防止Systick中斷在任務調度器開啟之前或過程中產生中斷,FreeRTOS會在開始運行第一個任務時重新打開中斷。

4.初始化一些全局變數,並將調度器標記為正在運行。

5.初始化任務運行時間統計功能的時基定時器,任務運行時間統計功能需要一個硬體定時器提供高精度的計數,這個硬體定時器就在這裡進行配置,如果配置不啟用任務運行時間統計功能的,就無需進行這項硬體定時器的配置。

6.調用 xPortStartScheduler() 啟動調度器。

xPortStartScheduler() 函數

/* 代碼確定了可以在中斷服務常式中調用的 FreeRTOS API 函數的最高優先順序。
這樣可以確保在中斷上下文中僅調用安全的API函數,從而保持中斷處理的效率和可靠性。*/
BaseType_t xPortStartScheduler(void)
{
#if (configASSERT_DEFINED == 1)
    {
        volatile uint32_t ulOriginalPriority; // 用於保存即將被覆蓋的中斷優先順序值。
        /* 指向第一個用戶中斷的優先順序寄存器地址。這個寄存器存儲了中斷的優先順序值 */
        volatile uint8_t *const pucFirstUserPriorityRegister = (uint8_t *)(portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER);
        /* 用於存儲計算得到的最大優先順序值 */
        volatile uint8_t ucMaxPriorityValue;

        /* 確定FreeRTOS ISR安全的API函數可以調用的最大優先順序。ISR安全函數是以“FromISR”結尾的。
        保存即將被覆蓋的中斷優先順序值 */
        ulOriginalPriority = *pucFirstUserPriorityRegister;

        /* 確定可用的優先順序位的數量。首先寫入所有可能的位 */
        *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

        ucMaxPriorityValue = *pucFirstUserPriorityRegister;

        /* Use the same mask on the maximum system call priority.
        對最大系統調用優先順序使用相同的掩碼。*/
        ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

        /* Calculate the maximum acceptable priority group value for the number
        of bits read back.*/
        ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
      
        while ((ucMaxPriorityValue & portTOP_BIT_OF_BYTE) == portTOP_BIT_OF_BYTE)
        {
            ulMaxPRIGROUPValue--;
            ucMaxPriorityValue <<= (uint8_t)0x01;
        }

        /* Shift the priority group value back to its position within the AIRCR
        register.*/
        ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
        ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

        /* Restore the clobbered interrupt priority register to its original
        value. */
        *pucFirstUserPriorityRegister = ulOriginalPriority;
    }
#endif /* conifgASSERT_DEFINED */

    /* Make PendSV and SysTick the lowest priority interrupts.
    使PendSV和SysTick為最低優先順序中斷 */

    /* 0xE000ED20 就是SHPR3寄存器的地址,用於配置 PendSV(可懸掛請求)、SysTick 的中斷優先順序*/
    portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;

    portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

    /* Start the timer that generates the tick ISR.  Interrupts are disabled
    here already. */
    /* 設置滴答定時器中斷頻率 */
    vPortSetupTimerInterrupt();

    /* Initialise the critical nesting count ready for the first task. */
    uxCriticalNesting = 0;

    /* 啟動第一個任務 */
    prvStartFirstTask();

    /* Should not get here! */
    /* 不應該到達這裡 */
    return 0;
}

1.配置 PendSV 和 SysTick 的中斷優先順序為最低優先順序。

2.調用函數 vPortSetupTimerInterrupt()配置 SysTick,該首先會將 SysTick 當前計數值清空,並根據 FreeRTOSConfig.h 文件中配置的configSYSTICK_CLOCK_HZ(SysTick 時鐘源頻率)和 configTICK_RATE_HZ(系統時鐘節拍頻率)計算並設置 SysTick 的重裝載值,然後啟動 SysTick 計數和中斷。

3.初始化臨界區嵌套計數器為 0。

4.調用函數 prvStartFirstTask() 啟動第一個任務。

prvStartFirstTask() 函數

函數 prvStartFirstTask() 用於初始化啟動第一個任務前的環境,主要是重新設置 MSP (主堆棧指針)指針,並使能全局中斷。

/* 初始化啟動第一個任務前的環境,主要是重新設置主堆棧MSP指針,並使能全局中斷 */
__asm void prvStartFirstTask( void )
{
	/* 8 位元組對齊 */
	PRESERVE8

	/* Use the NVIC offset register to locate the stack. */
	ldr r0, =0xE000ED08	/* 0xE000ED08 為 VTOR 地址 */
	ldr r0, [r0]		/* 獲取 VTOR 的值 */
	ldr r0, [r0]		/* 獲取 MSP 的初始值 */

	/* Set the msp back to the start of the stack. 
	 初始化 MSP */
	msr msp, r0
	/* Globally enable interrupts. */
	/* 使能全局中斷 */
	cpsie i
	cpsie f
	dsb
	isb
	/* Call SVC to start the first task. */
	/* 調用 SVC 啟動第一個任務 */
	svc 0
	nop
	nop
}

1.首先是使用了 PRESERVE8,進行 8 位元組對齊,這是因為,棧在任何時候都是需要 4 位元組對齊的,而在調用入口得 8 位元組對齊,在進行 C 編程的時候,編譯器會自動完成的對齊的操作,而對於彙編,就需要開發者手動進行對齊。

2.接下來的三行代碼是為了獲得 MSP 指針的初始值。

什麼是 MSP 指針?

程式在運行過程中需要一定的棧空間來保存局部變數等一些信息。當有信息保存到棧中時,MCU 會自動更新 SP 指針,使 SP 指針指向最後一個入棧的元素,那麼程式就可以根據 SP 指針來從棧中存取信息。ARM Cortex-M 提供了兩個棧空間,這兩個棧空間的堆棧指針分別是 MSP(主堆棧指針)和 PSP(進程堆棧指針)。

這兩個棧指針在任一時刻只能使用其中一個。

主堆棧指針(MSP):複位後預設使用的堆棧指針,用於操作系統內核及異常處理常式(中斷服務常式使用的永遠都是MSP)。

進程堆棧指針(PSP):由用戶的應用程式代碼使用。

在FreeRTOS 中 MSP 是給系統棧空間使用的,而 PSP 是給任務棧使用的,也就是說,FreeRTOS 任務的棧空間是通過 PSP 指向的,而在進入中斷服務函數時,則是使用 MSP 指針。

為什麼是 0xE000ED08?

0xE000ED08 是 VTOR(向量表偏移寄存器)的地址,VTOR 中保存了向量表的偏移地址。一般來說向量表是從起始地址 0x00000000 開始的,但是在有些情況下,可能需要修改或重定向向量表的首地址,因此 ARM Corten-M 提供了 VTOR 寄存器對向量表進行重定向。

向量表是用來保存中斷異常的入口函數地址,即棧頂地址的,並且向量表中的第一個字保存的就是棧頂的地址,在 start_stm32xxxxxx.s 文件中有如下定義:

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     SVC_Handler                ; SVCall Handler
                DCD     DebugMon_Handler           ; Debug Monitor Handler
                DCD     0                          ; Reserved
                DCD     PendSV_Handler             ; PendSV Handler
                DCD     SysTick_Handler            ; SysTick Handler

以上就是向量表的部分內容,可以看到向量表的第一個元素就是棧指針的初始值,也就是棧頂指針。所以 prvStartFirstTask() 該函數首先是獲取 VTOR 的地址,接著獲取VTOR 的值,也就是獲取向量表的首地址,最後獲取向量表中第一個字的數據,也就是棧頂指針。

3.在獲取了棧頂指針後,將 MSP 指針重新賦值為棧頂指針。這個操作相當於丟棄了程式之前保存在棧中的數據,因為FreeRTOS從開啟任務調度器到啟動第一個任務都是不會返回的,是一條不歸路,因此將棧中的數據丟棄,也不會有影響。

4.重新賦值 MSP 後,接下來就重新使能全局中斷,因為之前在函數 vTaskStartScheduler()中關閉了受 FreeRTOS 管理的中斷。

5.最後使用 SVC 指令,並傳入系統調用號 0,觸發 SVC 中斷。

vPortSVCHandler() 函數

當使能了全局中斷,並且手動觸發 SVC 中斷後,就會進入到 SVC 的中斷服務函數中。

__asm void vPortSVCHandler( void )
{
	PRESERVE8
	/* 獲取任務棧地址 */
	ldr	r3, =pxCurrentTCB	/* 恢覆上下文, r3 指向優先順序最高的就緒態任務的任務控制塊 */
	ldr r1, [r3]			/* 使用pxCurrentTCBConst獲取pxCurrentTCB地址. r1 為任務控制塊地址*/
	ldr r0, [r1]			/* pxCurrentTCB中的第一項是任務棧的頂部。r0 為任務控制塊的第一個元素(棧頂) */
	
	/* 模擬出棧,並設置 PSP,任務棧彈出到 CPU 寄存器 */
	ldmia r0!, {r4-r11}		/* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
	/* 設置 PSP 為任務棧指針 */
	msr psp, r0				/* 恢復任務棧指針 */
	isb
	/* 使能所有中斷 */
	mov r0, #0
	msr	basepri, r0
	/* 使用 PSP 指針,並跳轉到任務函數 */
	orr r14, #0xd
	bx r14
}

該函數用於跳轉到任務函數當中。

首先通過 pxCurrentTCB 獲取優先順序最高的就緒態任務的任務棧地址,優先順序最高的就緒態任務就是系統將要運行的任務。pxCurrentTCB 是一個全局變數,用於指向系統中優先順序最高的就緒態任務的任務控制塊,在前面創建 start_task 任務、空閑任務、定時器處理任務時自動根據任務的優先順序高低已經進行過賦值的。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. 配置 1.1 如果是配置全局文件,則編輯/etc/mail.rc 1.2 如果是配置當前用戶,則編輯~/.mailrc 2. 配置文件內容 # 這裡填入smtp地址,這裡的xxx為qq或者163等,如果用的雲伺服器,安全組策略要開放465/25埠,入站和出站都要開放該埠 set smtp= ...
  • 一、背景 在公司軟體的實際開發中,當一個版本的客戶端安裝包本地調試、測試驗證都沒問題後外發,到用戶實際機器上出問題了,怎麼辦? 很多人說,讓客戶給出復現步驟,我來試試!但是按照步驟操作之後還是沒效果。這時你又想到了是不是環境的差異,但是又說不上來是哪裡出問題。提供兩個辦法:1.寫日誌,編一個相近版本 ...
  • 1、用戶操作 阿裡雲預設是 root 用戶,我們一般要自己創建一個用戶,然後給該用戶 sudo 許可權 添加用戶 sudo adduser newUserName 賦予sudo許可權 sudo usermod -aG sudo newUserName 刪除用戶 sudo deluser --remove ...
  • 1、Docker 基本概念 什麼是 Docker? Docker 是一個開源的容器化平臺,允許開發者封裝他們的應用程式及其所有依賴項到一個標準化的單元中,這個單元被稱為“容器”。容器可以在任何支持 Docker 的環境中運行,從而確保應用程式的可移植性和一致性。 Docker 的優勢 一致性和可移植 ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是i.MXRT1050在GPIO上增加RC延時電路後導致邊沿中斷誤觸發問題探析。 前段時間有一個 RT1052 客戶反饋了一個有趣的問題,他們設計得是一個帶 LCD 屏交互的應用,應用以官方 SDK 里的 lvgl_demo_widget ...
  • 家裡的機頂盒淘汰下來,博主想要物盡其用,看看是否能將其改造為一臺Linux"開發機",為其安裝Ubuntu系統,故開始倒騰 ...
  • 寫在前面 本隨筆是非常菜的菜雞寫的。如有問題請及時提出。 可以聯繫:[email protected] GitHhub:https://github.com/WindDevil (目前啥也沒有 本節重點 主要是對 任務 的概念進行進一步擴展和延伸:形成 任務運行狀態:任務從開始到結束執行過程中所處的 ...
  • 第十八章 machine.Timer類實驗 1)實驗平臺:正點原子DNK210開發板 2)章節摘自【正點原子】DNK210使用指南 - CanMV版 V1.0 3)購買鏈接:https://detail.tmall.com/item.htm?&id=782801398750 4)全套實驗源碼+手冊+ ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...