FreeRTOS STM32CubeMX配置 內存管理 任務管理

前言

嵌入式開發有兩種選擇: 前後臺模式, 嵌入式操作系統. 兩種模式對比(圖自STM32RTOS培訓):
在這裏插入圖片描述

當產品功能簡單, 開發成員不多, 不需要複雜外設(USB/網絡等), 成本資源要求苛刻時, 用前後臺模式能滿足需求. 形式一般是初始化, 進While循環, 配合中斷. 當碰到複雜的功能, 複雜的外設(USB/網絡/FS等), 上操作系統會比較爽一些.

用ST或者NXP的MCU的話, 不用考慮, 上FreeRTOS的車就對了. FreeRTOS的任務狀態:
在這裏插入圖片描述

FreeRTOS官方文檔很詳細, 可以去翻一翻: https://www.freertos.org/FreeRTOS-quick-start-guide.html

先用起來

如果一開始花個幾周時間瞭解基本原理, 可能會逐漸心灰意冷, STM32CubeMX直接集成了FreeRTOS, 直接用起來, 看看怎麼用, 能幹些啥, 做着理解着, 心裏有底一些, 對精神的折磨也少一點.

這小節直接拿來點個燈, 創建兩個線程, 每個線程單獨點燈, 用ST官方的NUCLEO-F767ZI開發板, 用到的板子上的LED:

Name PIN
LED1 (Green, LD1) PB0
LED2 (Blue, LD2) PB7

打開STM32CubeMX(這裏用最新的5.4.0版本):

  • MCU選擇: 點擊 ACCESS TO MCU SELECTOR, 選擇 STM32F767ZI

  • 調試端口配置爲SWD: Pinout & Configuration -> System Core -> SYS -> Debug 選擇 Serial Wire

  • Pinout & Configuration -> System Core -> RCC -> HSE 選擇 Crystal/Ceramic Resonator

  • Clock Configuration:
    在這裏插入圖片描述

  • Pinout view圖上找到PB0, 單擊, 彈出的菜單中選擇GPIO_Output, 右擊, 選擇Enter User Label, 輸入LED1; 同樣方式設置PB7爲LED2.

  • Pinout & Configuration -> System Core -> SYS -> Timebase Source 選擇 TIM3. 設置這個主要是Generate Code式提示的, 大概是因爲HAL庫和FreeRTOS的時基都默認Systick的話, 可能導致HAL_Delay等的異常, 索性改掉HAL庫的時基, 大家各自安好:
    在這裏插入圖片描述

  • Pinout & Configuration -> Middleware -> FREERTOS -> Interface 選擇較新的CMSIS_V2:
    在這裏插入圖片描述

  • FREERTOSConfiguration -> Tasks and Queues -> 有一個默認的defaultTask, 這個可以重命名或者不去管它, 點擊Add按鈕, 添加兩個任務Task_LED1Task_LED2, 入口函數名也填進去, 優先級默認先不變或設置爲實時(osPriorityRealtime):
    在這裏插入圖片描述

  • Project Manager -> Project -> Browse 選擇工程位置(Project Location), 填入工程名(Project Name), Toolchain/IDE 選擇 MDK-ARM.

  • Project Manager -> Code Generator -> 勾選Copy only the necessary library files, 還有Generate peripheral initialization as a pair of .c/.h files per periphral

  • 點擊右上角 GENERATE CODE 按鈕生成代碼, 打開工程.

  • Keil 點擊魔術棒或者Project -> Options for Target ..., 默認配置DebugST-link Debugger, 點擊Setting -> Flash Download -> 勾選Reset and Run, 這樣下載後可以自動復位運行.

以上操作陌生的話, 可以參考之前的STM32CubeMX系列文章.

編譯一下整個工程, 看看默認佔用的空間, 如果是16kB Flash的單片機, 可能裝不下, 對於64k以上Flash用起來壓力不大, STM32F767ZI有2M Flash, 沒有壓力:
在這裏插入圖片描述
打開Application/User下的freertos.c文件, 在各自的入口函數裏面點燈:

/* USER CODE END Header_Entry_LED1 */
void Entry_LED1(void *argument)
{
  /* USER CODE BEGIN Entry_LED1 */
  /* Infinite loop */
  for(;;)
  {
		HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
		osDelay(1000);
  }
  /* USER CODE END Entry_LED1 */
}

