简 介
Azure RTOS ThreadX 是 Microsoft 提供的高级工业级实时操作系统 (RTOS)。它是专门为深度嵌入式实时 IoT 应用程序设计的。Azure RTOS ThreadX 提供高级计划、通信、同步、计时器、内存管理和中断管理功能。此外,Azure RTOS ThreadX 具有许多高级功能,包括 picokernel 体系结构、preemption-threshold 计划、event-chaining、执行分析、性能指标和系统事件跟踪。Azure RTOS ThreadX 非常易于使用,适用于要求极其苛刻的嵌入式应用程序。Azure RTOS ThreadX 在各种产品(包括消费者设备、医疗电子设备和工业控制设备)上的部署次数已达数十亿次。
具体的介绍和用户指南可以参考:
https://docs.microsoft.com/zh-cn/azure/rtos/threadx/
在前文描述移植基本内核的基础上,本文描述了如何基于MM32F3270系列MCU结合Azure RTOS ThreadX应用抢占任务preemption-threshold的使用,引导用户理解Azure RTOS ThreadX抢占任务preemption-threshold功能。
表 1 适用系列型号
系列 |
芯片型号 |
开发板 |
MM32F3270 |
MM32F3273G9P |
EVB-F3270 |
1
移植应用的准备
1.1 硬件开发板的准备
该移植过程中应用的开发板为MM32的EVB-F3270,板载MM32F3273G9P。
EVB-F3270 (MM32F3273G9P)的简要参数:
Arm Cortex-M3 内核
板载 MM32F3273G9P(LQFP144)
4 x Key、4 x LED
I2S Speaker
TF-Card
Ethernet PHY
1.2 软件的准备
库函数和例程(Lib Samples)
该移植过程中应用的 Firmware 分别为 MM32F3270 库函数和例程,下载地址:
https://www.mindmotion.com.cn/products/mm32mcu/mm32f/mm32f_mainstream/mm32f3270/
Azure RTOS ThreadX(源码)
ThreadX 的源代码已经开放,我们可以从 ThreadX 公共源代码存储库获取 Azure RTOS ThreadX,网址为:
https://github.com/azure-rtos/threadx/
具体的商用使用条件参考Azure的许可证说明:
https://www.microsoft.com/en-us/legal/intellectualproperty/tech-licensing/programs?msclkid=f7ab4ff3afa011ec90a79366a52034fa&activetab=pivot1:primaryr11
Microsoft publishes the Azure RTOS source code to GitHub. No license is required to install and use the software for internal development, testing, and evaluation purposes. A license is required to distribute or sell components and devices unless using Azure RTOS licensed hardware.
Azure RTOS 何时需要许可证?
Microsoft 将 Azure RTOS 源代码发布到 GitHub。安装和使用该软件进行内部开发、测试和评估无需许可证。分发或销售组件和设备需要许可证,除非使用 Azure RTOS 许可的硬件。
ThreadX 安装
可以通过将 GitHub 存储库克隆到本地计算机来安装 ThreadX。下面是用于在 PC 上创建 ThreadX 存储库的克隆的典型语法。
shell复制
git clone https://github.com/azure-rtos/threadx
或者,也可以使用 GitHub 主页上的“下载”按钮来下载存储库的副本。
下载后的仓库代码目录列表如下:
Azure RTOS ThreadX(源码)支持的开发环境
ThreadX 内核提供好了各种主流硬件平台和软件平台的移植文件,以Cortex_M3为例,可以支持以下六种开发环境:
本次移植过程使用Azure RTOS原有的sample_threadx.c Samples为例子,稍作修改,演示了抢占任务的功能与应用。
2
Threadx 抢占任务的应用
该章节介绍了抢占任务应用实现的背景与应用目的,该演示程序可在MM32F3273G9P的EVB-F3270上运行。
此示例在文件 main_preemption_demo.c中实现,旨在说明如何在嵌入式多线程环境中使用抢占任务,实现任务之间的优先级切换与抢占。
2.1 任务优先级与切换的知识
2.1.1 线程优先级
ThreadX 在创建线程时通过分配表示其“优先级”的数值来确定线程的重要性。
ThreadX 的最大优先级数可在 32 到 1024(增量为 32)之间进行配置。
因此:
TX_MAX_PRIORITIES的宏定义设置的数值必须是32的整数倍:
#define TX_MAX_PRIORITIES 32
实际的最大优先级数由 TX_MAX_PRIORITIES 常数在 ThreadX 库的编译过程中确定。设置更多优先级并不会显著增加处理开销。但是,每个包含 32 个优先级的组额外需要 128 字节的 RAM 进行管理。例如,32 个优先级需要 128 字节的 RAM,64 个优先级需要 256 字节的 RAM,而 96 个优先级需要 384 字节的 RAM。
默认情况下,ThreadX 具有 32 个优先级,范围从优先级 0 到优先级 31。数值越小,优先级越高。因此,优先级 0 表示最高优先级,而优先级 (TX_MAX_PRIORITIES-1) 表示最低优先级。
ThreadX没有空闲任务,如果大家创建空闲任务,需要将其设置为最低优先级。
通过协作调度或时间切片,多个线程可以具有相同的优先级。
此外,线程优先级还可以在运行时更改。
2.1.2 线程切换调度
ThreadX 根据线程的优先级来调度线程。优先级最高的就绪线程最先执行。如果有多个具有相同优先级的线程准备就绪,则按照先进先出 (FIFO) 的方式执行。
ThreadX操作系统支持三种调度方式:抢占式调度,时间切片调度和轮询式调度。
在一般的应用中,主要是抢占式调度和时间切片调度,轮询式调度用到的很少。
轮循任务调度
ThreadX 支持通过轮循调度处理具有相同优先级的多个线程。此过程通过以协作方式调用 tx_thread_relinquish 来实现。此服务为相同优先级的所有其他就绪线程提供了在 tx_thread_relinquish 调用方再次执行之前执行的机会。
时间切片任务调度
“时间切片”是轮循调度的另一种形式。时间片指定线程在不放弃处理器的情况下可以执行的最大计时器时钟周期数(计时器中断)。在 ThreadX 中,时间切片按每个线程提供。线程的时间片在创建时分配,可在运行时修改。当时间片过期时,具有相同优先级的所有其他就绪线程有机会在时间切片线程重新执行之前执行。
当线程挂起、放弃、执行导致抢占的 ThreadX 服务调用或自身经过时间切片后,该线程将获得一个新的线程时间片。
当时间切片的线程被抢占时,该线程将在其剩余的时间片内比具有相同优先级的其他就绪线程更早恢复执行。
抢占式任务调度
抢占任务调度是为了支持优先级更高的线程而暂时中断正在执行的线程的过程。每个任务都有不同的优先级,任务会一直运行直到被高优先级任务抢占或者遇到阻塞式的API函数。
此过程对正在执行的线程不可见。当更高优先级的线程完成时,控制权将转交回发生抢占的确切位置。这是实时系统中一项非常重要的功能,因为该功能有助于快速响应重要的应用程序事件。
尽管抢占是一项非常重要的功能,但也可能导致各种问题,包括资源不足、开销过大和优先级反转。为了缓解抢占的一些固有问题,ThreadX 提供了一个独特的高级功能,名为抢占阈值 (Preemption-threshold)。
抢占阈值允许线程指定禁用抢占的优先级上限。优先级高于上限的线程仍可以执行抢占,但不允许优先级低于上限的线程执行抢占。
例如,假设优先级为 20 的线程只与一组优先级介于 15 到 20 之间的线程进行交互。在其关键部分中,优先级为 20 的线程可将其抢占式阀值设置为 15,从而防止该线程和与之交互的所有线程发生抢占。这仍允许(优先级介于 0和 14 之间)真正重要的线程在其关键部分处理中抢占此线程的资源,这会使处理的响应速度更快。
当然,仍有可能通过将其抢占式阀值设置为 0 来为线程禁用所有抢占。此外,可以在运行时更改抢占阈值。
备注:
使用抢占阈值会禁用指定线程的时间切片。
优先级继承
ThreadX 还支持其互斥服务内的可选优先级继承。优先级继承允许低优先级线程暂时假设正在等待归较低优先级线程所有的互斥的高优先级线程的优先级。借助此功能,应用程序可以消除中间优先级线程的抢占,从而避免出现不确定的优先级倒置。当然,也可以使用“抢占式阀值”获得类似的结果。
2.1.3 抢占式任务调度
任务优先级需要解决的问题:
轮循任务调度
选择线程优先级是多线程处理最重要的方面之一。有时很容易根据感知的线程重要性概念来分配优先级,而不是确定运行时到底需要什么。滥用线程优先级会导致其他线程资源枯竭、产生优先级反转、减少处理带宽,致使应用程序出现难以理解的运行时行为。
如前所述,ThreadX 提供基于优先级的抢占式调度算法。优先级较低的线程只能在没有更高优先级的线程可以执行时才会执行。如果优先级较高的线程始终准备就绪,则不会执行优先级较低的线程。这种情况称为线程资源不足。
大多数线程资源不足的问题都是在调试初期检测到的,可通过确保优先级较高的线程不会连续执行来解决。另外,还可以在应用程序中添加此逻辑,以便逐渐提高资源不足的线程的优先级,直到有机会执行这些线程。
与线程优先级相关的另一个缺陷是“优先级倒置”。当优先级较高的线程由于优先级较低的线程占用其所需资源而挂起时,将会发生优先级倒置。当然,在某些情况下,有必要让两个优先级不同的线程共享一个公用资源。如果这些线程是唯一处于活动状态的线程,优先级倒置时间就与低优先级线程占用资源的时间息息相关。这种情况既具有确定性又非常正常。不过,如果在这种优先级反转的情况,中等优先级的线程变为活动状态,优先级反转时间就不再确定,并且可能导致应用程序失败。
主要有三种不同的方法可防止 ThreadX 中出现不确定的优先级反转。
首先,在设计应用程序优先级选择和运行时行为时,可以采用能够防止出现优先级反转问题的方式。
其次,优先级较低的线程可以利用抢占阈值来阻止中等优先级线程在其与优先级较高的线程共享资源时执行抢占。
最后,使用 ThreadX 互斥对象保护系统资源的线程可以利用可选的互斥优先级继承来消除不确定的优先级反转。
优先级开销
要减少多线程处理开销,最容易被忽视的一种方法是减少上下文切换的次数。如前所述,当优先级较高的线程执行优先于正在执行的线程时,则会发生上下文切换。值得一提的是,在发生外部事件(例如中断)和执行线程发出服务调用时,优先级较高的线程可能变为就绪状态。
为了说明线程优先级对上下文切换开销的影响,假设有一个包含三个线程的环境,这些线程分别命名为 thread_1、thread_2 和 thread_3。进一步假定所有线程都处于等待消息的挂起状态。当 thread_1 收到消息后,立即将其转发给 thread_2。随后,thread_2 将此消息转发给 thread_3。Thread_3 只是丢弃此消息。每个线程处理其消息后,则会返回并等待另一个消息。
执行这三个线程所需的处理存在很大的差异,具体取决于其优先级。如果所有线程都具有相同优先级,则会在执行每个线程之前发生一次上下文切换。当每个线程在空消息队列中挂起时,将会发生上下文切换。
但是,如果 thread_2 的优先级高于 thread_1,thread_3 的优先级高于 thread_2,上下文切换的次数将增加一倍。这是因为当其检测到优先级更高的线程现已准备就绪时,会在 tx_queue_send 服务中执行另一次上下文切换。
ThreadX 抢占阈值机制可以避免出现这些额外的上下文切换,并且仍支持前面提到的优先级选择。这是一项重要的功能,因为该功能允许在调度期间使用多个线程优先级,同时避免在线程执行期间出现一些不需要的上下文切换。
这里仅对抢占式调度进行说明。
运行条件:
创建 3 个任务 Task1,Task2 和 Task3。
Task1 的优先级为 1,Task2 的优先级为 2,Task3 的优先级为 3。
没有使用抢占式任务调度,Task1首先开始运行,此时虽然Task2 就绪,只能在等任务1 完全空闲下来或完成任务;Task2 任然在运行,此时虽然Task3也就绪了,同样需要等任务2空闲下来或者完成任务,才能转成运行态,执行程序运行。
而使用任务抢占式调度方式,运行过程变为:
A. 任务 Task1 在运行中,运行过程中由于 Task2 就绪(A点),在抢占式调度器的作用下任务 Task2 抢占Task1 的执行。Task2 进入到运行态,Task1 由运行态进入到就绪态。
B. 任务 Task2 在运行中,运行过程中由于 Task3 就绪(B点),在抢占式调度器的作用下任务 Task3 抢占 Task2的执行。Task3 进入到运行态,Task2 由运行态进入到就绪态。
C. 任务 Task3 运行过程中调用了阻塞式 API 函数(C点),比如 tx_thread_sleep,任务 Task3 被挂起,在抢占式调度器的作用下查找到下一个要执行的最高优先级任务是 Task2,任务 Task2 由就绪态进入到运行态。
D. 任务 Task2 运行完成(完成时结束任务2的抢占)前,任务3的阻塞或Sleep时间已经结束。下一一步继续执行的任务是 Task3,任务 Task3完成后,解除本次抢占。任务1由就绪态进入到运行态,直到运行完成。
2.2 Azure Threadx 任务抢占的相关函数
tx_thread_preemption_change 更改应用程序线程的抢占阈值
UINT tx_thread_preemption_change(
TX_THREAD *thread_ptr,
UINT new_threshold,
UINT *old_threshold);
函数说明
此服务更改指定线程的抢占阈值。抢占阈值阻止指定线程被等于或小于抢占阈值的线程抢占。
备注:
使用抢占阈值会禁用指定线程的时间切片。
参数
thread_ptr:指向以前创建的应用程序线程的指针。
new_threshold:新的抢占阈值优先级(0 至 TX_MAX_PRIORITIES-1)。
old_threshold:指向恢复为上一个抢占阈值的位置的指针。
返回值
TX_SUCCESS:(0X00) 成功更改抢占阈值。
TX_THREAD_ERROR:(0X0E) 应用程序线程指针无效。
TX_THRESH_ERROR:(0x18) 指定的新抢占阈值是无效的线程优先级(值不介于 0 到 TX_MAX_PRIORITIES-1 之间)或大于(优先级更低)当前线程优先级。
TX_PTR_ERROR:(0x03) 指向之前的抢占阈值存储位置的指针无效。
NX_CALLER_ERROR:(0x13) 此服务的调用方无效。
具体函数的中文说明可以参考:
https://docs.microsoft.com/zh-cn/azure/rtos/threadx/chapter4
具体函数的英文说明可以参考:
https://docs.microsoft.com/en-us/azure/rtos/threadx/threadx-smp/chapter4#tx_event_flags_set
示例
TX_THREAD my_thread;
UINT my_old_threshold;
UINT status;
/* Disable all preemption of the specified thread. The
current preemption-threshold is returned in
"my_old_threshold". Assume that "my_thread" has
already been created. */
status = tx_thread_preemption_change(&my_thread,
0, &my_old_threshold);
/* If status equals TX_SUCCESS, the application thread is
non-preemptable by another thread. Note that ISRs are
not prevented by preemption disabling. */
2.3 抢占任务的应用演示
2.3.1 工程目录的建立
打开目标工程文件夹“MM32F3270Project”:
移除原有样例.c 文件sample_threadx.c:
参考sample_threadx.c建立main_preemption_demo.c文件,并添加到工程项目中。
该Demo程序还用到了Led和阻塞时延时,因此还需添加led.c和delay.c。
特别要注意的是需要在led.c中配置USE_SYSTICK_DELAY 为 0。
#define USE_SYSTICK_DELAY 0
2.3.2 创建抢占任务示例
举一个极端点的例子:修改抢占任务阈值为0,以确保不被其他任务抢占。
创建一个待使用的抢占任务,并在任务中修改抢占任务阈值。
void thread_5_entry(ULONG thread_input)
{
UINT status;
ULONG actual_flags;
UINT old_threshold;
UINT i=10;
/* This thread simply waits for an event in a forever loop. */
while(1) {
// set preemption to 0(the highest, equal to close the thread task interrupt.
status = tx_thread_preemption_change(&thread_5, THREAD5_PREEMPTION_THRESHOLD_NEW, &old_threshold);
if(status == TX_SUCCESS)
{
//these code show how the tx_thread_preemption_change affect
//user can add code which can not be interruptted by other thread
while(i--){
LED1_TOGGLE();
DELAY_Ms(100);
}
i=10;
//restore preemption
tx_thread_preemption_change(&thread_5, THREAD5_PREEMPTION_THRESHOLD, &old_threshold);
}
tx_thread_sleep(100);
/* Increment the thread counter. */
thread_5_counter++;
/* Wait for event flag 0. */
status = tx_event_flags_get(&event_flags_0, 0x1, TX_OR_CLEAR,
&actual_flags, TX_WAIT_FOREVER);
/* Check status. */
if ((status != TX_SUCCESS) || (actual_flags != 0x1))
break;
}
}
3
Threadx 的抢占任务应用实现与调试
3.1 代码实现
下载调试默认会运行到main()函数,如下为全部实现的代码。
Demo演示代码
/* This is a small demo of the high-performance ThreadX kernel. It includes examples of six
threads of different priorities, using a message queue, semaphore, and an event flags group. */
#include "tx_api.h"
#include "delay.h"
#include "led.h"
#define DEMO_STACK_SIZE 1024
#define THREAD0_PRIORITY 1
#define THREAD0_PREEMPTION_THRESHOLD 1
#define THREAD5_PRIORITY 4
#define THREAD5_PREEMPTION_THRESHOLD 4
#define THREAD5_PREEMPTION_THRESHOLD_NEW 0
/* Define the ThreadX object control blocks... */
TX_THREAD thread_0;
TX_THREAD thread_5;
TX_EVENT_FLAGS_GROUP event_flags_0;
/* Define the counters used in the demo application... */
ULONG thread_0_counter;
ULONG thread_5_counter;
/* Define the thread stacks. */
UCHAR thread_0_stack[DEMO_STACK_SIZE];
UCHAR thread_5_stack[DEMO_STACK_SIZE];
/* Define thread prototypes. */
void thread_0_entry(ULONG thread_input);
void thread_5_entry(ULONG thread_input);
/* Define main entry point. */
int main()
{
LED_Init();
DELAY_Init(); //can not use systick
/* Enter the ThreadX kernel. */
tx_kernel_enter();
}
/* Define what the initial system looks like. */
void tx_application_define(void* first_unused_memory)
{
/* Create the main thread. */
tx_thread_create(
&thread_0,
"thread 0",
thread_0_entry,
0,
thread_0_stack,
DEMO_STACK_SIZE,
THREAD0_PRIORITY,
THREAD0_PREEMPTION_THRESHOLD,
TX_NO_TIME_SLICE,
TX_AUTO_START);
/* Create thread 5. This thread simply pends on an event flag which will be set
by thread_0. */
tx_thread_create(
&thread_5,
"thread 5",
thread_5_entry,
5,
thread_5_stack,
DEMO_STACK_SIZE,
THREAD5_PRIORITY,
THREAD5_PREEMPTION_THRESHOLD,
TX_NO_TIME_SLICE,
TX_AUTO_START);
/* Create the event flags group used by threads 0 and 5. */
tx_event_flags_create(&event_flags_0, "event flags 0");
}
/* Define the test threads. */
void thread_0_entry(ULONG thread_input)
{
UINT status;
/* This thread simply sits in while-forever-sleep loop. */
while(1) {
/* Increment the thread counter. */
thread_0_counter++;
/* Sleep for 10 ticks. */
tx_thread_sleep(10);
/* Set event flag 0 to wakeup thread 5. */
status = tx_event_flags_set(&event_flags_0, 0x1, TX_OR);
/* Check status. */
if (status != TX_SUCCESS)
break;
}
}
void thread_5_entry(ULONG thread_input)
{
UINT status;
ULONG actual_flags;
UINT old_threshold;
UINT i=10;
/* This thread simply waits for an event in a forever loop. */
while(1) {
// set preemption to 0(the highest, equal to close the thread task interrupt.
status = tx_thread_preemption_change(&thread_5, THREAD5_PREEMPTION_THRESHOLD_NEW, &old_threshold);
if(status == TX_SUCCESS)
{
//these code show how the tx_thread_preemption_change affect
//user can add code which can not be interruptted by other thread
while(i--){
LED1_TOGGLE();
DELAY_Ms(100);
}
i=10;
//restore preemption
tx_thread_preemption_change(&thread_5, THREAD5_PREEMPTION_THRESHOLD, &old_threshold);
}
tx_thread_sleep(100);
/* Increment the thread counter. */
thread_5_counter++;
/* Wait for event flag 0. */
status = tx_event_flags_get(&event_flags_0, 0x1, TX_OR_CLEAR,
&actual_flags, TX_WAIT_FOREVER);
/* Check status. */
if ((status != TX_SUCCESS) || (actual_flags != 0x1))
break;
}
}
3.2 下载与调试
进入调试模式,可以看到接收抢占任务中闪灯程序运行时,其他高优先级的任务不能从就绪态转为运行态,Demo演示成功。
该demo示例,建立了2个线程,程序执行一个发送事件标志,另外一个线程接收事件标志,实现执行Task Counter与发送&接收的Message Counter的累加。
全速运行后,可以看到相关的数值在变化:
但由于任务5中有运行长时间的抢占任务LED0 Toggle的动作,由thread_5_counter于thread_0_counter的巨大差值可以看出:该抢占任务导致Thread0中的发送到Thread5的事件标志,很多未被接收。从而成功的演示的抢占任务的特性。
4
小结
Azure RTOS ThreadX 的使用抢占任务可以方便实现任务优先级切换,避免的任务优先级的缺陷,结合MM32F3270的强大性能,可以实现Azure RTOS的强大的功能。
评论
查看更多