2024年3月

1、准备材料

正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(
Version 6.10.0

Keil µVision5 IDE(
MDK-Arm

野火DAP仿真器

XCOM V2.6串口助手

一个滑动变阻器

逻辑分析仪
nanoDLA

2、学习目标

本文主要学习FreeRTOS任务管理的相关知识,
包括FreeRTOS创建/删除任务、任务状态、任务优先级、延时函数、空闲任务和任务调度方法等知识

3、前提知识

3.1、任务函数长什么样?

FreeRTOS中任务是一个永远不会退出的 C 函数
,因此通常是作为无限循环实现,其不允许以任何方式从实现函数中返回,如果一个任务不再需要,可以显示的将其删除,其典型的任务函数结构如下所示

/**
  * @brief  任务函数
  * @retval None
  */
void ATaskFunction(void *pvParameters)  
{
	/*初始化或定义任务需要使用的变量*/
	int iVariable = 0;
	
	for(;;)
	{
		/*完成任务的功能代码*/
	
	}
	/*跳出循环的任务需要被删除*/
	vTaskDelete(NULL);
}

3.2、创建一个任务

FreeRTOS提供了三个函数来创建任务(其中名为 xTaskCreateRestricted() 的函数仅供高级用户使用,并且仅与 FreeRTOS MPU 端口相关,故此处不涉及该函数),具体的函数声明如下所示

/**
  * @brief  动态分配内存创建任务函数
  * @param  pvTaskCode:任务函数
  * @param  pcName:任务名称,单纯用于辅助调试
  * @param  usStackDepth:任务栈深度,单位为字(word)
  * @param  pvParameters:任务参数
  * @param  uxPriority:任务优先级
  * @param  pxCreatedTask:任务句柄,可通过该句柄进行删除/挂起任务等操作
  * @retval pdTRUE:创建成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:内存不足创建失败
  */
BaseType_t xTaskCreate(TaskFunction_t pvTaskCode,
					   const char * const pcName,
					   unsigned short usStackDepth,
					   void *pvParameters,
					   UBaseType_t uxPriority,
					   TaskHandle_t *pxCreatedTask);

/**
  * @brief  静态分配内存创建任务函数
  * @param  pvTaskCode:任务函数
  * @param  pcName:任务名称
  * @param  usStackDepth:任务栈深度,单位为字(word)
  * @param  pvParameters:任务参数
  * @param  uxPriority:任务优先级
  * @param  puxStackBuffer:任务栈空间数组
  * @param  pxTaskBuffer:任务控制块存储空间
  * @retval 创建成功的任务句柄
  */
TaskHandle_t xTaskCreateStatic(TaskFunction_t pvTaskCode,
							   const char * const pcName,
							   uint32_t ulStackDepth,
							   void *pvParameters,
							   UBaseType_t uxPriority,
							   StackType_t * const puxStackBuffer,
							   StaticTask_t * const pxTaskBuffer);

上述两个任务创建函数有如下几点不同,
之后如无特殊需要将一律使用动态分配内存的方式创建任务或其他实例

  1. xTaskCreateStatic 创建任务时需要用户指定任务栈空间数组和任务控制块的存储空间,而 xTaskCreate 创建任务其存储空间被动态分配,无需用户指定
  2. xTaskCreateStatic 创建任务函数的返回值为成功创建的任务句柄,而 xTaskCreate 成功创建任务的句柄需要以参数形式提前定义并指定,同时其函数返回值仅表示任务创建成功/失败

3.3、任务都有哪些状态?

在FreeRTOS应用中往往会存在多个任务,但是对于单核的STM32等单片机而言,同一时刻只会有一个任务运行,因此对于一个任务来说要么其处于运行状态,要么处于非运行状态,而对于任务的非运行状态又细分为以下三种状态(尚不考虑被删除的任务)

① 阻塞状态:
一个任务正在等待某个事件发生
,调用可以进入阻塞状态的API函数可以使任务进入阻塞状态,等待的事件通常为以下两种事件

  1. 时间相关事件:如 vTaskDelay() 或 vTaskDelayUntil(),处于运行状态的任务调用这两个延时函数就会进入阻塞状态,等待延时时间结束后会进入就绪状态,待任务调度后又会进入运行状态

  2. 同步相关事件:例如尝试进行读取空队列、尝试写入满队列、尝试获取尚未被释放的二值信号量等等操作都会使任务进入阻塞状态,这些同步事件会在后面的章节详细讲解

② 挂起状态:
一个任务暂时脱离调度器的调度
,挂起状态的任务对调度器来说不可见

  1. 让一个任务进入挂起状态的唯一方法是调用 vTaskSuspend() API函数
  2. 将一个任务从挂起状态唤醒的唯一方法是调用 vTaskResume() API函数(在中断中应调用挂起唤醒的中断安全版本vTaskResumeFromISR() API函数)
/**
  * @brief  挂起某个任务
  * @param  pxTaskToSuspend:被挂起的任务的句柄,通过传入NULL来挂起自身
  * @retval None
  */
void vTaskSuspend(TaskHandle_t pxTaskToSuspend);

/**
  * @brief  将某个任务从挂起状态恢复
  * @param  pxTaskToResume:正在恢复的任务的句柄
  * @retval None
  */
void vTaskResume(TaskHandle_t pxTaskToResume);

/**
  * @brief  vTaskResume的中断安全版本
  * @param  pxTaskToResume:正在恢复的任务的句柄
  * @retval 返回退出中断之前是否需要进行上下文切换(pdTRUE/pdFALSE)
  */
BaseType_t xTaskResumeFromISR(TaskHandle_t pxTaskToResume);

③ 就绪状态:
一个任务处于未运行状态但是既没有阻塞也没有挂起
,处于就绪状态的任务当前尚未运行,但随时可以进入运行状态

下图为一个任务在四种不同状态(阻塞状态、挂起状态、就绪状态和运行状态)下完整的状态转移机制图
(注释1)

在程序中可以使用 eTaskGetState() API 函数利用任务的句柄查询任务当前处于什么状态,任务的状态由枚举类型 eTaskState 表示,具体如下所示

/**
  * @brief  查询一个任务当前处于什么状态
  * @param  pxTask:要查询任务状态的任务句柄,NULL查询自己
  * @retval 任务状态的枚举类型
  */
eTaskState eTaskGetState(TaskHandle_t pxTask);

/*任务状态枚举类型返回值*/
typedef enum
{
	eRunning = 0,	/* 任务正在查询自身的状态,因此肯定是运行状态 */
	eReady,			/* 就绪状态 */
	eBlocked,		/* 阻塞状态 */
	eSuspended,		/* 挂起状态 */
	eDeleted,		/* 正在查询的任务已被删除,但其 TCB 尚未释放 */
	eInvalid		/* 无效状态 */
} eTaskState;

3.4、任务优先级

FreeRTOS每个任务都拥有一个自己的优先级,该优先级可以在创建任务时以参数的形式传入,也可以在需要修改时通过 vTaskPrioritySet() API函数动态设置优先级

任务优先级的设置范围为1~(configMAX_PRIORITIES-1),任务设置的优先级数字越大优先级越高
,设置优先级时可以直接使用数字进行设置,也可以使用内核定义好的枚举类型设置,另外可以使用 uxTaskPriorityGet() API函数获取任务的优先级,如下所示列出了部分优先级枚举类型定义

/*cmsis_os2.c中的定义*/
typedef enum {
  osPriorityNone          =  0,         ///< No priority (not initialized).
  osPriorityIdle          =  1,         ///< Reserved for Idle thread.
  osPriorityLow           =  8,         ///< Priority: low
  osPriorityNormal        = 24,         ///< Priority: normal
  osPriorityAboveNormal   = 32,         ///< Priority: above normal
  osPriorityHigh          = 40,         ///< Priority: high
  osPriorityRealtime      = 48,         ///< Priority: realtime
  osPriorityISR           = 56,         ///< Reserved for ISR deferred thread.
} osPriority_t;

任务的优先级主要决定了在任务调度时,多个任务同时处于就绪态时应该让哪个任务先执行,
FreeRTOS调度器则保证了任何时刻总是在所有可运行的任务中选择具有最高优先级的任务,并将其进入运行态
,如下所述为上述提到的两个设置和获取任务优先级函数的具体声明

/**
  * @brief  设置任务优先级
  * @param  pxTask:要修改优先级的任务句柄,通过NULL改变任务自身优先级
  * @param  uxNewPriority:要修改的任务优先级
  * @retval None
  */
void vTaskPrioritySet(TaskHandle_t pxTask, UBaseType_t uxNewPriority);

/**
  * @brief  获取任务优先级
  * @param  pxTask:要获取任务优先级的句柄,通过NULL获取任务自身优先级
  * @retval 任务优先级
  */
UBaseType_t uxTaskPriorityGet(TaskHandle_t pxTask);

3.5、延时函数

学习STM32时经常会使用到HAL库的延时函数HAL_Delay(),FreeRTOS也同样提供了vTaskDelay() 和 vTaskDelayUntil() 两个 API延时函数,如下所述

/**
  * @brief  延时函数
  * @param  xTicksToDelay:延迟多少个心跳周期
  * @retval None
  */
void vTaskDelay(TickType_t xTicksToDelay);

/**
  * @brief  延时函数,用于实现一个任务固定执行周期
  * @param  pxPreviousWakeTime:保存任务上一次离开阻塞态的时刻
  * @param  xTimeIncrement:指定任务执行多少心跳周期
  * @retval None
  */
void vTaskDelayUntil(TickType_t *pxPreviousWakeTime, TickType_t xTimeIncrement);

上述两个延时函数与 HAL_Delay() 作用都是延时,
但是FreeRTOS延时函数 API 可以让任务进入阻塞状态,而 HAL_Delay() 不具有该功能
,因此如果一个任务需要使用延时,一般应该使用 FreeRTOS 的 API 函数让任务进入阻塞状态等待延时结束,处于阻塞状态的任务便可以让出内核处理其他任务

对于 vTaskDelayUntil() API函数的
pxPreviousWakeTime
参数一般通过 xTaskGetTickCount() API函数获取,该函数作用为获取滴答信号当前计数值,具体如下所述

/**
  * @brief  获取滴答信号当前计数值
  * @retval 滴答信号当前计数值
  */
TickType_t xTaskGetTickCount(void);

/**
  * @brief  获取滴答信号当前计数值的中断安全版本
  */
TickType_t xTaskGetTickCountFromISR(void);

/**
  * @brief  周期任务函数结构
  * @retval None
  */
void APeriodTaskFunction(void *pvParameters)  
{
	/*获取任务创建后的滴答信号计数值*/
	TickType_t pxPreviousWakeTime = xTaskGetTickCount();
	
	for(;;)
	{
		/*完成任务的功能代码*/
		
		/*任务周期500ms*/
		vTaskDelayUntil(&pxPreviousWakeTime, pdMS_TO_TICKS(500));
	}
	/*跳出循环的任务需要被删除*/
	vTaskDelete(NULL);
}

当一个任务因为延时函数或者其他同步事件进入阻塞状态后,可以通过 xTaskAbortDelay() API 函数终止任务的阻塞状态
,即使事件任务等待尚未发生,或者任务进入时指定的超时时间阻塞状态尚未过去,都会使其进入就绪状态,具体函数描述如下所述

/**
  * @brief  终止任务延时,退出阻塞状态
  * @param  xTask:操作的任务句柄
  * @retval pdPASS:任务成功从阻塞状态中删除,pdFALSE:任务不属于阻塞状态导致删除失败
  */
BaseType_t xTaskAbortDelay(TaskHandle_t xTask);

3.6、为什么会有空闲任务?

3.6.1、概述

FreeRTOS 调度器决定在任何时刻处理器必须保持有一个任务运行
,当用户创建的所有任务都处于阻塞状态不能运行时,空闲任务就会被运行

空闲任务是一个优先级为0(最低优先级)的非常短小的循环,其优先级为 0 保证了不会影响到具有更高优先级的任务进入运行态,一旦有更高优先级的任务进入就绪态,空闲任务就会立刻切出运行态

空闲任务何时被创建?
当调用 vTaskStartScheduler() 启动调度器时就会自动创建一个空闲任务,如下图所示,另外空闲任务还负责将分配给已删除任务的内存释放掉

3.6.2、空闲任务钩子函数

空闲任务有一个钩子函数,可以通过配置
configUSE_IDLE_HOOK
参数为 Enable 启动空闲任务的钩子函数,如果是使用STM32CubeMX软件生成的工程则会自动生成空闲任务钩子函数,
当调度器调度内核进入空闲任务时就会调用钩子函数

通常空闲任务钩子函数主要被用于下方函数体内部注释列举的几种情况,如下所述为空闲任务钩子函数典型的任务函数结构

/**
  * @brief  空闲任务钩子函数
  * @retval NULL
  */
void vApplicationIdleHook(void)
{
	/*
		1.执行低优先级,或后台需要不停处理的功能代码
		2.测试系统处理裕量(内核执行空闲任务时间越长表示内核越空闲)
		3.将处理器配置到低功耗模式(Tickless模式)
	*/
}

除了空闲任务钩子函数外,FreeRTOS提供了一系列钩子函数供用户选择使用,具体读者可查看
FreeRTOS教程1 基础知识
文章“4.1.3、外设参数配置”小节参数列表中的“Hook function related definitions”,使用之前只需在STM32CubeMX中启用相关参数,然后在生成的代码中找到钩子函数使用即可

3.7、删除任务

一个任务不再需要时,需要显示调用 vTaskDelete() API函数将任务删除
,该函数需要传入要删除任务的句柄这个参数(传入NULL时表示删除自己),函数声明如下所述

/**
  * @brief  任务删除函数
  * @param  pxTaskToDelete:要删除的任务句柄,NULL表示删除自己
  * @retval None
  */
void vTaskDelete(TaskHandle_t pxTaskToDelete);

3.8、任务调度方法

调度器保证了总是在所有可运行的任务中选择具有最高优先级的任务,并将其进入运行态
,根据
configUSE_PREEMPTION
(使用抢占调度器) 和
configUSE_TIME_SLICING
(使用时间片轮询) 两个参数的不同,FreeRTOS涉及三种不同的调度方法

  1. 时间片轮询的抢占式调度方法(configUSE_PREEMPTION=1,configUSE_TIME_SLICING=1)
  2. 不用时间片轮询的抢占式调度方法(configUSE_PREEMPTION=1,configUSE_TIME_SLICING=0)
  3. 协作式调度方法(configUSE_PREEMPTION=0)

本文只介绍抢占式调度方法(
后续所有文章全部采用时间片轮询的抢占式调度方法
),不涉及协作式的调度方法

什么是时间片?

FreeRTOS基础时钟的一个定时周期称为一个时间片,所以其长度由
configTICK_RATE_HZ
参数决定,默认情况下为1000HZ(也即1ms)

对于时间片轮询的抢占式调度方法,其在任务调度过程中一般满足以下两点要求

  1. 高优先级的任务可以抢占低优先级的任务
  2. 同等优先级的任务根据时间片轮流执行

对于不用时间片轮询的抢占式调度方法,其在任务调度过程中一般满足以下两点要求

  1. 高优先级的任务同样可以抢占低优先级的任务
  2. 同等优先级的任务不会按照时间片轮流执行,可能出现任务间占用处理器时间相差很大的情况

任务调度主要是由任务调度器 scheduler 负责,其由 FreeRTOS 内核管理,用户一般无需控制任务调度器,但是 FreeRTOS 也给用户提供了启动、停止、挂起和恢复三个常见的控制 scheduler 的 API 函数,具体如下所述

/**
  * @brief  启动调度器
  * @retval None
  */
void vTaskStartScheduler(void);

/**
  * @brief  停止调度器
  * @retval None
  */
void vTaskEndScheduler(void);

/**
  * @brief  挂起调度器
  * @retval None
  */
void vTaskSuspendAll(void);

/**
  * @brief  恢复调度器
  * @retval 返回是否会导致发生挂起的上下文切换(pdTRUE/pdFALSE)
  */
BaseType_t xTaskResumeAll(void);

除了任务被时间片轮询切换或者高优先级抢占发生切换两种常见的调度方式外,还有其他的调度方式,比如任务自愿让出处理器给其他任务使用等函数,这些函数将在后续 “中断管理” 章节中被详细介绍,这里简单了解即可,如下所述

/**
  * @brief  让位于另一项同等优先级的任务
  * @retval None
  */
void taskYIELD(void);

/**
  * @brief  ISR 退出时是否执行上下文切换(汇编)
  * @param  xHigherPriorityTaskWoken:pdFASLE不请求上下文切换,反之请求上下文切换
  * @retval None
  */
portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);

/**
  * @brief  ISR 退出时是否执行上下文切换(C语言)
  * @param  xHigherPriorityTaskWoken:pdFASLE不请求上下文切换,反之请求上下文切换
  * @retval None
  */
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

3.9、工具函数

任务相关的实用工具函数较多,官方网站上一共列出了23个 API 函数,这里笔者仅简单介绍一些可能常用的 API 函数,如果读者有其他希望了解的函数,可以自行前往
FreeRTOS/API 引用/任务实用程序
中了解

另外读者应注意,
如果要使用下方某些函数则可能需要在CubeMX的FREERTOS/Include parameters参数配置页面中勾选启用对应的API函数
,具体可查看
FreeRTOS教程1 基础知识
文章"4.1.3、外设参数配置"小节下方的参数表格

3.9.1、获取任务信息

/**
  * @brief  获取一个任务的信息,需启用参数configUSE_TRACE_FACILITY(默认启用)
  * @param  xTask:需要查询的任务句柄,NULL查询自己
  * @param  pxTaskStatus:用于存储任务状态信息的TaskStatus_t结构体指针
  * @param  xGetFreeStackSpace:是否返回栈空间高水位值
  * @param  eState:指定查询信息时任务的状态,设置为eInvalid将自动获取任务状态
  * @retval None
  */
void vTaskGetInfo(TaskHandle_t xTask,
				  TaskStatus_t *pxTaskStatus,
				  BaseType_t xGetFreeStackSpace,
				  eTaskState eState);

/**
  * @brief  获取当前任务句柄
  * @retval 返回当前任务句柄
  */
TaskHandle_t xTaskGetCurrentTaskHandle(void);

/**
  * @brief  获取任务句柄(运行时间较长,不宜大量使用)
  * @param  pcNameToQuery:要获取任务句柄的任务名称字符串
  * @retval 返回指定查询任务的句柄
  */
TaskHandle_t xTaskGetHandle(const char *pcNameToQuery);

/**
  * @brief  获取空闲任务句柄
  * @注意:需要设置 INCLUDE_xTaskGetIdleTaskHandle 为1,在CubeMX中不可调,需自行定义
  * @retval 返回空闲任务句柄
  */
TaskHandle_t xTaskGetIdleTaskHandle(void);

/**
  * @brief  获取一个任务的高水位值(任务栈空间最少可用剩余空间大小,单位为字(word))
  * @param  xTask:要获取高水位值任务的句柄,NULL查询自己
  * @retval 
  */
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);

