1. 程式人生 > 其它 >【STM32H7】第10章 ThreadX任務棧大小確定及其溢位檢測

【STM32H7】第10章 ThreadX任務棧大小確定及其溢位檢測

論壇原始地址(持續更新):http://www.armbbs.cn/forum.php?mod=viewthread&tid=99514

第10章 ThreadX任務棧大小確定及其溢位檢測

本章節為大家講解ThreadX任務棧大小的確定方法以及棧溢位檢測方法。給任務分配多大的棧空間,一直是初學者比較頭疼的問題,本章就主要為大家講解如何解決此問題。

10.1 任務棧大小的確定

10.2 什麼是棧溢位

10.3 ThreadX的棧溢位檢測機制

10.4 實驗例程

10.6總結

10.1 任務棧大小的確定

在基於RTOS的應用設計中,每個任務都需要自己的棧空間,應用不同,每個任務需要的棧大小也是不同的。將如下的幾個選項簡單的累加就可以得到一個粗略的棧大小:

1、 函式的巢狀呼叫,針對每一級函式用到棧空間的有如下四項:

  • 函式區域性變數。
  • 函式形參,一般情況下函式的形參是直接使用的CPU暫存器,不需要使用棧空間,但是這個函式中如果還嵌套了一個函式的話,這個儲存了函式形參的CPU暫存器內容是要入棧的。所以建議大家也把這部分算在棧大小中。
  • 函式返回地址,針對M3、 M4和M7核心的MCU,一般函式的返回地址是專門儲存到LR(Link Register)暫存器裡面的,如果這個函式裡面還呼叫了一個函式的話,這個儲存了函式返回地址的LR暫存器內容是要入棧的。所以建議大家也把這部分算在棧大小中。
  • 函式內部的狀態儲存操作也需要額外的棧空間。

2、 任務切換,任務切換時所有的暫存器都需要入棧,對於帶FPU浮點處理單元的M4/M7核心MCU來說,FPU暫存器也是需要入棧的。

3、 針對M3核心和M4/M7核心的MCU來說,在任務執行過程中,如果發生中斷:

  • M3核心的MCU有8個暫存器是自動入棧的,這個棧是任務棧,進入中斷以後其餘暫存器入棧以及發生中斷巢狀都是用的系統棧。
  • M4/M7核心的MCU有8個通用暫存器和18個浮點暫存器是自動入棧的,這個棧是任務棧,進入中斷以後其餘通用暫存器和浮點暫存器入棧以及發生中斷巢狀都是用的系統棧。

4、 進入中斷以後使用的區域性變數以及可能發生的中斷巢狀都是用的系統棧,這點要注意。

實際應用中將這些都加起來是一件非常麻煩的工作,上面這些棧空間加起來的總和只是棧的最小需求,實際分配的棧大小可以在最小棧需求的基礎上乘以一個安全係數,一般取1.5-2。上面的計算是我們使用者可以確定的棧大小,專案應用中還存在無法確定的棧大小,比如呼叫printf函式就很難確定實際的棧消耗。又比如通過函式指標實現函式的間接呼叫,因為函式指標不是固定的指向一個函式進行呼叫,而是根據不同的程式設計可以指向不同的函式,使得棧大小的計算變得比較麻煩。

另外還要注意一點,建議不要編寫遞迴程式碼,因為我們不知道遞迴的層數,棧的大小也是不好確定的。

一般來說,使用者可以事先給任務分配一個大的棧空間,然後通過第8章介紹的除錯方法列印任務棧的使用情況,執行一段時間就會有個大概的範圍了。這種方法比較簡單且實用些。

  • 函式棧大小確定

函式的棧大小計算起來是比較麻煩的,那麼有沒有簡單的辦法來計算呢?有的,一般IDE開發環境都有這樣的功能,比如MDK會生成一個htm檔案,通過這個檔案使用者可以知道每個被呼叫函式的最大棧需求以及各個函式之間的呼叫關係。但是MDK無法確定通過函式指標實現函式呼叫時的棧需求。另外,發生中斷或中斷巢狀時的現場保護需要的棧空間也不會統計。

關於MDK生成的map和htm檔案的使用,我們安富萊電子有出過一期視訊教程,可以在這裡檢視:

http://www.armbbs.cn/forum.php?mod=viewthread&tid=15408

STM32H7的BSP驅動手冊第10章節:

http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980

10.2 什麼是棧溢位

前面為大家講解了如何確定任務棧的大小,那什麼又是棧溢位呢?簡單的說就是使用者分配的棧空間不夠用了,溢位了。下面我們舉一個簡單的例項,棧生長方向從高地址向低地址生長(M4/M7和M3是這種方式)。

(1) 上圖示識1的位置是RTOS的某個任務呼叫了函式test()前的SP棧指標位置。

void  test (void);
{
     int i;
     int array[10];

     :
     :
     // Code
}

(2) 上圖示識2的位置是呼叫了函式test需要儲存返回地址到棧空間。這一步不是必須的,對於M3和M4/M7核心是先將其儲存到LR暫存器中,如果LR暫存器中有儲存上一級函式的返回地址,需要將LR暫存器中的內容先入棧。

(3) 上圖示識3的位置是區域性變數int i和int array[10]佔用的棧空間,但申請了棧空間後已經

越界了。這個就是所謂的棧溢位了。如果使用者在函式test中通過陣列array修改了這部分越界區的資料且這部分越界的棧空間暫時沒有用到或者資料不是很重要,情況還不算嚴重,但是如果儲存的是關鍵資料,會直接導致系統崩潰。

(4) 上圖示識4的位置是區域性變數申請了棧空間後,棧指標向下偏移(返回地址+變數i+10個數組元素)*4 =48個位元組。

