一、UCOSIII简介
1.1 什么是μC/OS-III?
µC/OS/III是一个实时操作系统,也就是 RTOS(Real Time Operating System)。操作系统最直观的特点就体现在,操作系统能够使得一个 CPU 核心“同时运行”多个任务,这个特性就被称为“多任务”。然而,实际上,一个 CPU 核心在某一时刻只能运行一个任务,而操作系统中任务调度器的责任就是决定在某一时刻 CPU 究竟要运行哪一个任务,任务调度器使得 CPU 在各个任务之间来回切换并处理任务,由于切换处理任务的速度非常快,因此给人造成了一种同一时刻有多个任务同时运行的错觉。
操作系统的分类方式可以由任务调度器的工作方式决定,比如有的操作系统给每个任务分配同样的运行时间,时间到了就切换到下一个任务,Unix 操作系统就是这样的。RTOS 的任务调度器被设计为可预测的,而这正是嵌入式实时操作系统所需要的。在实时环境中,要求操作系统必须实时地对某一个事件做出响应,因此任务调度器的行为必须是可预测的。像 µC/OS-III这种传统的 RTOS 类操作系统是由用户给每个任务分配一个任务优先级,任务调度器就可以根据此优先级来决定下一刻应该运行哪个任务。
1.2 µC/OS-III 的特点
- 抢占式多任务管理:µC/OS-III 是一个支持多任务抢占的内核,因此总是优先执行任务优先级高的任务。
- 时间片调度:µC/OS-III 允许系统中有多个相同任务优先级的任务,如果系统中处于就绪状态的任务中,优先级最高的任务有多个,那么 µC/OS-III 将以时间片的方式调度任务,即根据用户指定的时间(时间片)轮流调度这些任务。
- 极短的中断禁用时间:µC/OS-III 通过锁定任务调度器代替禁用中断来保护一些关键区域
(临界区),这确保了 µC/OS-III 能够快速地响应中断。 - 任务数量不限:µC/OS-III 理论上支持不受限制的任务数量,但实际上,系统中任务的最大数量受处理器内存空间的限制。
- 任务优先级数量不限:µC/OS-III 支持的任务优先级数量不受限制,但对于大多数应用场景而言,使用 32~256 个任务优先级就绰绰有余了。
- 内核对象数量不限:µC/OS-III 提供了多种内核对象,如任务、信号量、事件标志、消息队列、软件定时器和内存区等,并且在不考虑处理器内存限制的情况下,用户可以无限制的创建这些内核对象。
- 时间戳:µC/OS-III 提供了时间戳功能,用户可以非常方便地测量系统在运行过程中,处理器处理某些事件所消耗的时间,以方便用户对系统进行针对性的优化。
- 自定义钩子函数:µC/OS-III 提供了一些在内核执行操作之前、之后或过程中的钩子函数,这样可以方便用户扩展 µC/OS-III 的功能。
- 防死锁:µC/OS-III 允许任务在等待某些内核对象前,设置一个等待的最大超时时间,这样可以有效地防止死锁的发生。
- 软件定时器:在 µC/OS-III 中,用户可以创建任意数量的“单次”和“周期”软件定时器,并且每个软件定时器都可以有独立的超时回调函数。
- 任务内嵌信号量:µC/OS-III 提供了任务的内嵌信号量功能,这使得任务可以直接获取来自其他任务或中断的信号,而不需要任何的中间内核对象,大大地提高了系统的运行效率。
- 任务内嵌消息队列:µC/OS-III 提供了任务的内嵌消息队列,这使得任务可以直接接收来自其他任务或中断的消息,而不需要任何的中间内核对象,大大地提高了系统的运行效率。
1.3 µC/OS各版本比较
特征 | µC/OS | µC/OS-II | µC/OS-III |
---|---|---|---|
发布年份 | 1992 | 1998 | 2009 |
抢占式多任务 | 是 | 是 | 是 |
最大任务数 | 64 | 255 | 无限制 |
单个优先级的任务数 | 1 | 1 | 无限制 |
时间片调度 | 否 | 否 | 是 |
信号量 | 是 | 是 | 是 |
互斥信号量 | 否 | 是 | 是(支持嵌套) |
事件标志 | 否 | 是 | 是 |
消息邮箱 | 是 | 是 | 否(不需要) |
消息队列 | 是 | 是 | 是 |
固定大小的内存管理 | 否 | 是 | 是 |
直接向任务发送信号 | 否 | 否 | 是 |
无需调度的发送机制 | 否 | 否 | 是 |
直接向任务发送消息 | 否 | 否 | 是 |
软件定时器 | 否 | 是 | 是 |
任务挂起、恢复 | 否 | 是 | 是(支持嵌套) |
防死锁 | 是 | 是 | 是 |
可裁剪 | 是 | 是 | 是 |
代码量 | 3K~8K | 6K~26K | 6K~24K |
数据量 | 1K+ | 1K+ | 1K+ |
可固化 | 是 | 是 | 是 |
运行时配置 | 否 | 否 | 是 |
编译时配置 | 是 | 是 | 是 |
内核对象命名 | 否 | 是 | 是 |
任务寄存器 | 否 | 是 | 是 |
用户钩子函数 | 否 | 是 | 是 |
二、µC/OS-III移植
移植以亮灯实验为基础
2.1 添加 µC/OS-III 相关文件
- 新建UCOSIII文件夹
- 将官方源码uC-CPU、uC-LIB和uCOS-III三个文件夹复制进来,然后新建uCOS_CONFIG文件夹
- 将Micrium\Software\EvalBoards\ST\STM32F429II-SK\BSP文件夹里的bsp.c和bsp.h两个文件复制进uCOS_CONFIG文件夹。
- 将Micrium\Software\EvalBoards\ST\STM32F429II-SK\uCOS-III文件夹里的app_cfg.h、cpu_cfg.h、includes.h、lib_cfg.h、os_app_hooks.c、os_app_hooks.h、os_cfg.h、os_cfg_app.h这8个文件也复制进来
- 在keil工程里将上面文件添加进来,添加文件时keil选择RealView文件夹里的内容
- 添加头文件路径
2.2 修改bsp.c和bsp.h文件
bsp.h主要是添加头文件,bsp.c主要是删除官方一些初始化,修改后如下:
#define BSP_MODULE
#include <bsp.h>
#define BSP_REG_DEM_CR (*(CPU_REG32 *)0xE000EDFC) //DEMCR寄存器
#define BSP_REG_DWT_CR (*(CPU_REG32 *)0xE0001000) //DWT控制寄存器
#define BSP_REG_DWT_CYCCNT (*(CPU_REG32 *)0xE0001004) //DWT时钟计数寄存器
#define BSP_REG_DBGMCU_CR (*(CPU_REG32 *)0xE0042004)
//DEMCR寄存器的第24位,如果要使用DWT ETM ITM和TPIU的话DEMCR寄存器的第24位置1
#define BSP_BIT_DEM_CR_TRCENA DEF_BIT_24
//DWTCR寄存器的第0位,当为1的时候使能CYCCNT计数器,使用CYCCNT之前应当先初始化
#define BSP_BIT_DWT_CR_CYCCNTENA DEF_BIT_00
/**********************************************************/
CPU_INT32U BSP_CPU_ClkFreq (void)
{
RCC_ClocksTypeDef rcc_clocks;
RCC_GetClocksFreq(&rcc_clocks); //获取各个时钟频率
return ((CPU_INT32U)rcc_clocks.HCLK_Frequency); //返回HCLK时钟频率
}
#if (CPU_CFG_TS_TMR_EN == DEF_ENABLED)
void CPU_TS_TmrInit (void)
{
CPU_INT32U fclk_freq;
fclk_freq = BSP_CPU_ClkFreq();
BSP_REG_DEM_CR |= (CPU_INT32U)BSP_BIT_DEM_CR_TRCENA; //使用DWT /* Enable Cortex-M4's DWT CYCCNT reg. */
BSP_REG_DWT_CYCCNT = (CPU_INT32U)0u; //初始化CYCCNT寄存器
BSP_REG_DWT_CR |= (CPU_INT32U)BSP_BIT_DWT_CR_CYCCNTENA;//开启CYCCNT
CPU_TS_TmrFreqSet((CPU_TS_TMR_FREQ)fclk_freq);
}
#endif
#if (CPU_CFG_TS_TMR_EN == DEF_ENABLED)
CPU_TS_TMR CPU_TS_TmrRd (void)
{
CPU_TS_TMR ts_tmr_cnts;
ts_tmr_cnts = (CPU_TS_TMR)BSP_REG_DWT_CYCCNT;
return (ts_tmr_cnts);
}
#endif
#if (CPU_CFG_TS_32_EN == DEF_ENABLED)
CPU_INT64U CPU_TS32_to_uSec (CPU_TS32 ts_cnts)
{
CPU_INT64U ts_us;
CPU_INT64U fclk_freq;
fclk_freq = BSP_CPU_ClkFreq();
ts_us = ts_cnts / (fclk_freq / DEF_TIME_NBR_uS_PER_SEC);
return (ts_us);
}
#endif
#if (CPU_CFG_TS_64_EN == DEF_ENABLED)
CPU_INT64U CPU_TS64_to_uSec (CPU_TS64 ts_cnts)
{
CPU_INT64U ts_us;
CPU_INT64U fclk_freq;
fclk_freq = BSP_CPU_ClkFreq();
ts_us = ts_cnts / (fclk_freq / DEF_TIME_NBR_uS_PER_SEC);
return (ts_us);
}
#endif
2.3 µC/OS-III配置项
2.3.1 os_cfg.h配置
os_cfg.h 文件是系统的配置文件,主要是让用户自己配置一些系统默认的功能,用户可以选择某些或者全部的功能,比如消息队列、信号量、互斥量、事件标志位等,系统默认全部使用的,如果如果用户不需要的话,则可以直接关闭,在对应的宏定义中设置为 0即可,这样子就不会占用系统的 SRAM,以节省系统资源
2.3.2 cpu_cfg.h配置
cpu_cfg.h 文件主要是配置 CPU 相关的一些宏定义,我们可以选择对不同的 CPU 进行配置,当然,如果我们没有对 CPU 很熟悉的话,就直接忽略这个文件即可,在这里我们只需要注意关于时间戳与前导零指令相关的内容,我们使用的 CPU 是 STM32,是 32 位的CPU,那么时间戳我们使用 32 位的变量即可,而且 STM32 支持前导零指令,可以使用它让系统进行寻找最高优先级的任务更加快捷
2.3.3 os_cfg_app.h配置
os_cfg_app.h 是系统应用配置的头文件,简单来说就是系统默认的任务配置,如任务的优先级、堆栈大小等基本信息,但是有两个任务是必须开启的,一个就是空闲任务,另一个就是时钟节拍任务,这两个是让系统正常运行的最基本任务,而其他任务我们自己按需配置即可。
在UCOSⅢ中有五个系统任务:空闲任务、时钟节拍任务、统计任务、定时任务和中断服务管理任务,在系统初始化的时候至少要创建两个任务:空闲任务和时钟节拍任务。空闲任务的优先级应该为最低 OS CFG PRIQ MAX-1,如果使用中断服务管理任务的话那么中断服务管理任务的优先级应该为最高0。其他3个任务的优先级用户可以自行设置,本手册中我们将统计任务设置为OS_CFG_PRIO_MAX-2,既倒数第二个优先级;时钟节拍任务也需要一个高优先级,我们将优先级1分配给时钟节拍任务;将优先级2分配给定时器任务。所以优先级0、1、2、OS_CFG_PRIO_MAX-2和OS_CFG_PRIO_MAX-1这5个优先级用户应用程序是不能使用的!
2.4 修改系统文件
2.4.1 不保留delay
首先修改工程的启动文件“ startup_stm32f10x_hd.s”。其中将 PendSV_Handler 和 SysTick_Handler 分 别 改 为OS_CPU_PendSVHandler 和 OS_CPU_SysTickHandler共两处。同时我们还需要将stm32f10x_it.c 文件与其头文件中的 PendSV_Handler 和 SysTick_Handler 函数注释掉
2.4.2 保留delay
- 修改sys.h文件中宏定义SYSTEM_SUPPORT_OS,将其定义为1
- 修改os_cpu_a.s文件
;
;********************************************************************************************************
; uC/OS-III
; The Real-Time Kernel
;
;
; (c) Copyright 2009-2013; Micrium, Inc.; Weston, FL
; All rights reserved. Protected by international copyright laws.
;
; ARM Cortex-M4 Port
;
; File : OS_CPU_A.ASM
; Version : V3.03.02
; By : JJL
; BAN
;
; For : ARMv7 Cortex-M4
; Mode : Thumb-2 ISA
; Toolchain : RealView Development Suite
; RealView Microcontroller Development Kit (MDK)
; ARM Developer Suite (ADS)
; Keil uVision
;********************************************************************************************************
;
;********************************************************************************************************
; PUBLIC FUNCTIONS
;********************************************************************************************************
IMPORT OSRunning ; External references
IMPORT OSPrioCur
IMPORT OSPrioHighRdy
IMPORT OSTCBCurPtr
IMPORT OSTCBHighRdyPtr
IMPORT OSIntExit
IMPORT OSTaskSwHook
IMPORT OS_CPU_ExceptStkBase
EXPORT OSStartHighRdy ; Functions declared in this file
EXPORT OSCtxSw
EXPORT OSIntCtxSw
EXPORT PendSV_Handler
;********************************************************************************************************
; EQUATES
;********************************************************************************************************
NVIC_INT_CTRL EQU 0xE000ED04 ; Interrupt control state register.
NVIC_SYSPRI14 EQU 0xE000ED22 ; System priority register (priority 14).
NVIC_PENDSV_PRI EQU 0xFFFF ; PendSV priority value (lowest).
NVIC_PENDSVSET EQU 0x10000000 ; Value to trigger PendSV exception.
;********************************************************************************************************
; CODE GENERATION DIRECTIVES
;********************************************************************************************************
PRESERVE8
THUMB
AREA CODE, CODE, READONLY
;PRESERVE8
;AREA |.text|, CODE, READONLY
;THUMB
;********************************************************************************************************
; START MULTITASKING
; void OSStartHighRdy(void)
;
; Note(s) : 1) This function triggers a PendSV exception (essentially, causes a context switch) to cause
; the first task to start.
;
; 2) OSStartHighRdy() MUST:
; a) Setup PendSV exception priority to lowest;
; b) Set initial PSP to 0, to tell context switcher this is first run;
; c) Set the main stack to OS_CPU_ExceptStkBase
; d) Trigger PendSV exception;
; e) Enable interrupts (tasks will run with interrupts enabled).
;********************************************************************************************************
OSStartHighRdy
LDR R0, =NVIC_SYSPRI14 ; Set the PendSV exception priority
LDR R1, =NVIC_PENDSV_PRI
STRB R1, [R0]
MOVS R0, #0 ; Set the PSP to 0 for initial context switch call
MSR PSP, R0
LDR R0, =OS_CPU_ExceptStkBase ; Initialize the MSP to the OS_CPU_ExceptStkBase
LDR R1, [R0]
MSR MSP, R1
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
CPSIE I ; Enable interrupts at processor level
OSStartHang
B OSStartHang ; Should never get here
;********************************************************************************************************
; PERFORM A CONTEXT SWITCH (From task level) - OSCtxSw()
;
; Note(s) : 1) OSCtxSw() is called when OS wants to perform a task context switch. This function
; triggers the PendSV exception which is where the real work is done.
;********************************************************************************************************
OSCtxSw
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
;********************************************************************************************************
; PERFORM A CONTEXT SWITCH (From interrupt level) - OSIntCtxSw()
;
; Note(s) : 1) OSIntCtxSw() is called by OSIntExit() when it determines a context switch is needed as
; the result of an interrupt. This function simply triggers a PendSV exception which will
; be handled when there are no more interrupts active and interrupts are enabled.
;********************************************************************************************************
OSIntCtxSw
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
;********************************************************************************************************
; HANDLE PendSV EXCEPTION
; void OS_CPU_PendSVHandler(void)
;
; Note(s) : 1) PendSV is used to cause a context switch. This is a recommended method for performing
; context switches with Cortex-M3. This is because the Cortex-M3 auto-saves half of the
; processor context on any exception, and restores same on return from exception. So only
; saving of R4-R11 is required and fixing up the stack pointers. Using the PendSV exception
; this way means that context saving and restoring is identical whether it is initiated from
; a thread or occurs due to an interrupt or exception.
;
; 2) Pseudo-code is:
; a) Get the process SP, if 0 then skip (goto d) the saving part (first context switch);
; b) Save remaining regs r4-r11 on process stack;
; c) Save the process SP in its TCB, OSTCBCurPtr->OSTCBStkPtr = SP;
; d) Call OSTaskSwHook();
; e) Get current high priority, OSPrioCur = OSPrioHighRdy;
; f) Get current ready thread TCB, OSTCBCurPtr = OSTCBHighRdyPtr;
; g) Get new process SP from TCB, SP = OSTCBHighRdyPtr->OSTCBStkPtr;
; h) Restore R4-R11 from new process stack;
; i) Perform exception return which will restore remaining context.
;
; 3) On entry into PendSV handler:
; a) The following have been saved on the process stack (by processor):
; xPSR, PC, LR, R12, R0-R3
; b) Processor mode is switched to Handler mode (from Thread mode)
; c) Stack is Main stack (switched from Process stack)
; d) OSTCBCurPtr points to the OS_TCB of the task to suspend
; OSTCBHighRdyPtr points to the OS_TCB of the task to resume
;
; 4) Since PendSV is set to lowest priority in the system (by OSStartHighRdy() above), we
; know that it will only be run when no other exception or interrupt is active, and
; therefore safe to assume that context being switched out was using the process stack (PSP).
;********************************************************************************************************
PendSV_Handler
CPSID I ; Prevent interruption during context switch
MRS R0, PSP ; PSP is process stack pointer
CBZ R0, PendSVHandler_nosave ; Skip register save the first time
;Is the task using the FPU context? If so, push high vfp registers.
TST R14, #0X10
IT EQ
VSTMDBEQ R0!,{S16-S31}
SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack
STM R0, {R4-R11}
LDR R1, =OSTCBCurPtr ; OSTCBCurPtr->OSTCBStkPtr = SP;
LDR R1, [R1]
STR R0, [R1] ; R0 is SP of process being switched out
; At this point, entire context of process has been saved
PendSVHandler_nosave
PUSH {R14} ; Save LR exc_return value
LDR R0, =OSTaskSwHook ; OSTaskSwHook();
BLX R0
POP {R14}
LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy;
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R0, =OSTCBCurPtr ; OSTCBCurPtr = OSTCBHighRdyPtr;
LDR R1, =OSTCBHighRdyPtr
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;
LDM R0, {R4-R11} ; Restore r4-11 from new process stack
ADDS R0, R0, #0x20
;Is the task using the FPU context? If so, push high vfp registers.
TST R14, #0x10
IT EQ
VLDMIAEQ R0!, {S16-S31}
MSR PSP, R0 ; Load PSP with new process SP
ORR LR, LR, #0x04 ; Ensure exception return uses process stack
CPSIE I
BX LR ; Exception return will restore remaining context
END
三、任务管理
单任务系统的编程方式,即裸机的编程方式,这种编程方式的框架一般都是在 main()函数中使用一个大循环,在循环中顺序地调用相应地函数以处理相应的事务。而使用UCOS就是使用多任务系统的特性,多任务系统的运行示意图:
3.1 UCOSIII启动和初始化
- 首先调用OSInit()初始化UCOSIII
- 创建任务,一般我们在main()函数中只创建一个start_task任务,其他任务都在start_task任务中创建,在调用OSTaskCreate()函数创建任务的时候一定要调用OS_CRITICAL_ENTER()函数进入临界区,任务创建完以后调用OS_CRITICAL_EXIT()函数退出临界区。
- 最后调用OSStart()函数开启UCOSIII
3.2 µC/OS-III 任务状态
µC/OS-III 中任务存在五种状态,分别为休眠态、就绪态、运行态、挂起态和中断态
一个任务在某一时刻一定是处于这五种状态中的一种,µC/OS-III 中的五种任务状态之间的转换图如下:
3.3 任务控制块
任务控制块(TCB,Task Control Block)是 µC/OS-III 内核用来存放任务信息的数据结构,因此每个任务都需要独自的任务控制块。任务控制块所需的内存空间需要由用户手动分配,并在调用任务相关的函数(例如函数 OSTaskCreate())时,传入任务控制块在内存中的首地址,任务控制块结构体定义在 os.h 文件中,大多数成员变量都是可以通过配置文件中的配置项进行裁剪的。
3.4 µC/OS-III任务栈
不论是逻辑编程还是 RTOS 编程,栈空间的使用都是非常重要的。函数中的局部变量、函数调用时的现场保护和现场恢复等都是要使用到栈空间的。对于 µC/OS-III,在创建一个任务前,需要为任务准备好一块内存空间,这一内存空间将作为任务的栈空间进行使用。
我们用OSTaskCreate()函数创建任务时,其中参数 stk_size 用于表示任务栈空间的大小,其单位由参数 p_stk_bast 的变量类型决定,从µC/OS-III 中创建任务函数的函数原型中可以看到,参数 p_stk_base 的变量类型为 CPU_STK *,对于 STM32 所使用的 ARM Cortex-M 对应的 CPU_STK 定义如下所示:
typedef unsigned int CPU_INT32U;
typedef CPU_INT32U CPU_STK;
3.5 任务优先级
任务优先级是决定任务调度器如何分配 CPU 使用权的因素之一。每一个任务都被分配一个0~(OS_CFG_PRIO_MAX-1)的任务优先级,并且 µC/OS-III 支持多了任务具有相同的任务优先级,宏 OS_CFG_PRIO_MAX 是在 µC/OS-III 的配置文 os_cfg.h 中定义配置的。
在 cpu_cfg.h文件中有宏CPU_CFG_LEAD_ZEROS_ASM_PRESENT,该宏用于配置 µC/OS-III 使用硬件指令的方法或是软件算法的方法计算前导零数量,µC/OS-III 使用位图的方式记录当前系统中存在的所有任务优先级,在 µC/OS-III 系统中存在的最高任务优先级时,就会使用到前导零计数。对于 STM32 而言,STM32 是具有硬件计算前导零的指令的,并且最大支持 32比特位的数,因此宏 OS_CFG_PRIO_MAX 的最大值就是 32。
3.6 任务调度与切换
3.6.1 抢占式调度
在抢占式调度中,如果一个具有高任务优先级的任务因等待某一事件而被挂起后,CPU 的使用权会交给任务优先级低的任务,此时,只要任务优先级高的任务等待的事件发生,那么µC/OS-III 会立即挂起正在运行的低任务优先级的任务,而去处理任务优先级高的任务,这一过程就是抢占的过程。
3.6.2 时间片调度
时间片调度是针对任务优先级相同的任务而言的,当多个具有相同任务优先级的任务就绪时,任务调度器会根据用户设置的任务时间片轮流地运行这些任务,当然这些任务的运行依然会被任务优先级更高的任务抢占。时间片是以一次系统时钟节拍为单位的,例如 µC/OS-III 默认设置的任务时间片为 100,则 µC/OS-III 会在当前任务运行 100 次系统时钟节拍的时间后,切换到另一个相同任务优先级的任务中运行。如下图所示:
(1)任务3正在运行,这时一个时钟节拍中断发生,但是任务3的时间片还没完成。
(2)任务3的时钟片用完。
(3)UCOSIII切换到任务1,任务1是优先级 N下的下一个就绪任务。
(4)任务1连续运行至时间片用完。
(5)任务3运行。
(6)任务3调用OSSchedRoundRobinYield()(在 os_core.c 文件中定义)函数放弃剩余的时间片,从而使优先级X下的下一个就绪的任务运行。
(7)UCOSIII切换到任务1。
(8)任务1执行完其时间片
四、任务相关API
4.1 任务创建与删除
4.1.1 OSTaskCreate()任务创建函数
- 函数原型在os_task.c文件里
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)
- 函数参数
- 函数错误代码
4.1.2 OSTaskDel()任务删除函数
- 函数原型
void OSTaskDel (OS_TCB *p_tcb,
OS_ERR *p_err)
- 函数形参
- 函数错误代码
4.2 任务挂起与恢复
4.2.1 OSTaskSuspend()任务挂起函数
- 函数原型
void OSTaskSuspend( OS_TCB* p_tcb,
OS_ERR* p_err)
- 函数形参
- 函数错误代码
4.2.2 OSTaskResume()任务恢复函数
- 函数原型
void OSTaskResume( OS_TCB* p_tcb,
OS_ERR* p_err)
- 函数形参
- 函数错误代码
- 注意事项
① 函数 OSTaskSuspend()与函数 OSTaskResume()必须成对出现。
② 被函数 OSTaskSuspend()挂起的任务只能通过调用函数 OSTaskResume()恢
4.3 系统内部任务
函数 | 描述 |
---|---|
OSSchedRoundRobinCfg() | 配置时间片调度功能 |
OSSchedRoundRobinYield() | 在时间片到期之前,强制进行任务调度 |
OSTaskChangePrio() | 修改任务优先级 |
OSTaskCreate() | 创建任务 |
OSTaskCreateHook() | 任务创建钩子函数 |
OSTaskDel() | 删除任务 |
OSTaskDelHook() | 任务删除钩子函数 |
OSTaskRegGet() | 获取任务本地存储寄存器的值 |
OSTaskRegGetID() | 获取当前任务本地存储寄存器的索引值 |
OSTaskRegSet() | 修改任务本地存储寄存器的值 |
OSTaskResume() | 恢复任务 |
OSTaskReturnHook() | 任务意外返回钩子函数 |
OSTaskStkChk() | 计算任务栈余量 |
OSTaskStkInit() | 初始化任务栈 |
OSTaskSuspend() | 挂起任务 |
OSTaskSwHook() | 任务切换钩子函数 |
OSTaskTimeQuantaSet() | 修改任务时间片 |
4.3.1 时间片调度函数
- 函数 OSSchedRoundRobinCfg()
该函数用于配置时间片调度功能,配置内容为是否开启时间片调度以及时间片的默认长度。
void OSSchedRoundRobinCfg( CPU_BOOLEAN en,
OS_TICK dflt_time_quanta,
OS_ERR* p_err)
- en:是否使能时间片调度
- dflt_time_quanta:默认的时间片长度
- p_err:指向接收错误代码变量的指针
错误代码OS_ERR_NONE:时间片功能配置成功
- 函数 OSSchedRoundRobinYield()
该函数用于在任务时间片到期前,强制进行任务调度
void OSSchedRoundRobinYield(OS_ERR* p_err)
形参p_err:指向接收错误代码变量的指针
错误代码描述如下:
4.3.2 空闲任务
- OS_IdleTask()
我们首先来看一下空闲任务:OS_IdleTask(),在os_core.c文件中定义。任务OS_IdleTask()是必须创建的,不过不需要手动创建,在调用OS Init()初始化UCOS的时候就会被创建。打开OS Init()函数,可以看到,在 OS Ini()中 调 用了函数 OS IdleTaskInit() - 函数 OSIdleTaskHook()
该函数为空闲任务的回调函数,在此函数中绝对不能调用可能引发空闲任务挂起的 API函数。使用空闲任务钩子函数的话需要将宏OS_CFG_APP_HOOKS_EN置1,即允许使用空闲任务的钩子函数。 - 函数 App_OS_SetAllHooks()
此函数用于初始化所有 µC/OS-III 钩子函数,要使用此函数需将配置文件 os_cfg.h 文件中的配置项 OS_CFG_APP_HOOKS_EN 配置为 1,此函数定义在文件 os_app_hooks.c 中
4.3.3 统计任务
- 函数原型
void OSStatTaskCPUUsageInit(OS_ERR* p_err)
- 形参p_err:指向接收错误代码变量的指针
- 错误代码描述
五、中断与时间管理
5.1 中断管理
5.1.1 中断处理过程
在STM32中是支持中断的,中断是一个硬件机制,主要用来向CPU通知一个异步事件发生了,这时CPU就会将当前CPU寄存器值入栈,然后转而执行中断服务程序,在CPU执行中断服务程序的时候有可能有更高优先级的任务就绪,那么当退出中断服务程序的时候,CPU就会直接执行这个高优先级的任务。
UCOSIII是支持中断嵌套的,既高优先级的中断可以打断低优先级的中断,在UCOSIII中使用OSIntNestingCtr来记录中断嵌套次数,最大支持250级的中断嵌套,每进入一次中断服务函数OSIntNestingCtr就会加1,当退出中断服务函数的时候OSIntNestingCtr就会减1。
我们在编写UCOSIII的中断服务程序的时候需要使用到两个函数OSIntEnter()和OSIntExit()。首先调用OSIntEnter()进入中断,并记录中断嵌套次数。然后退出调用OSIntExit(),发起一次中断任务切换
5.1.2 中断消息发布方式
相比UCOSII,UCOSIII对从中断发布消息或者信号的处理有两种模式:直接发布和延迟发布两种方式。我们可以通过宏 OS_CFG_ISR_POST_DEFERRED_EN 来选择使用直接发布还是延迟发布。宏 OS_CFG_ISR_POST_DEFERRED_EN 在 os_cfgh文件中有定义,当定义为0时使用直接发布模式,定义为1的时候使用延迟发布模式。不管使用那种方式,我们的应用程序不需要做出任何的修改,编译器会根据不同的设置编译相应的代码。
-
直接发布
-
延迟发布
-
两者对比
直接发布模式下,UCOSⅢ通过关闭中断来保护临界段代码。延迟发布模式下,UCOSII 通过锁定任务调度来保护临界段代码。 在延迟发布模式下,UCOSⅢ在访问中断队列时,仍然需要关闭中断,但这个时间是非常短的。如果应用中存在非常快速的中断请求源,则当UCOSIII在直接发布模式下的中断关闭时间不能满足要求的时候,可以使用延迟发布模式来降低中断关闭时间。
5.1.3 中断管理配置
-
µC/OS-III 中断配置项
①CPU_CFG_NVIC_PRIO_BITS
此宏用于定义中断优先级配置寄存器的实际使用位数,中断优先级配置寄存器实际使用到多少比特位,这个宏就应该定义成多少,因为 STM32 的优先级配置寄存器都只使用到了高四比特位,因此对于 STM32 而言,这个宏应该配置为 4。
②CPU_CFG_KA_IPL_BOUNDARY
此宏用于定义受 µC/OS-III 管理的最高中断优先等级,中断优先级低于此宏定义值(中断优先级数值大于此宏定义值)的中断受 µC/OS-III 管理。此宏定义的值可以根据用户的实际应用场景来决定,本教程的配套例程源码全部将此宏定义配置为 4,即中断优先级为 4 ~ 15 的中断由 µC/OS-III 管理,而中断优先级为 0~3 的中断不由 µC/OS-III 管理 -
PendSV 中断优先级配置
PendSV 主要用于任务切换,因此在 µC/OS-III 内核开始进行多任务处理前,也就是在µC/OS-III 内核启动之前,就需要配置好 PendSV。在文件 os_cpu_a.asm 中有标号(汇编中的标号,类似于 C 语言中的函数名)OSStartHighRdy,OSStartHighRdy 用于开启系统中第一个任务,在函数 OSStart()中被调用 -
SysTick 中断优先级配置
SysTick 主要用于为 µC/OS-III 内核提供时钟节拍,在调用函数 OSStart()后,需要调用函数OS_CPU_SysTickInit()对 SysTick 进行配置,在对 SysTick 的配置过程中,就包括对 SysTick 中断优先级的配置
5.1.4临界区保护
有一些代码我们需要保证其完成运行,不能被打断,这些不能被打断的代码就是临界段代码,也叫临界区。我们在进入临界段代码的时候使用宏OS_CRITICAL_ENTER(),退出临界区的时候使用宏 OS_CRITICAL_EXITO或者OS_CRITICAL_EXIT_NO_SCHED()。
当宏 OS_CFG_ISR_POST_DEFERRED_EN 定义为0的时候,进入临界区的时候 UCOSII 会使用关中断的方式,退出临界区以后重新打开中断。当OS_CFG_ISR_POST_DEFERRED_EN 定义为1的时候进入临界区前是给调度器上锁,并在退出临界区的时候给调度器解锁。进入和退出临界段的宏在os.h文件中有定义
#define OS_CRITICAL_ENTER() CPU_CRITICAL_ENTER()
#define OS_CRITICAL_EXIT() CPU_CRITICAL_EXIT()
#define OS_CRITICAL_EXIT_NO_SCHED() CPU_CRITICAL_EXIT()
5.2 时间管理
5.2.1 µC/OS-III 系统时钟节拍
系统时钟节拍的来源是 SysTick。在 µC/OS-III 内核启动后,还必须使用函数 OS_CPU_SysTickInit()对 SysTick 进行配置。配置项 OS_CFG_TICK_RATE_HZ 就是用来配置系统时钟节拍的频率的。
这里要注意的是,虽然 STM32 的 SysTick 的时钟频率可以有两种配置,分别为与 CPU 内核同频率和 CPU 内核频率的八分之一,但是,函数 OS_CPU_SysTickInit()在配置 SysTick 的时候,是强制将 SysTick 配置为与 CPU 内核同频率的,因此在计算 cnts 的时候,就直接使用 CPU内核的频率作为 SysTick 的频率。
函数 OS_CPU_SysTickInit()会根据传入的参数 cnts 配置 SysTick 的重装载值,那么 SysTick 就能够按照配置文件 os_cfg_app.h 中配置项 OS_CFG_TICK_RATE_HZ 的频率发生溢出,并且在上面的代码中,也使能了 SysTick 的中断,因此就能够在 SysTick 的中断中处理µC/OS-III 的系统时钟节拍了。
5.2.2 µC/OS-III 任务延时相关函数
- 函数 OSTimeDly()
void OSTimeDly( OS_TICK dly,
OS_OPT opt,
OS_ERR *p_err)
函数 OSTimeDly()的形参描述,如下表所示:
函数 OSTimeDle()的延时选项(opt)描述,如下表所示:
函数 OSTimeDly()的错误代码描述,如下表所示:
- 函数 OSTimeDlyHMSM()
void OSTimeDlyHMSM( CPU_INT16U hours,
CPU_INT16U minutes,
CPU_INT16U seconds,
CPU_INT32U milli,
OS_OPT opt,
OS_ERR *p_err)
函数 OSTimeDlyHMSM()的形参描述,如下表所示:
函数 OSTimeDlyHMSM()的延时选项(opt)描述,如下表所示:
函数 OSTimeDlyHMSM()的错误代码描述,如下表所示:
- 函数 OSTimeDlyResume()
void OSTimeDlyResume( OS_TCB *p_tcb,
OS_ERR *p_err)
函数 OSTimeDlyResume()的形参描述,如以下表所示:
函数 OSTimeDlyResume()的错误代码描述,如下表所示:
5.2.3 其他系统时钟节拍相关 API 函数
- 函数 OSTimeGet()
OS_TICK OSTimeGet(OS_ERR *p_err)
- p_err:指向接收错误代码变量的指针
- 错误代码OS_ERR_NONE:成功获取系统节拍计数器的值
- 函数 OSTimeSet()
void OSTimeSet( OS_TICK ticks,
OS_ERR *p_err)
函数 OSTimeSet()的函数形参描述,如下表所示:
函数 OSTimeSet()的错误代码描述,如下表所示:
六、信号量
在UCOSⅢ中有可能会有多个任务会访问共享资源,因此信号量最早用来控制任务存取共享资源,现在信号量也被用来实现任务间的同步以及任务和ISR间同步。在可剥夺的内核中,当任务独占式使用共享资源的时候,会出现低优先级的任务先于高优先级任务运行的现象,这个现象被称为优先级反转,为了解决优先级反转这个问题,UCOSⅢ引入了互斥信号量这个概念。
6.1 信号量简介
信号量像是一种上锁机制,代码必须获得对应的钥匙才能继续执行,一旦获得了钥匙,也就意味着该任务具有进入被锁部分代码的权限。一旦执行至被锁代码段,则任务一直等待,直到对应被锁部分代码的钥匙被再次释放才能继续执行。信号量分为两种:二进制信号量与计数型信号量,二进制信号量只能取0和1两个值,计数型信号量不止可以取2个值,在共享资源中只有任何可以使用信号量,中断服务程序则不能使用。
-
µC/OS-III二值信号量
某一资源对应的信号量为1的时候,那么就可以使用这一资源,如果对应资源的信号量为0,那么等待该信号量的任务就会被放进等待信号量的任务表中。在等待信号量的时候也可以设置超时,如果超过设定的时间任务没有等到信号量的话那么该任务就会进入就绪态。任务以“发信号”的方式操作信号量。可以看出如果一个信号量为二进制信号量的话,一次只能一个任务使用共享资源。 -
µC/OS-III 计数型信号量
有时候我们需要可以同时有多个任务访问共享资源,这个时候二进制信号量就不能使用了,计数型信号量就是用来解决这个问题的。比如某一个信号量初始化值为10,那么只有前10个请求该信号量的任务可以使用共享资源,以后的任务需要等待前10个任务释放掉信号量。每当有任务请求信号量的时候,信号量的值就会减1,直到减为0。当有任务释放掉信号量的时候信号量的值就会加1。 -
任务同步
信号量现在更多的被用来实现任务的同步以及任务和ISR间的同步,信号量用于任务同步。用一个小旗子代表信号量,小旗子旁边的数值N为信号量计数值, 表示发布信号量的次数累积值,ISR可以多次发布信号量,发布的次数会记录为N。一般情况下,N的初始值是0,表示事件还没有发生过。在初始化时,也可以将N的初值设为大于零的某个值,来表示初始情况下有多少信号量可用。
等待信号量的任务旁边的小沙漏表示等待任务可以设定超时时间。超时的意思是该任务只会等待一定时间的信号量,如果在这段时间内没有等到信号量,UCOSⅢ就会将任务置于就绪表中,并返回错误码。
- 优先级反转
在使用二值信号量和计数型信号量的时候,会经常地遇到优先级翻转的问题,优先级翻转的问题在抢占式内核中是很常见的,但是在实时操作系统中是不允许出现优先级翻转的现象的,因为优先级翻转会破坏任务执行的预期顺序,可能会导致未知的严重后果,下面就展示了一个优先级翻转的例子,如下图所示:
如上图所示,定义:任务 H 为优先级最高的任务,任务 L 为优先级最低的任务,任务 M 为优先级介于任务 H 与任务 L 之间的任务。
(1) 任务 H 和任务 M 为挂起状态,等待某一事件发生,此时任务 L 正在运行。
(2) 此时任务 L要访问共享资源,因此需要获取信号量。
(3) 任务 L 成功获取信号量,并且此时信号量已无资源,任务 L 开始访问共享资源。
(4)此时任务 H 就绪,抢占任务 L 运行。
(5) 任务 H 开始运行。
(6) 此时任务 H要访问共享资源,因此需要获取信号量,但信号量已无资源,因此任务 H挂起等待信号量资源。
(7) 任务 L 继续运行。
(8) 此时任务 M就绪,抢占任务 L 运行。
(9) 任务 M 正在运行。
(10) 任务 M 运行完毕,继续挂起。
(11) 任务 L 继续运行。
(12)此时任务 L 对共享资源的访问操作完成,释放信号量,虽有任务 H 因成功获取信号 量,解除挂起状态并抢占任务 L 运行。
(13) 任务H得以运行。
从上面优先级翻转的示例中,可以看出,任务 H 为最高优先级的任务,因此任务 H 执行的操作需要有较高的实时性,但是由于优先级翻转的问题,导致了任务 H 需要等到任务 L 释放信号量才能够运行,并且,任务 L 还会被其他介于任务 H 与任务 L 任务优先级之间的任务 M 抢占,因此任务 H 还需等待任务 M 运行完毕,这显然不符合任务 H 需要的高实时性要求。
6.2 信号量相关API函数
函数定义在文件 os_sem.c 中
函数 | 描述 |
---|---|
OSSemCreate() | 创建一个信号量 |
OSSemDel() | 删除一个信号量 |
OSSemPend() | 尝试获取信号量资源 |
OSSemPendAbort() | 终止任务挂起等待信号量资源 |
OSSemPost() | 释放信号量资源 |
OSSemSet() | 强制设置信号量的资源数 |
- 函数 OSSemCreate()
void OSSemSet( OS_SEM *p_sem,
OS_SEM_CTR cnt,
OS_ERR *p_err)
函数 OSSemCreate()的形参描述,无返回值,如下表所示:
形参 | 描述 |
---|---|
p_sem | 指向信号量结构体的指针 |
p_name | 指向作为信号量名的 ASCII 字符串的指针 |
cnt | 信号量资源数的初始值 |
p_err | 指向接收错误代码变量的指针 |
- 函数 OSSemCreate()的错误代码描述,如下表所示:
- 函数 OSSemDel()
OS_OBJ_QTY OSSemDel( OS_SEM *p_sem,
OS_OPT opt,
OS_ERR *p_err)
函数 OSSemDel()的形参描述,如下表所示:
- 返回值:OS_OBJ_QTY 类型变量,删除信号量时,被终止挂起任务的数量
- 函数 OSSemDel()的错误代码描述,如下表所示:
错误代码 | 描述 |
---|---|
OS_ERR_NONE | 信号量删除成功 |
OS_ERR_DEL_ISR | 在中断中非法调用该函数 |
OS_ERR_ILLEGAL_DEL_RUN_TIME | 定义了OS_SAFETY_CRITICAL_IEC61508,且在 OSStart()之后非法地删除内核对象 |
OS_ERR_OBJ_PTR_NULL | 指向信号量结构体的指针为空 |
OS_ERR_OBJ_TYPE | 待被删除内核对象的类型不是信号量 |
OS_ERR_OPT_INVALID | 无效的函数操作选项 |
OS_ERR_OS_NOT_RUNING | µC/OS-III 内核还未运行 |
OS_ERR_TASK_WAITING | 有任务挂起等待待被删除的信号量 |
- 函数 OSSemPend()
OS_SEM_CTR OSSemPend(OS_SEM *p_sem,
OS_TICK timeout,
OS_OPT opt,
CPU_TS *p_ts,
OS_ERR *p_err)
函数 OSSemPend()的形参描述,如下表所示:
- 返回值:OS_SEM_CTR 类型返回值,信号量资源数更新后的值
- 函数 OSSemPend()的错误代码描述,如下表所示:
- 函数 OSSemPendAbort()
OS_OBJ_QTY OSSemPendAbort( OS_SEM *p_sem,
OS_OPT opt,
OS_ERR *p_err)
函数 OSSemPendAbort()的形参描述,如下表所示:
- 返回值:OS_OBJ_QTY 类型返回值,被终止挂起的任务数量
- 函数 OSSemPendAbort()的错误代码描述,如下表所示:
- OSSemPost()
OS_SEM_CTR OSSemPost( OS_SEM *p_sem,
OS_OPT opt,
OS_ERR *p_err)
函数 OSSemPost()的形参描述,如下表所示:
- 返回值:OS_SEM_CTR 类型返回值,信号量资源数更新后的值
- 函数 OSSemPost()的错误代码描述,如下表所示:
- 函数OSSemSet()
void OSSemSet( OS_SEM *p_sem,
OS_SEM_CTR cnt,
OS_ERR *p_err)
函数 OSSemSet()的形参描述,无返回值,如下表所示:
函数 OSSemSet()的错误代码描述,如下表所示:
6.3 互斥信号量
6.3.1 互斥信号量简介
互斥信号量也叫互斥锁,可以理解为是一种特殊的二值信号量,互斥信号量拥有优先级继承的机制使得互斥信号量能够在一定的程度上解决优先级翻转的问题。互斥信号量的优先级继承机制体现在,当一个互斥信号量正被一个低优先级的任务持有时,如果此时有一个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会因获取不到互斥锁而被挂起,不过接下来,高优先级的任务会将持有互斥信号量的低优先级任务的任务优先级提成到与高优先级任务的任务优先级相同的任务优先级,这个过程就是优先级继承。优先级继承可以尽可能地减少高优先级任务挂起等待互斥锁的时间,并且将优先级翻转问题带来的影响降到最低。
但是优先级继承并不是能完全解决优先级翻转带来的问题,因为优先级继承仅仅是将持有互斥信号量的低优先级任务的任务优先级提高的与高优先级任务相同的任务优先级,而非直接将互斥信号量直接从低优先级的任务手上“抢”过来,因此高优先级的任务还是需要等待低优先级的任务释放互斥信号量,高优先级的任务才能够获取到互斥信号量。例子如下:
6.3.2 互斥信号量相关API
函数 | 描述 |
---|---|
OSMutexCreate() | 创建一个互斥信号量 |
OSMutexDel() | 删除一个互斥信号量 |
OSMutexPend() | 尝试获取互斥信号量 |
OSMutexPendAbort() | 终止任务挂起等待互斥信号量 |
OSMutexPost() | 释放互斥信号量 |
- 函数 OSMutexCreate()
void OSMutexCreate( OS_MUTEX* p_mutex,
CPU_CHAR* p_name,
OS_ERR* p_err)
函数 OSMutexCreate()的形参描述,无返回值,如下表所示:
- 函数 OSMutexCreate()的错误代码描述,如下表所示:
- 函数 OSMutexDel()
OS_OBJ_QTY OSMutexDel(OS_MUTEX* p_mutex,
OS_OPT opt,
OS_ERR* p_err)
函数 OSMutexDel()的形参描述,如下表所示:
- 返回值:OS_OBJ_QTY 类型返回值,删除互斥信号量时,被终止挂起任务的数量
- 函数 OSMutexDel()的错误代码描述,如下表所示:
- 函数 OSMutexPend()
void OSMutexPend( OS_MUTEX* p_mutex,
OS_TICK timeout,
OS_OPT opt,
CPU_TS* p_ts,
OS_ERR* p_err)
函数 OSMutexPend()的形参描述,无返回值,如下表所示:
- 函数 OSMutexPend()的错误代码描述,如下表所示:
错误代码 | 描述 |
---|---|
OS_ERR_NONE | 成功获取互斥信号量 |
OS_ERR_MUTEX_OWNER | 任务重复获取互斥信号量 |
OS_ERR_MUTEX_OVF | 互斥信号量持有递归计数器计溢出 |
OS_ERR_OBJ_DEL | 待获取的互斥信号量已经被删除 |
OS_ERR_OBJ_PTR_NULL | 指向互斥信号量结构体的指针为空 |
OS_ERR_OBJ_TYPE | 待获取内核对象的类型不是互斥信号量 |
OS_ERR_OPT_INVALID | 无效的函数操作选项 |
OS_ERR_OS_NOT_RUNING | µC/OS-III 内核还未运行 |
OS_ERR_PEND_ABORT | 任务挂起等待互斥信号量时被终止(还未超时) |
OS_ERR_PEND_ISR | 在中断中非法调用该函数 |
OS_ERR_PEND_WOULD_BLOCK | 获取互斥信号量失败,并且不挂起任务等待互斥信号量 |
OS_ERR_SCHED_LOCKED | 任务调度器已锁定 |
OS_ERR_STATUS_INVALID | 无效的任务挂起结果 |
OS_ERR_TIMEOUT | 任务挂起等待互斥信号量超时 |
- 函数 OSMutexPendAbort()
OS_OBJ_QTY OSMutexPendAbort( OS_MUTEX* p_mutex,
OS_OPT opt,
OS_ERR* p_err)
函数 OSMutexPendAbort()的形参描述,如下表所示:
- 返回值:OS_OBJ_QTY 类型变量,被终止挂起任务的数量
- 函数 OSMutexPendAbort()的错误代码描述,如下表所示:
- 函数 OSMutexPost()
void OSMutexPost( OS_MUTEX* p_mutex,
OS_OPT opt,
OS_ERR* p_err);
函数 OSMutexPost()的形参描述,无返回值,如下表所示:
- 函数 OSMutexPost()的错误代码描述,如下表所示:
6.4 µC/OS-III任务内嵌信号量
6.4.1 内嵌信号量简介
任务内嵌信号量本质上就是一个信号量,但是任务内嵌信号量并不需要信号量这么一个中间的内核对象,任务内嵌信号量是分配于每一个任务的任务控制块结构体中的,因此每一个任务都有独自的任务内嵌信号量,任务内嵌信号量只能被该任务获取,但是可以由其他任务或者中断释放,如下图所示:
如上如所示,当任务或中断需要往指定任务的内嵌信号量发出信号时,是需要调用相应的API 函数即可,每个任务的内嵌信号量在创建的时候都已经被创建好了,并且发出的信号能够直接到达指定的任务中,因此使用内嵌信号量的效率比使用内核对象的信号量高得多,在实际的开发当中,可以优先考虑使用任务内嵌信号量。
6.4.2 任务内嵌信号量相关API函数
函数 | 描述 |
---|---|
OSTaskSemPend() | 获取任务内嵌信号量 |
OSTaskSemPendAbort() | 终止任务挂起等待任务内嵌信号量 |
OSTaskSemPost() | 释放指定任务的任务内嵌信号量 |
OSTaskSemSet() | 强制设置指定的任务内嵌信号量为指定值 |
- 函数 OSTaskSemPend()
OS_SEM_CTR OSTaskSemPend( OS_TICK timeout,
OS_OPT opt,
CPU_TS* p_ts,
OS_ERR* p_err)
- 函数 OSTaskSemPend()的形参描述,如下表所示:
- 返回值:OS_SEM_CTR 类型,任务内嵌信号量更新后的资源数
- 函数 OSTaskSemPend()的错误代码描述,如下表所示:
- 函数 OSTaskSemPendAbort()
CPU_BOOLEAN OSTaskSemPendAbort(OS_TCB* p_tcb,
OS_OPT opt,
OS_ERR* p_err)
-
函数 OSTaskSemPendAbort()的形参描述,如下表所示:
-
值描述:CPU_BOOLEAN,终止任务挂起是否成功
-
函数 OSTaskSemPendAbort()的错误代码描述,如下表所示:
- 函数 OSTaskSemPost()
OS_SEM_CTR OSTaskSemPost( OS_TCB* p_tcb,
OS_OPT opt,
OS_ERR* p_err)
- 函数 OSTaskSemPost()的形参描述,如下表所示:
- 返回值:OS_SEM_CTR 类型,任务内嵌信号量更新后的资源数
- 函数 OSTaskSemPost()的错误代码描述,如下表所示:
- 函数 OSTaskSemSet()
OS_SEM_CTR OSTaskSemSet( OS_TCB* p_tcb,
OS_SEM_CTR cnt;
OS_ERR* p_err)
- 函数 OSTaskSemSet()的形参描述,如下表所示:
- 返回值:OS_SEM_CTR 类型返回值,任务内嵌信号量设置前的资源数
- 函数 OSTaskSemSet()的错误代码描述,如下表所示:
七、消息队列
7.1 消息队列简介
信号量一般用于任务之间的同步,但是在实际的项目开发当中,经常会遇到需要在任务之间进行通信,µC/OS-III 提供了消息队列的机制用于任务间的是消息的传递。消息一般包含:指向数据的指针,表明数据长度的变量和记录消息发布时刻的时间戳,指针指向的可以是一块数据区或者甚至是一个函数,消息的内容必须一直保持可见性,因为发布数据采用的是引用传递是指针传递而不是值传递,也就说,发布的数据本身不产生数据拷贝。在UCOSⅡ中有消息邮箱和消息队列,但是在UCOSⅢ中只有消息队列。消息队列是由用户创建的内核对象,数量不限制,下图展示了用户可以对消息队列进行的操作。
从上图可以看出,消息是通过消息队列进行传输的,任务和中断都能够操作消息队列,但是中断只能往消息队列中发送消息,而不能从消息队列中接收消息。在UCOSIⅢ中对于消息队列的读取既可以采用先进先出(FIFO)的方式,也可以采用后进先出(LIFO)的方式。当任务或者中断服务程序需要向任务发送一条紧急消息时LIFO的机制就非常有用了。采用后进先出的方式,发布的消息会绕过其他所有的已经位于消息队列中的消息而最先传递给任务。
在上图中可以看到,在靠近接收任务的地方有一个代表超时“沙漏”,这表示任务从消息队列中接收消息是可以指定超时时间的,这个超时时间说明接收消息的任务愿意在消息队列中无消息的时候,等待一定的时间,如果超过了指定的这段时间,消息队列中还是没有消息,那么将接收任务将收到超时相应的错误代码,当然,也可以指定等待的时间为无限长,那么接收消息的任务将一直被挂起,直到消息队列中有消息。
消息队列中有一个列表,记录了所有正在等待获得消息的任务,上图所示为多个任务可以在一个消息队列中等待,当一则消息被发布到队列中时,最高优先级的等待任务将获得该消息,发布方也可以向消息队列中所有等待的任务广播一则消息。
7.2 消息队列相关API
函数 | 描述 |
---|---|
OSQCreate() | 创建一个消息队列 |
OSQDel() | 删除一个消息队列 |
OSQFlush() | 清空消息队列中的所有消息 |
OSQPend() | 获取消息队列中的消息 |
OSQPendAbort() | 终止任务挂起等待消息队列 |
OSQPost() | 发送消息到消息队列 |
- 函数 OSQCreate()
void OSQCreate( OS_Q* p_q,
CPU_CHAR* p_name,
OS_MSG_QTY max_qty,
OS_ERR* p_err)
- 函数 OSQCreate()的形参描述,无返回值,如下表所示:
- 函数 OSQCreate()的错误代码描述,如下表所示:
- 函数 OSQDel()
OS_OBJ_QTY OSQDel( OS_Q* p_q,
OS_OPT opt,
OS_ERR* p_err)
- 函数 OSQDel()的形参描述,如下表所示:
- 返回值:OS_OBJ_QTY 类型,删除消息队列时,被终止挂起任务的数量
- 函数 OSQDel()的错误代码描述,如下表所示:
- 函数 OSQFlush()
OS_MSG_QTY OSQFlush( OS_Q* p_q,
OS_ERR* p_err)
- 函数 OSQFlush()的形参描述,如下表所示:
- 返回值:OS_MSG_QTY 类型,被释放的消息数量
- 函数 OSQFlush()的错误代码描述,如下表所示:
- 函数 OSQPend()
void *OSQPend( OS_Q* p_q,
OS_TICK timeout,
OS_OPT opt,
OS_MSG_SIZE* p_msg_size,
CPU_TS* p_ts,
OS_ERR* p_err)
- 函数 OSQPend()的形参描述,如下表所示:
- void *类型返回值:指向消息的指针
- 函数 OSQPend()的错误代码描述,如下表所示:
错误代码 | 描述 |
---|---|
OS_ERR_NONE | 消息接收成功 |
OS_ERR_OBJ_DEL | 指定的消息队列已经被删除 |
OS_ERR_OBJ_PTR_NULL | 指向消息队列结构体的指针为空 |
OS_ERR_OBJ_TYPE | 操作的内核对象的类型不是消息队列 |
OS_ERR_OPT_INVALID | 无效的函数操作选项 |
OS_ERR_OS_NOT_RUNING | µC/OS-III 内核还未运行 |
OS_ERR_PEND_ABORT | 任务挂起等待消息队列时被终止(还未超时) |
OS_ERR_PEND_ISR | 在中断中非法调用该函数 |
OS_ERR_PEND_WOULD_BLOCK | 获取消息队列失败,并且不挂起任务等待互斥信号量 |
OS_ERR_PTR_INVALID | 参数 p_msg_size 指针为空 |
OS_ERR_SCHED_LOCKED | 任务调度器已锁定 |
OS_ERR_TIMEOUT | 任务挂起等待消息队列超时 |
- 函数 OSQPendAbort()
OS_OBJ_QTY OSQPendAbort( OS_Q* p_q,
OS_OPT opt,
OS_ERR* p_err)
- 函数 OSQPendAbort()的形参描述,如下表所示:
- OS_OBJ_QTY 类型返回值:被终止挂起任务的数量
- 函数 OSQPendAbort()的错误代码描述,如下表所示:
- 函数 OSQPost()
void OSQPost( OS_Q* p_q,
void* p_void,
OS_MSG_SIZE msg_size,
OS_OPT opt,
OS_ERR* p_err)
- 函数 OSQPost()的形参描述,无返回值,如下表所示:
形参 | 描述 |
---|---|
p_q | 指向消息队列结构体的指针 |
p_void | 指向消息的指针 |
msg_size | 消息的大小,单位:字节 |
opt | 函数操作选项 |
p_err | 指向接收错误代码变量的指针 |
- 函数 OSQPost()的错误代码描述,如下表所示:
7.3 µC/OS-III任务内嵌消息队列
7.3.1 内嵌消息队列简介
任务内嵌消息队列本质上就是一个消息队列,但是任务内嵌消息队列并不需要消息队列这么一个中间的内核对象,任务内嵌消息队列是分配于每一个任务的任务控制块结构体中的,因此每一个任务都有独自的任务内嵌消息队列,任务内嵌消息队列只能被该任务接收,但是可以由其他任务或中断发送,如下图所示:
当任务或中断需要往指定任务的内嵌消息队列发送消息时,只需要调用相应的API函数即可,每个任务的内嵌消息队列是在任务创建的时候就已经被创建好的了,并且发出的消息能够直接到达指定任务的任务内嵌消息队列中因此使用任务内嵌消息队列的效率会比使用内核对象的消息队列高得多,在实际开发当中,可以优先考虑使用任务内嵌消息队列
7.3.2 内嵌消息队列相关API
函数 | 描述 |
---|---|
OSTaskQFlush() | 清空任务内嵌消息队列中的所有消息 |
OSTaskQPend() | 获取任务内嵌消息队列中的消息 |
OSTaskQPendAbort() | 终止任务挂起等待任务内嵌消息队列 |
OSTaskQPost() | 发送消息到任务内嵌消息队列 |
- 函数 OSTaskQFlush()
OS_MSG_QTY OSTaskQFlush( OS_TCB* p_tcb,
OS_ERR* p_err)
- 函数 OSTaskQFlush()的形参描述,如下表所示:
- OS_MSG_QTY 类型返回值:被释放的消息数量
- 函数 OSTaskQFlush()的错误代码描述,如下表所示:
- 函数 OSTaskQPend()
void *OSTaskQPend( OS_TICK timeout,
OS_OPT opt,
OS_MSG_SIZE* p_msg_size,
CPU_TS* p_ts,
OS_ERR* p_err)
- 函数 OSTaskQPend()的形参描述,如下表所示:
- void *类型返回值:指向消息的指针
- 函数 OSTaskQPend()的错误代码描述,如下表所示:
- 函数 OSTaskQPendAbort()
CPU_BOOLEAN OSTaskQPendAbort( OS_TCB* p_tcb,
OS_OPT opt,
OS_ERR* p_err)
- 函数 OSTaskQPendAbort()的形参描述,如下表所示:
- 返回值:CPU_BOOLEAN,终止任务挂起是否成功
- 函数 OSTaskQPendAbort()的错误代码描述,如下表所示:
错误代码 | 描述 |
---|---|
OS_ERR_NONE | 终止任务挂起等待任务内嵌消息队列成功 |
OS_ERR_OPT_INVALID | 无效的函数操作选项 |
OS_ERR_OS_NOT_RUNING | µC/OS-III 内核还未运行 |
OS_ERR_PEND_ABORT_ISR | 在中断中非法调用该函数 |
OS_ERR_PEND_ABORT_NONE | 没有任务挂起等待任务内嵌消息队列 |
OS_ERR_PEND_ABORT_SELF | 任务终止自己挂起等待任务内嵌消息队列 |
- 函数 OSTaskQPost()
void OSTaskQPost( OS_TCB* p_tcb,
void* p_void,
OS_MSG_SIZE msg_size,
OS_OPT opt,
OS_ERR* p_err)
- 函数 OSTaskQPost()的形参描述,无返回值,如下表所示:
- 函数 OSTaskQPost()的错误代码描述,如下表所示:
八、事件标志
8.1 事件标志简介
前面有讲信号量用来任务同步,但是信号量一般用于任务间的单事件同步,而对于任务间的多事件同步,事件标志更适合。事件标志是一个用于指示事件是否发生的比特位,因为一个事件是否发生只有两种情况,分别为事件发生和事件未发生,因此只需一个比特位就能够表示事件是否发生,µC/OS-III 用 1表示事件发生,用 0 表示事件未发生。
有时候一个任务可能需要和多个事件同步,这个时候就需要使用事件标志组。事件标志组 与任务之间有两种同步机制:或同步和 与同步,当任何一个事件发生,任务都被同步的 同步机制是“或”同步;需要所有的事件都发生任务才会被同步的同步机制是“与”同步,这两种同步机制如图所示。
- 在UCOSII中事件标志组是
OS_FLAG_GRP
,在os.h
文件中有定义,事件标志组中 也包含了一串任务,这些任务都在等待着事件标志组中的部分(或全部)事件标志被置1或被清 零,在使用之前,必须创建事件标志组。 - 任务和ISR(中断服务程序)都可以发布事件标志,但是,只有任务可以创建、删除事 件标志组以及取消其他任务对事件标志组的等待。
- 任务可以通过调用函数
OSFlagPend()
等待事件标志组中的任意个事件标志,调用函数OSFlagPend()
的时候可以设置一个超时时间,如果过了超时时间请求的事件还没有被发布,那 么任务就会重新进入就绪态。 - 我们可以设置同步机制为“或”同步还是“与”同步。
8.2 事件标志相关API
- 函数
OSFlagCreate()
定义在文件 os_flag.c 中
void OSFlagCreate (OS_FLAG_GRP *p_grp,
CPU_CHAR *p_name,
OS_FLAGS flags,
OS_ERR *p_err)
函数 OSFlagCreate()的形参描述,如下表所示:
无返回值,函数OSFlagCreate()的错误代码描述,如下表所示:
- 函数
OSFlagDel()
OS_OBJ_QTY OSFlagDel (OS_FLAG_GRP *p_grp,
OS_OPT opt,
OS_ERR *p_err)
- 返回值:OS_OBJ_QTY 类型返回值,被终止挂起等待事件标志任务的数量
函数 OSFlagDel()的形参描述,如下表所示:
函数 OSFlagDel()的错误代码描述,如下表所示:
- 函数
OSFlagPend()
OS_FLAGS OSFlagPend (OS_FLAG_GRP *p_grp,
OS_FLAGS flags,
OS_TICK timeout,
OS_OPT opt,
CPU_TS *p_ts,
OS_ERR *p_err)
- 返回值:OS_FLAGS类型返回值,任务实际等待到的事件标志
函数 OSFlagPend()的形参描述,如下表所示:
函数 OSFlagPend()的错误代码描述,如下表所示:
- 函数
OSFlagPendAbort()
OS_OBJ_QTY OSFlagPendAbort (OS_FLAG_GRP *p_grp,
OS_OPT opt,
OS_ERR *p_err)
- 返回值:OS_OBJ_QTY类型返回值,被终止挂起任务的数量
函数 OSFlagPendAbort()的形参描述,如下表所示:
函数 OSFlagPendAbort()的错误代码描述,如下表所示:
- 函数
OSFlagPendGetFlagsRdy()
OS_FLAGS OSFlagPendGetFlagsRdy(OS_ERR *p_err)
- 返回值:OS_FLAGS类型返回值,任务获取到的事件标志
- 形参p_err:指向接收错误代码变量的指针
函数 OSFlagPendGetFlagsRdy()的错误代码描述,如下表所示:
- 函数
OSFlagPost()
OS_FLAGS OSFlagPost (OS_FLAG_GRP *p_grp,
OS_FLAGS flags,
OS_OPT opt,
OS_ERR *p_err)
- 返回值:OS_FLAGS类型返回值,事件标志组更新后的事件标志值
函数 OSFlagPost()的形参描述,如下表所示:
函数 OSFlagPost()的错误代码描述,如下表所示:
九、软件定时器
9.1 软件定时器简介
学过单片机的都知道有硬件定时器,而软件定时器指的是由软件实现的定时器,并不是由具体的硬件组成,µC/OS-III 提供的软件定时器是一种向下计数的定时器。
9.1.1 定时频率
实际上软件定时器的定时器频率并不等于系统时钟节拍的频率,函数OS_TmrInit()
代码中可以看到一下代码:
OSTmrUpdateCnt = OSCfg_TickRate_Hz / OSCfg_TmrTaskRate_Hz;
- OSTmrUpdateCnt:这个系数就是用于将软件定时器的时间转换为系统时钟节拍的节拍数
- OSCfg_TickRate_Hz和OSCfg_TmrTaskRate_Hz:这两个变量又分别由文件
os_cfg_app.h
中的两个宏定义定义,分别为宏定义 OS_CFG_TICK_RATE_HZ和宏定义OS_CFG_TMR_TASK_RATE_HZ
所以软件定时器的精度取决于定时系统的时基频率,而且定时器所定时的数值必须是这个定时器任务精度的整数倍,因为软件定时器常用于不精确时间尺度的任务。
9.1.2 定时器状态
软件定时器任务负责更新软件定时器的定时器计数器,并且在软件定时器超时的时候,调用软件定时器的软件定时器超时回调函数,因此不能在软件定时器的超时回调函数中使用可能到时任务被阻塞、被挂起甚至被删除的函数。
软件定时器一共可以有四种状态,分别为未使用态、停止态、运行态和完成态,这四种软件定时器状态的描述如下所示:
- 未使用态:当软件定时器被定义但还未被创建或软件定时器被删除时,软件定时器就处于未使用态。
- 停止态:当软件定时器被创建但还未开启定时器或处于运行态的软件定时器被停止时,软件定时器就处于停止态。
- 运行态:当处于停止态的软件定时器被启动或单次定时器定时超时后重新启动或周期定时器定时超时后自动重新启动时,软件定时器处于运行态。
- 完成态:当单次定时器定时超时后,软件定时器处于完成态。
9.1.3 定时器模式
定时器其实就是一个递减计数器,当计数器递减到0的时候就会触发一个动作这个动作就是回调函数,当定时器计时完成时就会自动的调用这个回调函数。µC/OS-III 提供了两种软件定时器,分别为单次定时器和周期定时器:
-
单次定时器:单次定时器的一旦定时超时,只会执行一次其软件定时器超时回调函数,超时后可以被手动重新开启,但单次定时器不会自动重新开启定时。
-
周期定时器:周期定时器的一旦被开启,会在每次超时时,自动地重新启动定时器,从而周期地执行其软件定时器回调函数。
-
单次定时器(dly>0,period=0)
-
重新触发单次定时器
-
周期定时器(dly=0,period>0)
-
周期定时器(dly>0,period>0)
9.1 软件定时器相关API
- 函数
OSTmrCreate()
void OSTmrCreate (OS_TMR *p_tmr,
CPU_CHAR *p_name,
OS_TICK dly,
OS_TICK period,
OS_OPT opt,
OS_TMR_CALLBACK_PTR p_callback,
void *p_callback_arg,
OS_ERR *p_err)
函数 OSTmrCreate()的形参描述,如下表所示:
函数 OSTmrCreate()的错误代码描述,如下表所示:
- 函数
OSTmrDel()
CPU_BOOLEAN OSTmrDel (OS_TMR *p_tmr,
OS_ERR *p_err)
- 返回值:CPU_BOOLEAN 类型返回值,软件定时器删除是否成功
函数 OSTmrDel()的形参描述,如下表所示:
函数 OSTmrDel()的错误代码描述,如下表所示:
- 函数
OSTmrRemainGet()
OS_TICK OSTmrRemainGet (OS_TMR *p_tmr,
OS_ERR *p_err)
- 返回值:OS_TICK 类型返回值,软件定时器的剩余超时时钟节拍数
函数 OSTmrRemainGet()的形参描述,如下表所示:
函数OSTmrRemainGet()的错误代码描述,如下表所示:
- 函数
OSTmrSet()
void OSTmrSet (OS_TMR *p_tmr,
OS_TICK dly,
OS_TICK period,
OS_TMR_CALLBACK_PTR p_callback,
void *p_callback_arg,
OS_ERR *p_err)
函数 OSTmrSet()的形参描述,如下表所示:
函数OSTmrSet()的错误代码描述,如下表所示:
- 函数
OSTmrStart()
CPU_BOOLEAN OSTmrStart (OS_TMR *p_tmr,
OS_ERR *p_err)
- 返回值:CPU_BOOLEAN 类型返回值,软件定时器开启是否成功
函数 OSTmrSet()的形参描述,如下表所示:
函数 OSTmrStart()的错误代码描述,如下表所示:
- 函数
OSTmrStateGet()
OS_STATE OSTmrStateGet (OS_TMR *p_tmr,
OS_ERR *p_err)
- 返回值:OS_STATE 类型返回值,软件定时器的状态
函数OSTmrStateGet()的形参描述,如下表所示:
函数 OSTmrStateGet()的错误代码描述,如下表所示:
- 函数
OSTmrStop()
CPU_BOOLEAN OSTmrStop (OS_TMR *p_tmr,
OS_OPT opt,
void *p_callback_arg,
OS_ERR *p_err)
- CPU_BOOLEAN类型返回值:停止软件定时器是否成功
函数 OSTmrStop()的形参描述,如下表所示:
函数OSTmrStop()的错误代码描述,如下表所示:
十、内存管理
10.1 内存管理简介
内存管理是一个操作系统必备的系统模块,我们在用VC++或者Visual Studio学习C语言 的时候会使用malloc()
和free()
这两个函数来申请和释放内存。我们在使用Keil MDK编写STM32 程序的时候就可以使用malloc()和free(),但是不建议这么用,这样的操作会将原来大块内存逐 渐的分割成很多个小块内存,产生大量的内存碎片,最终导致应用不能申请到大小合适的连续 内存。
为此 µC/OS-III 提供了一个内存管理的方案,µC/OS-III 将一块大内存作为一个内存区,一个内存区中有多个大小均相同的内存块组成,如下图所示:
因为在同一个内存区内在内存块大小都相同,因此使用 µC/OS-III 的内存管理方案申请和释放内存,就完全不会在内存区内产生内存碎片,这样一来便极大地提高了系统中内存的使用效率。在使用内存管理之前首先要创建存储区,在创建存储区之前我们先了解一个重要的结构体,存储区控制块:OS_MEM,结构体OS_MEM如下,取掉了与调试有关的变量:
struct os_mem { /* MEMORY CONTROL BLOCK*/
OS_OBJ_TYPE Type; //内核对象类型
void *AddrPtr; //内存池的起始地址
CPU_CHAR *NamePtr; //内存池名称。
void *FreeListPtr; //空闲内存块列表。
OS_MEM_SIZE BlkSize; //内存块大小。
OS_MEM_QTY NbrMax; //内存池中内存块的总数量。
OS_MEM_QTY NbrFree; //空闲内存块数量。
#if OS_CFG_DBG_EN > 0u
OS_MEM *DbgPrevPtr;
OS_MEM *DbgNextPtr;
#endif
};
用户可以根据实际的需求,创建多个不同的内存区,每个内存区中内存块的数量和大小都可以是不同的,完全由实际的需求决定,如下图所示:
内存区所需的内存空间可以由编译器静态分配,也可以使用 malloc 函数动态分配,只需要保证分配给内存区的内存空间不被释放即可。
注意:内存池中的内存块是通过单链表连接起来的,类似于消息池,内存池在创建的时候内存块地址是连续的,但是经过多次申请以及释放后,空闲内存块列表的内存块在地址上不一定是连续的。
10.2 内存管理相关API函数
- 函数
OSMemCreate()
void OSMemCreate (OS_MEM *p_mem,
CPU_CHAR *p_name,
void *p_addr,
OS_MEM_QTY n_blks,
OS_MEM_SIZE blk_size,
OS_ERR *p_err)
函数OSMemCreate()的形参描述,如下表所示:
函数 OSMemCreate()的错误代码描述,如下表所示:
- 函数
OSMemGet()
void *OSMemGet (OS_MEM *p_mem,
OS_ERR *p_err)
- 返回值:void*类型返回值,指向内存块的起始地址
函数 OSMemGet()的形参描述,如下表所示:
函数 OSMemGet()的错误代码描述,如下表所示:
- 函数
OSMemPut()
void OSMemPut (OS_MEM *p_mem,
void *p_blk,
OS_ERR *p_err)
函数OSMemPut()的形参描述,如下表所示:
函数OSMemPut()的错误代码描述,如下表所示: