【FreeRTOS】基于G431+Cubemx自用筆記

系列文章目錄

留空


文章目錄

  • 系列文章目錄
  • 前言
  • 一、從頭開始創建一個FreeRTOS工程
    • 1.1 在 "Timebase Source" 中,選擇其他TIM
    • 1.2 配置FreeRTOS的參數
    • 1. 3 添加任務
  • 二、動態任務的創建/刪除
    • 2.1 函數介紹
      • 2.1.1 創建動態任務`xTaskCreate()`
      • 2.1.2 創建靜態任務`xTaskCreateStatic()`
      • 2.1.3 刪除任務 `vTaskDelete()`
    • 2.2 編寫例題代碼
      • 2.2.1 添加任務
      • 2.2.2 編寫任務
      • 2.2.3 完整代碼
  • 三、任務掛起與恢復
    • 3.1 函數介紹
      • 3.1.1 任務掛起`vTaskSuspend()`
      • 3.1.2 任務恢復 `vTaskResume()`
      • 3.1.3 從中斷任務恢復 `xTaskResumeFromISR()`
      • 3.1.4 獲取任務狀態 `eTaskGetState()`
    • 3.2 編寫例題代碼
      • 3.2.1 任務掛起/恢復
      • 3.2.2 從中斷恢復任務
  • 四、FreeRTOS中斷管理
    • 4.1 概念理解
      • 4.1.1 中斷管理
      • 4.1.2 中斷優先級推薦設置
      • 4.1.3 FreeRTOS相關宏
    • 4.2 函數介紹
      • 4.2.1 禁用中斷 `portDISABLE_INTERRUPTS()`
      • 4.2.2 啟用中斷 `portENABLE_INTERRUPTS()`
    • 4.3 編寫例題代碼
  • 五、臨界段代碼保護及任務調度器的掛起和恢復
    • 5.1 概念理解
      • 5.1.1 臨界段代碼保護
      • 5.1.2 任務調度器
    • 5.2 函數介紹
      • 5.2.1 臨界段保護函數(任務級)
      • 5.2.2 臨界段保護函數(中斷級)
      • 5.2.3 任務調度器的掛起和恢復函數
  • 六、列表和列表項
    • 6.1 概念理解
    • 6.2 函數介紹
      • 6.2.1 列表/列表項結構體
      • 6.2.2 初始化列表
      • 6.2.3 初始化列表項
      • 6.2.4 列表項插入列表
      • 6.2.5 列表項末尾插入列表
      • 6.2.6 列表移出列表項
    • 6.3 編寫例題代碼
  • 七、啟動任務調度器【內容太多,先略】
    • 7.1 概念理解
  • 八、時間片調度
    • 8.1 概念理解
    • 8.2 函數介紹
    • 8.3 編寫例題代碼
  • 總結


前言

自用
豬豬豬:還在更新中


因為參加完藍橋杯后,想學RTOS,所以直接無縫銜接,此筆記是基于藍橋杯板子G431RBT6學習的!

一、從頭開始創建一個FreeRTOS工程

基本的配置跳過,只記錄有關FreeRTOS的創建!

1.1 在 “Timebase Source” 中,選擇其他TIM

在 STM32 + FreeRTOS 項目中,FreeRTOS 默認使用 SysTick 作為時基,而 STM32CubeMX 默認的 HAL 庫也是使用 SysTick這兩個會沖突,導致系統運行不正常,尤其是出現任務調度異常、延時失效等問題。

所以把Timebase Source改成了TIM17
在這里插入圖片描述

1.2 配置FreeRTOS的參數

在這里插入圖片描述

關鍵參數(全部默認即可)

參數名稱設置值描述
USE_PREEMPTIONEnabled啟用搶占式調度,允許高優先級任務搶占低優先級任務的CPU時間。
CPU_CLOCK_HZSystemCoreClockCPU的時鐘頻率,通常由系統定義,表示處理器的時鐘速度。
TICK_RATE_HZ1000系統的時基(tick)頻率為1000Hz,即每1毫秒產生一個tick。
MAX_PRIORITIES56系統中任務的最大優先級數,FreeRTOS使用優先級來調度任務。
MINIMAL_STACK_SIZE128 Words任務的最小堆棧大小為128個詞(word)。
MAX_TASK_NAME_LEN16任務名稱的最大長度為16個字符。
TOTAL_HEAP_SIZE3072 Bytes為FreeRTOS堆分配的總內存大小為3072字節。
Memory Management schemeheap_4使用的內存管理方案,不同的方案可能有不同的內存分配和釋放策略。

以下是 FreeRTOS Mode and Configuration 界面中全部參數,按功能模塊分類(可跳過)

(1)Kernel Settings(內核設置)

參數名稱當前配置值含義說明
USE_PREEMPTIONEnabled啟用搶占式調度(高優先級任務可立即搶占低優先級任務)
CPU_CLOCK_HZSystemCoreClockCPU時鐘頻率(通常由MCU定義,如SystemCoreClock=16MHz
TICK_RATE_HZ1000系統Tick頻率(1kHz=1ms一個Tick)
MAX_PRIORITIES56最大任務優先級數(0為最低,55為最高)
MINIMAL_STACK_SIZE128 Words空閑任務(Idle Task)的堆棧大小(單位:字,具體字節數需乘以字長)
MAX_TASK_NAME_LEN16任務名稱的最大字符長度
USE_16_BIT_TICKSDisabled禁用16位Tick計數器(使用32位計數器,支持更長運行時間)
IDLE_SHOULD_YIELDEnabled空閑任務主動讓出CPU給同等優先級的用戶任務(節能場景可能需要禁用)
USE_PORT_OPTIMISED _TASK_SELECTIONDisabled禁用硬件優化任務選擇(通用軟件實現,兼容性更好)
USE_TICKLESS_IDLEDisabled禁用Tickless低功耗模式(始終維持Tick中斷)

(2)Mutexes & Semaphores(互斥量與信號量)

參數名稱當前配置值含義說明
USE_MUTEXESEnabled啟用互斥量(Mutex)支持。
USE_RECURSIVE_MUTEXESEnabled啟用遞歸互斥量(同一任務可重復加鎖)。
USE_COUNTING_SEMAPHORESEnabled啟用計數信號量。
QUEUE_REGISTRY_SIZE8隊列注冊表大小(用于調試工具跟蹤隊列/信號量)。

(3)Memory Management(內存管理)

參數名稱當前配置值含義說明
TOTAL_HEAP_SIZE3072 Bytes動態內存堆總大小(根據任務和隊列數量調整)。
Memory Management schemeheap_4使用動態內存分配方案4(合并空閑塊,避免碎片化)。
Memory AllocationDynamic / Static支持動態和靜態內存分配(需用戶提供靜態內存時需配置configSUPPORT_STATIC_ALLOCATION)。

1. 3 添加任務

下圖是STM32CubeMX 的默認任務,可以修改它的名稱和函數類型,但不能刪除它。這是 CubeMX 提供的一個固定設置,用于初始化FreeRTOS和提供一個最基本的任務框架。
在這里插入圖片描述

參數說明

配置項當前值解釋說明
Task NamedefaultTask任務的名稱,這里是 defaultTask。任務名稱用于標識該任務
PriorityosPriorityNormal任務的優先級,osPriorityNormal 表示任務的優先級為正常(即中等優先級)
Stack Size (Words)128任務堆棧的大小,單位是字(Words),這里的 128 表示任務棧有128個字的空間。每個字的大小通常是4字節(32位系統)
Entry FunctionStartDefaultTask任務的入口函數,任務開始執行時會調用該函數。這里的 StartDefaultTask 是該任務的函數名稱
Code Generation OptionDefault代碼生成選項,設置為 Default 表示使用默認的代碼生成設置
ParameterNULL傳遞給任務的參數,這里設置為 NULL,表示任務不需要傳入任何參數
AllocationDynamic任務棧內存分配方式,設置為 Dynamic 表示任務棧的內存是在運行時動態分配的
Buffer NameNULL緩沖區名稱,設置為 NULL 表示沒有指定緩沖區。通常用于處理一些任務的輸入輸出緩沖區
Control Block NameNULL任務控制塊名稱,設置為 NULL 表示沒有指定任務的控制塊(在FreeRTOS中用于存儲任務的元數據)

關于 STM32CubeMX 中的默認任務:

  • 默認任務:這是 CubeMX 在生成的代碼中自動創建的第一個任務。它通常用于進行系統初始化、測試和調試。
  • 修改默認任務:雖然不能刪除默認任務,但可以:
    • 修改任務的名稱
    • 修改任務執行的函數(即默認任務執行的代碼)
    • 修改任務的優先級

后面,我們手寫代碼時,我們可以通過 FreeRTOS 提供的 API 創建自己的任務、隊列、信號量等對象。

最后!創建工程!

然后在,工程文件夾內,創建一個文件夾BSP,拿來放寫好的底層驅動文件。

在這里插入圖片描述

OK!完成!!(基本配置完成的文件放在最后了:LED KEY Usart Delay)

二、動態任務的創建/刪除

2.1 函數介紹

2.1.1 創建動態任務xTaskCreate()

(1)函數原型

BaseType_t xTaskCreate(TaskFunction_t pxTaskCode,    // 任務函數const char * const pcName,    // 任務名稱configSTACK_DEPTH_TYPE usStackDepth,  // 棧大小void *pvParameters,           // 傳入任務的參數UBaseType_t uxPriority,       // 任務優先級TaskHandle_t *pxCreatedTask   // 返回任務句柄(可以是 NULL)
);

(2)參數解釋

參數含義舉例
pxTaskCode任務函數名(任務函數就是編寫任務具體做什么)比如:任務函數為void Task_LED(void *pvParameters),任務函數名就是Task_LED
pcName給任務起個名字(調試查看用)"LED_Task"
usStackDepth分配給任務的棧大小(注意單位是“字”,不是字節一般 128~512 比較常見
pvParameters傳遞給任務函數的參數可以傳結構體、變量、NULL
uxPriority任務優先級,值越大越重要通常范圍 0~configMAX_PRIORITIES-1
pxCreatedTask返回這個任務的“身份證”(句柄),我們可以以后用它去操作這個任務。如想刪掉、掛起這個任務,就需要通過句柄去操作&xxx_Handle,或者傳 NULL 表示我不關心這個任務的句柄

(3)返回值說明

返回值含義
pdPASS創建成功
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY內存不足,創建失敗(系統堆不夠)

(4)示例代碼

/***** (1)任務函數(任務是要做什么) ******/
void LED_Task(void *pvParameters)
{while (1){HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);  // 翻轉LEDvTaskDelay(500);   // 延時500ms}
}/***** (2)創建任務函數 ******/
// 創建的任務,系統會把它加入調度器,由 FreeRTOS 自動進行任務切換調度
xTaskCreate(LED_Task,"LED",128,NULL,2,NULL);

這個任務的功能是:每隔 500ms 翻轉一次 GPIOB 的 PIN_0 引腳,從而實現 LED 的閃爍效果

參數名類型示例值含義
1pxTaskCodeTaskFunction_tLED_Task任務函數指針,告訴 FreeRTOS 這個任務要做什么。這里是一個控制 LED 閃爍的函數。
2pcNameconst char *"LED"任務名稱,用于調試和查看任務狀態時顯示的名字。
3usStackDepthuint16_t128棧大小,單位是“字”(word),不是字節。STM32 中 1 字 = 4 字節,因此此任務分配了 512 字節棧空間。
4pvParametersvoid *NULL傳遞給任務的參數。如果不需要傳遞參數,寫 NULL
5uxPriorityUBaseType_t2任務優先級。值越大,優先級越高。
6pxCreatedTaskTaskHandle_t *NULL接收創建的任務句柄的指針。如果后續要操作該任務(如刪除、掛起等),需傳入句柄變量地址;后續不需要這些操作就傳 NULL

如果我們要看任務是否創建成功:

/***** (1)任務函數:LED 閃爍 ******/
void LED_Task(void *pvParameters)
{while (1){HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);  // 翻轉 LED 引腳vTaskDelay(500);   // 延時 500ms}
}/***** (2)任務創建函數:包含成功判斷 ******/
void CreateTasks(void)
{BaseType_t xReturn;  // 用于接收任務創建結果xReturn = xTaskCreate(LED_Task,"LED",128,NULL,2,NULL);if (xReturn == pdPASS) // 創建成功{printf("LED_Task 創建成功!\r\n");}else // 創建失敗{printf("LED_Task 創建失敗!\r\n");}
}

假設我們傳入了任務句柄變量,例如 &LEDTaskHandle

// 定義一個任務句柄
TaskHandle_t LEDTaskHandle; ///創建任務 `LED_Task`,并把這個任務的“控制權”交給變量 `LEDTaskHandle`
xTaskCreate(LED_Task, "LED", 128, NULL, 2, &LEDTaskHandle);

可以后續使用句柄對任務進行操作

  • 刪除LED任務:vTaskDelete(LEDTaskHandle);
  • 掛起LED任務:vTaskSuspend(LEDTaskHandle);

2.1.2 創建靜態任務xTaskCreateStatic()

這個函數是為 不使用動態內存分配(malloc) 的場景準備的。我們要自己準備好棧空間任務控制塊

TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode,const char * const pcName,const uint32_t ulStackDepth,void * const pvParameters,UBaseType_t uxPriority,StackType_t * const puxStackBuffer,     // 提前分配好的棧StaticTask_t * const pxTaskBuffer       // 提前準備好的任務控制塊
);

【后續沒用到,我就是一個直接跳過!!】

2.1.3 刪除任務 vTaskDelete()

(1)函數原型

void vTaskDelete(TaskHandle_t xTaskToDelete);

(2)參數解釋

參數含義
xTaskToDelete要刪除的任務的句柄。如果想刪除當前任務,可以傳入 NULL
  • vTaskDelete(NULL); → 刪除當前正在運行的任務
  • vTaskDelete(xxx_Handle); → 刪除指定句柄的任務

(3)示例代碼

/*****(1)任務函數,運行后自刪*****/
void LED_Task(void *pvParameters)
{HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);	printf("LED_Task 自我刪除!\r\n");vTaskDelete(NULL);  // 刪除自己
}/*****(2)創建任務*****/
xTaskCreate(LED_Task, "LED", 128, NULL, 2, NULL);