(5) 上圖示識5的位置可能是其它任務的棧空間,也可能是全域性變數或者其它用途的儲存區,如果test函式在使用中還有用到棧的地方就會從這裡申請,這部分越界的空間暫時沒有用到或者資料不是很重要,情況還不算嚴重,但是如果儲存的是關鍵資料,會直接導致系統崩潰。

10.3 ThreadX的棧溢位檢測機制

10.3.1 實現原理

(注:有些應用場景,這種棧檢測是檢測不出來的)。

ThreadX提供了在執行時檢查每個任務的棧是否損壞的功能。預設情況下,ThreadX在建立過程中使用0xEF資料模式填充任務的每個位元組。如果應用程式使能了巨集定義TX_ENABLE_STACK_CHECKING編譯工程,則ThreadX將檢查每個任務的棧在掛起或恢復時是否損壞。如果檢測到棧損壞,則ThreadX將呼叫使用者使用函式tx_thread_stack_error_notify設定的回撥函式。否則,如果未指定堆疊錯誤處理程式,則ThreadX將呼叫內部_tx_thread_stack_error_handler例程。

  • 棧溢位檢測方法

除了TreadX提供的棧溢位檢測機制,還有其它的棧溢位檢測機制,大家可以在Mircrium官方釋出的如下這個博文中學習:

https://www.micrium.com/detecting-stack-overflows-part-2-of-2/

10.3.2 實現方法

  • 使能棧檢測

推薦直接在tx_port.h裡面使能:

#define TX_ENABLE_STACK_CHECKING

  • 註冊回撥:

大家可以隨意設定註冊的函式名:

tx_thread_stack_error_notify(my_stack_error_handler);

  • 回撥函式的實現

程式碼如下:

void my_stack_error_handler(TX_THREAD *thread_ptr)
{
        App_Printf("===============================================================\r\n");
        App_Printf("如下任務被檢測出棧溢位\r\n");
        App_Printf("===============================================================\r\n");
        App_Printf(" 任務優先順序 任務棧大小 當前使用棧  最大棧使用   任務名\r\n");
        App_Printf("   Prio     StackSize   CurStack    MaxStack   Taskname\r\n");
        
        TX_THREAD      *p_tcb;                /* 定義一個任務控制塊指標 */

        p_tcb = &AppTaskStartTCB;
        
        /* 遍歷任務控制列表TCB list),列印所有的任務的優先順序和名稱 */
        do
        {
                if(p_tcb != (TX_THREAD *)thread_ptr)
                {
                        p_tcb = p_tcb->tx_thread_created_next;
                }
                else
                {
                                       
                        App_Printf("   %2d        %5d      %5d       %5d      %s\r\n",
                                  p_tcb->tx_thread_priority,
                                  p_tcb->tx_thread_stack_size,
                                  (int)p_tcb->tx_thread_stack_end - (int)p_tcb->tx_thread_stack_ptr,
                                  (int)p_tcb->tx_thread_stack_end - (int)p_tcb->tx_thread_stack_highest_ptr,
                                   p_tcb->tx_thread_name);
                        
                        while(1);
                }
        }while(1);
}

為方便起見,我們這裡直接將棧溢位任務的任務名打印出來,方便大家檢視那個任務出問題了,然後通過while(1)阻塞在這裡:

10.4 實驗例程

配套例子:

V7-3005_ThreadX Task Stack Checking

實驗目的:

  1. 學習ThreadX任務管理。

實驗內容:

1、共建立瞭如下幾個任務,通過按下按鍵K1可以通過串列埠或者RTT列印任務堆疊使用情況

===================================================

OS CPU Usage = 1.94%

===================================================

Prio StackSize CurStack MaxStack Taskname

2 4092 383 391 App Task Start

3 4092 543 659 App Msp Pro

4 4092 391 391 App Task UserIF

5 4092 543 659 App Task COM

30 1020 519 519 App Task STAT

31 1020 143 71 App Task IDLE

0 1020 391 391 System Timer Thread

串列埠軟體可以使用SecureCRT或者H7-TOOL RTT檢視列印資訊。

App Task Start任務 :啟動任務,這裡用作BSP驅動包處理。

App Task MspPro任務 :訊息處理,這裡未使用。

App Task UserIF任務 :按鍵訊息處理。

App Task COM任務 :這裡用作LED閃爍。

App Task STAT任務 :統計任務

App Task IDLE任務 :空閒任務

System Timer Thread任務:系統定時器任務

2、 (1) 凡是用到printf函式的全部通過函式App_Printf實現。

(2) App_Printf函式做了訊號量的互斥操作,解決資源共享問題。

3、預設上電是通過串列埠列印資訊,如果使用RTT列印資訊

(1) MDK AC5,MDK AC6或IAR通過使能bsp.h檔案中的巨集定義為1即可

#define Enable_RTTViewer 1

(2) Embedded Studio繼續使用此巨集定義為0, 因為Embedded Studio僅製作了除錯狀態RTT方式檢視。

實驗操作:

  1. K1按鍵按下列印任務執行情況。
  2. K2按鍵按觸發任務AppTaskUserIF棧溢位,並且會打印出問題的任務名,並將程式阻塞執行。

串列埠列印資訊方式(AC5):

波特率 115200,資料位 8,奇偶校驗位無,停止位 1

RTT列印資訊方式(AC5):

程式執行框圖:

10.5 總結

本章節主要為大家講解了任務棧大小的確定以及棧溢位檢測的兩種方法,建議實際操作下本章節配套

的例子,對棧溢位有一個感性的認識,隨著以後的學習再深入理解並運用。