/**
  * @brief  获取一个任务的任务名称字符串
  * @param  xTaskToQuery:要获取名称字符串的任务的句柄,NULL查询自己
  * @retval 返回一个任务的任务名称字符串
  */
char* pcTaskGetName(TaskHandle_t xTaskToQuery);

3.9.2、获取内核信息

/**
  * @brief  获取系统内所有任务状态,为每个任务返回一个TaskStatus_t结构体数组
  * @param  pxTaskStatusArray:数组的指针,数组每个成员都是TaskStatus_t类型,用于存储获取到的信息
  * @param  uxArraySize:设置数组pxTaskStatusArray的成员个数
  * @param  pulTotalRunTime:返回FreeRTOS运行后总的运行时间,NULL表示不返回该数据
  * @retval 返回实际获取的任务信息条数
  */
UBaseType_t uxTaskGetSystemState(TaskStatus_t * const pxTaskStatusArray,
								 const UBaseType_t uxArraySize,
								 unsigned long * const pulTotalRunTime);

/**
  * @brief  返回调度器状态
  * @retval 0:被挂起,1:未启动,2:正在运行
  */
BaseType_t xTaskGetSchedulerState(void);

/**
  * @brief  获取内核当前管理的任务总数
  * @retval 返回内核当前管理的任务总数
  */
