STM32 —— UC/OS III 任务详解

STM32 —— UC/OS III 任务详解

Main 函数框架

/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "gpio.h"
#include "usart.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <includes.h>
#include "stm32f1xx_hal.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* 任务优先级 */
#define START_TASK_PRIO		3

/* 任务堆栈大小	*/
#define START_STK_SIZE 		64

/* 任务栈 */	
CPU_STK START_TASK_STK[START_STK_SIZE];
/* 任务控制块 */
OS_TCB StartTaskTCB;
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
int fputc(int ch,FILE *f){
	HAL_UART_Transmit(&huart1,(uint8_t *)&ch,1,0xffff);
	return ch;
}
/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* 任务函数定义 */
void start_task(void *p_arg);
static  void  AppTaskCreate(void);
static  void  AppObjCreate(void);
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /**Initializes the CPU, AHB and APB busses clocks 
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /**Initializes the CPU, AHB and APB busses clocks 
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
	OS_ERR  err;
	OSInit(&err);
  HAL_Init();
	SystemClock_Config();
	//MX_GPIO_Init(); 这个在BSP的初始化里也会初始化
  MX_USART1_UART_Init();	
	/* 创建任务 */
	OSTaskCreate((OS_TCB     *)&StartTaskTCB,                /* Create the start task                                */
				 (CPU_CHAR   *)"start task",
				 (OS_TASK_PTR ) start_task,
				 (void       *) 0,
				 (OS_PRIO     ) START_TASK_PRIO,
				 (CPU_STK    *)&START_TASK_STK[0],
				 (CPU_STK_SIZE) START_STK_SIZE/10,
				 (CPU_STK_SIZE) START_STK_SIZE,
				 (OS_MSG_QTY  ) 0,
				 (OS_TICK     ) 0,
				 (void       *) 0,
				 (OS_OPT      )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
				 (OS_ERR     *)&err);
	/* 启动多任务系统,控制权交给uC/OS-III */
	OSStart(&err);            /* Start multitasking (i.e. give control to uC/OS-III). */
               
}


void start_task(void *p_arg)
{
	OS_ERR err;
	CPU_SR_ALLOC();
	p_arg = p_arg;
	
	/* YangJie add 2021.05.20*/
  BSP_Init();                                                   /* Initialize BSP functions */
  //CPU_Init();
  //Mem_Init();                                                 /* Initialize Memory Management Module */

#if OS_CFG_STAT_TASK_EN > 0u
   OSStatTaskCPUUsageInit(&err);  		//统计任务                
#endif
	
#ifdef CPU_CFG_INT_DIS_MEAS_EN			//如果使能了测量中断关闭时间
    CPU_IntDisMeasMaxCurReset();	
#endif

#if	OS_CFG_SCHED_ROUND_ROBIN_EN  		//当使用时间片轮转的时候
	 //使能时间片轮转调度功能,时间片长度为1个系统时钟节拍,既1*5=5ms
	OSSchedRoundRobinCfg(DEF_ENABLED,1,&err);  
#endif		
	
	OS_CRITICAL_ENTER();	//进入临界区
				 
	OS_TaskSuspend((OS_TCB*)&StartTaskTCB,&err);		//挂起开始任务			 
	OS_CRITICAL_EXIT();	//进入临界区
}
/**
  * 函数功能: 启动任务函数体。
  * 输入参数: p_arg 是在创建该任务时传递的形参
  * 返 回 值: 无
  * 说    明:无
  */

/* USER CODE BEGIN 4 */
/**
  * 函数功能: 创建应用任务
  * 输入参数: p_arg 是在创建该任务时传递的形参
  * 返 回 值: 无
  * 说    明:无
  */
static  void  AppTaskCreate (void)
{
  
}


/**
  * 函数功能: uCOSIII内核对象创建
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明:无
  */
