【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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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…