UBaseType_t uxTaskGetNumberOfTasks(void);

/**
  * @brief  获取内核中所有任务的字符串列表信息
  * @param  pcWriteBuffer:字符数组指针,用于存储获取的字符串信息
  * @retval None
  */
void vTaskList(char *pcWriteBuffer);

3.9.3、其他函数

/**
  * @brief  获取一个任务的标签值
  * @param  xTask:要获取任务标签值的任务句柄,NULL表示获取自己的标签值
  * @retval 返回任务的标签值
  */
TaskHookFunction_t xTaskGetApplicationTaskTag(TaskHandle_t xTask); 

/**
  * @brief  获取一个任务的标签值的中断安全版本函数
  */
TaskHookFunction_t xTaskGetApplicationTaskTagFromISR(TaskHandle_t xTask);

/**
  * @brief  设置一个任务的标签值,标签值保存在任务控制块中
  * @param  xTask:要设置标签值的任务的句柄,NULL表示设置自己
  * @param  pxTagValue:要设置的标签值
  * @retval None
  */
void vTaskSetApplicationTaskTag(TaskHandle_t xTask, 
								TaskHookFunction_t pxTagValue);

4、实验一:尝试任务基本操作

4.1、实验目的

  1. 创建一个任务
    TASK_GREEN_LED ,每 100ms 改变一次 GREEN_LED 的状态
  2. 使用静态内存分配创建一个任务
    TASK_RED_LED ,每 500ms 改变一次 RED_LED 的状态
  3. 创建一个任务 TASK_KEY_SCAN ,用于实现按键扫描功能,当开发板上的 KEY2 按键按下时
    删除任务
    TASK_GREEN_LED ,当开发板上的 KEY1 按键按下时
    挂起任务
    TASK_RED_LED ,当开发板上的 KEY0 按键按下时
    恢复任务
    TASK_RED_LED

4.2、CubeMX相关配置

首先读者应按照
FreeRTOS教程1 基础知识
章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求

本实验需要初始化开发板上 GREEN_LED 和 RED_LED 两个 LED 灯作为显示,具体配置步骤请阅读“
STM32CubeMX教程2 GPIO输出 - 点亮LED灯
”,注意虽开发板不同但配置原理一致,如下图所示

本实验需要初始化开发板上 KEY2、KEY1 和 KEY0 用户按键做普通输入,具体配置步骤请阅读“
STM32CubeMX教程3 GPIO输入 - 按键响应
”,注意虽开发板不同但配置原理一致,如下图所示

本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读“
STM32CubeMX教程9 USART/UART 异步通信
”,如下图所示

单击 Middleware and Software Packs/FREERTOS ,在 Configuration 中单击 Tasks and Queues 选项卡,首先双击默认任务修改其参数,然后单击 Add 按钮按要求增加另外两个任务,由于按键扫描任务比闪烁 LED 灯任务重要,因此将其优先级配置为稍高,配置好的界面如下图所示

假设之前配置空工程时已经配置好了 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可

4.3、添加其他必要代码

按照 “
STM32CubeMX教程9 USART/UART 异步通信
” 实验 “6、串口printf重定向” 小节增加串口 printf 重定向代码,具体不再赘述

打开 freertos.c 文件夹,按要求增加三个任务的实现代码,其中阻塞延时函数 osDelay() 为 vTaskDelay() 函数的包装版本,具体源代码如下所述

/*GREEN_LED闪烁任务函数*/
void TASK_GREEN_LED(void *argument)
{
  /* USER CODE BEGIN TASK_GREEN_LED */
  /* Infinite loop */
  for(;;)
  {
	//每隔100ms闪烁一次GREEN_LED
	HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
	printf("TASK_GREEN_LED, GREEN LED BLINK!\r\n");
    osDelay(pdMS_TO_TICKS(100));
  }
  /* USER CODE END TASK_GREEN_LED */
}

/*RED_LED闪烁任务函数*/
void TASK_RED_LED(void *argument)
{
  /* USER CODE BEGIN TASK_RED_LED */
  /* Infinite loop */
  for(;;)
  {
	//每隔500ms闪烁一次RED_LED
	HAL_GPIO_TogglePin(RED_LED_GPIO_Port, RED_LED_Pin);
	printf("TASK_RED_LED, RED LED BLINK!\r\n");
    osDelay(pdMS_TO_TICKS(500));
  }
  /* USER CODE END TASK_RED_LED */
}

/*KEY_SCAN按键扫描任务函数*/
void TASK_KEY_SCAN(void *argument)
{
  /* USER CODE BEGIN TASK_KEY_SCAN */
  uint8_t key_value = 0;
  /* Infinite loop */
  for(;;)
  {
	key_value = 0;
	//按键KEY2按下
	if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
		key_value = 3;
	//按键KEY1按下
	if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
		key_value = 2;
	//按键KEY0按下
	if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
		key_value = 1;
	
	if(key_value != 0)
	{
		if(key_value == 3)
		{
			printf("\r\n\r\nKEY2 PRESSED, Delete TASK_GREEN_LED!\r\n\r\n");
			//此处可使用vTaskDelete(task_GREEN_LEDHandle),但要注意不能重复删除句柄
			osThreadTerminate(task_GREEN_LEDHandle);
		}
		else if(key_value == 2)
		{
			printf("\r\n\r\nKEY1 PRESSED, Suspend TASK_RED_LED!\r\n\r\n");
			vTaskSuspend(task_RED_LEDHandle);
		}
		else if(key_value == 1)
		{
			printf("\r\n\r\nKEY0 PRESSED, Resume TASK_RED_LED!\r\n\r\n");
			vTaskResume(task_RED_LEDHandle);
		}
		//有按键按下就进行按键消抖
		osDelay(300);
	}
	else
		osDelay(10);
  }
  /* USER CODE END TASK_KEY_SCAN */
}

当实现三个任务的函数体之后就不需要其他任何操作了,因为任务的创建、调用等工作的程序代码 STM32CubeMX 软件已经自动生成了,这里为方便初学者理解做一下简单介绍,之后便不再重复介绍

打开工程项目中 main.c 文件,我们可以发现在主函数 main() 中调用了 MX_FREERTOS_Init() 函数,该函数中已经自动创建了我们在 STM32CubeMX 软件中创建的三个任务,其中 osThreadNew() 函数为 xTaskCreate() / xTaskCreateStatic() 的包装函数,如下图所示

4.4、烧录验证

烧录程序,打开串口助手,可以发现串口上源源不断地输出 TASK_GREEN_LED 和 TASK_RED_LED 运行的提示,每输出5次 TASK_GREEN_LED 然后就会输出1次 TASK_RED_LED,同时开发板上的红色和绿色LED灯也不停闪烁

当按下开发板上的 KEY2 按键,串口提示删除 TASK_GREEN_LED ,之后会发现只有 TASK_RED_LED 运行的串口输出;当按下开发板上的 KEY1 按键,串口提示挂起 TASK_RED_LED,之后 TASK_RED_LED 会停止执行;最后按下开发板上的 KEY0 按键,串口提示恢复 TASK_RED_LED,TASK_RED_LED 恢复运行

上述整个过程串口输出信息如下图所示

如果不操作按键,其任务流程应该如下所述

  1. 在 t1 时刻,调度器刚刚开始运行,其浏览任务列表发现有两个进入就绪态的任务,即刚刚创建好的任务 TASK_GREEN_LED 和 TASK_RED_LED,由于两个任务优先级均相同,但是 TASK_GREEN_LED 先建立,因此调度器决定先执行该任务,TASK_GREEN_LED 调用了延时函数 osDelay() 让任务进入阻塞状态,然后调度器发现还有就绪的任务,于是切换到任务 TASK_RED_LED ,同理执行到延时函数让任务进入了阻塞状态
  2. 在 t2 时刻,调度器发现任务列表里已经没有就绪的任务(两个任务都进入了阻塞状态),于是选择执行空闲任务
  3. 在 t3 时刻,任务 TASK_GREEN_LED 延时结束,从阻塞状态进入就绪状态,由于任务 TASK_GREEN_LED 优先级高于空闲任务,因此该任务抢占空闲任务进入运行状态,执行完函数体再次遇到延时函数 osDelay() 让任务进入阻塞状态,然后不断重复步骤3的过程
  4. 在 t7 时刻,任务 TASK_GREEN_LED 和 TASK_RED_LED 同时延时结束,从阻塞状态进入就绪状态,然后调度器重复步骤1的过程

上述任务流程图具体如下图所示

4.5、探讨延时函数特性

如果
将任务 TASK_GREEN_LED 和 TASK_RED_LED 函数体内的延时函数 osDelay() 更改为 HAL 库的延时函数 HAL_Delay() 函数
,根据“3.5、延时函数”小节内容可知,HAL_Delay() 函数不会使任务进入阻塞状态

值得注意的是这两个任务目前优先级相同,均为 osPriorityNormal ,因此根据 “3.8、任务调度方法” 小节内容可知,采用时间片轮询的抢占式调度方式对于同等优先级的任务采用时间片轮询执行,所以如果不操作按键,只修改延时函数后的任务流程应该如下图所述

从图上可以看出,由于任务不会进入阻塞状态,因此两个同等优先级的任务会按照时间片轮流执行,而空闲函数则不会得到执行

4.6、任务被饿死了

接着上面所述,假设
将任务 TASK_RED_LED 的优先级修改为 osPriorityBelowNormal
,该优先级低于任务 TASK_GREEN_LED 的优先级,然后保持延时函数为 HAL_Delay() 函数不变,并且不操作按键,其任务流程应该如下所述

从图上可以看出,由于任务不会进入阻塞状态,因此高优先级的任务会一直得到执行,从而将低优先级的任务饿死了,所以在实际使用中,任务应该使用能够进入阻塞状态的延时函数

4.7、使用 vTaskDelayUntil()

根据 ”4.4、烧录验证“ 小节任务流程图可知,对任务延时并不能达到让任务以固定周期执行,
如果读者希望能够让一个任务严格按照固定周期执行,可以使用 vTaskDelayUntil() 函数实现
,修改任务函数如下所示

/*GREEN_LED闪烁任务函数*/
void TASK_GREEN_LED(void *argument)
{
  /* USER CODE BEGIN TASK_GREEN_LED */
  TickType_t previousWakeTime = xTaskGetTickCount();
  /* Infinite loop */
  for(;;)
  {
	//进入临界段
	taskENTER_CRITICAL();
	//每隔100ms闪烁一次GREEN_LED
	HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
	printf("TASK_GREEN_LED, GREEN LED BLINK!\r\n");
	//退出临界段
	taskEXIT_CRITICAL();
    //也可使用osDelayUntil(pdMS_TO_TICKS(100));
    vTaskDelayUntil(&previousWakeTime, pdMS_TO_TICKS(100));
  }
  /* USER CODE END TASK_GREEN_LED */
}

/*RED_LED闪烁任务函数*/
void TASK_RED_LED(void *argument)
{
  /* USER CODE BEGIN TASK_RED_LED */
  TickType_t previousWakeTime = xTaskGetTickCount();
  /* Infinite loop */
  for(;;)
  {
	//进入临界段
	taskENTER_CRITICAL();
	//每隔500ms闪烁一次RED_LED
	HAL_GPIO_TogglePin(RED_LED_GPIO_Port, RED_LED_Pin);
	printf("TASK_RED_LED, RED LED BLINK!\r\n");
	//退出临界段
	taskEXIT_CRITICAL();
	//也可使用osDelayUntil(pdMS_TO_TICKS(500));
	vTaskDelayUntil(&previousWakeTime, pdMS_TO_TICKS(100));
  }
  /* USER CODE END TASK_RED_LED */
}

由于 TASK_GREEN_LED 100ms 执行一次,TASK_RED_LED 500ms 执行一次,所以存在同时执行的情况,可能会导致串口输出数据出错,因此这里使用了临界段保护串口输出程序,临界段相关知识将在后续
FreeRTOS教程3 中断管理
文章中介绍到

使用逻辑分析仪采集红色和绿色两个 LED 灯引脚电平变化,可以发现其执行周期与设置一致,误差可以接受,具体如下图所示

与单纯使用延时函数的程序做对比,可以发现只使用延时函数的任务执行周期误差较大,无法做到固定周期运行,具体如下图所示

5、实验二:获取任务信息

5.1、实验目的

  1. 创建任务 TASK_ADC,该任务通过 ADC1 的 IN5 通道周期采集电位器的电压值,并通过串口输出采集到的 ADC 值;
  2. 创建任务 TASK_KEY_SCAN ,当按键 KEY2 按下时根据任务句柄
    获取单个任务的信息
    并通过串口输出到串口助手上;当按键 KEY1 按下时
    获取每个任务的高水位值
    并通过串口输出到串口助手上;当按键 KEY0 按下时
    获取系统任务列表
    并通过串口输出到串口助手上;

5.2、CubeMX相关配置

同样读者应按照
FreeRTOS教程1 基础知识
章节配置一个可以正常编译通过的FreeRTOS空工程,然后在此空工程的基础上增加本实验所提出的要求

本实验需要初始化开发板上 KEY2、KEY1 和 KEY0 用户按键做普通输入,具体配置步骤请阅读“
STM32CubeMX教程3 GPIO输入 - 按键响应
”,注意虽开发板不同但配置原理一致,如下图所示

本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读“
STM32CubeMX教程9 USART/UART 异步通信
”,如下图所示

单击 Analog 中的 ADC1 ,勾选 IN5 ,在下方的参数配置中仅将 IN5 的采样时间修改为 15Cycles 即可,对 ADC 单通道采集感兴趣的读者可以阅读“
STM32CubeMX教程13 ADC - 单通道转换
”实验,如下图所示

单击 Middleware and Software Packs/FREERTOS ,在 Configuration 中单击 Tasks and Queues 选项卡,首先双击默认任务修改其参数,然后单击 Add 按钮按要求增加另外一个任务,配置好的界面如下图所示

由于需要使用到一些获取信息的函数,有些默认情况下并不能使用,需要用户配置参数将其加入到编译中,因此需要做以下两个操作

  1. 在 Config parameters 中启用 USE_TRACE_FACILITY 参数和 USE_STATS_FORMATTING_FUNCTIONS 参数,目的是为了使用 vTaskList() API 函数
  2. 在生成的工程代码中找到 FreeRTOSConfig.h 文件,在用户代码区域添加下述代码,目的是为了使用获取空闲任务句柄 xTaskGetIdleTaskHandle() API 函数
#define INCLUDE_xTaskGetIdleTaskHandle     1

配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可

5.3、添加其他必要代码

首先添加串口 printf 重定向函数,不再赘述,然后打开 freertos.c 文件,添加需要使用到的 ADC 的头文件,如下所述

/*添加头文件*/
#include "adc.h"

最后根据实验目的编写程序完成 TASK_ADC 和 TASK_KEY_SCAN 两个任务,具体如下所示

/*ADC周期采集任务*/
void TASK_ADC(void *argument)
{
  /* USER CODE BEGIN TASK_ADC */
    TickType_t previousWakeTime = xTaskGetTickCount();
  /* Infinite loop */
  for(;;)
  {
	//开始临界代码段,不允许任务调度
	taskENTER_CRITICAL();
	HAL_ADC_Start(&hadc1);
	if(HAL_ADC_PollForConversion(&hadc1,200)==HAL_OK)
	{
		uint32_t val=HAL_ADC_GetValue(&hadc1);
		uint32_t Volt=(3300*val)>>12;
		printf("val:%d, Volt:%d\r\n",val,Volt);
	}
	//结束临界代码段,重新允许任务调度
	taskEXIT_CRITICAL();
	//500ms周期
	vTaskDelayUntil(&previousWakeTime, pdMS_TO_TICKS(500));
  }
  /* USER CODE END TASK_ADC */
}

/*按键扫描KEY_SCAN任务*/
void TASK_KEY_SCAN(void *argument)
{
	/* USER CODE BEGIN TASK_KEY_SCAN */
	uint8_t key_value = 0;
	TaskHandle_t taskHandle = task_ADCHandle;
	/* Infinite loop */
	for(;;)
	{
		key_value = 0;
		//按键KEY2按下
		if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
			key_value = 3;
		//按键KEY1按下
		if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
			key_value = 2;
		//按键KEY0按下
		if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
			key_value = 1;
		//如果有按键按下
		if(key_value != 0)
		{
			if(key_value == 3)
			{
				taskHandle = task_ADCHandle;
				TaskStatus_t taskInfo;
				//是否获取高水位值
				BaseType_t getFreeStackSpace = pdTRUE;  	
				//当前的状态,设置为eInvalid将自动获取任务状态
				eTaskState taskState = eInvalid; 		
				//获取任务信息					
				vTaskGetInfo(taskHandle, &taskInfo, getFreeStackSpace, taskState);	
				//开始临界代码段,不允许任务调度	
				taskENTER_CRITICAL();			
				printf("\r\n--- KEY2 PRESSED ---\r\n");
				printf("Task_Info: Show task info,Get by vTaskGetInfo();\r\n");
				printf("Task Name = %s\r\n", (uint8_t *)taskInfo.pcTaskName);
				printf("Task Number = %d\r\n", (uint16_t)taskInfo.xTaskNumber);
				printf("Task State = %d\r\n", taskInfo.eCurrentState);
				printf("Task Priority = %d\r\n", (uint8_t)taskInfo.uxCurrentPriority);
				printf("High Water Mark = %d\r\n\r\n", taskInfo.usStackHighWaterMark);
				//结束临界代码段,重新允许任务调度
				taskEXIT_CRITICAL();
			}
			else if(key_value == 2)
			{
				//开始临界代码段,不允许任务调度	
				taskENTER_CRITICAL();
				printf("\r\n--- KEY1 PRESSED ---\r\n");
				//获取空闲任务句柄
				taskHandle = xTaskGetIdleTaskHandle();
				//获取任务高水位值				
				UBaseType_t hwm = uxTaskGetStackHighWaterMark(taskHandle);
				printf("Idle Task'Stack High Water Mark = %d\r\n", (uint16_t)hwm);
				//Task_ADC的任务句柄
				taskHandle=task_ADCHandle;				
				hwm = uxTaskGetStackHighWaterMark(taskHandle);
				printf("Task_ADC'Stack High Water Mark = %d\r\n", (uint16_t)hwm);
				//Task_KEY_SCAN的任务句柄
				taskHandle=task_KEY_SCANHandle;				
				hwm = uxTaskGetStackHighWaterMark(taskHandle);
				printf("Task_KEY_SCAN'Stack High Water Mark = %d\r\n", (uint16_t)hwm);
				//获取系统任务个数
				UBaseType_t taskNum=uxTaskGetNumberOfTasks();  
				printf("There are now %d tasks in total!\r\n\r\n", (uint16_t)taskNum);
				//结束临界代码段,重新允许任务调度
				taskEXIT_CRITICAL();
			}
			else if(key_value == 1)
			{
				//开始临界代码段,不允许任务调度	
				taskENTER_CRITICAL();
				printf("\r\n--- KEY0 PRESSED ---\r\n");
				char infoBuffer[300];
				//获取任务列表
				vTaskList(infoBuffer);
				printf("%s\r\n\r\n",infoBuffer);
				//结束临界代码段,重新允许任务调度
				taskEXIT_CRITICAL();
			}
			//按键消抖
			osDelay(300);
		}
		else
			osDelay(10);
	}
	/* USER CODE END TASK_KEY_SCAN */
}

5.4、烧录验证

烧录程序,打开串口助手,可以发现串口上源源不断地输出 TASK_ADC 采集到的 ADC 值,首先从一端缓慢旋转滑动变阻器直到另一端,可以发现采集到的 ADC 值从 0 逐渐变为最大值 4095 ,表示 ADC 采集任务正常运行

按下 KEY2 按键,串口会输出任务 TASK_ADC 的相关信息,包括任务名称、任务数量、任务状态、任务优先级和任务栈高水位值等信息

按下 KEY1 按键,串口会输出空闲任务、 ADC 采集任务和按键扫描任务三个任务的高水位值,同时会输出系统中一共存在的任务数量

为什么有4个任务?

按下 KEY0 按键,串口会以列表形式输出系统中的所有任务,可以看到第4个任务是名为 Tmr Svc 的定时器守护任务,vTaskList() API 函数会将每个任务以 “Task_Name \tX\t25\t128\t2\r\n" 形式写入缓存数组中,从左往右依次表示任务名称、任务状态(X:运行,R:就绪,B:阻塞)、任务优先级、栈空间高水位置和任务编号

上述整个过程串口输出信息如下图所示

6、注释详解

注释1
:图片来源
Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf

参考资料

STM32Cube高效开发教程(基础篇)

Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf

image

背景

在一个微服务架构的项目中,一个业务操作可能涉及到多个服务,这些服务往往是独立部署,构成一个个独立的系统。这种分布式的系统架构往往面临着分布式事务的问题。为了保证系统数据的一致性,我们需要确保这些服务中的操作要么全部成功,要么全部失败。通过使用RocketMQ实现分布式事务,我们可以协调这些服务的操作,保证数据的一致性。

功能原理

RocketMQ的分布式事务消息功能,在普通消息基础上,支持二阶段的提交。将二阶段提交和本地事务绑定,实现全局提交结果的一致性。

整个事务消息的详细交互流程如下图所示:

image

1、生产者将消息发送至RocketMQ服务端。

2、RocketMQ服务端将消息持久化成功之后,向生产者返回Ack确认消息已经发送成功,此时消息被标记为"暂不能投递",这种状态下的消息即为半事务消息。

3、生产者开始执行本地事务逻辑。

4、生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:

  • 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。

  • 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。

5、在断网或者是生产者应用重启的特殊情况下,若服务端未收到生产者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者集群中任一生产者实例发起消息回查。

6、生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。