/* USER CODE BEGIN Header_Entry_LED2 */
/**
* @brief Function implementing the Task_LED2 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_Entry_LED2 */
void Entry_LED2(void *argument)
{
  /* USER CODE BEGIN Entry_LED2 */

  /* Infinite loop */
  for(;;)
  {
		HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
		osDelay(1000);
  }
  /* USER CODE END Entry_LED2 */
}

編譯下載, 可以看到板子上的兩個LED每隔1s翻轉一次.

修改下LED1的代碼, 加一行HAL_Delay(1000);:

/* USER CODE END Header_Entry_LED1 */
void Entry_LED1(void *argument)
{
  /* USER CODE BEGIN Entry_LED1 */
  /* Infinite loop */
  for(;;)
  {
		HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
		HAL_Delay(1000);
		osDelay(1000);
  }
  /* USER CODE END Entry_LED1 */
}

編譯下載, 可以看到LED1變成了2s翻轉一次, 而LED2不受影響, 仍然是1s翻轉一次.

MCU的初學者可能記得按鍵延時消抖的delay_ms(10), while中放一個這玩意很傷, 於是費勁九牛二虎之力用定時器把這個delay_ms消掉, 方法可參見 從單片機到操作系統③,走進FreeRTOS一文, 現在有了FreeRTOS, 有了STM32CubeMX, 這些都方便多了.

看一下上面工程中生成的代碼, 初始化函數MX_FREERTOS_Init()中創建的LED1的任務(線程):

  /* definition and creation of Task_LED1 */
  const osThreadAttr_t Task_LED1_attributes = {
    .name = "Task_LED1",
    .priority = (osPriority_t) osPriorityLow,
    .stack_size = 128
  };
  Task_LED1Handle = osThreadNew(Entry_LED1, NULL, &Task_LED1_attributes);

可能許多人常見的是osThreadCreate, osThreadNew聲明瞭LED1的入口函數Entry_LED1, 就是上面填進去的名字. 初始化完成後, 就開啓了調度器:

  /* Start scheduler */
  osKernelStart();

Entry_LED1Entry_LED2每個線程都有一個for(;;), 給人的感覺是單片機開啓了分身.

搶佔式 vs 協同式調度

FreeRTOS可選兩種調度算法: 優先級搶佔式調度, 協同式(協作式)調度.
FREERTOSConfiguration裏面默認的是搶佔式:
在這裏插入圖片描述
改成Disable, 重新生成代碼, Entry_LED1加入HAL_Delay(2000):

/* USER CODE END Header_Entry_LED1 */
void Entry_LED1(void *argument)
{
  /* USER CODE BEGIN Entry_LED1 */
  /* Infinite loop */
  for(;;)
  {
		HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
		HAL_Delay(2000);
		osDelay(1000);
  }
  /* USER CODE END Entry_LED1 */
}

編譯下載, 發現變成了: LED1亮, 2s後LED2纔開始亮, 再1s後LED1滅, 再2s後LED2滅.

搶佔式調度的特性:
在這裏插入圖片描述
協作式調度的特性:
在這裏插入圖片描述
(備註: 如果此時把LED2的任務優先級調的比LED1高, 發現LED2和LED1是同時亮起, 但時間不準確).

TICK_RATE_HZ

滴答, 時基, 默認1000Hz. 這樣osDelay(1000)才表示1s.
在這裏插入圖片描述
如果把這個值改爲1, 那麼osDelay(1000)的時間尺度將變得特別長且不準確, 實測達68s之久, 所以這個值還是不要改.

其實STM32的HAL庫默認的時基也是1kHz, 如果改了, HAL_Delay(1000)也不再是1s了.

Kernel settings參數

上面兩個參數着重介紹了下, 其餘的可以瞭解的Kernel setting參數列表彙總一下:

Kernel Parameters Default Value Description
USE_PREEMPTION Enabled Set to 1 to use the preemptive RTOS scheduler, or 0 to use the cooperative RTOS scheduler. (一般用搶佔式調度)
TICK_RATE_HZ 1000 Sets the tick interrupt frequency. The value is specified in Hz. configTICK_RATE_HZ must be between 1 and 1 000. (默認的1000最好不改)
MINIMAL_STACK_SIZE 128 Words Sets the size of the stack allocated to the idle task. The value is specified in words (here, of 32 bits), not bytes. configMINIMAL_STACK_SIZE must be between 64 Words and 3.84 KWords. The value chosen by the user should take into account the number of threads, the total heap size and the system stack size. If the totality of used stack size is higher than the total heap size, FreeRTOS will not be able to create and manage the tasks. For information: max value = configTOTAL_HEAP_SIZE/4 (when allocation is dynamic), max value = MCU ram size/4 (when allocation is static)(特別注意, Project Manager中堆棧的配置, 必要時要放大)
MAX_TASK_NAME_LEN 16 Sets the maximum number of characters that can be used for the name of a task. The NULL terminator is included in the count of characters. configMAX_TASK_NAME_LEN must be between 12 and 255.
IDLE_SHOULD_YIELD Enabled IDLE_SHOULD_YIELD controls the behavior of the idle task if there are application tasks that also run at the idle priority. It only has an effect if the preemptive scheduler is being used. - if IDLE_SHOULD_YIELD is set to 0, then the idle task will never yield to another task, and will only leave the Running state when it is preempted. - if IDLE_SHOULD_YIELD is set to 1, then the idle task will never perform more than one iteration of its defined functionality without yielding to another task if there is another idle priority task that is in the Ready state. This ensures a minimum amount of time is spent in the idle task when application tasks are available to run.
USE_TASK_NOTIFICATIONS Enabled Each RTOS task has a 32-bit notification value. An RTOS task notification is an event sent directly to a task that can unblock the receiving task, and optionally update the receiving task’s notification value. RTOS task notification functionality is enabled by default, and can be excluded from a build (saving 8 bytes per task) by setting configUSE_TASK_NOTIFICATIONS to 0 in FreeRTOSConfig.h (achieved when setting USE_TASK_NOTIFICATIONS to Disabled).
RECORD_STACK_HIGH_ADDRESS Diabled When set to 1 (Enabled) the stack start address is saved into each task’s TCB (assuming stack grows down).

內存管理

FreeRTOS允許用戶選擇不同的內存管理方式(heap_1~heap_5), 默認爲heap_4:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
至於heap_5.c, 有興趣去翻下源碼.

內存分配的示例:
在這裏插入圖片描述

空閒任務和空閒鉤子

osKernelStart();函數原型爲:

osStatus_t osKernelStart (void) {
  osStatus_t stat;

  if (IS_IRQ()) {
    stat = osErrorISR;
  }
  else {
    if (KernelState == osKernelReady) {
      KernelState = osKernelRunning;
      vTaskStartScheduler();
      stat = osOK;
    } else {
      stat = osError;
    }
  }

  return (stat);
}

調用了vTaskStartScheduler(), 裏面自動創建了一個空閒任務portTASK_FUNCTION, 關於空閒任務:
在這裏插入圖片描述

空閒任務的鉤子函數:
在這裏插入圖片描述
默認配置如下圖:
在這裏插入圖片描述

運行時等的參數

默認如下圖:
在這裏插入圖片描述

線程(任務)管理

默認配置如下, 可參考註釋食用:
在這裏插入圖片描述
只有上面的Include definetions使能, 在cmsis_os2.h定義的線程管理函數(任務API)才能正常用:

//  ==== Thread Management Functions ====

/// Create a thread and add it to Active Threads.
/// \param[in]     func          thread function.
/// \param[in]     argument      pointer that is passed to the thread function as start argument.
/// \param[in]     attr          thread attributes; NULL: default values.
/// \return thread ID for reference by other functions or NULL in case of error.
osThreadId_t osThreadNew (osThreadFunc_t func, void *argument, const osThreadAttr_t *attr);

/// Get name of a thread.
/// \param[in]     thread_id     thread ID obtained by \ref osThreadNew or \ref osThreadGetId.
/// \return name as NULL terminated string.
const char *osThreadGetName (osThreadId_t thread_id);

/// Return the thread ID of the current running thread.
/// \return thread ID for reference by other functions or NULL in case of error.
osThreadId_t osThreadGetId (void);