或由其他任務/定時器刪除:

/***** 任務函數1,運行后刪任務2 *****/
void LED_Task(void *pvParameters)
{HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);	vTaskDelete(Task2_Handle);  // 刪除任務2
}
/***** 任務函數2 *****/
void LED2_Task(void *pvParameters)
{HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);	
}/*****(2)創建任務*****/
xTaskCreate(LED_Task, "LED", 128, NULL, 2, NULL);              // 不保存句柄
xTaskCreate(LED2_Task, "LED2", 128, NULL, 2, &Task2_Handle);   // 有句柄,用于后續刪掉操作!

2.2 編寫例題代碼

這里參考正點原子例題!

在這里插入圖片描述

2.2.1 添加任務

打開工程,這里一共四個任務,我們先創建好任務函數和添加任務

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */// 定義三個任務句柄,用于后續管理和控制任務(如掛起、恢復等)
TaskHandle_t TaskLED1_Handle;
TaskHandle_t TaskLED2_Handle;
TaskHandle_t TaskKEY_Handle;/* USER CODE END Variables *//* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */// 四個任務函數的聲明
void Task_Start(void *argument);   
void Task1_LED(void *argument);  
void Task2_LED(void *argument);    
void Task3_KEY(void *argument);    /* USER CODE END FunctionPrototypes *//****** (1) 創建四個任務函數 *******/void Task_Start(void *argument)
{printf("Hello! Task Start!\r\n");// 創建 LED1 任務,優先級 26,堆棧大小 128xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);// 創建 LED2 任務,優先級 27,堆棧大小 128xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle);	// 創建 KEY 按鍵任務,優先級 28,堆棧大小 128xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);// 刪除當前任務vTaskDelete(NULL); 
}void Task1_LED(void *argument)
{while (1){static int N1 = 0;N1++;printf("Task_1 -- %d\r\n", N1);vTaskDelay(500);}
}void Task2_LED(void *argument)
{while (1){static int N2 = 0;N2++;printf("Task_2 -- %d\r\n", N2);vTaskDelay(1000);		}
}void Task3_KEY(void *argument)
{while (1){static int N3 = 0;N3++;printf("Task_3 -- %d\r\n", N3);vTaskDelay(100);  }
}
... ...
void MX_FREERTOS_Init(void) {... .../* USER CODE BEGIN RTOS_THREADS *//* 添加 FreeRTOS 啟動任務 *//****** (2) 添加任務 *******/// 創建啟動任務,優先級 25,堆棧大小 128,啟動時由調度器自動運行xTaskCreate(Task_Start, "TaskStart", 128, NULL, 25, NULL);/* USER CODE END RTOS_THREADS */
}

創建好了四個任務,每個任務對應有任務函數添加任務,打印自增看看任務咋運行的

我們給任務分配了優先級

任務名稱函數名優先級(數字越大優先級越高)說明
啟動任務Task_Start25啟動時創建其他任務后自刪除
LED1任務Task1_LED26控制LED1,每500ms打印一次
LED2任務Task2_LED27控制LED2,每1000ms打印一次
按鍵處理任務Task3_KEY28處理按鍵輸入,優先級最高

我們在第一章可以看到,優先級設置56個,為什么這是25到28呢???

我們看看默認任務的優先級是多少

/* Definitions for defaultTask */osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {.name = "defaultTask",.priority = (osPriority_t) osPriorityNormal,.stack_size = 128 * 4
};

.priority = (osPriority_t) osPriorityNormal,

點擊進去看看這個普通優先級到底多少級

  osPriorityBelowNormal6  = 16+6,       ///< Priority: below normal + 6osPriorityBelowNormal7  = 16+7,       ///< Priority: below normal + 7osPriorityNormal        = 24,         ///< Priority: normalosPriorityNormal1       = 24+1,       ///< Priority: normal + 1osPriorityNormal2       = 24+2,       ///< Priority: normal + 2

哦哦哦,原來是24,那為了避免默認任務打擾我們,直接從25開始!

OKOK,說這么多,先把程序下載到板子看看啥情況,記得打開串口哦

在這里插入圖片描述

怎么個事,我的Task3呢!!!

函數介紹里,xTaskCreate()會返回值,可以根據返回值判斷任務是否成功

創建任務不成功的原因有很多,有一個可能就是給FreeRTOS分配的地方太小,裝不下那么多任務

Task_Start添加幾行代碼,我們打印出來看看

void Task_Start(void *argument)
{printf("Hello!Task Start!\r\n");xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle);	xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);BaseType_t xReturn;  // 用于接收任務3的創建結果xReturn = xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);;if (xReturn == pdPASS) // 創建成功{printf("Task3 創建成功!\r\n");}else // 創建失敗{printf("Task3 創建失敗!\r\n");}printf("Free Heap: %d\r\n", xPortGetFreeHeapSize());// 刪除自己vTaskDelete(NULL); 
}

串口輸出

在這里插入圖片描述

Task3創建失敗,但是Free Heap: 568不是還有空地方嗎??

xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);

豬豬豬:我們創建任務時,任務堆棧實際占用的內存大小 = 128 words × 4 字節 = 512 字節

再看你當時打印的 Free Heap:568 字節,確實還剩下一點,但:

原因說明
剩余堆空間不夠你還有 568 字節,但新任務創建至少要分配 堆棧空間 + TCB 控制塊內存(約 100~200 字節),總共就超過 568 字節了。
堆碎片化即使堆總量看起來夠用,但因為分散,可能沒有一整塊連續的大內存區域給任務使用,導致創建失敗。

那么解決辦法

  • 減小任務堆棧大小,一般簡單任務(比如只打印或輪詢按鍵)用不了這么大棧。
// 試試減小堆棧到 100 或 96(word 單位)
xTaskCreate(Task3_KEY, "Task3", 100, NULL, 28, &TaskKEY_Handle);
  • 增加堆大小,在 FreeRTOSConfig.h 中修改:
#define configTOTAL_HEAP_SIZE    ( ( size_t ) ( 5 * 1024 ) )  // 改為 5KB 或更大

選擇了第二種,改為 5KB!

重新下載,看看是不是這個原因
在這里插入圖片描述

OK!成功了

前四行是開始任務創建的三個任務,后面也可以看出來是優先級最高的Task3執行,然后就是Task2,最后是Task1。但是為什么創建任務的第一次打印不是Task3最開始呢???

我們看看開始任務里,我們最先創建的是Task1,而且它高于開始任務。

xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);
xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle);	
xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);

所以,Task1被創建完成后,直接就開始執行了,Task2是FreeRTOS執行完Task1后再回到Task_Start里創建的,Task3同理!那開始的時候怎么才能按優先級執行呢?臨界區!后面我們會詳細說明,這里只需要知道這個是停止執行任務的OK了。

void Task_Start(void *argument)
{printf("Hello!Task Start!\r\n");taskENTER_CRITICAL(); // 進入臨界區xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle);	xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);vTaskDelete(NULL); // 刪除自己taskEXIT_CRITICAL(); // 退出臨界區
}

加上這兩行代碼即可,我們再下載,打開串口看看(截圖太麻煩啦,直接復制粘貼了)

Hello!Task Start!
Task_3--1
Task_2--1
Task_1--1
Task_3--2
Task_3--3
Task_3--4
Task_3--5
Task_2--2
Task_1--2
Task_3--6
Task_3--7
Task_3--8
Task_3--9
Task_3--10
Task_2--3
Task_1--3

OKOK,這回就對了。

2.2.2 編寫任務

根據題目要求我們把任務函數補充完整

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */uint8_t LEDx = 0x00;
uint8_t LED1_Flag = 1;
uint8_t LED2_Flag = 1;TaskHandle_t TaskLED1_Handle;
TaskHandle_t TaskLED2_Handle;
TaskHandle_t TaskKEY_Handle;/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */void Task_Start(void *argument);
void Task1_LED(void *argument);
void Task2_LED(void *argument);
void Task3_KEY(void *argument);/****** (1) 創建四個任務函數 *******/void Task_Start(void *argument)
{printf("Hello!Task Start!\r\n");taskENTER_CRITICAL(); // 進入臨界區xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle);	xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);// 刪除自己vTaskDelete(NULL); taskEXIT_CRITICAL(); // 退出臨界區
}void Task1_LED(void *argument)
{while (1){static int N1 = 0;N1 ++;printf("Task_1--%d\r\n",N1);switch(LED1_Flag) // LED1閃爍{case 1: LEDx |= 0x01; LED1_Flag = 2; break;case 2: LEDx &= ~(1 << 0); LED1_Flag = 1; break;default: break;}LED_Disp(LEDx);vTaskDelay(500);}
}void Task2_LED(void *argument)
{while (1){static int N2 = 0;N2 ++;printf("Task_2--%d\r\n",N2);switch(LED2_Flag) // LED2閃爍{case 1: LEDx |= 0x02; LED2_Flag = 2; break;case 2: LEDx &= ~(1 << 1); LED2_Flag = 1; break;default: break;}LED_Disp(LEDx);	vTaskDelay(500);		}
}void Task3_KEY(void *argument)
{while (1){static int N3 = 0;N3 ++;printf("Task_3--%d\r\n",N3);KEY_Proc(); // 掃描檢測按鍵if(KEY_Down == 1) // 按鍵1--刪掉任務1{vTaskDelete(TaskLED1_Handle);printf("刪掉了Task_1!!\r\n");}vTaskDelay(100);	}
}

然后下載,查看燈,按下按鍵1,打開串口看看

Hello!Task Start!
Free Heap: 1992
Task_3--1
Task_2--1
Task_1--1
Task_3--2
Task_3--3
Task_3--4
Task_3--5
Task_2--2
Task_1--2
Task_3--6
Task_3--7
Task_3--8
Task_3--9
Task_3--10
刪掉了Task_1!!
Task_2--3
Task_3--11
Task_3--12
Task_3--13
Task_3--14
Task_2--4
Task_3--15
Task_3--16

可以看到,下載完成后,兩個燈幾乎同亮同滅,按下按鍵1后,LED1停止閃爍,串口輸出已刪掉提示

然后我再次按下按鍵1 ,遇到的問題:

第一次按鍵正常刪除 Task1_LED,但按第二次后串口卡頓,Task3_KEY 不再打印,卡死

第二次進入 vTaskDelete(TaskLED1_Handle),但是任務1已經被刪掉了,所以這時候的 TaskLED1_Handle == NULL ,問題根本在于:

vTaskDelete(NULL);  // 當句柄為 NULL 時,刪除的是自己!

連續按兩次后,**Task3_KEY 中自己把自己刪了!**所以就“無了”,串口沒輸出、任務也不在了。

為了避免這個問題!

void Task3_KEY(void *argument)
{while (1){static int N3 = 0;N3 ++;printf("Task_3--%d\r\n",N3);KEY_Proc();if(KEY_Val == 1 && TaskLED1_Handle != NULL) // 關鍵!防止再次誤刪if(KEY_Down == 1){vTaskDelete(TaskLED1_Handle);printf("刪掉了Task_1!!\r\n");TaskLED1_Handle = NULL;  // 關鍵!防止再次誤刪}vTaskDelay(100);	}
}

OK,解決!

2.2.3 完整代碼

/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "LED.h"
#include "lcd.h"
#include "KEY.h"
#include "usart.h"
#include "stdio.h"
/* USER CODE END Includes *//* USER CODE BEGIN Variables */
uint8_t LEDx = 0x00;
uint8_t LED1_Flag = 1;
uint8_t LED2_Flag = 1;TaskHandle_t TaskLED1_Handle;
TaskHandle_t TaskLED2_Handle;
TaskHandle_t TaskKEY_Handle;
/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */
void Task_Start(void *argument);
void Task1_LED(void *argument);
void Task2_LED(void *argument);
void Task3_KEY(void *argument);/****** (1) 創建四個任務函數 *******/void Task_Start(void *argument)
{printf("Hello!Task Start!\r\n");taskENTER_CRITICAL(); // 進入臨界區xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle);	xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);//	BaseType_t xReturn;  // 用于接收任務創建結果
//	
//	xReturn = xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);;//	if (xReturn == pdPASS) // 創建成功
//	{
//			printf("Task3 創建成功!\r\n");
//	}
//	else // 創建失敗
//	{
//			printf("Task3 創建失敗!\r\n");
//	}printf("Free Heap: %d\r\n", xPortGetFreeHeapSize());vTaskDelete(NULL); // 刪除自己taskEXIT_CRITICAL(); // 退出臨界區
}void Task1_LED(void *argument)
{while (1){static int N1 = 0;N1 ++;printf("Task_1--%d\r\n",N1);switch(LED1_Flag){case 1: LEDx |= 0x01; LED1_Flag = 2; break;case 2: LEDx &= ~(1 << 0); LED1_Flag = 1; break;default: break;}LED_Disp(LEDx);vTaskDelay(500);}
}void Task2_LED(void *argument)
{while (1){static int N2 = 0;N2 ++;printf("Task_2--%d\r\n",N2);switch(LED2_Flag){case 1: LEDx |= 0x02; LED2_Flag = 2; break;case 2: LEDx &= ~(1 << 1); LED2_Flag = 1; break;default: break;}LED_Disp(LEDx);	vTaskDelay(500);		}
}void Task3_KEY(void *argument)
{while (1){static int N3 = 0;N3 ++;printf("Task_3--%d\r\n",N3);KEY_Proc();if(KEY_Val == 1 && TaskLED1_Handle != NULL)if(KEY_Down == 1){vTaskDelete(TaskLED1_Handle);printf("刪掉了Task_1!!\r\n");TaskLED1_Handle = NULL;  // 關鍵!防止再次誤刪}vTaskDelay(100);	}
}void MX_FREERTOS_Init(void) {/* USER CODE BEGIN RTOS_THREADS *//* add threads, ... *//****** (2) 添加任務 *******/xTaskCreate(Task_Start, "TaskStart", 128, NULL, 25, NULL);
}

番外:

除了開始任務,其他三個任務都是死循環,如果放在main.cwhile函數中

int main(void)
{HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_I2C1_Init();osKernelInitialize();MX_FREERTOS_Init();//  osKernelStart();while (1){Led1_Test();LED2_Test();KEY_Test();}
}

會發現,只有LED1燈閃,LED2和按鍵沒反應,因為程序被卡死在LED1_Test();,進不到下一個程序了。

豬豬豬:FreeRTOS 啟動后,main()主循環就被“棄用了”

  osKernelInitialize();    // 初始化 RTOS 內核MX_FREERTOS_Init();      // 創建任務osKernelStart();         // 啟動 RTOS,開始多任務調度!

一旦執行到 osKernelStart()控制權就交給 FreeRTOS 的調度器了,程序不會再執行之后的代碼,包括 while(1),所以要先注釋掉osKernelStart(); !

三、任務掛起與恢復

3.1 函數介紹

通過本實驗,掌握 FreeRTOS 中與 任務掛起與恢復 相關的 API 函數,包括:

  • vTaskSuspend()?掛起任務
  • vTaskResume()?恢復被掛起的任務
  • xTaskResumeFromISR() 從中斷服務函數中恢復任務

3.1.1 任務掛起vTaskSuspend()

(1)函數原型

void vTaskSuspend(TaskHandle_t xTaskToSuspend);

(2)參數解釋

參數說明
xTaskToSuspend要掛起的任務句柄。 如果傳 NULL,表示掛起當前任務
  • 掛起任務后,該任務會停止運行,直到被恢復。
  • 被掛起的任務不會被調度器調度,CPU 不會再執行它。

(3)示例

// 掛起 LEDTask 任務
vTaskSuspend(LEDTaskHandle);  // 自己掛起自己
vTaskSuspend(NULL); 

3.1.2 任務恢復 vTaskResume()

(1)函數原型

void vTaskResume(TaskHandle_t xTaskToResume);

(2)參數解釋

參數說明
xTaskToResume要恢復的任務句柄
  • 將之前掛起的任務重新加入就緒隊列,使其可以繼續執行。
  • 只能用于恢復由 vTaskSuspend() 掛起的任務

(3)示例

vTaskResume(LEDTaskHandle);  // 讓 LEDTask 任務恢復運行

3.1.3 從中斷任務恢復 xTaskResumeFromISR()

(1)函數原型

BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume);

(2)參數解釋

參數名含義
xTaskToResume要恢復的任務的句柄,僅能用于被 vTaskSuspend() 掛起的任務

(3)返回值說明

返回值含義
pdTRUE任務恢復后就緒,建議在中斷中進行一次任務切換
pdFALSE無需切換上下文(恢復任務未使更高優先級任務就緒)

(4)示例

void EXTI0_IRQHandler(void)
{BaseType_t xHigherPriorityTaskWoken = pdFALSE;xTaskResumeFromISR(LEDTaskHandle);  // 恢復任務portYIELD_FROM_ISR(xHigherPriorityTaskWoken);  // 判斷是否需要任務切換
}

3.1.4 獲取任務狀態 eTaskGetState()

(1)函數原型

eTaskState eTaskGetState(TaskHandle_t xTask);

(2)參數解釋

參數名含義
xTask要查詢狀態的任務的句柄

(3)返回值說明

返回值含義
eReady任務已準備好執行,但當前沒有在運行。任務在就緒隊列中等待調度。
eRunning任務當前正在運行。
eBlocked任務因等待某些資源(例如信號量、隊列等)而被阻塞。
eSuspended任務已被掛起,不能被調度執行。
eDeleted任務已經被刪除。

(4)示例

void Task3_KEY(void *argument)
{while (1){static int N3 = 0;N3++;printf("Task_3--%d\r\n", N3);// 查詢任務 1 (TaskLED1) 的狀態eTaskState taskState = eTaskGetState(TaskLED1_Handle);if (taskState == eSuspended){printf("TaskLED1 is suspended.\r\n");}else if (taskState == eRunning){printf("TaskLED1 is running.\r\n");}else if (taskState == eBlocked){printf("TaskLED1 is blocked.\r\n");}else if (taskState == eReady){printf("TaskLED1 is ready.\r\n");}else{printf("TaskLED1 is deleted.\r\n");}// 延時vTaskDelay(100);}
}

說明eTaskGetState() 用來查詢 xxx_Handle 的狀態。根據返回的狀態值 (eSuspended, eRunning, eBlocked, eReady, eDeleted),以便根據任務的當前狀態做出適當的邏輯判斷。

3.2 編寫例題代碼

正點原子例題

在這里插入圖片描述

3.2.1 任務掛起/恢復

在任務3里進行任務1的掛起和恢復

在前一章的完整代碼下,其他的函數不變,更改一下void Task3_KEY(void *argument)

void Task3_KEY(void *argument)
{while (1){static int N3 = 0;N3 ++;printf("Task_3--%d\r\n",N3);KEY_Proc();// 按下按鍵1 掛起任務1if(KEY_Down == 1){vTaskSuspend(TaskLED1_Handle);printf("-----掛起任務1-----\r\n");}// 按下按鍵2 恢復任務1else if(KEY_Down == 2){vTaskResume(TaskLED1_Handle);printf("-----恢復任務1-----\r\n");}vTaskDelay(100);	}
}

下載到板子,打開串口助手,串口輸出如下:

Hello!Task Start!
Free Heap: 1984
Task_3--1
Task_2--1
Task_1--1
Task_3--2
... ...
Task_2--3
Task_1--5
Task_3--20
Task_3--21
Task_3--22
Task_3--23
-----掛起任務1-----
Task_3--24
Task_3--25
Task_3--26
Task_3--27
Task_3--28
Task_2--4
Task_3--29
Task_3--30
Task_3--31
Task_3--32
Task_3--33
Task_3--34
Task_3--35
Task_3--36
Task_3--37
Task_2--5
Task_3--38
Task_3--39
Task_3--40
Task_3--41
Task_3--42
Task_3--43
Task_3--44
-----恢復任務1-----
Task_1--6
Task_3--45
Task_3--46
Task_2--6
Task_3--47
Task_3--48
Task_1--7
Task_3--49
Task_3--50
Task_3--51
Task_3--52
Task_3--53
Task_1--8
Task_3--54
Task_3--55
Task_2--7
Task_3--56
-----掛起任務1-----
Task_3--57
Task_3--58
Task_3--59
Task_3--60
Task_3--61
Task_3--62
Task_3--63
Task_2--8
Task_3--65
Task_3--66
Task_3--67
Task_3--68
Task_3--69
Task_3--70
Task_3--71
Task_3--72
Task_3--73
Task_2--9
Task_3--74
Task_3--75
Task_3--76
Task_3--77
Task_3--78
Task_3--79
Task_3--80
Task_3--81
Task_3--82
Task_2--10
Task_3--83
Task_3--84
Task_3--85
Task_3--86
Task_3--87
-----恢復任務1-----
Task_1--9
Task_3--88
Task_3--89
Task_3--90
Task_2--11
Task_1--10
Task_3--92

可以看見按下按鍵1,掛起后任務1后就沒有再執行過任務1,恢復后繼續執行

第一次掛起前最后是Task_1--5,掛起后沒有輸出;恢復后繼續之前的輸出Task_1--6。第二次同理!

但是有一個問題,如果我們重復按下,就會一直顯示“恢復任務“,但其實并沒有,第一次按下時已經恢復了任務1,后面按下都是無效的!!所以我們改一下代碼,避免無效恢復。

這個時候我們就會用到函數 eTaskGetState獲取任務狀態,如果被掛起,才執行恢復

void Task3_KEY(void *argument)
{while (1){... ... else if(KEY_Down == 2) {switch(Task1_State)  // (2)根據判斷執行{case 2: // 未被掛起printf("-----KEY2--已經恢復過啦-----\r\n");//printf("-----KEY2--任務1沒被掛起-----\r\n");break;case 1:	// 掛起vTaskResume(TaskLED1_Handle);printf("-----恢復任務1-----\r\n");break;default: break;}						}// (1)判斷任務1 是否被掛起if(eTaskGetState(TaskLED1_Handle) == eSuspended){Task1_State = 1; // 掛起}else{Task1_State = 2; // 未被掛起}		vTaskDelay(100);	}
}

下載到板子,打開串口助手,串口輸出如下:

Hello!Task Start!
Free Heap: 1984
Task_3--1
Task_2--1
Task_1--1
Task_3--2
Task_3--3
Task_3--4
-----掛起任務1-----
Task_3--5
Task_3--6
Task_3--7
Task_3--8
Task_3--9
Task_3--10
Task_2--2
Task_3--11
-----恢復任務1-----
Task_1--2
Task_3--12
Task_3--13
... ...
Task_1--5
Task_3--26
-----KEY2--已經恢復過啦-----
Task_3--27
Task_2--4
Task_3--29
Task_1--6
Task_3--30

OK!

3.2.2 從中斷恢復任務

跟上面類似,只不過用的函數不同

首先在CubeMX打開按鍵中斷,我是把PB2設置為上升沿中斷觸發

在這里插入圖片描述

然后編寫中斷代碼

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{if(GPIO_Pin == GPIO_PIN_2) // 判斷是否是 PB2 引腳觸發的中斷{BaseType_t xResume = pdFALSE; // 用來接收 xTaskResumeFromISR() 的返回值switch(Task1_State) // 判斷任務1是否掛起{case 2: // 未被掛起printf("-----EXTI--已經恢復過啦-----\r\n");//printf("-----EXTI--任務1沒被掛起-----\r\n");break;case 1: // 掛起//如果該任務優先級高于當前運行任務,將返回 pdTRUE,否則返回 pdFALSExResume = xTaskResumeFromISR(TaskLED1_Handle); if(xResume == pdTRUE){//pdTRUE,說明 ISR 中恢復的任務的優先級高,需要立即切換到該任務運行。portYIELD_FROM_ISR(xResume);}	printf("-----從中斷中恢復任務1-----\r\n");break;default: break;}	}
}

這里跟之前的有點不一樣,多出了一個立即切換到該任務運行的判斷,啥意思呢??

假設你在一個公司,正在做自己手頭的工作。突然,老板交給你一個任務。

  • 如果老板說這個任務非常緊急,你就必須立刻去做老板的任務,等到老板的任務做完再去做你手頭的工作。
  • 如果老板說這個任務不太緊急,可以等下再做,那么你就不需要立刻停止當前的工作,等你做完手頭的工作,再去處理老板的任務。

代碼中的 xTaskResumeFromISR()portYIELD_FROM_ISR()

  • xTaskResumeFromISR() :在中斷中恢復任務,就像是老板突然發出“任務”,并且會返回值告訴你是否緊急
    • pdTRUE:表示中斷被恢復的任務優先級更高,非常緊急
    • pdFALSE:表示當前任務優先級更高或相同,不急
  • portYIELD_FROM_ISR() :這個是立刻切換去執行的函數
    • portYIELD_FROM_ISR(pdTRUE):急急如律令,立刻去執行中斷被恢復的任務
    • portYIELD_FROM_ISR(pdFALSE):不急,繼續當前的任務,等會兒再說。

OK,下載到板子,打開串口助手,串口輸出如下:

Hello!Task Start!
Free Heap: 1984
Task_3--1
Task_2--1
Task_1--1
Task_3--2
Task_3--3
Task_3--4
Task_3--5
Task_1--2
Task_3--6
Task_3--7
Task_3--8
Task_3--9
Task_3--10
-----掛起任務1-----
Task_2--2
Task_3--11
Task_3--12
Task_3--13
Task_3--14
Task_3--15
Task_3--16
Task_3--17
Task_3--18
-----從中斷中恢復任務1-----
Task_1--3
Task_2--3
Task_3--20
Task_3--21
Task_3--22
Task_3--23
Task_1--4
Task_3--24
Task_3--25
Task_3--26
-----EXTI--已經恢復過啦-----
Task_3--27
Task_3--28
Task_1--5
Task_2--4
Task_3--29
Task_3--30
Task_3--31
Task_3--32

OK,完美!

四、FreeRTOS中斷管理

4.1 概念理解

4.1.1 中斷管理

STM32的中斷優先級的兩個組成部分:

  • 搶占優先級(Preemption Priority):決定一個中斷是否可以“打斷”另一個正在執行的中斷。
  • 子優先級(Sub Priority):在搶占優先級相同的情況下,決定兩個中斷“誰先響應”。

我們可以打開Cubemx,點擊NVIC查看,已經自動的幫我們把一些中斷設置改了。

在這里插入圖片描述

當前設置:4 bits for pre-emption priority, 0 bits for subpriority,即 NVIC_PRIORITYGROUP_4

  • 這意味著所有中斷的優先級完全由搶占優先級決定,子優先級不起作用。
  • 在這種設置下,優先級范圍為 0(最高優先級)到 15(最低優先級)。

這也是FreeRTOS官方建議的中斷設置!

4.1.2 中斷優先級推薦設置

為什么要用 NVIC_PRIORITYGROUP_4 呢??

  • FreeRTOS 內核只關注搶占優先級(Preemption Priority)
  • 子優先級對 FreeRTOS 是“透明”的,它不會參與調度判斷
  • 如果設置了子優先級,FreeRTOS 不會管,結果就容易出“錯”
  • 簡化優先級配置邏輯,降低出錯率

舉個例子:假設你用了 2 位搶占、2 位子優先級(NVIC_PRIORITYGROUP_2)

  • 兩個中斷 A 和 B:
    • 搶占優先級相同
    • 子優先級不同
  • FreeRTOS 會認為它們優先級一樣(只看搶占),但實際中:
    • Cortex-M 內核允許按子優先級執行順序
    • 這可能讓“低優先級中斷”先執行 → 打亂預期調度!

4.1.3 FreeRTOS相關宏

打開FreeRTOSConfig.h ,有關中斷定義的相關宏

/* Cortex-M 特定的設置 */
/* 檢查是否已經定義了中斷優先級位數 */
#ifdef __NVIC_PRIO_BITS/* 如果使用 CMSIS,直接使用系統定義的優先級位數 */#define configPRIO_BITS         __NVIC_PRIO_BITS
#else/* 如果沒有使用 CMSIS,默認使用 4 位優先級 */#define configPRIO_BITS         4
#endif/* * 設置最低的中斷優先級,這個優先級可以用來設置中斷的優先級。* 數值越小,優先級越高。*/
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY   15/* * 設置可以調用 FreeRTOS API 的最高中斷優先級。* 優先級數值越低,優先級越高。*/
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5/* 計算內核的中斷優先級 */
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )/* * 配置系統調用的最大中斷優先級,確保系統調用的優先級不為零。*/
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )/* * 將 FreeRTOS 的中斷處理函數映射到 CMSIS 標準中斷處理函數。* SVC 用于系統調用,PendSV 用于上下文切換。*/
#define vPortSVCHandler    SVC_Handler
#define xPortPendSVHandler PendSV_Handler/* * 設置是否使用自定義的 SysTick 處理函數,0 表示使用默認處理函數。*/
#define USE_CUSTOM_SYSTICK_HANDLER_IMPLEMENTATION 0

這段配置是 FreeRTOS 和 Cortex-M 中斷優先級對接的重要部分,可以總結為以下幾信息:

  1. configPRIO_BITS 表示中斷優先級一共用了幾位,我們是NVIC_PRIORITYGROUP_4
  2. configLIBRARY_LOWEST_INTERRUPT_PRIORITYconfigLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 是 FreeRTOS允許參與調度(或調用 API)的中斷優先級范圍,它只能管5~15這部分!!
    • 數值 < 5 的高優中斷:FreeRTOS不控制,也不允許在這些中斷里用任何 FreeRTOS API。這些高優先級的中斷可以寫,比如緊急故障中斷、DMA完成中斷等;
    • 數值 ≥ 5 且 ≤ 15 的中斷:可以在中斷里調用 FreeRTOS 的函數(比如發消息、信號量)
    • FreeRTOS 自己的調度器用的是優先級 15(也就是最慢的調度中斷);
  3. 后面的 configKERNEL_INTERRUPT_PRIORITYconfigMAX_SYSCALL_INTERRUPT_PRIORITY 是為了把上面這些優先級轉換成芯片實際使用的格式,Cortex-M 的優先級是左對齊的,所以需要 << (8 - configPRIO_BITS) 來位移。
  4. vPortSVCHandlerxPortPendSVHandler 這些名字是把 FreeRTOS 的關鍵中斷函數(系統調用和任務切換)映射到 CMSIS 的標準函數名,確保啟動文件能識別。
  5. USE_CUSTOM_SYSTICK_HANDLER_IMPLEMENTATION 設置為 0 表示用 FreeRTOS 默認的 SysTick_Handler。如果有特別需求,比如自己控制滴答定時器,設為 1 可以自己寫這個中斷函數。默認即可。

4.2 函數介紹

4.2.1 禁用中斷 portDISABLE_INTERRUPTS()

(1)函數原型

void portDISABLE_INTERRUPTS(void);
  • 禁用所有可屏蔽中斷,常用于進入臨界區,保護關鍵代碼不被打斷。
  • 禁用中斷期間,FreeRTOS 將不會進行任務切換,也不會響應中斷服務。

4.2.2 啟用中斷 portENABLE_INTERRUPTS()

(1)函數原型

void portENABLE_INTERRUPTS(void);
  • 恢復中斷響應,使系統能夠再次處理中斷和任務切換。
  • 通常用于臨界區結束后,與 portDISABLE_INTERRUPTS() 配套使用。

(3)示例

portDISABLE_INTERRUPTS();  // 禁用中斷// 臨界區域操作
buffer[index++] = data;
// 臨界區域操作portENABLE_INTERRUPTS();   // 恢復中斷

4.3 編寫例題代碼

正點原子例題

在這里插入圖片描述

主要就是看FreeRTOS能管理的中斷范圍,是在5~15之間。

現在打開CubeMX,增加兩個定時器中斷。

在這里插入圖片描述

TIM6TIM7,配置相同!

在這里插入圖片描述

然后在NVIC中使能,如果TIM6使能不了,就取消最后一欄的勾(FreeRTOS不允許設置0~4

首先,開啟兩個定時器,編寫兩個定時器代碼(注:在main.c中寫!!)

int main(void)
{... .../* USER CODE BEGIN 2 */HAL_TIM_Base_Start_IT(&htim6);HAL_TIM_Base_Start_IT(&htim7);delay_init(170); // 下一個代碼用,用于阻塞延時/* USER CODE END 2 */... ...while (1){}
}
... ...
/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim == &htim6){printf("優先級4--TIM6--中斷開啟!!\r\n");}else if(htim == &htim7){printf("優先級6--TIM7--中斷開啟!!\r\n");}
}

下載到板子,打開串口助手,串口輸出如下:

優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
... ...

然后創建一個任務,去控制兩個定時器中斷的開關。

uint8_t Num = 0;/****** (1) 創建一個任務函數 *******/
void Task1(void *argument)
{while (1){if(++Num == 5){Num = 0;portDISABLE_INTERRUPTS(); // 禁用中斷printf("---關掉---中斷啦---\r\n");delay_ms(5000); // 阻塞5sportENABLE_INTERRUPTS(); // 重新啟用中斷printf("---開啟---中斷啦---\r\n");}vTaskDelay(1000);}
}
/* USER CODE END FunctionPrototypes */
... ...
void MX_FREERTOS_Init(void) 
{... .../* USER CODE BEGIN RTOS_THREADS *//* add threads, ... *//****** (2) 添加任務 *******/xTaskCreate(Task1, "Task1", 128, NULL, 25, NULL);... ...
}

豬豬豬:這里的Delay是移植正點原子的,我已經放在了基礎文件里。

下載,打開串口!

優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
---關掉--中斷啦---
優先級4--TIM6--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
---開啟--中斷啦---
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
---關掉--中斷啦---
優先級4--TIM6--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級4--TIM6--中斷開啟!!

由此可知!!FreeRTOS的任務也不能控制0~4優先級的任務。

優先級數值中斷優先級含義是否受 FreeRTOS 管理能否調用 xxxFromISR
0~4高優先級? 不受 FreeRTOS 管理? 不可調用 FromISR
5~15低優先級? 可由 FreeRTOS 管理? 可調用 FromISR

configMAX_SYSCALL_INTERRUPT_PRIORITY 通常設置為 5,所以優先級 < 5 的中斷不受 FreeRTOS 管理。

宏 / 指令關閉范圍會關閉 TIM6(優先級4)嗎
portDISABLE_INTERRUPTS()FreeRTOS 管理的中斷(優先級5~15)? 不會
__disable_irq()所有中斷(包括高優先級)? 會

__disable_irq()這個是 芯片級別 的中斷屏蔽,不受 FreeRTOS 限制,也會關閉所有高優先級中斷。

五、臨界段代碼保護及任務調度器的掛起和恢復

豬豬豬:前面我們學習的是任務的掛起和恢復,現在我們要看任務調度器!!

5.1 概念理解

5.1.1 臨界段代碼保護

(1)什么是臨界段??

這段代碼非常敏感,別人不能來打擾我,我要一口氣干完!

(2)哪些場景需要??

  • 外設初始化:例如,在初始化 I2C、SPI 等通信外設時,需要確保時序正確。如果在初始化過程中被打斷,可能導致設備處于不一致狀態。
  • 數據傳輸:進行數據傳輸或硬件控制時,若操作被打斷,可能會導致數據丟失或損壞。
  • 共享資源訪問:多個任務可能同時訪問共享資源,例如全局變量、硬件外設、隊列等。如果沒有適當的同步保護,會導致資源競爭和數據沖突。

(3)什么打斷當前程序的運行

就是中斷和任務調度

中斷:你正在干活,比如說你在搬磚,結果手機響了(來了個中斷),你就得先放下磚,去接電話。比如:定時器中斷、串口中斷、外部中斷、DMA中斷等,它隨時可能打斷你當前在干的事情!

任務調度:你是個低優先級任務,剛寫一半代碼,結果來了個高優先級任務,FreeRTOS 覺得你不夠重要,于是暫停你,讓別人先跑。

(4)怎么不被打斷

關中斷!禁止任務調度!

  • 關中斷:關閉中斷,確保當前任務執行期間不會被其他中斷打擾。
  • 禁止任務調度:暫停任務調度,防止低優先級任務被高優先級任務搶占。

臨界區直接屏蔽了中斷,系統任務調度、ISR都得靠中斷!

5.1.2 任務調度器

任務調度器是 FreeRTOS 的大腦,它負責決定系統中哪個任務什么時候運行。簡單來說,它就像一個指揮官,按照任務的優先級和時間安排來指揮各個任務的執行。如果有多個任務在等待,調度器會決定哪個任務先執行,哪個任務稍后執行。

在任務執行過程中,有時候會有一些“臨界段代碼”,這段代碼很重要,必須一氣呵成執行完,不允許被打斷。

但是,問題來了:任務調度器隨時可能打斷當前任務并切換到另一個任務。如果在執行臨界段代碼時被調度器打斷,可能會導致任務未能完成這段關鍵操作。為了避免這種情況,可以使用可以“暫停”任務調度器,確保當前任務不會被打斷,這樣,任務可以放心地執行關鍵代碼,不會因為調度器的干擾而導致錯誤。

5.2 函數介紹

5.2.1 臨界段保護函數(任務級)

(1)函數原型

void taskENTER_CRITICAL(void);
void taskEXIT_CRITICAL(void);

說明:

函數作用使用場景
taskENTER_CRITICAL()進入臨界段(任務級) 本質上是關閉中斷,防止任務切換在任務函數中使用
taskEXIT_CRITICAL()退出臨界段,恢復中斷在任務函數中使用

這些函數用于在任務中關閉中斷,保護臨界代碼不被打斷。

(2)示例代碼

taskENTER_CRITICAL();
IIC_Init();
taskEXIT_CRITICAL();

使用 taskENTER_CRITICAL()taskEXIT_CRITICAL() 包裹,確保在初始化期間不會被其他任務或中斷打斷。

5.2.2 臨界段保護函數(中斷級)

(1)函數原型

BaseType_t taskENTER_CRITICAL_FROM_ISR(void);
void taskEXIT_CRITICAL_FROM_ISR(UBaseType_t uxSavedStatusValue);

說明:

函數作用使用場景
taskENTER_CRITICAL_FROM_ISR()進入臨界段,返回值當前中斷狀態,并關閉中斷在中斷服務函數中使用
taskEXIT_CRITICAL_FROM_ISR(xxx)退出臨界段,恢復之前保存的中斷狀態在中斷服務函數中使用

這些函數用于中斷服務函數中保護臨界代碼。

(2)示例代碼

UBaseType_t status;  // 定義一個變量,用于保存當前中斷狀態
status = taskENTER_CRITICAL_FROM_ISR(); // 關閉中斷,并保存當前中斷狀態
IIC_WriteByte(0xA5);
taskEXIT_CRITICAL_FROM_ISR(status); // 開啟中斷,并恢復之前保存的中斷狀態

status 變量保存了進入臨界段前的中斷狀態,確保在臨界段內執行完關鍵操作后,可以正確地恢復系統的中斷狀態,避免不必要的中斷丟失或系統行為異常。

5.2.3 任務調度器的掛起和恢復函數

(1)函數原型

void vTaskSuspendAll(void);
BaseType_t xTaskResumeAll(void);

(2)函數說明

函數作用
vTaskSuspendAll()掛起任務調度器,禁止任務調度器進行任務切換
xTaskResumeAll()恢復任務調度器,允許任務切換繼續進行

(3)返回值說明

xTaskResumeAll():返回一個 BaseType_t 類型的值。

  • 返回值 pdTRUE 表示調度器已經成功恢復。
  • 返回值 pdFALSE 表示調度器沒有恢復。

(4)示例代碼

/*****(1)掛起任務調度器 ******/
void Critical_Section(void)
{vTaskSuspendAll();   // 掛起任務調度器,禁止任務調度IIC_WriteByte(0xA5);  // 比如在 I2C 總線中寫入數據xTaskResumeAll();     // 恢復任務調度器,允許任務切換
}/*****(2)調用函數******/
void Task_Function(void *pvParameters)
{while (1){Critical_Section();  // 執行掛起調度器保護的臨界段代碼vTaskDelay(100);      // 延時100ms}
}

通過這兩個函數,可以確保在某些重要操作中,任務調度不會打斷重要操作。

【這章沒有實驗】

六、列表和列表項

6.1 概念理解

(1)什么是列表?什么是列表項?

列表可以類比為一個“容器”,專門用于存放和排序很多個列表項。它是 FreeRTOS 中管理調度、事件、延時等機制的基礎容器。列表項就是存放在列表中的項目。

列表相當于鏈表,列表項相當于節點,FreeRTOS 中的列表是一個雙向環形鏈表。

  • 每一個列表項也就是一個個的任務,如果中途增加任務,就插入到列表項,中途刪掉任務,就從列表中移出。

  • 列表項的地址是非連續的,是人為鏈接的,所以數目可以后期改變。

列表項1  <--->  列表項2  <--->  列表項3  <---> ... ...  <---> 末尾列表項^                                                            ^|                                                            |+------------------------------------------------------------+

假設,我們去醫院看病,醫院有多個科室,比如:

  • 內科排隊列表(List_t)
  • 外科排隊列表(List_t)
  • 急診排隊列表(List_t)

每個科室有自己的一個排隊列表,用于管理等候的病人順序,這就像 FreeRTOS 中的一個列表。

每次來掛號,護士會給你一張掛號單,上面寫著:到你就診的時間(或優先級)、你本人的信息、你現在在哪個隊伍等等,這張掛號單就是 FreeRTOS 中的列表項。

6.2 函數介紹

6.2.1 列表/列表項結構體

首先!我們先看看每個結構體的定義和成員。

(1)列表項

ListItem_t 是 FreeRTOS 鏈表中的單個元素,每個元素就是鏈表中的一個節點。我們來看具體的定義:

struct xLIST_ITEM
{listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE           /* 校驗值,確保數據完整性 */configLIST_VOLATILE TickType_t xItemValue;          /* 列表項的值, 用于排序 */struct xLIST_ITEM * configLIST_VOLATILE pxNext;     /* 指向下一個列表項的指針 */struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /* 指向上一個列表項的指針 */void * pvOwner;        /* 指向擁有這個列表項的對象(如 TCB),從而形成一個雙向鏈接 */struct xLIST * configLIST_VOLATILE pxContainer;     /* 指向包含該列表項的列表 */listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE          /* 校驗值,確保數據完整性 */
};·
  • xItemValue: 這是列表項的核心值,通常用來決定排序!比如在任務調度中,可以通過 xItemValue 來表示任務的優先級,數值較小的任務具有較高的優先級(具體根據 FreeRTOS 的排序規則)。
  • pxNextpxPrevious: 指向列表中前/后一個 ListItem_t 元素的指針,這使得鏈表變成了雙向鏈表,每個節點都知道自己前后節點的位置。
  • pvOwner: 這是一個指向實際擁有該列表項的對象的指針(指向我們的任務)。這樣,可以在列表項和實際任務之間形成雙向關聯。
  • pxContainer: 這是一個指向該列表項所在列表的指針,表明這個列表項屬于哪個列表

(2)迷你列表項

struct xMINI_LIST_ITEM
{listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE           /* 校驗值,確保數據完整性 */configLIST_VOLATILE TickType_t xItemValue;          /* 列表項的值 */struct xLIST_ITEM * configLIST_VOLATILE pxNext;     /* 指向下一個列表項的指針 */struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /* 指向上一個列表項的指針 */
};

迷你列表項也就是末尾列表項,它是一個精簡版的列表項結構,只有最基本的字段。

(3)列表

typedef struct xLIST
{listFIRST_LIST_INTEGRITY_CHECK_VALUE      /* 校驗值,確保數據完整性 */volatile UBaseType_t uxNumberOfItems;     /* 列表項的數量 */ListItem_t * configLIST_VOLATILE pxIndex; /* 用于遍歷列表,指向上次訪問的列表項 */MiniListItem_t xListEnd;                  /* 標記列表結束的特殊節點,始終位于列表的尾部 */listSECOND_LIST_INTEGRITY_CHECK_VALUE     /* 校驗值,確保數據完整性 */
} List_t;
  • uxNumberOfItems: 列表中的項數。記錄當前列表中有多少個列表項,但不算上末尾列表項!
  • pxIndex: 用于遍歷列表的指針,它指向上次通過 listGET_OWNER_OF_NEXT_ENTRY() 獲取的列表項。它幫助 FreeRTOS 在迭代時跟蹤當前位置。
  • xListEnd: 這是一個特殊的迷你列表項(MiniListItem_t),標記了列表的結束。其 xItemValue 為最大值,用來作為列表的末尾節點。它不存儲實際的數據,只起到標記作用,確保遍歷時能正確停止。

所以,剛創建時,列表中的列表項的數量是0,但是列表中已經有了迷你列表項!

┌──────────── List_t ────────────┐
│                                │
│   ┌───────────────┐            │
│   │   xListEnd    │            │
│   │ pxNext → 自己 │?────┐       │
│   │ pxPrev → 自己 │─────┘       │
│   └───────────────┘            │
└────────────────────────────────┘

創建一個列表項之后

┌──────────── List_t ────────────┐
│                                │
│   ┌─────────────┐              │
│   │ ListItem A  │?────────┐    │
│   │ pxNext → End│         │    │
│   │ pxPrev → End│         │    │
│   └─────────────┘         │    │
│        ▲                  ▼    │
│   ┌───────────────┐       │    │
│   │   xListEnd    │───────┘    │
│   │ pxNext → A    │            │
│   │ pxPrev → A    │            │
│   └───────────────┘            │
└────────────────────────────────┘

創建兩個之后

┌────────────┐     ┌────────────┐     ┌────────────┐
│ ListItem A │ ?──?│ ListItem B │ ?──?│  xListEnd  │
└────────────┘     └────────────┘     └────────────┘▲                                      ▲└──────────────────────────────────────┘

以此類推!!

6.2.2 初始化列表

(1)函數原型

void vListInitialise( List_t *pxList );

參數解釋:

參數名類型說明
pxListList_t*指向要初始化的列表結構體指針

完整函數:

void vListInitialise( List_t *pxList )
{/* 初始化列表中當前索引為末尾項 */pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );/* 初始化列表項的數量為 0 */pxList->uxNumberOfItems = ( UBaseType_t ) 0U;/* xListEnd 是 MiniListItem 類型,單獨作為末尾項 */pxList->xListEnd.xItemValue = portMAX_DELAY;/* 雙向連接:xListEnd 的前后指針都指向自己 */pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );
}

(2)示例代碼

List_t myList;
vListInitialise(&myList);

6.2.3 初始化列表項

(1)函數原型

void vListInitialiseItem( ListItem_t *pxItem );

參數解釋:

參數名類型說明
pxItemListItem_t*指向要初始化的列表項指針

完整函數:

void vListInitialiseItem( ListItem_t *pxItem )
{/* 初始化時不屬于任何列表 */pxItem->pxContainer = NULL;
}

(2)示例代碼

ListItem_t myItem;
vListInitialiseItem(&myItem);

6.2.4 列表項插入列表

(1)函數原型

void vListInsert( List_t *pxList, ListItem_t *pxNewListItem );

參數解釋:

參數名類型說明
pxListList_t*目標列表
pxNewListItemListItem_t*要插入的列表項,需預先設置 xItemValue

完整函數:

void vListInsert( List_t *pxList, ListItem_t *pxNewListItem )
{ListItem_t *pxIterator;TickType_t xValueOfInsertion = pxNewListItem->xItemValue;/* 從列表的頭開始,遍歷每個列表項,直到找到插入點 */for (pxIterator = (ListItem_t *) &(pxList->xListEnd); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext){/* 繼續尋找插入位置,空循環體 */}/* 現在找到插入點,更新指針 */pxNewListItem->pxNext = pxIterator->pxNext;          // 新項的下一個項是當前項的下一個pxNewListItem->pxPrevious = pxIterator;              // 新項的前一個項是當前項pxIterator->pxNext->pxPrevious = pxNewListItem;      // 更新當前項下一個項的前一個指針pxIterator->pxNext = pxNewListItem;                  // 更新當前項的下一個指針為新項/* 設置新列表項的容器為目標列表 */pxNewListItem->pxContainer = pxList;/* 列表項數目增加 */(pxList->uxNumberOfItems)++;
}

(2)示例代碼

ListItem_t item;
item.xItemValue = 10;vListInsert(&myList, &item);

6.2.5 列表項末尾插入列表

(1)函數原型

void vListInsertEnd( List_t *pxList, ListItem_t *pxNewListItem );

參數解釋:

參數名類型說明
pxListList_t*目標列表
pxNewListItemListItem_t*要插入的列表項

完整函數:

void vListInsertEnd( List_t *pxList, ListItem_t *pxNewListItem )
{/* 獲取當前列表的末尾項,即 xListEnd 前的項 */ListItem_t *pxIndex = pxList->xListEnd.pxPrevious;/* 將新列表項插入到 xListEnd 之前 */pxNewListItem->pxNext = ( ListItem_t * ) &( pxList->xListEnd ); // 下一個是xListEndpxNewListItem->pxPrevious = pxIndex;                            // 前一個是插入前的末尾項pxIndex->pxNext = pxNewListItem;                      // 更新當前末尾項的下一個項為新項pxList->xListEnd.pxPrevious = pxNewListItem;          // 更新 xListEnd 的前一個項為新項/* 設置新列表項的容器為目標列表 */pxNewListItem->pxContainer = pxList;/* 列表項數目增加 */(pxList->uxNumberOfItems)++;
}

(2)示例代碼

ListItem_t item;
vListInsertEnd(&myList, &item);

6.2.6 列表移出列表項

(1)函數原型

UBaseType_t uxListRemove( ListItem_t *pxItemToRemove );

參數解釋:

參數名類型說明
pxItemToRemoveListItem_t*要從列表中移除的列表項

返回值:

  • 類型:UBaseType_t
  • 說明:移除操作后列表中剩余的項數量

完整函數:

UBaseType_t uxListRemove( ListItem_t *pxItemToRemove )
{/* 獲取當前列表的容器 */List_t * const pxList = pxItemToRemove->pxContainer;/* 更新前后節點的指針,移除目標項 */pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious; // 后一個節點的前指針指向前一個節點pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;   // 前一個節點的下一個指針指向后一個節點/* 清除目標項的容器指針,表示已從列表中移除 */pxItemToRemove->pxContainer = NULL;/* 列表項數目減少 */pxList->uxNumberOfItems--;/* 返回更新后的列表項數量 */return pxList->uxNumberOfItems;
}

(2)示例代碼

uxListRemove(&item);

6.3 編寫例題代碼

正點原子例題

在這里插入圖片描述

首先,直接把第二章動態任務的創建和刪除代碼復制一份,然后把多余任務刪掉

... ...
/****** (1) 創建三個任務函數 *******/void Task_Start(void *argument)
{printf("Hello!Task Start!\r\n");taskENTER_CRITICAL(); // 進入臨界區xTaskCreate(Task1, "Task1", 128, NULL, 26, &Task1_Handle);xTaskCreate(Task2, "Task2", 128, NULL, 27, &Task2_Handle);	printf("Free Heap: %d\r\n", xPortGetFreeHeapSize());// 刪除自己vTaskDelete(NULL); taskEXIT_CRITICAL(); // 退出臨界區
}void Task1(void *argument)
{while (1){		switch(LED1_Flag){case 1: LEDx |= 0x01; LED1_Flag = 2; break;case 2: LEDx &= ~(1 << 0); LED1_Flag = 1; break;default: break;}LED_Disp(LEDx);vTaskDelay(500);}
}void Task2(void *argument)
{// 等會在任務2進行列表和列表項操作while (1){vTaskDelay(1000);		}
}
... ...
void MX_FREERTOS_Init(void) {... .../* USER CODE BEGIN RTOS_THREADS *//* add threads, ... *//****** (2) 添加任務 *******/xTaskCreate(Task_Start, "TaskStart", 128, NULL, 25, NULL)
}

OK!接下來我們需要做的就是

  • 初始化列表和列表項

  • (1) 初始化并添加 列表項1

  • (2) 添加 列表項2

  • (3) 添加 列表項3

  • (4) 刪除 列表項2

  • (5) 重新在末尾添加 列表項2

直接在任務2中添加,以下是完整代碼:

List_t List1;              // 創建一個列表 List1
ListItem_t ListItem1;      // 創建列表項1 ListItem1
ListItem_t ListItem2;      // 創建列表項2 ListItem2
ListItem_t ListItem3;      // 創建列表項3 ListItem3void Task2(void *argument)
{/**************** 初始化列表、列表項 ****************/// 初始化列表vListInitialise(&List1);// 初始化列表項	vListInitialiseItem(&ListItem1);vListInitialiseItem(&ListItem2);vListInitialiseItem(&ListItem3);// 設置列表項的數值ListItem1.xItemValue = 40;ListItem2.xItemValue = 60;ListItem3.xItemValue = 50;/**************** 【1】將列表項1插入列表 ****************/vListInsert(&List1, &ListItem1);/**************** 【2】將列表項2插入列表 ****************/vListInsert(&List1, &ListItem2);/**************** 【3】將列表項3插入列表 ****************/vListInsert(&List1, &ListItem3);/**************** 【4】將列表項2移出列表 ****************/uxListRemove(&ListItem2);/**************** 【5】將列表項2插入列表末尾 ****************/List1.pxIndex = List1.pxIndex->pxNext; // pxIndex 后移vListInsertEnd(&List1, &ListItem2);while (1){vTaskDelay(1000);		}
}

但是過程我們看不到,需要添加打印代碼!如下:

void Task2(void *argument)
{/**************** 【1】初始化列表、列表項 ****************/// 初始化列表vListInitialise(&List1);// 初始化列表項	vListInitialiseItem(&ListItem1);vListInitialiseItem(&ListItem2);vListInitialiseItem(&ListItem3);// 設置列表項的數值ListItem1.xItemValue = 40;ListItem2.xItemValue = 60;ListItem3.xItemValue = 50;// 打印列表和其他列表項的地址printf("/********* 列表和列表項地址 *********/\r\n");printf("項地址:\r\n");printf("List1: %#x\r\n", (int)&List1);                      // 打印 TestList 的地址printf("List1->pxIndex: %#x\r\n", (int)(List1.pxIndex));    // 打印 pxIndex 的地址printf("List1->xListEnd: %#x\r\n", (int)&(List1.xListEnd)); // 打印 xListEnd 的地址printf("ListItem1: %#x\r\n", (int)&ListItem1);                 // 打印 ListItem1 的地址printf("ListItem2: %#x\r\n", (int)&ListItem2);                 // 打印 ListItem2 的地址printf("ListItem3: %#x\r\n", (int)&ListItem3);                 // 打印 ListItem3 的地址printf("*************************************/\r\n");/**************** 【1】將列表項1插入列表 ****************/vListInsert(&List1, &ListItem1);   // 插入 ListItem1 到 List1 中// 打印添加后的列表項連接情況printf("/******* (1)添加列表項 ListItem1 *******/\r\n");  printf("List1->xListEnd->pxNext: %#x\r\n", (int)(List1.xListEnd.pxNext)); printf("ListItem1->pxNext: %#x\r\n", (int)(ListItem1.pxNext));            printf("/--------- 前后向連接分割線 ----------/\r\n");  printf("List1->xListEnd->pxPrevious: %#x\r\n", (int)(List1.xListEnd.pxPrevious)); printf("ListItem1->pxPrevious: %#x\r\n", (int)(ListItem1.pxPrevious));            printf("**************************************/\r\n"); /**************** 【2】將列表項2插入列表 ****************/vListInsert(&List1, &ListItem2);   // 插入 ListItem1 到 List1 中printf("/****** (2)添加列表項 ListItem2 *******/\r\n");printf("List1->xListEnd->pxNext   = %#x\r\n", (int)(List1.xListEnd.pxNext));printf("ListItem1->pxNext         = %#x\r\n", (int)(ListItem1.pxNext));printf("ListItem2->pxNext         = %#x\r\n", (int)(ListItem2.pxNext));printf("/--------- 前后向連接分割線 ----------/\r\n");printf("List1->xListEnd->pxPrevious = %#x\r\n", (int)(List1.xListEnd.pxPrevious));printf("ListItem1->pxPrevious       = %#x\r\n", (int)(ListItem1.pxPrevious));printf("ListItem2->pxPrevious       = %#x\r\n", (int)(ListItem2.pxPrevious));printf("/************************************/\r\n");/**************** 【3】將列表項3插入列表 ****************/vListInsert(&List1, &ListItem3);printf("/******** (3)添加列表項 ListItem3 ********/\r\n");printf("List1->xListEnd->pxNext   = %#x\r\n", (int)(List1.xListEnd.pxNext));printf("ListItem1->pxNext         = %#x\r\n", (int)(ListItem1.pxNext));printf("ListItem2->pxNext         = %#x\r\n", (int)(ListItem2.pxNext));printf("ListItem3->pxNext         = %#x\r\n", (int)(ListItem3.pxNext));printf("/--------- 前后向連接分割線 ----------/\r\n");printf("List1->xListEnd->pxPrevious = %#x\r\n", (int)(List1.xListEnd.pxPrevious));printf("ListItem1->pxPrevious       = %#x\r\n", (int)(ListItem1.pxPrevious));printf("ListItem2->pxPrevious       = %#x\r\n", (int)(ListItem2.pxPrevious));printf("ListItem3->pxPrevious       = %#x\r\n", (int)(ListItem3.pxPrevious));printf("/**************************************/\r\n");/**************** 【4】將列表項2移出列表 ****************/uxListRemove(&ListItem2); printf("/********* (4)刪除列表項 ListItem2 *********/\r\n");printf("項目地址:\r\n");printf("List1->xListEnd.pxNext   = %#x\r\n", (int)(List1.xListEnd.pxNext));printf("ListItem1->pxNext        = %#x\r\n", (int)(ListItem1.pxNext));printf("ListItem3->pxNext        = %#x\r\n", (int)(ListItem3.pxNext));printf("/----------- 前后向連接分割線 ------------/\r\n");printf("List1->xListEnd.pxPrevious = %#x\r\n", (int)(List1.xListEnd.pxPrevious));printf("ListItem1->pxPrevious      = %#x\r\n", (int)(ListItem1.pxPrevious));printf("ListItem3->pxPrevious      = %#x\r\n", (int)(ListItem3.pxPrevious));printf("/****************************************/\r\n");/**************** 【5】將列表項2插入列表末尾 ****************/vListInsertEnd(&List1, &ListItem2); // 將 ListItem2 添加到鏈表末尾printf("/********* (5)重新在末尾添加列表項 ListItem2 *********/\r\n");printf("項目地址:\r\n");printf("List1->pxIndex            = %#x\r\n", (int)List1.pxIndex);printf("List1->xListEnd.pxNext    = %#x\r\n", (int)(List1.xListEnd.pxNext));printf("ListItem2->pxNext         = %#x\r\n", (int)(ListItem2.pxNext));printf("ListItem1->pxNext         = %#x\r\n", (int)(ListItem1.pxNext));printf("ListItem3->pxNext         = %#x\r\n", (int)(ListItem3.pxNext));printf("/----------------- 前后向連接分割線 ----------------/\r\n");printf("List1->xListEnd.pxPrevious = %#x\r\n", (int)(List1.xListEnd.pxPrevious));printf("ListItem2->pxPrevious      = %#x\r\n", (int)(ListItem2.pxPrevious));printf("ListItem1->pxPrevious      = %#x\r\n", (int)(ListItem1.pxPrevious));printf("ListItem3->pxPrevious      = %#x\r\n", (int)(ListItem3.pxPrevious));printf("/*************** 鏈表重連后的結構完成 **************/\r\n");while (1){	vTaskDelay(1000);		}
}

OK!下載,打開串口輸出:

Hello!Task Start!
Free Heap: 2616
/********* 列表和列表項地址 *********/
項地址:
List1: 0x200000ac
List1->pxIndex: 0x200000b4
List1->xListEnd: 0x200000b4
ListItem1: 0x200000c0
ListItem2: 0x200000d4
ListItem3: 0x200000e8
*************************************/
/******* (1)添加列表項 ListItem1 *******/
List1->xListEnd->pxNext: 0x200000c0
ListItem1->pxNext: 0x200000b4
/--------- 前后向連接分割線 ----------/
List1->xListEnd->pxPrevious: 0x200000c0
ListItem1->pxPrevious: 0x200000b4
**************************************/
/****** (2)添加列表項 ListItem2 *******/
List1->xListEnd->pxNext   = 0x200000c0
ListItem1->pxNext         = 0x200000d4
ListItem2->pxNext         = 0x200000b4
/--------- 前后向連接分割線 ----------/
List1->xListEnd->pxPrevious = 0x200000d4
ListItem1->pxPrevious       = 0x200000b4
ListItem2->pxPrevious       = 0x200000c0
/************************************/
/******** (3)添加列表項 ListItem3 ********/
List1->xListEnd->pxNext   = 0x200000c0
ListItem1->pxNext         = 0x200000e8
ListItem2->pxNext         = 0x200000b4
ListItem3->pxNext         = 0x200000d4
/--------- 前后向連接分割線 ----------/
List1->xListEnd->pxPrevious = 0x200000d4
ListItem1->pxPrevious       = 0x200000b4
ListItem2->pxPrevious       = 0x200000e8
ListItem3->pxPrevious       = 0x200000c0
/**************************************/
/********* (4)刪除列表項 ListItem2 *********/
項目地址:
List1->xListEnd.pxNext   = 0x200000c0
ListItem1->pxNext        = 0x200000e8
ListItem3->pxNext        = 0x200000b4
/----------- 前后向連接分割線 ------------/
List1->xListEnd.pxPrevious = 0x200000e8
ListItem1->pxPrevious      = 0x200000b4
ListItem3->pxPrevious      = 0x200000c0
/****************************************/
/********* (5)重新在末尾添加列表項 ListItem2 *********/
項目地址:
List1->pxIndex            = 0x200000b4
List1->xListEnd.pxNext    = 0x200000c0
ListItem2->pxNext         = 0x200000b4
ListItem1->pxNext         = 0x200000e8
ListItem3->pxNext         = 0x200000d4
/----------------- 前后向連接分割線 ----------------/
List1->xListEnd.pxPrevious = 0x200000d4
ListItem2->pxPrevious      = 0x200000e8
ListItem1->pxPrevious      = 0x200000b4
ListItem3->pxPrevious      = 0x200000c0
/*************** 鏈表重連后的結構完成 **************/

太長啦,我們一個一個分析!!!!!!!

1、初始化列表和列表項

Hello!Task Start!
Free Heap: 2616
/********* 列表和列表項地址 *********/
項地址:
List1: 0x200000ac
List1->pxIndex: 0x200000b4
List1->xListEnd: 0x200000b4
ListItem1: 0x200000c0
ListItem2: 0x200000d4
ListItem3: 0x200000e8
*************************************/

初始化時,List1->xListEndpxNextpxPrevious 都指向自身。

┌──────────────────────────── List_t ───────────────────────────┐
│                                                               │
│  List1 地址: 0x200000ac                                        │
│  pxIndex → 0x200000b4 (xListEnd)                              │
│                                                               │
│      ┌──────────────── xListEnd ────────────────────┐         │
│      │ 地址: 0x200000b4                              │         │
│      │ pxNext    → 0x200000b4 (xListEnd)             │        │
│      │ pxPrevious→ 0x200000b4 (xListEnd)             │        │
│      └───────────────────────────────────────────────┘        │
└───────────────────────────────────────────────────────────────┘

先注意一個事:

List1->pxIndex: 0x200000b4
List1->xListEnd: 0x200000b4

pxIndex 這個指針用于在鏈表中記錄當前位置。它并不是用來存儲鏈表開始或結束的位置,而是用來標記當前操作或遍歷的節點。默認情況下,pxIndex 指向的是 xListEnd(鏈表的末尾標記),所以地址相同!!

2、列表項1插入到列表

/******* (1)添加列表項 ListItem1 *******/
List1->xListEnd->pxNext: 0x200000c0
ListItem1->pxNext: 0x200000b4
/--------- 前后向連接分割線 ----------/
List1->xListEnd->pxPrevious: 0x200000c0
ListItem1->pxPrevious: 0x200000b4
**************************************/

列表項1和末尾列表項互指。

┌──────────────────────────── List_t ───────────────────────────┐
│                                                               │
│  List1 地址: 0x200000ac                                        │
│  pxIndex → 0x200000b4 (xListEnd)                              │
│                                                               │
│      ┌────────────── ListItem1 ───────────────┐               │
│      │ 地址: 0x200000c0                        │               │
│      │ pxNext    → 0x200000b4 (xListEnd)      │               │
│      │ pxPrevious→ 0x200000b4 (xListEnd)      │               │
│      └────────────────────────────────────────┘               │
│                        ▲                                      │
│                        ▼                                      │
│      ┌──────────────── xListEnd ────────────────┐             │
│      │ 地址: 0x200000b4                          │             │
│      │ pxNext    → 0x200000c0 (ListItem1)       │             │
│      │ pxPrevious→ 0x200000c0 (ListItem1)       │             │
│      └──────────────────────────────────────────┘             │
└───────────────────────────────────────────────────────────────┘

3、列表項2和3插入到列表

/******* 添加列表項 ListItem2 ********/
List1->xListEnd->pxNext   = 0x200000c0
ListItem1->pxNext         = 0x200000d4
ListItem2->pxNext         = 0x200000b4
/--------- 前后向連接分割線 ----------/
List1->xListEnd->pxPrevious = 0x200000d4
ListItem1->pxPrevious       = 0x200000b4
ListItem2->pxPrevious       = 0x200000c0
/************************************/
/******** 添加列表項 ListItem3 *********/
List1->xListEnd->pxNext   = 0x200000c0
ListItem1->pxNext         = 0x200000e8
ListItem2->pxNext         = 0x200000b4
ListItem3->pxNext         = 0x200000d4
/------------ 前后向連接分割線 ----------/
List1->xListEnd->pxPrevious = 0x200000d4
ListItem1->pxPrevious       = 0x200000b4
ListItem2->pxPrevious       = 0x200000e8
ListItem3->pxPrevious       = 0x200000c0
/**************************************/

可以發現,順序并1-2-3,而是1-3-2。

┌────────────────────────────── List_t ──────────────────────────────┐
│                                                                    │
│  List1 地址 : 0x200000ac                                           │
│  pxIndex   → 0x200000b4 (xListEnd)                                 │
│                                                                    │
│    ┌──────────────────── ListItem1 ─────────────────────┐         │
│    │ 地址        : 0x200000c0                            │         │
│    │ pxPrevious  → 0x200000b4 (xListEnd)                │         │
│    │ pxNext      → 0x200000e8 (ListItem3)               │?────┐   │
│    └────────────────────────────────────────────────────┘     │   │
│                          ▲                                    │   │
│                          ▼                                    │   │
│    ┌──────────────────── ListItem3 ─────────────────────┐     │   │
│    │ 地址        : 0x200000e8                            │     │   │
│    │ pxPrevious  → 0x200000c0 (ListItem1)               │     │   │
│    │ pxNext      → 0x200000d4 (ListItem2)               │     │   │
│    └────────────────────────────────────────────────────┘     │   │
│                          ▲                                    │   │
│                          ▼                                    │   │
│    ┌──────────────────── ListItem2 ─────────────────────┐     │   │
│    │ 地址        : 0x200000d4                            │     │   │
│    │ pxPrevious  → 0x200000e8 (ListItem3)               │      │   │
│    │ pxNext      → 0x200000b4 (xListEnd)                │      │   │
│    └────────────────────────────────────────────────────┘      │   │
│                          ▲                                     │   │
│                          ▼                                     │   │
│    ┌───────────────────── xListEnd ─────────────────────┐      │   │
│    │ 地址        : 0x200000b4                            │      │   │
│    │ pxPrevious  → 0x200000d4 (ListItem2)               │      │   │
│    │ pxNext      → 0x200000c0 (ListItem1)               │?─────┘   │
│    └────────────────────────────────────────────────────┘          │
└────────────────────────────────────────────────────────────────────┘

這是因為我們剛剛設置的列表項的數值

 ListItem1.xItemValue = 40; ListItem2.xItemValue = 60; ListItem3.xItemValue = 50;

當我們將這些列表項插入到列表中時,列表會根據 xItemValue 的值進行排序!!!

3、刪掉列表項2

/********* (4)刪除列表項 ListItem2 *********/
項目地址:
List1->xListEnd.pxNext   = 0x200000c0
ListItem1->pxNext        = 0x200000e8
ListItem3->pxNext        = 0x200000b4
/----------- 前后向連接分割線 ------------/
List1->xListEnd.pxPrevious = 0x200000e8
ListItem1->pxPrevious      = 0x200000b4
ListItem3->pxPrevious      = 0x200000c0
/****************************************/

移出列表項2后,列表的結構通過調整 pxNextpxPrevious 指針得以重新連接,使得 ListItem1ListItem3xListEnd又形成了一個連續的雙向鏈表。

┌────────────────────────────── List_t ──────────────────────────────┐
│                                                                    │
│  List1 地址 : 0x200000ac                                            │
│  pxIndex   → 0x200000b4 (xListEnd)                                 │
│                                                                    │
│    ┌──────────────────── ListItem1 ─────────────────────┐          │
│    │ 地址        : 0x200000c0                            │         │
│    │ pxPrevious  → 0x200000b4 (xListEnd)                │         │
│    │ pxNext      → 0x200000e8 (ListItem3)               │?────┐   │
│    └────────────────────────────────────────────────────┘     │   │
│                          ▲                                    │   │
│                          ▼                                    │   │
│    ┌──────────────────── ListItem3 ─────────────────────┐     │   │
│    │ 地址        : 0x200000e8                            │     │   │
│    │ pxPrevious  → 0x200000c0 (ListItem1)               │     │   │
│    │ pxNext      → 0x200000b4 (ListItem2)               │     │   │
│    └────────────────────────────────────────────────────┘     │   │
│                          ▲                                     │   │
│                          ▼                                     │   │
│    ┌───────────────────── xListEnd ─────────────────────┐      │   │
│    │ 地址        : 0x200000b4                            │      │   │
│    │ pxPrevious  → 0x200000e8 (ListItem2)               │      │   │
│    │ pxNext      → 0x200000c0 (ListItem1)               │?─────┘   │
│    └────────────────────────────────────────────────────┘          │
└────────────────────────────────────────────────────────────────────┘

4、列表項2插入列表末尾

/********* (5)重新在末尾添加列表項 ListItem2 *********/
項目地址:
List1->pxIndex            = 0x200000b4
List1->xListEnd.pxNext    = 0x200000c0
ListItem2->pxNext         = 0x200000b4
ListItem1->pxNext         = 0x200000e8
ListItem3->pxNext         = 0x200000d4
/----------------- 前后向連接分割線 ----------------/
List1->xListEnd.pxPrevious = 0x200000d4
ListItem2->pxPrevious      = 0x200000e8
ListItem1->pxPrevious      = 0x200000b4
ListItem3->pxPrevious      = 0x200000c0
/*************** 鏈表重連后的結構完成 **************/

我們是將 pxNewListItem 插入到 xListEnd.pxPrevious 后面,即鏈表的邏輯尾部(實際是尾前一項),也就是xListEnd 前面(因為 xListEnd 是一個固定項,永遠在尾部)。

所以,框圖跟刪掉列表項2之前一模一樣

xListEnd <--> ListItem1 <--> ListItem3 <--> ListItem2 <--> xListEnd

關鍵點:pxIndex 的作用

  • pxIndex 這個指針用于在鏈表中記錄當前位置。它并不是用來存儲鏈表開始或結束的位置,而是用來標記當前操作或遍歷的節點。
  • 如果 pxIndex 沒有被改變,默認情況下它指向鏈表的結尾(xListEnd)。
  • 當調用 vListInsertEnd(&List1, &ListItem2) 時,ListItem2 會被插入到 xListEnd 之前,也就是pxIndex 之前,即鏈表的末尾。

如果我們改動一下呢??

	List1.pxIndex = &ListItem1;	// 讓其指向列表項1vListInsertEnd(&List1, &ListItem2); // 將 ListItem2 添加到鏈表末尾

重新下載輸出

/********* (5)重新在末尾添加列表項 ListItem2 *********/
項目地址:
List1->pxIndex            = 0x200000c0
List1->xListEnd.pxNext    = 0x200000d4
ListItem2->pxNext         = 0x200000c0
ListItem1->pxNext         = 0x200000e8
ListItem3->pxNext         = 0x200000b4
/----------------- 前后向連接分割線 ----------------/
List1->xListEnd.pxPrevious = 0x200000e8
ListItem2->pxPrevious      = 0x200000b4
ListItem1->pxPrevious      = 0x200000d4
ListItem3->pxPrevious      = 0x200000c0
/*************** 鏈表重連后的結構完成 **************/

我們來對比看看

字段第一次(pxIndex = 0x200000b4)第二次(pxIndex = 0x200000c0)
List1->pxIndex0x200000b40x200000c0
List1->xListEnd.pxNext0x200000c00x200000d4
List1->xListEnd.pxPrevious0x200000d40x200000e8
列表項順序1 → 3 → 2 → xListEnd2 → 1 → 3 → xListEnd

列表項2被插入到列表項1的前面了!!

所以,在末尾插入列表項,是靠List1->pxIndex決定的!!

OK,一章又完美結束!

七、啟動任務調度器【內容太多,先略】

7.1 概念理解

我們點開main.c,可以看見

  /* Init scheduler */osKernelInitialize();/* Call init function for freertos objects (in cmsis_os2.c) */MX_FREERTOS_Init();/* Start scheduler */osKernelStart();

這個是CubeMX自動生成的

步驟函數名作用
1osKernelInitialize()初始化 RTOS 內核
2MX_FREERTOS_Init()創建任務、信號量、隊列等 RTOS 對象
3osKernelStart()啟動調度器,開始運行任務

第三句就是啟動調度器!

  • osKernelStart() 是 CMSIS-RTOS v2 接口下的 FreeRTOS 調用方式;
  • 如果你使用的是原始 FreeRTOS API,則對應函數是 vTaskStartScheduler();
  • 調用這個函數后,RTOS 會接管 MCU 的控制權,不會再返回主函數

我們點進函數看看

osStatus_t osKernelStart (void) {osStatus_t stat;// 檢查當前是否在中斷上下文中執行if (IS_IRQ()) {stat = osErrorISR;  // 如果是在中斷中調用 osKernelStart,返回錯誤}else {// 檢查當前內核狀態是否為“就緒”if (KernelState == osKernelReady) {/* 設置 SVC 的優先級為默認值(在 FreeRTOS 中用于上下文切換) */SVC_Setup();/* 更改內核狀態為“正在運行” */KernelState = osKernelRunning;/* 啟動 FreeRTOS 的調度器,開始任務調度 */vTaskStartScheduler();// 啟動成功,返回 osOK(正常)stat = osOK;} else {// 如果不是“就緒”狀態,不允許啟動,返回錯誤stat = osError;}}// 返回啟動結果return (stat);
}

有點迷迷糊糊,再看看

代碼行說明
IS_IRQ()判斷當前代碼是否在中斷上下文中執行。不能在中斷中啟動調度器!
KernelState == osKernelReady啟動調度器之前,內核狀態必須是“就緒”狀態。
SVC_Setup()配置 SVC(Supervisor Call)優先級。FreeRTOS 利用 SVC 觸發上下文切換。
KernelState = osKernelRunning;狀態標志位更新,表示 RTOS 現在正在運行。
vTaskStartScheduler();核心函數,正式啟動調度器,執行最高優先級任務。
stat = osOK / osError / osErrorISR返回啟動狀態,供調用者判斷是否成功。

最最最重要的是!vTaskStartScheduler()

它的作用是:

  • 創建空閑任務 (Idle Task),確保系統始終有任務在運行。
  • (可選)創建定時器任務 (Timer Task) ,用于管理軟件定時器。
  • 初始化調度器內核相關變量。
  • 關閉中斷,調用底層啟動調度函數(啟動系統時鐘節拍中斷和任務切換)。
  • 進入任務調度狀態,開始多任務運行。

==

八、時間片調度

8.1 概念理解

什么是時間片?

  • 就是每個任務可以占用 CPU 的時間長度
  • 等于系統滴答定時器的中斷周期(SysTick,比如 1ms)

假設,我們創建了 3 個優先級相同的任務:Task1, Task2, Task3,系統每 1ms 觸發一次 SysTick,即每個任務時間片為 1ms。

時間線 →
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│Task1 │Task2 │Task3 │Task1 │Task2 │Task3 │Task1 │Task2 │Task3 │...
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘↑       ↑       ↑1ms     2ms     3ms ...

① 時間片不能“累加”

  • 如果一個任務時間片沒用完(比如提前進入阻塞態),它不會“攢時間”留著以后用
  • 系統立刻調度下一個就緒態任務。

② 阻塞狀態打斷調度

  • 比如 Task3 運行到一半用 vTaskDelay(100) 進入阻塞,即使還有時間也不會等待!
  • 立馬進入下一個就緒任務(如 Task1),而不是等到 Task3 時間片結束。

② 時間片大小

  • 取決于滴答定時器中斷頻率

舉個小例子:

假設,食堂里有三個學生排隊打飯:小明、小紅、小剛。他們都同等重要(優先級一樣)。

食堂規定:每個人只能打飯 30秒鐘,就必須輪到下一個人繼續打飯,這 30 秒就是“時間片”。

特別情況:有人提前走了,如果小紅打飯打了一半,有急事走了(類比阻塞)。

那食堂阿姨不會等他時間30秒用完,而是立刻讓下一個人(小剛)繼續打飯。

8.2 函數介紹

8.3 編寫例題代碼

正點原子例題

在這里插入圖片描述


總結

自用

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/82590.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/82590.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/82590.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

LVGL(lv_bar進度條)

文章目錄 一、lv_bar 是什么&#xff1f;二、基本使用創建一個進度條設置進度值 三、條形方向與填充方向四、范圍模式&#xff08;Range&#xff09;五、事件處理&#xff08;可選&#xff09;六、自定義樣式&#xff08;可選&#xff09;七、綜合示例八、配合 lv_timer 或外部…

AI對話小技巧

角色設定&#xff1a;擅于使用 System 給 GPT 設定角色和任務&#xff0c;如“哲學大師"指令注入&#xff1a;在 System 中注入常駐任務指令&#xff0c;如“主題創作"問題拆解&#xff1a;將復雜問題拆解成的子問題&#xff0c;分步驟執行&#xff0c;如&#xff1a…

C++ 核心基礎:數字、數組、字符串、指針與引用詳解

C++ 核心基礎:數字、數組、字符串、指針與引用詳解 1. C++ 基礎語法1.1 標識符與保留字1.2 數據類型概述1.3 基本輸入輸出2.1 基本整數類型(int、short、long、long long)2.2 無符號整數類型(unsigned int、unsigned short、unsigned long、unsigned long long)2.3 整數類…

HarmonyOS運動開發:如何集成百度地圖SDK、運動跟隨與運動公里數記錄

前言 在開發運動類應用時&#xff0c;集成地圖功能以及實時記錄運動軌跡和公里數是核心需求之一。本文將詳細介紹如何在 HarmonyOS 應用中集成百度地圖 SDK&#xff0c;實現運動跟隨以及運動公里數的記錄。 一、集成百度地圖 SDK 1.引入依賴 首先&#xff0c;需要在項目的文…

如何理解k8s中的controller

一、基本概念 在k8s中&#xff0c;Controller&#xff08;控制器&#xff09;是核心組件之一&#xff0c;其負責維護集群狀態并確保集群內的實際狀態與期望狀態一致的一類組件。控制器通過觀察集群的當前狀態并將其與用戶定義的期望狀態進行對比&#xff0c;做出相應的調整來實…

《Go小技巧易錯點100例》第三十二篇

本期分享&#xff1a; 1.sync.Map的原理和使用方式 2.實現有序的Map sync.Map的原理和使用方式 sync.Map的底層結構是通過讀寫分離和無鎖讀設計實現高并發安全&#xff1a; 1&#xff09;雙存儲結構&#xff1a; 包含原子化的 read&#xff08;只讀緩存&#xff0c;無鎖快…

【MySQL】行結構詳解:InnoDb支持格式、如何存儲、頭信息區域、Null列表、變長字段以及與其他格式的對比

&#x1f4e2;博客主頁&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;博客倉庫&#xff1a;https://gitee.com/JohnKingW/linux_test/tree/master/lesson &#x1f4e2;歡迎點贊 &#x1f44d; 收藏 ?留言 &#x1f4dd; 如有錯誤敬請指正&#xff01; &…

LabVIEW多通道并行數據存儲系統

在工業自動化監測、航空航天測試、生物醫學信號采集等領域&#xff0c;常常需要對多個傳感器通道的數據進行同步采集&#xff0c;并根據后續分析需求以不同采樣率保存特定通道組合。傳統單線程數據存儲方案難以滿足實時性和資源利用效率的要求&#xff0c;因此設計一個高效的多…

【Linux系列】bash_profile 與 zshrc 的編輯與加載

&#x1f49d;&#x1f49d;&#x1f49d;歡迎來到我的博客&#xff0c;很高興能夠在這里和您見面&#xff01;希望您在這里可以感受到一份輕松愉快的氛圍&#xff0c;不僅可以獲得有趣的內容和知識&#xff0c;也可以暢所欲言、分享您的想法和見解。 推薦:kwan 的首頁,持續學…

針對Mkdocs部署到Githubpages加速訪問速度的一些心得

加速網站訪問的一些心得 在使用 MkDocs 構建網站時&#xff0c;為了提高訪問速度&#xff0c;我們可以采取以下一些措施&#xff1a; 1. 優化圖片 使用合適的圖片格式&#xff0c;如 WebP、JPEG2000 等&#xff0c;減少圖片文件大小&#xff0c;從而加快加載速度。 可以使用…

Mysql中切割字符串作為in的查詢條件

問題&#xff1a;需要將一個字符串切割成數組作為in的查詢條件&#xff0c;如&#xff1a; select * from table_1 where name in (select slit(names) from table_2 where id 3); names 返回的格式是’name1,name2,name3…,需要將name按照逗號切割作為in的查詢條件&#xff1b…

云計算中的虛擬化:成本節省、可擴展性與災難恢復的完美結合

云計算中虛擬化的 4 大優勢 1. 成本效益 從本質上講&#xff0c;虛擬化最大限度地減少了硬件蔓延。團隊可以將多個虛擬機整合到單個物理主機上&#xff0c;而不是為每個工作負載部署單獨的服務器。這大大減少了前期硬件投資和持續維護。 結果如何&#xff1f;更低的功耗、更低…

Linux : 多線程【線程概念】

Linux &#xff1a; 多線程【線程概念】 &#xff08;一&#xff09;線程概念線程是什么用戶層的線程linux中PID與LWP的關系 (二) 進程地址空間頁表(三) 線程總結線程的優點線程的缺點線程異常線程用途 &#xff08;一&#xff09;線程概念 線程是什么 在一個程序里的一個執行…

IDEA轉戰TREA AI IDE : springboot+maven+vue項目配置

一、trea下載安裝 Trae官方網址&#xff1a; https://www.trae.com.cn/ Trae官方文檔&#xff1a;https://docs.trae.com.cn/docs/what-is-trae?_langzh w3cschool&#xff1a; https://www.w3cschool.cn/traedocs/ai-settings.html 安裝這里省略&#xff0c;正常安裝即可。…

Java--圖書管理系統(簡易版)

目錄 目錄 前言 &#x1f514;1.library包 1.1 Book類 1.2 BookList類 &#x1f514;2.user包 2.1User類(父類) 2.2Admin(管理員) 2.3 NormalUser(普通用戶) &#x1f514;3.Operation包 &#x1f550;3.1 IOperation接口 &#x1f551;3.2ListOperation(查看操作)…

深入淺出:Spring Boot 中 RestTemplate 的完整使用指南

在分布式系統開發中&#xff0c;服務間通信是常見需求。作為 Spring 框架的重要組件&#xff0c;RestTemplate 為開發者提供了簡潔優雅的 HTTP 客戶端解決方案。本文將從零開始講解 RestTemplate 的核心用法&#xff0c;并附贈真實地圖 API 對接案例。 一、環境準備 在 Spring…

大數據處理利器:Hadoop 入門指南

一、Hadoop 是什么&#xff1f;—— 分布式計算的基石 在大數據時代&#xff0c;處理海量數據需要強大的技術支撐&#xff0c;Hadoop 應運而生。Apache Hadoop 是一個開源的分布式計算框架&#xff0c;致力于為大規模數據集提供可靠、可擴展的分布式處理能力。其核心設計理念是…

685SJBH計量管理系統

摘 要 計量&#xff0c;在我國已有五千年的歷史。計量的發展與社會進步聯系在一起&#xff0c;它是人類文明的重要組成部分。它的發展經歷了古典階段、經典階段和現代階段。而企業的計量管理是對測量數據、測量過程和測量設備的管理。 本系統通過分析現有計量系統的業務邏輯…

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

目錄 前言為什么要做前端監控前端監控目標穩定性用戶體驗業務 前端監控流程常見埋點方案代碼埋點可視化埋點無痕埋點 創建項目第一步、創建monitor文件&#xff0c;cmd進入文件進行npm init -y 項目初始化第二步、創建src/index.js和src/index.html文件第三步、創建webpack.con…