RTX V4随笔
参考自《安富莱STM32-V4开发板RTX教程》
任务管理
任务设置
RTX 操作系统的配置工作是通过配置文件 RTX_Conf_CM.c 实现。在 MDK 工程中打开文件RTX_Conf_CM.c,可以看到如下图所示的工程配置向导:
用于任务配置的主要是如下两个参数:
- Number of concurrent running tasks
参数范围 0 – 250
表示同时运行的最大任务数,这个数值一定要大于等于用户实际创建的任务数,空闲任务不包含在这个里面。比如当前的数值是 6,就表示用户最多可以创建 6 个任务。 - Number of tasks with user-provided stack
参数范围 0 – 250
表示自定义任务堆栈的任务数,如果这个参数定义为 0 的话,表示所有的任务都是使用的配置向导里面第三个参数 Task statck size 大小。比如:
Number of concurrent running tasks = 6
Number of tasks with user-provided stack = 0
表示允许用户创建 6 个任务,所有的 6 个任务都是分配第三个参数 Task statck size 大小的任务堆栈空间。
Number of concurrent running tasks = 6
Number of tasks with user-provided stack = 3
表示允许用户创建 6 个任务,其中 3 个任务是用户自定义任务堆栈大小,另外 3 个任务是用的第三个参数 Task statck size 大小的任务堆栈空间。
任务栈设置
不管是裸机编程还是 RTOS 编程,栈的分配大小都非常重要。局部变量,函数调时现场保护和返回地址,函数的形参,进入中断函数前和中断嵌套等都需要栈空间,栈空间定义小了会造成系统崩溃。 裸机的情况下,用户可以在这里配置栈大小:
不同于裸机编程,在 RTOS 下,每个任务都有自己的栈空间。任务的栈大小可以在配置向导中通过如下参数进行配置:
需要大家注意的是,默认情况下用户创建的任务栈大小是由参数 Task stack size 决定的。如果觉得每个任务都分配同样大小的栈空间不方便的话,可以采用自定义任务栈的方式创建任务。采用自定义方式更灵活些。
系统栈设置
在 RTOS 下,上面两个截图中设置的栈大小有了一个新的名字叫系统栈空间,而任务栈是不使用这里的空间的。任务栈不使用这里的栈空间,哪里使用这里的栈空间呢?答案就在中断函数和中断嵌套。
由于 Cortex-M3 和 M4 内核具有双堆栈指针,MSP 主堆栈指针和 PSP 进程堆栈指针,或者叫 PSP任务堆栈指针也是可以的。
在 RTX 操作系统中,主堆栈指针 MSP 是给系统栈空间使用的,进程堆栈指针 PSP 是给任务栈使用的。
也就是说,在 RTX 任务中,所有栈空间的使用都是通过 PSP 指针进行指向的。
一旦进入了中断函数已经可能发生的中断嵌套都是用的 MSP 指针。
这个知识点要记住他,当前可以不知道这是为什么,但是一定要记住。
实际应用中系统栈空间分配多大,主要是看可能发生的中断嵌套层数,下面我们就按照最坏执行情况进行考虑,所有的寄存器都需要入栈,此时分为两种情况:
-
64字节
对于 Cortex-M3 内核和未使用 FPU(浮点运算单元)功能的 Cortex-M4 内核在发生中断时需
要将 16 个通用寄存器全部入栈,每个寄存器占用 4 个字节,也就是 16*4 = 64 字节的空间。
可能发生几次中断嵌套就是要 64 乘以几即可。当然,这种是最坏执行情况,也就是所有的寄存
器都入栈。
(注:任务执行的过程中发生中断的话,有 8 个寄存器是自动入栈的,这个栈是任务栈,进入中
断以后其余寄存器入栈以及发生中断嵌套都是用的系统栈)。 -
200字节
对于具有 FPU(浮点运算单元)功能的 Cortex-M4 内核,如果在任务中进行了浮点运算,那么
在发生中断的时候除了 16 个通用寄存器需要入栈,还有 34 个浮点寄存器也是要入栈的,也就是
(16+34)*4 = 200 字节的空间。当然,这种是最坏执行情况,也就是所有的寄存器都入栈。
(注:任务执行的过程中发送中断的话,有 8 个通用寄存器和 18 个浮点寄存器是自动入栈的,
这个栈是任务栈,进入中断以后其余通用寄存器和浮点寄存器入栈以及发生中断嵌套都是用的系
统栈。)。
RTX初始化与启动
使用如下任一函数即可实现RTX的初始化:
- os_sys_init()
- os_sys_init_prio()
- os_sys_init_user()
其中os_sys_init_user详细讲解一下:
void os_sys_init_user (
void (*task)(void), /* 任务函数 */
U8 priority, /* 任务优先级 (1-254) */
void* stack, /* 任务栈 */
U16 size); /* 任务栈大小,单位字节*/
函数 os_sys_init_user 用于实现 RTX 操作系统的初始化和启动任务的创建,并且使用这个函数做初始化还可以自定义任务栈的大小。
- 第 1 个参数填启动任务的函数名。
- 第 2 个参数是任务的优先级设置,用户可以设置的任务优先级范围是 1-254。优先级 0 用于空闲任务,如果用户将这个参数设置为 0 的话,RTX 系统会将其更改为 1。优先级 255 被保留用于最重要的任务。
- 第 3 个参数是任务栈地址。
- 第 4 个参数是任务栈大小。
使用这个函数要注意以下几个问题
- 必须在 main 函数中调用 os_sys_init_user。
- 任务栈空间必须 8 字节对齐,可以将任务栈数组定义成 uint64_t 类型即可。
- 优先级 255 代表更重要的任务。
- 对于 RTX 操作系统来说,优先级参数中数值越小优先级越低,也就是说空闲任务的优先级是最低的,因为它的优先级数值是 0
任务创建
使用如下四个函数中任一函数即可实现RTX的任务创建:
- os_tsk_crete()
- os_tsk_create_ex()
- os_tsk_crete_user()
- os_tsk_crete_user_ex()
重点解释os_tsk_crete_user函数:
OS_TID os_tsk_create_user(
void (*task)(void), /* 任务函数 */
U8 priority, /* 任务优先级 (1-254) */
void* stk, /* 任务栈*/
U16 size ); /* 任务栈大小*/
函数 os_tsk_create_use 用于实现 RTX 操作系统的任务创建,并且还可以自定义任务栈的大小。
- 第 1 个参数填创建任务的函数名。
- 第 2 个参数是任务的优先级设置,用户可以设置的任务优先级范围是 1-254。优先级 0 用于空闲任务,如果用户将这个参数设置为 0 的话,RTX 系统会将其更改为 1。优先级 255 被保留用于更重要的任务。如果新创建的任务优先级比当前正在执行任务的优先级高,那么就会立即切换到高优先级任务去执行。
- 第 3 个参数是任务栈地址。
- 第 4 个参数是任务栈大小。
- 函数的返回值是任务的 ID,使用 ID 号可以区分不同的任务。
使用这个函数要注意以下问题
- 任务栈空间必须 8 字节对齐,可以将任务栈数组定义成 uint64_t 类型即可。
任务删除
使用如下四个函数中任一函数即可实现RTX的任务删除:
- os_tsk_delete
- os_tsk_delete_self
OS_RESULT os_tsk_delete (
OS_TID task_id ); /* 任务 ID */
函数 os_tsk_delete 用于实现 RTX 操作系统的任务删除
- 第 1 个参数填要删除任务的 ID。
- 如果任务删除成功,函数返回 OS_R_OK,其余所有情况返回 OS_R_NOK,比如所写的任务 ID 不存在。
使用这个函数要注意以下问题
- 如果用往此函数里面填的任务 ID 是 0 的话,那么删除的就是当前正在执行的任务,此任务被删除后,
RTX 会切换到任务就绪列表里面下一个要执行的高优先级任务。
任务优先级修改
通过如下函数可设置任务优先级:
- os_tsk_prio
- os_tsk_prio_self
OS_RESULT os_tsk_prio (
OS_TID task_id, /* 任务 ID */
U8 new_prio ); /* 新的任务优先级 (1-254) */
函数 os_tsk_prio 用于修改任务的优先级。
- 第 1 个参数填任务的 ID。如果 ID 参数是 0,那么设置就是当前任务的优先级。
- 第 2 个参数修改任务的优先级,如果 new_prio 的数值比当前执行任务的优先级大,将触发一次任务切换,切换到任务 ID 为 task_id 的任务中。如果 new_pro 的数值比当前执行任务的优先级小,当前任务会继续执行。
- 如果任务优先级修改成功,函数返回 OS_R_OK,其余所有情况返回 OS_R_NOK,比如所写的任务ID 不存在或者任务还没有启动。
**使用这个函数要注意以下几个问题 **
- 参数 new_prio 的范围是 1-254。
- 被修改任务的新优先级会一直保持有效直到用户再次修改。
- 优先级 0 用于空闲任务,如果用户将这个参数设置为 0 的话,RTX 系统会将其更改为 1。优先级 255
被保留用于最重要的任务。 - 对于 RTX 操作系统来说,优先级参数中数值越小优先级越低,也就是说空闲任务的优先级是最低的,因为它的优先级数值是 0
特权级和非特权级模式
对于使用 Cortex-M3 或者 M4 内核的芯片来说,RTX 操作系统可以让任务运行在特权级或者非特权级模式,这两种模式是 M3 或者 M4 内核本身所具有的特性。
在特权级模式下,用户可以访问和配置系统控制寄存器,比如 NVIC 中断控制器。然而,如果是在非特权级模式下,系统控制寄存器是不允许访问的,一旦访问将导致硬件异常。
- Unprivileged
非特权级,起到保护用户任务的作用,防止用户可以在任意任务中访问和修改系统寄存器,操作不当会造成系统崩溃。 - Privileged
特权级,这种模式下用户可以在任意任务中对系统控制寄存器的访问和修改。
非特权级模式下不可访问的寄存器
- 核心外设:主要是MPU、NVIC、SCB和STK四个单元。
- M3/M4内核的特殊功能寄存器:程序状态寄存器组(PSRs或xPSR)、中断屏蔽寄存器组(PRIMASK、FAULTMASK以及BASEPRI)、控制寄存器(CONTROL)
非特权级模式下核心外设寄存器的初始化
主要有以下两种方法进行初始化:
- 使用SVC(Supervisor Call)软中断
- 在初始化和开启RTX多任务前做核心外设的初始化
RTX任务特权等级的深入认识
深入了解 Cortex-M3/M4 内核的特权等级就不得不说说两种操作模式,Cortex-M3/M4 支持两种操作模式,两种操作模式分别是:
- Handler mode,中断模式,异常服务程序是处在中断模式。
- Thread mode,线程模式,异常服务程序以外的程序都是处在线程模式。
特权级 | 非特权级(用户级) | |
---|---|---|
异常handler的代码 | handler | 错误的模式 |
主应用程序的代码 | 线程模式 | 线程模式 |
中断和异常的区别
在 ARM 编程领域中,凡是打断程序顺序执行的事件,都被称为异常(exception)。
除了外部中断外, 当有指令执行了“非法操作”, 或者访问被禁的内存区间, 因各种错误产生的 fault, 以及不可屏蔽中断发生时,都会打断程序的执行,这些情况统称为异常。在不严格的上下文中,异常与中断也可以混用。
另外,程序代码也可以主动请求进入异常状态的( 常用于系统调用)。
当处理器处在线程状态下时,既可以使用特权级,也可以使用用户级;另一方面,handler 模式总是特权级的。在系统复位后,处理器进入线程模式+特权级。
在特权级下的代码可以通过置位 CONTROL[0]来进入用户级。而不管是任何原因产生了任何异常,处理器都将以特权级来运行其服务例程,异常返回后,系统将回到产生异常时所处的级别。用户级下的代码不能再试图修改 CONTROL[0]来回到特权级。它必须通过一个异常 handler,由那个异常 handler 来修改CONTROL[0],才能在返回到线程模式后拿到特权级。
为了避免系统堆栈因应用程序的错误使用而毁坏,我们可以给应用程序专门配一个堆栈,不让它共享操作系统内核的堆栈。在这个管理制度下,运行在线程模式的用户代码使用 PSP,而异常服务例程则使用MSP。这两个堆栈指针的切换是智能全自动的,就在异常服务的始末由硬件处理。
临界段、任务锁和中断锁
临界段
代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断。
中断锁
中断锁就是 RTOS 提供的开关中断函数。
任务锁
简单的说,为了防止当前任务的执行被其它高优先级的任务打断而提供的锁机制就是任务锁。实现任务锁可以通过给调度器加锁或者直接关闭RTOS内核定时器(就是前面一直说的系统滴答定时器)来实现。
- 通过给调度器加锁实现
给调度器加锁的话,就无法实现任务切换,高优先级任务也就无法抢占低优先级任务的执行,同时高优先级任务也是无法向低优先级任务切换的。像 uCOS-II 和 uCOS-III 是采用的这种方法实现任务锁。
特别注意,这种方式只是禁止了调度器工作,并没有关闭任何中断。 - 通过关闭 RTOS 内核定时器实现
关闭了 RTOS 内核定时器的话,也就关闭了通过 RTOS 内核定时器中断实现任务切换的功能,因为在退出定时器中断时需要检测当前需要执行的最高优先级任务,如果有高优先级任务就绪的话需要做任务切换。RTX 操作系统是采用的这种方式实现任务锁的。
使用如下函数可以实现任务锁的开锁和解锁:
- tsk_lock
- tsk_unlock
void tsk_lock (void);
函数 tsk_lock 用于禁止 RTX 内核定时器中断,因此也就禁止了任务切换。
使用这个函数要注意以下问题:
- 函数 tsk_lock 不支持嵌套调用。
- 不允许在中断服务程序中调用 tsk_lock。
- RTX 内核定时器被关闭期间,RTX 内核任务调度器和需要时间片调度的任务被阻塞。设置的任务延迟时间不再工作。因此,强烈建议关 RTX 内核定时器中断的时间越短越好。
延迟函数
- os_dly_wait函数
- os_itv_set函数
- os_itv_wait函数
- os_time_get函数
os_dly_wait延迟函数
函数 os_dly_wait 用于任务的延迟.参数 delay_time 用于设置延迟的时钟节拍个数,范围 1-0xFFFE。
注意:同一个任务中 os_dly_wait 和 os_itv_wait 不能混合调用,只能选择其中一个。
os_itv_set延迟函数
函数 os_itv_set 用于设置周期性延迟的时间间隔,此函数必须配合 os_itv_wait 函数一起使用。用户调用函数 os_itv_set 设置了周期性时间延迟的时间间隔后,然后调用函数 os_itv_wait 函数等待时间到。
参数 interval_time 用于设置周期性延迟的时间间隔,单位时钟节拍数,参数范围 1-0x7FFF。
os_itv_wait延迟函数
函数 os_itv_wait 函数用于等待周期性延迟时间到,此函数必须配合 os_itv_set 函数一起使用。用户调用函数 os_itv_set 设置了周期性时间延迟的时间间隔后,然后调用函数 os_itv_wait 函数等待时间到。
os_time_get延迟函数
函数 os_time_get 用于获取系统当前运行时钟节拍数。
函数 os_dly_wait 和 os_itv_wait 的区别
函数 os_dly_wait 实现的是周期性延迟,而函数 os_itv_wait 实现的是相对性延迟。
举例:
运行条件:
有一个 bsp_KeyScan 函数,这个函数处理时间大概耗时 2ms。 有两个任务,一个任务 Task1 是用的 os_dly_wait 延迟,延迟 10ms,另一个任务 Task2 是用的os_itv_wait 延迟,延迟 10ms。
不考虑任务被抢占而造成的影响。
实际运行过程效果:
Task1:
bsp_KeyScan+ os_dly_wait(10) ---> bsp_KeyScan + os_dly_wait(10)
|----2ms + 10ms 为一个周期------| |----2ms + 10ms 为一个周期----|
这个就是相对性的含义 。
Task2:
bsp_KeyScan + os_itv_wait ------> bsp_KeyScan + os_itv_wait
|----10ms 为一个周期(2ms 包含在 10ms 内)---| |----10ms 为一个周期------|
这就是周期性的含义。
事件标志组
任务间事件标志组的实现是指各个任务之间使用事件标志组实现任务的通信或者同步机制。
RTX 每个任务创建的时候,会自动创建 16 个事件标志,事件标志被存储到每个任务的任务控制块中。也就是说每个任务支持 16 个事件标志。
发送事件的中断任务在中断函数中应使用isr_evt_set
函数对事件标志设置。
实际应用中,建议不要在中断中实现消息处理,用户可以在中断服务程序里面发送消息通知任务,在任务中实现消息处理,这样可以有效的保证中断服务程序的实时响应。同时此任务也需要设置为高优先级,以便退出中断函数后任务可以得到及时执行。
事件标志组API函数
使用如下 6 个函数可以实现 RTX 的事件标志组
- os_evt_clr 清除至少一个事件标志。
- os_evt_get 获取事件标志,使 os_evt_wait_or 运行
- os_evt_set 设置至少一个事件标志
- os_evt_wait_and 等待最少所有的事件标志被设置
- os_evt_wait_or 等待至少一个事件标志被设置
- is_evt_set 设置至少一个事件标志
os_evt_set
void os_evt_set (
U16 event_flags, /* 16 位的事件标志设置 */
OS_TID task ); /* 要设置事件标志的任务 ID */
函数 os_evt_set 用于设置指定任务的事件标志。
- 第1个参数表示16个可设置的事件标志位。因为RTX的每个任务创建时有16个可设置的事件标志,这里用 U16 类型的变量 event_flag 就可以表示,变量 event_flag 的某个位设置为 1,那么指定 RTX任务的事件标志相应位就设置为 1。变量 event_flag 设置为 0 的位对 RTX 任务的事件标志相应位没有影响。比如设置变量 event_flag = 0x0003 就表示将 RTX 任务事件标志的位 0 和位 1 设置为 1,其余位没有变化。
- 第 2 个参数是任务 ID。
使用这个函数要注意以下问题:
- 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序中使用的是isr_evt_set
is_evt_set
void isr_evt_set (
U16 event_flags, /* 16 位的事件标志设置 */
OS_TID task ); /* 要设置事件标志的任务 ID */
相关描述参照os_evt_set。
使用这个函数要注意以下问题:
- 此函数是用于中断服务程序中调用的,故不可以在任务中调用此函数,任务中使用的是 os_evt_set。
- 事件标志函数 isr_evt_set 的调用不能太频繁,太频繁的话会大大增加系统内核的开销,会造成事件标志得不到及时处理从而造成丢失事件标志的情况。
os_evt_wait_and
函数 os_evt_wait_and 用于等待所有事件标志被设置。
OS_RESULT os_evt_wait_and (
U16 wait_flags, /* 16 位的事件标志等待 */
U16 timeout ); /* 超时时间设置 */
- 第 1 个参数表示任务等待的事件标志位。因为 RTX 的每个任务创建时有 16 个可以设置的事件标志,这里用 U16 类型的变量 event_flag 就可以设置,变量 event_flag 的那位设置为 1,那么 RTX 任务的事件标志就等待那个位被设置为 1。而且要所有要求的位都被设置为 1 才可以。比如设置变量event_flag = 0x0003 就表示 RTX 任务在等待事件标志的位 0 和位 1 都被设置为 1。
- 第 2 个参数表示设在的等待时间,范围 0-0xFFFF,当参数设置为 0-0xFFFE 时,表示等这么多个时钟节拍,参数设置为 0xFFFF 时表示无限等待直到事件标志满足要求。
- 函数返回 OS_R_EVT 表示等待的事件标志位都被设置了,也就是返回成功。返回 OS_R_TMO 表示超时。
使用这个函数要注意以下问题:
- 当要求的事件标志位都被设置为 1 时或者设置的超时时间溢出时,函数 os_evt_wait_and 才会返回。
- 如果函数 os_evt_wait_and 返回前所要求的事件标志位都设置了,那么此函数会在返回前将相应的事件标志位清零,其它位不受此影响。
os_evt_wait_or
函数 os_evt_wait_or用于等待至少一个事件标志被设置。
OS_RESULT os_evt_wait_or (
U16 wait_flags, /* 16 位的事件标志等待 */
U16 timeout ); /* 超时时间设置 */
- 第 1 个参数表示任务等待的事件标志位。因为 RTX 的每个任务创建时有 16 个可以设置的事件标志,这里用 U16 类型的变量 event_flag 就可以设置,变量 event_flag 的那位设置为 1,那么 RTX 任务的事件标志就等待那个位被设置为 1。只要有其中一个标志位被设置即可。比如设置变量event_flag = 0x0003 就表示 RTX 任务在等待事件标志的位 0 和位 1 任一位设置为 1即可。
- 第 2 个参数表示设在的等待时间,范围 0-0xFFFF,当参数设置为 0-0xFFFE 时,表示等这么多个时钟节拍,参数设置为 0xFFFF 时表示无限等待直到事件标志满足要求。
- 函数返回 OS_R_EVT 表示等待的事件标志位任一被设置了,也就是返回成功。返回 OS_R_TMO 表示超时。
使用这个函数要注意以下问题:
- 当要求的事件标志位任一被设置为 1 时或者设置的超时时间溢出时,函数 os_evt_wait_and 才会返回。
- 如果函数 os_evt_wait_and 返回前所要求的事件标志位任一设置了,那么此函数会在返回前将相应的事件标志位清零,其它位不受此影响。
消息邮箱
RTX 的消息邮箱实际上就是消息队列,通过内核提供的服务,任务或中断服务子程序可以将一个消息(注意,RTX 消息邮箱传递的是消息的地址而不是实际的数据)放入到消息队列。同样,一个或者多个任务可以通过内核服务从消息队列中得到消息。通常,先进入消息队列的消息先传给任务,也就是说,任务先得到的是最先进入到消息队列的消息,即先进先出的原则(FIFO)。
- 使用消息邮箱可以让 RTOS 内核有效的管理任务,全局数组是无法做到的,任务的超时等机制需要用户自己去实现。
- 使用了全局数组就要防止多任务的访问冲突,使用消息邮箱已经处理好了这个问题。用户无需担心。
- 使用消息邮箱可以有效的解决中断服务程序跟任务之间消息传递的问题。
- FIFO 机制更有利于数据的处理。
运行过程主要有以下两种情况:
- 中断服务程序向消息邮箱放数据地址,任务 Task1 从消息邮箱取数据地址,如果放数据地址的速度快于取数据的速度,那么会出现消息邮箱存放满的情况。由于中断服务程序里面的消息邮箱发送函数isr_mbx_send 不支持超时设置,所有发送前要通过函数 isr_mbx_check 检测邮箱是否满。
- 中断服务程序向消息邮箱放数据地址,任务 Task1 从消息邮箱取数据地址,如果放数据地址的速度慢于取数据的速度,那么会出现消息邮箱存为空的情况。在 RTX 的任务中可以通过函数 os_mbx_wait获取消息,因为此函数可以设置超时等待,直到消息邮箱中有消息存放或者设置时间超时。
上面就是一个简单 RTX 消息邮箱通信过程。实际应用中,中断方式的消息机制切记注意以下四个问题:
- 中断函数的执行时间越短越好,防止其它低于这个中断优先级的异常不能得到及时响应。
- 实际应用中,建议不要在中断中实现消息处理,用户可以在中断服务程序里面发送消息通知任务,在任务中实现消息处理,这样可以有效的保证中断服务程序的实时响应。同时此任务也需要设置为高优先级,以便退出中断函数后任务可以得到及时执行。
- 中断服务程序中一定要调用专用于中断的消息邮箱函数 isr_mbx_send,isr_mbx_receive 和isr_mbx_check。
- 在 RTX 操作系统中实现中断函数和裸机编程是一样的。
- 另外强烈推荐用户将 Cortex-M3 内核的 STM32F103 和 Cortex-M4 内核的 STM32F407 的NVIC 优先级分组设置为 4,即:NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);这样中断优先级的管理将非常方便。
- 用户要在 RTX 多任务开启前就设置好优先级分组,一旦设置好切记不可再修改。
消息邮箱API函数
- os_mbx_check
- os_mbx_declare
- os_mbx_init
- os_mbx_send
- os_mbx_wait
- isr_mbx_check
- isr_mbx_receive
- isr_mbx_send
os_mbx_send
OS_RESULT os_mbx_send (
OS_ID mailbox, /* 消息邮箱 ID标识 */
void* message_ptr, /* 消息指针,即数据的地址 */
U16 timeout ); /* 超时时间设置 */
函数 os_mbx_send 用于向消息邮箱存放数据指针,或者说数据地址。如果消息邮箱已经满了,调用此函数的任务将被挂起,等待消息邮箱可用,直到消息邮箱有空间可用或者超时时间溢出才会返回。
- 第 1 个参数填写消息邮箱的 ID 标识,即函数 os_mbx_declare 第一个参数。
- 第 2 个参数填写消息指针,即数据的地址。
- 第 3 个参数表示设置的等待时间,范围 0-0xFFFF,当参数设置为 0-0xFFFE 时,表示等待这么多个时钟节拍,参数设置为 0xFFFF 时表示无限等待直到消息邮箱有可用的空间。
- 返回值 OS_R_OK,表示消息指针成功放到消息邮箱中。 返回值 OS_R_TMO,表示消息邮箱已经满了,在设置的超时时间范围内也没有等到可用的空间。
使用这个函数要注意以下问题:
- 使用此函数前一定要调用函数 os_mbx_init 进行初始化。
- 此函数用于往消息邮箱放消息指针,另一个函数 os_mbx_wait 用于从消息邮箱取出消息指针,取出后相应的位置也就释放出来了,方便下次存放数据。
os_mbx_wait
OS_RESULT os_mbx_wait (
OS_ID mailbox, /* 消息邮箱的 ID标识 */
void** message, /* 存放消息指针的变量地址 */
U16 timeout ); /* 超时时间设置 */
函数 os_mbx_wait 用于从消息邮箱中获取消息。如果消息邮箱为空,调用此函数的任务将被挂起,直到消息邮箱有消息可用或是设置的超时时间溢出才会返回。
- 第 1 个参数填写消息邮箱的 ID 标识,即函数 os_mbx_declare 第一个参数。
- 第 2 个参数填写从消息邮箱中获取消息后,存放消息的指针变量。
- 第 3 个参数表示设置的等待时间,范围 0-0xFFFF,当参数设置为 0-0xFFFE 时,表示等待这么多个时钟节拍,参数设置为 0xFFFF 时表示无限等待直到消息邮箱有消息。
- 返回值 OS_R_OK,表示从消息邮箱中有消息,立即从消息邮箱中获得消息,无需等待。
返回值 OS_R_TMO,表示消息邮箱为空,在设置的超时时间范围内也没有等到消息。
返回值 OS_R_MBX,表示在设置的超时时间范围内收到消息。
isr_mbx_check
OS_RESULT isr_mbx_check (
OS_ID mailbox ); /*消息邮箱的 ID标识*/
函数 isr_mbx_check 用来检测消息邮箱剩余空间可以存储的消息个数。建议配合函数 isr_mbx_send 一起使用。
- 第 1 个参数填写消息邮箱的 ID 标识,即函数 os_mbx_declare 第一个参数。
- 函数返回消息邮箱剩余空间可以存储的消息个数。
使用这个函数要注意以下问题:
- 使用此函数前一定要调用函数 os_mbx_init 进行初始化。
- 此函数只能在中断服务程序中调用。
互斥信号量
互斥信号量就是信号量的一种特殊形式,也就是信号量初始值为 1 的情况。有些 RTOS 中也将信号量初始值设置为 1 的情况称之为二值信号量。为什么叫二值信号量呢?因为信号量资源被获取了,信号量值就是 0,信号量资源被释放,信号量值就是 1,把这种只有 0 和 1 两种情况的信号量称之为二值信号量。互斥信号量的主要作用就是对资源实现互斥访问。
互斥信号量可以防止优先级翻转,而二值信号量不支持。
优先级翻转问题
运行条件:
- 创建 3 个任务 Task1,Task2 和 Task3,优先级分别为 3,2,1。也就是 Task1 的优先级最高
- 任务 Task1 和 Task3 互斥访问串口打印 printf,采用二值信号实现互斥访问。
- 起初 Task3 通过二值信号量正在调用 printf,被任务 Task1 抢占,开始执行任务 Task1,也就是上图的起始位置。
运行过程描述如下:
- 任务 Task1 运行的过程需要调用函数 printf,发现任务 Task3 正在调用,任务 Task1 会被挂起,等待 Task3 释放函数 printf。
- 在调度器的作用下,任务 Task3 得到运行,Task3 运行的过程中,由于任务 Task2 就绪,抢占了 Task3的运行。优先级翻转问题就出在这里了,从任务执行的现象上看,任务 Task1 需要等待 Task2 执行完毕才有机会得到执行,这个与抢占式调度正好反了,正常情况下应该是高优先级任务抢占低优先级任务的执行,这里成了高优先级任务 Task1 等待低优先级任务 Task2 完成。所以这种情况被称之为优先级翻转问题。
- 任务 Task2 执行完毕后,任务 Task3 恢复执行,Task3 释放互斥资源后,任务 Task1 得到互斥资源,从而可以继续执行。
上面就是一个产生优先级翻转问题的现象。
RTX互斥信号量的实现
运行条件:
- 创建 2 个任务 Task1 和 Task2,优先级分别为 1 和 3,也就是任务 Task2 的优先级最高
- 任务 Task1 和 Task2 互斥访问串口打印 printf。
- 使用 RTX 的互斥信号量实现串口打印 printf 的互斥访问。
运行过程描述如下:
- 低优先级任务 Task1 执行过程中先获得互斥资源 printf 的执行。此时任务 Task2 抢占了任务 Task1的执行,任务 Task1 被挂起。任务 Task2 得到执行。
- 任务 Task2 执行过程中也需要调用互斥资源,但是发现任务 Task1 正在访问,此时任务 Task1 的优先级会被提升到跟 Task2 同一个优先级,也就是优先级 3,这个就是所谓的优先级继承(Priority inheritance),这样就有效的防止了优先级翻转问题。任务 Task2 被挂起,任务 Task1 有新的优先级继续执行。
- 任务 Task1 执行完毕并释放互斥资源后,优先级恢复到原来的水平。由于互斥资源可以使用,任务 Task2 获得互斥资源后开始执行。
上面就是一个简单 RTX 互斥信号量的实现过程。
互斥信号量API函数
- os_mut_init
- os_mut_release
- os_mut_wait
os_mut_wait
OS_RESULT os_mut_wait (
OS_ID mutex, /* OS_MUT 类型变量 */
U16 timeout ); /* 超时时间 */
函数 os_mut_wait 用于获取互斥信号量资源,如果互斥资源可用,那么调用函数 os_mut_wait 后可以成功获取互斥资源,在此函数的源码将计数值加 1(互斥信号量源码的实现上跟信号量不同)。如果互斥资源不可用,调用此函数的任务将由运行态转到挂起态,等待信号量资源可用,也就是计数值为 0 的时候。如果一个低优先级的任务通过互斥信号量正在访问互斥资源,那么当一个高优先级的任务也通过互斥信号量访问这个互斥资源的话,会将这个低优先级任务的优先级提升到和高优先级任务一样的优先级,这就是所谓的优先级继承,通过优先级继承可以有效防止优先级翻转问题。当低优先级任务释放了互斥资源之后,重新恢复到原来的优先级。
- 第 1 个参数填写数据类型为 OS_MUT 的变量,同时也作为 ID 标识
- 第 2 个参数表示设在的等待时间,范围 0-0xFFFF,当参数设置为 0-0xFFFE 时,表示等待这么多个时钟节拍,参数设置为 0xFFFF 时表示无限等待直到互斥资源可用。
- 函数返回 OS_R_MUT 表示函数设置的超时时间范围内收到互斥信号量可用资源。
函数返回 OS_R_TMO 表示超时。
函数返回 OS_R_OK 表示无需等待,立即获得互斥资源。
使用这个函数要注意以下问题:
- 使用此函数前一定要调用函数 os_mut_init 进行初始化。
os_mut_release
OS_RESULT os_mut_release (
OS_ID mutex ); /* OS_MUT 类型变量 */
函数 os_mut_release 用于释放互斥资源,调用此函数会将计数值减 1。只有当计数值减到 0 的时候其它的任务才可以获取互斥资源。也就是说如果用户调用 os_mut_wait 和 os_mut_release,需要配套使用。通过函数 os_mut_wait 实现互斥信号量计数值加 1,通过函数 os_mut_release 实现互斥信号量计数值减1 操作,这样的话,这两个函数可以实现嵌套调用,但是一定要保证成对调用,要不会造成互斥资源无法
正确释放。 如果拥有互斥资源的任务的优先级被提升了,那么此函数会恢复任务以前的优先级。
- 第 1 个参数参数填写数据类型为 OS_MUT 的变量,同时也作为 ID 标识。
- 返回值 OS_R_OK,表示互斥信号量成功释放。
返回值 OS_R_NOR,表示互斥信号量的内部计数值已经是 0 或者调用此函数的任务不是互斥资源的
拥有者。
使用这个函数要注意以下问题:
- 使用此函数前一定要调用函数 os_mut_init 进行初始化。
内存管理
在 ANSI C 中,可以用 malloc()和 free()2 个函数动态的分配内存和释放内存,但是,在嵌入式实时操作系统中,调用 malloc()和 free()却是危险的,因为多次调用这两个函数会把原来很大的一块连续内场区域逐渐地分割成许多非常小而且彼此又不相邻的内存块,也就是内存碎片。由于这些内存碎片的大量存在,使得程序到后来连一段非常小的连续内存也分配不到。另外,由于内存管理算法上的原因,malloc()和 free()函数的执行时间是不确定的。 在 RTX 中,操作系统把连续的大块内存按分区来管理。每个分区中包含整数个大小相同的内存块。
利用这种机制,就可以得到和释放固定大小的内存块。这样内存的申请和释放函数的执行时间就是确定的了。在一个系统中可以有多个内存分区,这样,应用程序就可以从不同的内存分区中得到不同大小的内存块。但是特定的内存块在释放时,必须重新放回到它以前所属于的内存分区。显然,采用这样的内存管理算法,上面的内存碎片文件就得到了解决。
其实 RTX 的内存管理也非常好理解,可以理解成一个二维数组,比如我们定义一个二维数组为:uint8_t mpool[10][32]。对应到 RTX 的内存管理上就是定义了 10 个内存块,每块大小是 32 字节。如果还需要其它大小的内存块,还可以多定义几个其它大小的。
内存管理API函数
- _declare_box
- _declare_box8
- _init_box
- _init_box8
- _alloc_box
- _calloc_box
- _free_box