static  void  AppObjCreate (void)
{

}

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */

  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{ 
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

UC/OSIII 的任务简介v

在 UC/OSIII 中任务就是程序实体,UC/OSIII 能够管理和调度这些小任务(程序)。UC/OSIII 中的任务由三部分组成:任务堆栈、任务控制块和任务函数

任务堆栈:上下文切换的时候用来保存任务的工作环境,就是STM32的内部寄存器值;
任务控制块:任务控制块用来记录任务的各个属性;
任务函数:由用户编写的任务处理代码,是实实在在干活的,一般写法如下:

   void XXX_task(void *p_arg)
   {
       while(1)
       {
            ...    //任务处理过程
        }
    }

可以看出用任务函数通常是一个无限循环,当然了,也可以是一个只执行一次的任务。任务的参数是一个 void 类型的,这么做的目的是可以可以传递不同类型的数据甚至是函数

可以看出任务函数其实就是一个 C 语言的函数,但是在使用 UC/OIII 的情况下这个函数不能有用户自行调用,任务函数何时执行执行,何时停止完全有操作系统来控制

UCOSIII的系统任务

UCOSIII默认有5个系统任务:

空闲任务:UC/OSIII 创建的第一个任务,UC/OSIII 必须创建的任务,此任务有 UC/OSIII 自动创建,不需要用户手动创建

时钟节拍任务:此任务也是必须创建的任务

统计任务:可选任务,用来统计 CPU 使用率和各个任务的堆栈使用量。此任务是可选任务,由宏 OS_CFG_STAT_TASK_EN 控制是否使用此任务

定时任务:用来向用户提供定时服务,也是可选任务,由宏 OS_CFG_TMR_EN 控制是否使用此任务

中断服务管理任务:可选任务,由宏 OS_CFG_ISR_POST_DEFERRED_EN 控制是否使用此任务

前两个系统任务时必须创建的任务,而后三者不是。控制后三者任务的宏都是在文件 OS_CFG.h 中

UCOSIII的任务状态

从用户的角度看,UC/OSIII 的任务一共有 5 种状态:

  1. 休眠态:任务已经在CPU的flash中了,但是还不受UCOSIII管理

  2. 就绪态:系统为任务分配了任务控制块,并且任务已经在就绪表中登记,这时这个任务就具有了运行的条件,此时任务的状态就是就绪态

  3. 运行态:任务获得CPU的使用权,正在运行

  4. 等待态:正在运行的任务需要等待一段时间,或者等待某个事件,这个任务就进入了等待态,此时系统就会把CPU使用权转交给别的任务

  5. 中断服务态:当发送中断,当前正在运行的任务会被挂起,CPU转而去执行中断服务函数,此时任务的任务状态叫做中断服务态

这5种状态之间相互转化的关系如下图:

UC/OSIII 的任务详解

前面说到 UC/OSIII 中的任务由三部分组成:任务堆栈、任务控制块和任务函数。下面就对这三个部分进行分析:

任务堆栈

任务堆栈是任务的重要部分,堆栈是在 RAM 中按照“先进先出(FIFO)”的原则组织的一块连续的存储空间。为了满足任务切换和响应中断时保存 CPU 寄存器中的内容及任务调用其它函数时的需要,每个任务都应该有自己的堆栈

任务堆栈创建

#define START_STK_SIZE 		512	//堆栈大小,这里需要根据开发板定义不同大小
CPU_STK START_TASK_STK[START_STK_SIZE];	//定义一个数组来作为任务堆栈

任务堆栈的大小是多少呢?

CPU_STK 为 CPU_INT32U 类型,也就是 unsigned int 类型,为 4 字节的,那么任务堆栈 START_TASK_STK 的大小就为:512 * 4 = 2048 字节

任务堆栈初始化

任务如何才能切换回上一个任务并且还能接着从上次被中断的地方开始运行?

恢复现场即可,现场就是 CPU 的内部各个寄存器。因此在创建一个新任务时,必须把系统启动这个任务时所需的 CPU 各个寄存器初始值事先存放在任务堆栈中。这样当任务获得 CPU 使用权时,就把任务堆栈的内容复制到 CPU 的各个寄存器,从而可以任务顺利地启动并运行

把任务初始数据存放到任务堆栈的工作就叫做任务堆栈的初始化,UC/OSIII 提供了完成堆栈初始化的函数:OSTaskStkInit()。

CPU_STK  *OSTaskStkInit (OS_TASK_PTR    p_task,
                         void          *p_arg,
                         CPU_STK       *p_stk_base,
                         CPU_STK       *p_stk_limit,
                         CPU_STK_SIZE   stk_size,
                         OS_OPT         opt)
{
    ...                        //函数内容
    return (p_stk);
}

当然,用户一般不会直接操作堆栈初始化函数,任务堆栈初始化函数由任务创建函数 OSTaskCreate() 调用。不同的CPU对于的寄存器和对堆栈的操作方式不同,因此在移植 UC/OSIII 的时候需要用户根据各自所选的 CPU 来编写任务堆栈初始化函数

void  OSTaskCreate (OS_TCB        *p_tcb,		//任务控制块
                    CPU_CHAR      *p_name,		//任务名字
                    OS_TASK_PTR    p_task,		//任务函数
                    void          *p_arg,		//传递给任务函数的参数
                    OS_PRIO        prio,		//任务优先级
                    CPU_STK       *p_stk_base,		//------任务堆栈基地址
                    CPU_STK_SIZE   stk_limit,		//------任务堆栈深度限位
                    CPU_STK_SIZE   stk_size,		//------任务堆栈大小
                    OS_MSG_QTY     q_size,
                    OS_TICK        time_quanta,
                    void          *p_ext,		//用户补充的存储区
                    OS_OPT         opt,
                    OS_ERR        *p_err)		//存放该函数错误时的返回值
{
    ...                    //函数内容
}

函数 OSTaskCreate() 中的参数 p_stk_base (任务堆栈基地址)如何确定?

根据堆栈的增长方式,堆栈有两种增长方式:

  1. 向上增长:堆栈的增长方向从低地址向高地址增长

  2. 向下增长:堆栈的增长方向从高地址向低地址增长

函数 OSTaskCreate() 中的参数 p_stk_base 是任务堆栈基地址,那么如果 CPU 的堆栈是向上增长的话那么基地址就 &START_TASK_STK[0];如果 CPU 堆栈是向下增长的话基地址就是 &START_TASK_STK[START_STK_SIZE-1] 。STM32 的堆栈是向下增长的

任务控制块

任务控制块是用来记录与任务相关的信息的数据结构,每个任务都要有自己的任务控制块。我们使用 OSTaskCreate() 函数来创建任务的时候就会给任务分配一个任务控制块。任务控制块由用户自行创建,如下代码为创建一个任务控制块:

   OS_TCB StartTaskTCB;  //创建一个任务控制块

OS_TCB 为一个结构体,描述了任务控制块,任务控制块中的成员变量用户不能直接访问,更不可能改变他们

OS_TCB 为一个结构体,其中有些成员采用了条件编译的方式来确定

struct os_tcb {
    CPU_STK             *StkPtr;                            /* 指向当前任务堆栈的栈顶 */
 
    void                *ExtPtr;                            /* 指向用户可定义的数据区 */
 
    CPU_STK             *StkLimitPtr;                       /* 可指向任务堆栈中的某个位置 */
 
    OS_TCB              *NextPtr;                           /* Pointer to next     TCB in the TCB list  */
    OS_TCB              *PrevPtr;                           /* Pointer to previous TCB in the TCB list   */
 
    OS_TCB              *TickNextPtr;
    OS_TCB              *TickPrevPtr;
 
    OS_TICK_SPOKE       *TickSpokePtr;                      /* Pointer to tick spoke if task is in the tick list    */
 
    CPU_CHAR            *NamePtr;                           /* Pointer to task name */
 
    CPU_STK             *StkBasePtr;                        /* Pointer to base address of stack  */
 
#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)
    OS_TLS               TLS_Tbl[OS_CFG_TLS_TBL_SIZE];
#endif
 
    OS_TASK_PTR          TaskEntryAddr;                 /* Pointer to task entry point address  */
    void                *TaskEntryArg;                  /* Argument passed to task when it was created */
 
    OS_PEND_DATA        *PendDataTblPtr;                /* Pointer to list containing objects pended on */
    OS_STATE             PendOn;                        /* Indicates what task is pending on */
    OS_STATUS            PendStatus;                    /* Pend status */
 
    OS_STATE             TaskState;                     /* See OS_TASK_STATE_xxx */
    OS_PRIO              Prio;                          /* Task priority (0 == highest) */
    CPU_STK_SIZE         StkSize;                       /* Size of task stack (in number of stack elements) */
    OS_OPT               Opt;                           /* Task options as passed by OSTaskCreate()*/
 
    OS_OBJ_QTY           PendDataTblEntries;            /* Size of array of objects to pend on  */
 
    CPU_TS               TS;                            /* Timestamp */
 
    OS_SEM_CTR           SemCtr;                        /* Task specific semaphore counter */
 
                                                        /* DELAY / TIMEOUT   */
    OS_TICK              TickCtrPrev;                   /* Previous time when task was            ready   */
    OS_TICK              TickCtrMatch;                  /* Absolute time when task is going to be ready    */
    OS_TICK              TickRemain;                    /* Number of ticks remaining for a match  */
                                                        /* ... run-time by OS_StatTask()   */
    OS_TICK              TimeQuanta;
    OS_TICK              TimeQuantaCtr;
 
#if OS_MSG_EN > 0u
    void                *MsgPtr;                        /* Message received   */
    OS_MSG_SIZE          MsgSize;
#endif
 
#if OS_CFG_TASK_Q_EN > 0u
    OS_MSG_Q             MsgQ;                          /* Message queue associated with task   */
#if OS_CFG_TASK_PROFILE_EN > 0u
    CPU_TS               MsgQPendTime;                  /* Time it took for signal to be received     */
    CPU_TS               MsgQPendTimeMax;               /* Max amount of time it took for signal to be received */
#endif
#endif
 
#if OS_CFG_TASK_REG_TBL_SIZE > 0u
    OS_REG               RegTbl[OS_CFG_TASK_REG_TBL_SIZE];  /* Task specific registers   */
#endif
 
#if OS_CFG_FLAG_EN > 0u
    OS_FLAGS             FlagsPend;                     /* Event flag(s) to wait on      */
    OS_FLAGS             FlagsRdy;                      /* Event flags that made task ready to run    */
    OS_OPT               FlagsOpt;                      /* Options (See OS_OPT_FLAG_xxx)    */
#endif
 
#if OS_CFG_TASK_SUSPEND_EN > 0u
    OS_NESTING_CTR       SuspendCtr;                    /* Nesting counter for OSTaskSuspend()    */
#endif
 
#if OS_CFG_TASK_PROFILE_EN > 0u
    OS_CPU_USAGE         CPUUsage;                      /* CPU Usage of task (0.00-100.00%)   */
    OS_CPU_USAGE         CPUUsageMax;                   /* CPU Usage of task (0.00-100.00%) - Peak   */
    OS_CTX_SW_CTR        CtxSwCtr;                      /* Number of time the task was switched in  */
    CPU_TS               CyclesDelta;                   /* value of OS_TS_GET() - .CyclesStart    */
    CPU_TS               CyclesStart;                   /* Snapshot of cycle counter at start of task resumption  */
    OS_CYCLES            CyclesTotal;                   /* Total number of # of cycles the task has been running  */
    OS_CYCLES            CyclesTotalPrev;               /* Snapshot of previous # of cycles                       */
 
    CPU_TS               SemPendTime;                   /* Time it took for signal to be received    */
    CPU_TS               SemPendTimeMax;                /* Max amount of time it took for signal to be received   */
#endif
 
#if OS_CFG_STAT_TASK_STK_CHK_EN > 0u
    CPU_STK_SIZE         StkUsed;                       /* Number of stack elements used from the stack   */
    CPU_STK_SIZE         StkFree;                       /* Number of stack elements free on   the stack    */
#endif
 
#ifdef CPU_CFG_INT_DIS_MEAS_EN
    CPU_TS               IntDisTimeMax;                 /* Maximum interrupt disable time    */
#endif
#if OS_CFG_SCHED_LOCK_TIME_MEAS_EN > 0u
    CPU_TS               SchedLockTimeMax;              /* Maximum scheduler lock time      */
#endif
 
#if OS_CFG_DBG_EN > 0u
    OS_TCB              *DbgPrevPtr;
    OS_TCB              *DbgNextPtr;
    CPU_CHAR            *DbgNamePtr;
#endif
};

任务控制块初始化

US/OCIII 提供了用于任务控制块初始化的函数:OS_TaskInitTCB() 。但是,用户不需要自行初始化任务控制块。因为和任务堆栈初始化函数一样,函数 OSTaskCreate() 在创建任务的时候会对任务的任务控制块进行初始化。

UC/OSIII的任务就绪表

UC/OSIII 中任务优先级数由宏 OS_CFG_PRIO_MAX 来配置,UC/OSIII 中数值越小,优先级越高,最低可用优先级就是OS_CFG_PRIO_MAX-1 。默认 OS_CFG_PRIO_MAX 的值为 64。

UC/OSIII 中就绪表由2部分组成:

优先级位映射表 OSPrioTbl[] :用来记录哪个优先级下有任务就绪

就绪任务列表 OSRdyList[] :用来记录每一个优先级下所有就绪的任务

优先级位映射表 OSPrioTbl[]

CPU_DATA   OSPrioTbl[OS_PRIO_TBL_SIZE]; 

在 STM32 中 CPU_DATA 为 unsigned int ,有 4 个字节,32 位。因此表 OSPrioTbl 每个参数有 32 位,其中每个位对应一个优先级下是否有任务就绪

OS_PRIO_TBL_SIZE = ((OS_CFG_PRIO_MAX - 1u) / DEF_INT_CPU_NBR_BITS)+ 1)
DEF_INT_CPU_NBR_BITS = CPU_CFG_DATA_SIZE * DEF_OCTET_NBR_BITS