/// Get current thread state of a thread.
/// \param[in]     thread_id     thread ID obtained by \ref osThreadNew or \ref osThreadGetId.
/// \return current thread state of the specified thread.
osThreadState_t osThreadGetState (osThreadId_t thread_id);

/// Get stack size of a thread.
/// \param[in]     thread_id     thread ID obtained by \ref osThreadNew or \ref osThreadGetId.
/// \return stack size in bytes.
uint32_t osThreadGetStackSize (osThreadId_t thread_id);

/// Get available stack space of a thread based on stack watermark recording during execution.
/// \param[in]     thread_id     thread ID obtained by \ref osThreadNew or \ref osThreadGetId.
/// \return remaining stack space in bytes.
uint32_t osThreadGetStackSpace (osThreadId_t thread_id);

/// Change priority of a thread.
/// \param[in]     thread_id     thread ID obtained by \ref osThreadNew or \ref osThreadGetId.
/// \param[in]     priority      new priority value for the thread function.
/// \return status code that indicates the execution status of the function.
osStatus_t osThreadSetPriority (osThreadId_t thread_id, osPriority_t priority);

/// Get current priority of a thread.
/// \param[in]     thread_id     thread ID obtained by \ref osThreadNew or \ref osThreadGetId.
/// \return current priority value of the specified thread.
osPriority_t osThreadGetPriority (osThreadId_t thread_id);

/// Pass control to next thread that is in state \b READY.
/// \return status code that indicates the execution status of the function.
osStatus_t osThreadYield (void);

/// Suspend execution of a thread.
/// \param[in]     thread_id     thread ID obtained by \ref osThreadNew or \ref osThreadGetId.
/// \return status code that indicates the execution status of the function.
osStatus_t osThreadSuspend (osThreadId_t thread_id);

/// Resume execution of a thread.
/// \param[in]     thread_id     thread ID obtained by \ref osThreadNew or \ref osThreadGetId.
/// \return status code that indicates the execution status of the function.
osStatus_t osThreadResume (osThreadId_t thread_id);

/// Detach a thread (thread storage can be reclaimed when thread terminates).
/// \param[in]     thread_id     thread ID obtained by \ref osThreadNew or \ref osThreadGetId.
/// \return status code that indicates the execution status of the function.
osStatus_t osThreadDetach (osThreadId_t thread_id);

/// Wait for specified thread to terminate.
/// \param[in]     thread_id     thread ID obtained by \ref osThreadNew or \ref osThreadGetId.
/// \return status code that indicates the execution status of the function.
osStatus_t osThreadJoin (osThreadId_t thread_id);

/// Terminate execution of current running thread.
__NO_RETURN void osThreadExit (void);

/// Terminate execution of a thread.
/// \param[in]     thread_id     thread ID obtained by \ref osThreadNew or \ref osThreadGetId.
/// \return status code that indicates the execution status of the function.
osStatus_t osThreadTerminate (osThreadId_t thread_id);

/// Get number of active threads.
/// \return number of active threads.
uint32_t osThreadGetCount (void);

/// Enumerate active threads.
/// \param[out]    thread_array  pointer to array for retrieving thread IDs.
/// \param[in]     array_items   maximum number of items in array for retrieving thread IDs.
/// \return number of enumerated threads.
uint32_t osThreadEnumerate (osThreadId_t *thread_array, uint32_t array_items);

比如上面點燈中, LED2作爲系統初始化完成的指示燈, 點亮後就不再變化了, 可以把自己所在的任務給刪除:

/* USER CODE END Header_Entry_LED2 */
void Entry_LED2(void *argument)
{
  /* USER CODE BEGIN Entry_LED2 */

  /* Infinite loop */
  for(;;)
  {
		HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
		osThreadTerminate(Task_LED2Handle);
  }
  /* USER CODE END Entry_LED2 */
}

今天先介紹到這裏, 後面大概會結合STM32CubeMX FREERTOS後面幾個選項卡, 總結任務間通信(隊列, 信號量, 互斥量), 軟件定時器, 臨界區等的知識或用法.

微信公衆號

歡迎掃描二維碼關注本人微信公衆號, 及時獲取或者發送給我最新消息:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章