7、生产者根据检查到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。

注意问题

消息类型
事务消息仅支持在MessageType为Transaction的主题使用,即事务消息只能发送至类型为事务消息的主题中。

消息消费
RocketMQ事务消息保证生产者本地事务和下游消息发送事务的一致性,但不保证消息消费结果和上游事务的一致性。因此需要下游业务自行保证消息正确处理,建议消费端做好消费重试。

中间状态
RocketMQ事务消息一致性为最终一致性,即在消息提交到下游消费端处理完成之前,下游和上游事务之间的状态会不一致。因此,事务消息仅适合能接受异步执行的场景。

事务超时
RocketMQ事务消息的生命周期存在超时机制,即半事务消息被生产者发送服务端后,如果在指定时间内服务端无法确认提交或者回滚状态,则消息默认会被回滚。

示例代码

以下为RocketMQ 4.x版本事务消息示例代码,

import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.concurrent.*;

public class RocketMqTransactionDemo {
	public static void main(String[] args) throws Exception {
		// 创建事务消息生产者
		TransactionMQProducer producer = new TransactionMQProducer("transaction_producer");
		producer.setNamesrvAddr("127.0.0.1:9876");

		// 设置事务监听器
		TransactionListener transactionListener = new MyTransactionListener();
		producer.setTransactionListener(transactionListener);

		// 设置事务回查的线程池,可以不必设置,如果不设置也会默认生成一个
		ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue <Runnable> (2000), new ThreadFactory() {
			@Override
			public Thread newThread(Runnable r) {
				Thread thread = new Thread(r);
				thread.setName("client-transaction-msg-check-thread");
				return thread;
			}
		});
		producer.setExecutorService(executorService);

		// 启动生产者
		producer.start();

		// 发送事务消息
		Message message = new Message("transaction_topic", "test_tag", "test_key", "Hello RocketMQ".getBytes());
		producer.sendMessageInTransaction(message, null);

		// 关闭生产者
		producer.shutdown();
	}
}

/**
 * 事务监听器
 */
class MyTransactionListener implements TransactionListener {
	@Override
	public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
		// 执行本地事务操作
		System.out.println("执行本地事务操作,消息内容:" + new String(msg.getBody()));
		return LocalTransactionState.COMMIT_MESSAGE; // 提交事务,允许消费者消费该消息
		// return LocalTransactionState.ROLLBACK_MESSAGE;// 回滚事务,消息将被丢弃不允许消费。
		// return LocalTransactionState.UNKNOW;// 暂时无法判断状态,等待固定时间以后Broker端根据回查规则向生产者进行消息回查。
	}

	@Override
	public LocalTransactionState checkLocalTransaction(MessageExt msg) {
		// 检查本地事务状态
		System.out.println("检查本地事务状态,消息内容:" + new String(msg.getBody()));
		return LocalTransactionState.COMMIT_MESSAGE;
	}
}

代码解释:
1、事务消息的生产者使用
TransactionMQProducer
创建。
2、
MyTransactionListener
作为事务监听器,实现了接口
TransactionListener
,该接口有两个方法,分别是:

  • executeLocalTransaction

    半事务消息发送成功后,执行本地事务的方法,具体执行完本地事务后,可以在该方法中返回以下三种状态:
    LocalTransactionState.COMMIT_MESSAGE:
    提交事务,允许消费者消费该消息。
    LocalTransactionState.ROLLBACK_MESSAGE:
    回滚事务,消息将被丢弃不允许消费。
    LocalTransactionState.UNKNOW:
    暂时无法判断状态,等待固定时间以后RocketMQ服务端根据回查规则向生产者进行消息回查。

  • checkLocalTransaction

    二次确认消息没有收到,RocketMQ服务端回查生产者端事务结果的方法。回查规则:本地事务执行完成后,若RocketMQ服务端收到的本地事务返回状态为LocalTransactionState.UNKNOW,或生产者应用退出导致本地事务未提交任何状态。则RocketMQ服务端会向消息生产者发起事务回查,第一次回查后仍未获取到事务状态,则之后每隔一段时间会再次回查。

F-String
(格式化字符串字面值)是在Python 3.6中引入的,它是一种非常强大且灵活的字符串格式化方法。

它允许你在字符串中嵌入表达式,这些表达式在运行时会被求值并转换为字符串,
这种特性使得
F-String
在编写
Python
代码时能够更简洁、更直观地处理字符串。

本文总结了
5个
实用的
F-String
技巧,相信一定能让你的代码输出更加的美观,清晰。

1. 大数字


Python
的数据分析的项目中,经常接触到很大的数字,直接打印出大数字的话,很难看出它究竟有多少位。

n = 100000000
print(n)
# 100000000


F-String
来格式化,可以将大数字用千分位的形式显示出来。

# 千分位的分隔符用 _ 或者 , 都可以

print(f"{n:_}")
# 100_000_000

print(f"{n:,}")
# 100,000,000

2. 浮点数

对于浮点数,除了可以用千分位分隔之外,还可以指定显示几位小数。

n = 1234.5678
print(f"{n:.2f}")
# 1234.57

print(f"{n:.0f}")
# 1235

print(f"{n:,.3f}")
# 1,234.568

print(f"{n:_.3f}")
# 1_234.568

显示前会自动做四舍五入的计算。

3. 对齐

对齐不仅是让输出更加美观,更重要的是清晰,能够让我们更容易找到输出中的关键的信息。

比如,下面对齐输出
name

value

name = "var"
value = 100

# >20 表示右对齐,并且占用20个字符的空间
print(f"{name:>20}: {value}")

# <20 表示左对齐,并且占用20个字符的空间
print(f"{name:<20}: {value}")

# ^20 表示居中对齐,并且占用20个字符的空间
print(f"{name:^20}: {value}")
# 运行结果
                 var: 100
var                 : 100
        var         : 10

占用的
20
个字符的空间,包括
var
这个字符串,也就是说,占用的空间包含
var

17
个空格。

F-String
格式化输出时,默认是用空格来占位的,我们也可以定义自己的占位符。

# 下面的例子分别用 _ # | 来占位
print(f"{name:_>20}: {value}")
print(f"{name:#<20}: {value}")
print(f"{name:|^20}: {value}")
# 运行结果
_________________var: 100
var#################: 100
||||||||var|||||||||: 100

4. 日期

日期的格式化也是很常用的,python的日期格式化字符都可以在
F-String
中使用。

from datetime import datetime

now = datetime.now()
print(f"{now:%Y-%m-%d (%H:%M:%S)}")
# 2024-03-12 (00:17:05)

print(f"{now:%c}")
# Tue Mar 12 00:17:05 2024

print(f"{now:%I%p}")
# 12AM

5. 变量描述

最后这个功能很有用,但估计没多少人知道。

所谓
变量描述
,也就是说在输出变量的时候,不仅输出变量的值,也输出变量的名字。
比如,一般我们输出变量是这样:

a = 10
b = 20
s = "hello"

print(f"{a + b}")
# 30

print(f"{s}")
# hello

这样输出之后,我们不知道
30

hello
是哪个变量的值。
所以,我们一般会像下面这样输出:

print(f"a + b = {a + b}")
# a + b = 30

print(f"s = {s}")
# s = hello

F-String
有个技巧,可以避免自己手动去输入变量的名字(比如上面的
a + b =

s =
)。

print(f"{a + b = }")
# a + b = 30

print(f"{s = }")
# s = hello

这样不仅简化的写代码,而且变量名称发生变化的时候,也可以减少修改的地方。

本文分享自华为云社区《
CWE 4.14 与 ISA/IEC 62443
》,作者:Uncle_Tom。

1. 序言

随着 5G 的应用,物联的网发展,越来越多的自动化控制系统、云服务在工业控制系统被广泛使用。为了实现生产自动化,很多企业都引入了由 PLC(可编程逻辑控制器)控制的自动化生产设备和相关的自动化生产系统。用来连接各个自动化生产设备和生产系统的生产网络一般被称为 OT(Operation Technology)网络。而这些网络互联的普及与融合造成了 OT 环境系统安全受到威胁。再加上近来不断升温的政治冲突、恐怖主义与经济犯罪,这些都是引发面向产业关键基础设施进行攻击的动机。

2. CWE 4.14

在 28 年才能一遇的龙年 2 月 29 日,CWE 发布了新的一个版本 4.14。在这个版本发布的公告里,用了“其中包含了许多激动人心的更新(includes a number of exciting updates)”。这些更新主要包括:

  • 有 4 个与硬件微架构相关的弱点;
  • 1 个新视图:工业自动化和控制系统的
    CWE-1424:ISA/IEC 62443 要求解决的弱点视图
    ;
  • 对观察和示范示例的更新, 其中包括来自HACK@DAC安全挑战竞赛的 10 个新示范示例。

2.1. 瞬态执行漏洞

新增的 4 个与硬件微架构相关的弱点:

瞬态执行攻击,也称为推测执行攻击,在过去几年中引起了人们的极大兴趣,因为它们会导致关键数据泄露。自2018年1月首次披露Spectre和Meltdown攻击以来,已经针对不同的处理器展示了许多新的瞬态执行攻击类型。

瞬态执行攻击由两个主要组成部分组成:瞬态执行本身和用于实际过滤信息的隐蔽通道。

瞬态执行是现代处理器的基本特征的结果,这些特征旨在提高性能和效率,而隐蔽通道是由于微架构组件的时间和空间共享而产生的意外信息泄漏通道。

在过去的几十年里,计算机架构师一直在努力提高计算系统的性能。在各种处理器微架构中引入了不同的优化来提高性能,包括流水线、无序执行和分支预测。有些优化需要对执行的指令,这导致某些指令的瞬时执行。在指令集体系结构(ISA)级别,当今的处理器运行正常并返回正确的结果。然而,在当今的大多数处理器中,复杂的底层微体系结构状态在瞬态执行过程中会被修改,即使ISA级别没有泄漏,数据也可能从这些微体系结构中泄漏。

例如,在等待条件分支被解析的同时,分支预测用于预测该分支是否将被采用,并且处理器在分支的结果已知之前开始沿着预测的控制流路径推测执行。指令的这种推测性执行导致处理器的微体系结构状态被修改,即使指令是沿着错误推测的控制流路径执行的。沿着错误推测的路径执行指令被称为瞬态执行(因为指令是瞬态执行的)。理想情况下,如果存在错误推测,应该会消失而没有副作用。当检测到错误推测时,应该清除体系结构和微体系结构的副作用, 但在当今的大多数处理器中都没有这样做,这导致了瞬态执行攻击。

瞬态执行与隐蔽通道相结合会导致瞬态执行攻击,从而危及系统的机密性。如图所示,在这种攻击过程中,机密(也称为敏感数据)在瞬态执行期间是可用的——这将瞬态执行攻击与传统的隐蔽通道攻击区分开来,在传统的隐蔽信道攻击中,数据被认为总是对发送方可用,而不仅仅是在瞬态执行过程中。在瞬态执行期间访问秘密数据后并将其编码到隐蔽通道中,然后攻击者可以从隐蔽通道中提取(即解码)机密数据。

CVE 信息数据库中,与英特尔产品信息的漏洞:

  • 在 2018 年 14 个 CVE ID 中,瞬态执行攻击被分配了 9 个;
  • 在 2019 年 9 个 CVE ID 中,瞬态执行攻击被分配了 4 个。
  • 这些攻击还会影响其他供应商,如 AMD 或 Arm。

2.2. CWE-1424:ISA/IEC 62443 要求解决的弱点视图

新增的
CWE-1424:ISA/IEC 62443 要求解决的弱点视图
,给出了 ISA/IEC 62443 标准中涉及的弱点,也是我们需要在开发和检测中需要关注的问题。关于 ISA/IEC 62443 后面我们再做进一步的介绍。这里先对视图所涵盖的弱点做个介绍。

为了更好的分析这个视图,我们按照
CWE-1400:软件安全保障综合分类
中的分类,将所属的 CWE 进行了分类,如下图。有关缺陷的分类可以参考
《应用软件的缺陷分类》
中关于
CWE-1400:软件安全保障综合分类
的介绍。

CWE-1424:ISA/IEC 62443 要求解决的弱点视图
,并不是通过在 CWE 中 Relationships 节点下增加关联关系来定义这个视图的。而是利用 xml 的 xpath 来给出了这个视图下的关联 CWE 节点。这个 xpath 的表达式是定义在 CWE 节点的 Filter 节点,
CWE-1424:ISA/IEC 62443 要求解决的弱点视图
的节点定义如下。

    <View ID="1424" Name="Weaknesses Addressed by ISA/IEC 62443 Requirements" Type="Implicit" Status="Draft">
         <Objective>This view (slice) covers weaknesses that are addressed by following requirements in the ISA/IEC 62443 series of standards for industrial automation and control systems (IACS). Members of the CWE ICS/OT SIG analyzed a set of CWEs and mapped them to specific requirements covered by ISA/IEC 62443. These mappings are recorded in Taxonomy_Mapping elements.</Objective>
         <Filter>/Weakness_Catalog/Weaknesses/Weakness[./Taxonomy_Mappings/Taxonomy_Mapping/@Taxonomy_Name='ISA/IEC 62443']</Filter>
         <Mapping_Notes>
            <Usage>Prohibited</Usage>
            <Rationale>This entry is a View. Views are not weaknesses and therefore inappropriate to describe the root causes of vulnerabilities.</Rationale>
            <Comments>Use this View or other Views to search and navigate for the appropriate weakness.</Comments>
            <Reasons>
               <Reason Type="View"/>
            </Reasons>
         </Mapping_Notes>
     <Notes>
       <Note Type="Maintenance">The Taxonomy_Mappings to ISA/IEC 62443 were added between CWE 4.9 and CWE 4.14, but some mappings are still under review and might change in future CWE versions. These draft mappings were performed by members of the "Mapping CWE to 62443" subgroup of the CWE ICS/OT Special Interest Group (SIG).</Note>
     </Notes>
    </View>

从 Filter 的值

<Filter>/Weakness_Catalog/Weaknesses/Weakness[./Taxonomy_Mappings/Taxonomy_Mapping/@Taxonomy_Name='ISA/IEC 62443']</Filter>

可以看到这些关联关系是维护在 CWE 节点的 Taxonomy_Mapping 且属性Taxonomy_Name 值为 ISA/IEC 62443。以
CWE-1242:包含未记录的特征或鸡位(Inclusion of Undocumented Features or Chicken Bits)
为例。

2.3. 有趣的芯片 “chicken bit”

在访问控制的分类里,
CWE-1242:包含未记录的特征或鸡位(Inclusion of Undocumented Features or Chicken Bits)
的名字引起了我的好奇心。什么"chicken bit"?

查了下,原来它是芯片的一种配置位,可用于在芯片内部某些功能块流片后控制禁用或启用。有时,设计人员对某些新功能并不完全有信心,或者它们可能尚未在所有角落得到充分验证。因此,为了降低流片后的错误风险,设计中保留了一个配置位,可以切换以禁用新功能,并恢复到遗留功能,可以说是一种补救措施。

通常这些位是一次性可编程(one-time-programmable(OTP)),最终用户无法访问它们。这些位在芯片测试的生产过程中被编程,然后被锁定以防止进一步修改。

鸡,以其受惊和不确定的天性而闻名。因此这个位得到了一个有趣的名字“鸡位”(chicken bit), 工程师可以在流片后,处理意外的观察结果,也是一种自救的逃生通道。

这让我想起了 chicken 在英文的俚语中指“胆小鬼,懦夫”。以及最常用的 cheicken out,这可不是什么 “鸡走了”,“小鸡快跑”,而是指 “因胆怯而退缩”。一般都表达那种 “本来计划好的,却临阵脱逃”。除了chicken out,还有许多其他动物的 out。

duck out

指巧妙地逃脱应该做的事,逃避责任,逃税,逃婚 …。

比如 莎士比亚的《罗密欧与朱丽叶》中的台词:为了逃脱强制婚姻,朱丽叶饮下毒药(To duck out of the forced marriage, Juliet swallowed poison)。

fish out

意为“捞出,掏出”。这样理解你可能会觉得非常地形象:鱼都是藏在水里的,要出来,就得靠“捞”,从包里“掏出”某物也是类似的动作。比如:他从口袋里摸出一枚硬币(He fished out a coin from his pocket)。

3. ISA/IEC 62443 工业自动化和控制系统(IACS) 系列标准

3.1. 起源

2005年,国际自动化学会(International Society of Automation(ISA))成立了ISA99 -工业自动化和控制系统安全委员会,负责制定工业自动化和控制系统 (Industry Automation & Control System)的网络安全标准;

2007年,ISA99与国际电工组织委员会(International Electrotechnical Commission(IEC))成立联合工作组,共同制定并继续开发ISA/IEC 62443系列标准和技术报告(Technical Report(TR)),并陆续发布了IEC 62443系列标准;

2018年底,联合国欧洲经济委员会(UNECE)在其年会上确认,将把广泛使用的ISA/IEC 62443系列纳入其即将推出的网络安全共同监管框架(CRF)。CRF将作为联合国在欧洲的官方政策立场声明,为欧盟贸易市场内的网络安全实践建立一个共同的立法基础。IEC 62443 逐渐成为国际通行的工控网络安全标准,并在不同工业行业和领域实现了应用。

3.2. 范围和目的

范围

ISA/IEC 62443 系列的范围是工业自动化和控制系统( Security of Industrial Automation and Control Systems (IACS)) 的安全。

IACS 定义为:

收集工业过程运行中涉及的人员、硬件、软件和政策,这些人员、硬件、软件和政策可能影响或影响其安全、可靠和可靠操作。

目的

ISA/IEC 62443 系列标准定义了实施和维护电子安全工业自动化和控制系统(IACS)的要求和流程。这些标准设定了安全最佳实践,并提供了一种评估安全性能水平的方法。对应对网络安全挑战给出了一个整体的方法,弥合了运营和信息技术之间以及流程安全和网络安全之间的差距。

ISA/IEC 62443 系列解决了工业自动化和控制系统 (IACS) 整个生命周期的安全性问题,适用于所有自动化和控制系统,而不仅仅是工业。

ISA/IEC 62443 标准提供的指南包括:

  • 定义所有负责控制系统网络安全的利益相关者可以使用的通用术语、概念和模型;
  • 帮助资产所有者确定满足其独特业务和风险需求所需的安全级别;
  • 为产品开发人员建立一套通用的要求和网络安全生命周期方法,包括认证产品和供应商开发流程的机制;
  • 定义对保护控制系统至关重要的风险评估流程。

3.3. 标准的结构

IEC 62443系列标准的文件包括四个标准系列:通用、政策和程序、系统、组件。

通用

该类别(IEC 62443-1-x)的部分包括四份文件。描述了工业自动化和控制系统(IACS)网络安全的通用方面的内容,如术语、概念、模型、缩略语、系统符合性度量及工控设备的安全生命周期。

  • 1-1 部分 - 术语、概念和模型:介绍了整个系列使用的概念和模型。
  • 1-2 部分 - 术语表和定义:是整个系列中使用的术语和缩写的列表。
  • 1-3 部分 - 系统安全一致性指标:描述了一种开发从标准中的流程和技术要求派生的定量指标的方法。
  • 1-4 部分 - IACS 安全生命周期和用例:提供了对 IACS 安全性底层生命周期的更详细描述,以及说明各种应用程序的几个用例。

政策和程序

该类别(IEC 62443-2-x)的部分包括五份文件。规定了IACS安全管理系统的要求、安全管理系统实施指南、IACS环境中补丁更新管理以及对资产所有者和服务提供商的安全程序(SP)要求。

  • 2-1 部分 - 建立 IACS 安全性程序:描述了定义和实施有效的IACS网络安全管理系统所需的内容。
  • 2-2 部分 - IACS 安全计划评级:提供了一种根据 ISA/IEC 62443 系列标准中的要求评估操作 IACS 提供的保护级别的方法。
  • 2-3 部分 - IACS 环境中的补丁管理:提供有关 IACS 补丁管理的指导。
  • 2-4 部分 - IACS 服务提供商的安全计划要求:指定了对 IACS 服务提供商(如系统集成商或维护提供商)的要求。
  • 2-5 部分:IACS 资产所有者实施指南:提供制定有效的 IACS 网络安全计划指导。

系统

该类别(IEC 62443-3-x)的部分包括三份文件。提供了系统安全要求、安全等级和安全风险管理以及系统设计的基本指导和原则。包括将整体工业自动化和控制系统设计分配到各个区域(Zone)和管道 (Conduit) 的方法,以及安全等级(Security Level)的定义和要求。

  • 3-1 部分 - IACS 安全技术: 描述了各种安全技术在 IACS 环境中的应用。
  • 3-2 部分 - 安全风险评估系统设计: 涉及IACS的网络安全风险评估和系统设计。该标准的输出是区域和管道模型以及相关的风险评估和目标安全级别。这些都记录在网络安全要求规范中。该标准主要针对资产所有者和系统集成商。
  • 3-3 部分 - 系统安全要求和安全级别: 介绍了基于安全级别的 IACS 系统的要求。主要受众包括控制系统供应商、系统集成商和资产所有者。

组件

该类别(IEC 62443-4-x)的部分包括两份文件。

  • 4-1 部分 - 产品安全开发生命周期要求: 描述产品开发人员的安全开发生命周期的要求。主要受众包括控制系统和组件产品的供应商。
  • 4-2 部分 - IACS 组件的技术安全要求: 描述基于安全级别的 IACS 组件的要求。组件包括嵌入式设备、主机设备、网络设备和软件应用程序。

3.4. ISA/IEC 62443 对应到国家标准

中国也是 ISA/IEC 62443 标准制定的参与者。同时也将 ISA/IEC 62443 标准的大部分映射到国标(GB)中。

3.5. ISA/IEC 62443 与 CWE 的映射

从前面的介绍,可以看到 ISA/IEC 62443 的系列规范是工业自动化和控制系统中保障安全的非常重要的规范。CWE 的"62443 与 CWE 的映射"工作组,这次只发布了研究中的部分成果。

目前对应的 CWE 弱点有 39 个。

按照
CWE-1400:软件安全保障综合分类
中的分类汇总如下:

按照 ISA/IEC 62443 系列规范的对应关系如下:

4. 结论

  • CWE 4.14 增加了芯片的瞬态执行漏洞;
  • CWE 4.14 增加了CWE-1424:ISA/IEC 62443 要求解决的弱点视图;
  • CWE-1424:ISA/IEC 62443 要求解决的弱点视图的作用:进一步增强了对规范遵从的可度量性;对工业软件的安全性给出了明确的缺陷检查指导,提升了安全的防护能力。

5. 参考

点击关注,第一时间了解华为云新鲜技术~

前言

在现代软件开发中,重复性的增删改查逻辑代码的编写往往非常耗时且容易出错。为了提高开发效率,减少手动维护的成本,代码生成器就成为了一个非常重要的工具,本文小编就将为大家介绍一下如何利用一个开源项目快速生成数据接口。

实现方式

环境准备

技术栈:Java,Spring-Boot,MyBatisPlus,Maven(可选)

在开始前,请先确保自己有Java开发环境,并下载好这两个项目。

附上这两个文件的Gitee地址:
https://gitee.com/GrapeCity/gc-excel_-data_interface

其中,my-api是我们自己的项目,最终的接口服务也是从这个项目中启动的,我们可以叫它api服务。mybatis-plus-generator-ui是前面提到的开源项目,我们同样给它起个名字,叫接口生成器,它的作用就是帮我们在api服务中生成代码文件。

项目目录结构

my-api工程

TestApplication.java是Springboot的入口文件,除了必要的引入外,还规定了要扫描的Mapper。

Application.yml是配置文件,主要是配置了数据库的连接串,读者需要将其改为自己的数据库连接。

mybatis-plus-generator-ui工程

TestApplication.java是生成器的启动文件,直接读取数据库中的表。

Controller.java.btl是生成controller的模板文件,开源项目的源码中未开启跨域,这里为了本地测试,我开启了跨域,读者后续有其他需求,也可以在codetpls目录下找到对应的模板文件,按需增减。

启动项目

然后就可以启动生成器了,在TestApplication下点击运行即可,启动成功浏览器后打开localhost:端口号,即可看到ui界面:

生成代码

看到页面上方的“代码生成”按钮了吗,先别急着点它,咱们需要先配置一下生成的文件的包名是什么,点击上方的输出配置:

读者需要将这里的包名改为api服务的包名,这里我已经全都修改好了:

然后返回到上一个页面,选择你要生成接口的数据库表,点击“代码生成”,勾选所有“本次需要生成的文件”的复选框,修改“目标项目根目录”为
api服务
的根目录。其他配置可以不填,点击开始生成,api服务下就已经生成好所有勾选的文件了:

测试

到这里就实现了在api服务中自动生成代码,接下来将这个服务启动:

然后我们将生成的接口放到Postman里面去测试。

可以看到返回的数据和数据库一致:

总结

以上就是使用代码生成器之如何快速生成后端接口的全过程,希望可以对各位读者带来帮助。

扩展链接:

轻松构建低代码工作流程:简化繁琐任务的利器

优化预算管理流程:Web端实现预算编制的利器

如何在.NET电子表格应用程序中创建流程图