OS_CFG_PRIO_MAX 由用户自行定义,默认为 64 。而 CPU_CFG_DATA_SIZE = CPU_WORD_SIZE_32 = 4 ,DEF_OCTET_NBR_BITS = 8

所以,当系统有 64 个优先级的时候:OS_PRIO_TBL_SIZE = ((64-1)/(4*8)+1) = 2

对比图就很明确了,由于由 64 个优先级,所以用两个 32 位的数来存储。每一位相对应于一个优先级,该位为 1 表示该优先级有任务就绪,该位为 0 表示该优先级没有任务就绪。

如何找到已经就绪了的最高优先级的任务?

函数 OS_PrioGetHighest() 用于找到就绪了的最高优先级的任务

OS_PRIO  OS_PrioGetHighest (void)
{
    CPU_DATA  *p_tbl;                    
    OS_PRIO    prio;
 
    prio  = (OS_PRIO)0;
    p_tbl = &OSPrioTbl[0];                                //指向优先级位映射表的第一个32bit的数
    while (*p_tbl == (CPU_DATA)0) {                         /* 判断该32位的数为0       */
        prio += DEF_INT_CPU_NBR_BITS;                       /* 将32位的数拿出来               */
        p_tbl++;
    }
    prio += (OS_PRIO)CPU_CntLeadZeros(*p_tbl);              /* Find the position of the first bit set at the entry    */
    return (prio);
}

这段程序的思路:先判断每一个 32 位的数是否为 0 ,不为0的话,再通过函数 CPU_CntLeadZeros() 来计算前导零数量。这个函数用汇编来写的:

CPU_CntLeadZeros
        CLZ     R0, R0                          ; Count leading zeros
        BX      LR

就绪任务列表OSRdyList[]

通过上一步我们已经知道了哪个优先级的任务已经就绪了,但是 UC/OSIII 支持时间片轮转调度,同一个优先级下可以有多个任务,因此我们还需要在确定是优先级下的哪个任务就绪了

先看一下 os_rdy_list[] 的定义:

struct  os_rdy_list {
    OS_TCB           *HeadPtr	         //用于创建链表,指向链表头
    OS_TCB           *TailPtr;           //用于创建链表,指向链表尾
    OS_OBJ_QTY       NbrEntries;         //此优先级下的任务数量
};

UC/OSIII 支持时间片轮转调度,因此在一个优先级下会有多个任务,那么我们就要对这些任务做一个管理,这里使用 OSRdyList[] 数组管理这些任务。OSRdyList[] 数组中的每个元素对应一个优先级,比如 OSRdyList[0] 就用来管理优先级 0 下的所有任务。 OSRdyList[0] 为 OS_RDY_LIST 类型,从上面 OS_RDY_LIST 结构体可以看到成员变量: HeadPtr 和 TailPtr 分别指向 OS_TCB ,我们知道 OS_TCB 是可以用来构造链表的,因此同一个优先级下的所有任务是通过链表来管理的, HeadPtr 和 TailPtr 分别指向这个链表的头和尾, NbrEntries 用来记录此优先级下的任务数量,下图表示了优先级 4 现在有 3 个任务时候的就绪任务列表:

这里记住一句话:同一优先级下如果有多个任务的话最先运行的永远是 HeadPtr 所指向的任

参考资料

  1. 【UCOSIII】嵌入式实时操作系统UCOSIII及其任务
posted @ 2022-11-09 23:56  ppqppl  阅读(145)  评论(0编辑  收藏  举报