测试说明一下 TMOS 的调度机制    ...... 矜辰所致

前言

在之前我们学习过 TMOS 的使用,随着我们学习使用的深入,在实际应用中,为了保证某些任务/事件的执行顺序和实时性,我们开始考虑 TMOS 的调度是怎样的一个逻辑:

  • 不同任务的不同事件同时置位的执行顺序?
  • 同一个任务的不同事件同时置位的执行顺序?
  • 一次轮询执行会执行几个事件?
  • 在主循环中,加入其他进操作,对 TMOS 轮询有啥影响?

所以本文我们来测试说明一下 TMOS 调度的一些问题。

相关博文:
沁恒微 蓝牙 芯片 TMOS 使用
.
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!

一、基础说明

TMOS 的基础使用 请参考 沁恒微 蓝牙 芯片 TMOS 使用

使用 TMOS 调度器,会在主循环里面一直轮询:

__HIGH_CODE
__attribute__((noinline))
void Main_Circulation()
{
    while(1)
    {
        TMOS_SystemProcess();      
    }
}

只有开始执行 TMOS_SystemProcess() ,才会开始执行任务,这个函数就表示 TMOS 轮询一次,根据任务的优先级(先注册的任务会有更高的优先级)去执行任务,当然也包括蓝牙协议栈的核心任务(蓝牙协议栈的任务应该是最高优先级,这个是库内部实现的事情)。

需要知道的是,TMOS 的阻塞运行的,它一定是执行完了某个任务的事件之后,再进行下一个任务的执行。 所以之前文章我们说到,不要在单个 TMOS 任务中执行超过连接间隔一半时长的任务,就是因为如果阻塞时间过长,就会影响蓝牙通讯。

问题:对于本文来说,我们还要搞清楚,如果我一个优先级高的任务 a ,我执行完了一个事件 1 返回以后,我还有一个事件 2 也置位了,它会继续执行优先级高的任务 a 里面的事件 2,还是会去执行低优先级 b 里面置位的任务。

二、测试说明

我们知道 TMOS 会轮询,但是为了搞清任务执行的顺序 和 一次执行的数量 这些相关的问题 ,我们可以来进行一些测试。

2.1 测试一(任务执行顺序和一次轮询执行多少任务)

思路如下:

1、主循环中加入其他操作一直执行

2、然后使用两个任务a,b,
3、每个任务设置 2 个事件, a1,a2,b1,b2
4、在初始化的时候,就直接把这 4 个事件置位(就等于 TMOS 在一次扫描的时候,这两个任务都有事件需要执行。)
5、观察执行效果

其实通过这个测试可以说明很多问题,我们先看测试代码:

#define a1_EVENT      0x0001
#define a2_EVENT      (0x0001<<1)

#define b1_EVENT      0x0001
#define b2_EVENT      (0x0001<<1)

static uint8_t TASK_a_ID = INVALID_TASK_ID; // Task ID for internal task/event processing 
static uint8_t TASK_b_ID = INVALID_TASK_ID; // Task ID for internal task/event processing 

uint16_t Task_a_ProcessEvent(uint8_t task_id, uint16_t events)
{
    if(events & SYS_EVENT_MSG){
        return (events ^ SYS_EVENT_MSG);
    }

    if(events & a1_EVENT){
        PRINT("a1 run\n");
        return (events ^ a1_EVENT);
    }

    if(events & a2_EVENT){
        PRINT("a2 run\n");
        return (events ^ a2_EVENT);
    }
    return 0;
}

uint16_t Task_b_ProcessEvent(uint8_t task_id, uint16_t events)
{
    if(events & SYS_EVENT_MSG){
        return (events ^ SYS_EVENT_MSG);
    }

    if(events & b1_EVENT){
        PRINT("b1 run\n");
        return (events ^ b1_EVENT);
    }

    if(events & b2_EVENT){
        PRINT("b2 run\n");
        return (events ^ b2_EVENT);
    }
    return 0;
}


__HIGH_CODE
__attribute__((noinline))
void Main_Circulation()
{
    while(1)
    {
        TMOS_SystemProcess();
        PRINT("AAA!\n");
    }
}

int main(void)
{
	//前面神略...
	PRINT("%s\n", VER_LIB);
	CH58x_BLEInit();
	HAL_Init();
	
	TASK_a_ID = TMOS_ProcessEventRegister(Task_a_ProcessEvent);
	TASK_b_ID = TMOS_ProcessEventRegister(Task_b_ProcessEvent);
	
	PRINT("TASK_a_ID : %d\n", TASK_a_ID);
	PRINT("TASK_b_ID : %d\n", TASK_b_ID);
	tmos_set_event(TASK_b_ID,b2_EVENT);
	tmos_set_event(TASK_b_ID,b1_EVENT);
	tmos_set_event(TASK_a_ID,a2_EVENT);
	tmos_set_event(TASK_a_ID,a1_EVENT);
	
	// GAPRole_PeripheralInit();
	// Peripheral_Init();
	Main_Circulation();
}

大家可以先 猜测一下程序运行结果, a1,b1,a2,b2, 和 AAA 会怎么运行 。

测试结果如下:

在这里插入图片描述

如果我们把注册 ID 的语句调换一下:

TASK_b_ID = TMOS_ProcessEventRegister(Task_b_ProcessEvent);
TASK_a_ID = TMOS_ProcessEventRegister(Task_a_ProcessEvent);

测试结果如下:

在这里插入图片描述

测试小结

通过这个测试结果,我们可以回答之前的几个问题:

  1. 一次 TMOS_SystemProcess(); 只会进入一个任务的回调函数 。
    注意,这里为什么不说一次只执行一个事件,是因为执行几个事件不是应用层自己的业务逻辑,并不是 TMOS 决定的,TMOS 发现有任务事件置位了,就会进入该任务的回调函数,函数里面怎么执行,是应用层自己写的,注意我们在每个事件执行里面都写了 return 语句!
    一个任务回调函数返回了,TMOS 跳转到下一个任务的时候会暂时释放控制权,在主循环里打印了AAA 。

  2. 先注册的任务优先级更高(注册回调函数时返回 ID 值越小),TMOS 最开始执行的时候,按照优先级的顺序执行,但是执行返回以后,TMOS 不会从新遍历整个任务列表,它会往下面轮询其他的任务,即便刚刚执行完的任务有更高优先级并且还有事件置位,它也会去找下一个任务。

  3. 主函数中加入其他操作,会在 TMOS 一次轮询结束之后,顺序执行。

2.1 测试二(事件执行顺序和数量说明)

事件的执行其实完全是应用层面的事情,如下语句:

if(events & b1_EVENT){
    PRINT("b1 run\n");
    return (events ^ b1_EVENT);
}

每次执行完了就退出返回,如果我们执行完事件继续让他置位,那么它后面的事件永远不会执行。

关于这个点 ,直接看程序很好理解,只不过有很多伙伴因为 TMOS 的关系,所以会想着,这些事件有没有优先级,同一个任务中事件与事件之间会不会抢占之类的问题,往往就忽略了,这只是一个正常的回调函数,一个普通的程序逻辑。

我们也可以做个简单的测试,在上面测试基础上添加:

1、主循环中加入其他操作一直执行
2、然后使用两个任务a,b,
3、每个任务设置 2 个事件, a1,a2,b1,b2
4、在初始化的时候,就直接把这 4 个事件置位(就等于 TMOS 在一次扫描的时候,这两个任务都有事件需要执行。)
5、在每一个事件执行代码中,依然把本事件置位。
6、观察执行效果

测试代码(其他地方不变,):

uint16_t Task_a_ProcessEvent(uint8_t task_id, uint16_t events)
{
    if(events & SYS_EVENT_MSG){
        return (events ^ SYS_EVENT_MSG);
    }

    if(events & a1_EVENT){
        PRINT("a1 run\n");
        tmos_set_event(TASK_a_ID,a1_EVENT);
        return (events ^ a1_EVENT);
    }

    if(events & a2_EVENT){
        PRINT("a2 run\n");
        tmos_set_event(TASK_a_ID,a2_EVENT);
        return (events ^ a2_EVENT);
    }
    return 0;
}

uint16_t Task_b_ProcessEvent(uint8_t task_id, uint16_t events)
{
    if(events & SYS_EVENT_MSG){
        return (events ^ SYS_EVENT_MSG);
    }

    if(events & b1_EVENT){
        PRINT("b1 run\n");
        tmos_set_event(TASK_b_ID,b1_EVENT);
        return (events ^ b1_EVENT);
    }

    if(events & b2_EVENT){
        PRINT("b2 run\n");
        tmos_set_event(TASK_b_ID,b2_EVENT);
        return (events ^ b2_EVENT);
    }
    return 0;
}

测试结果如下:

在这里插入图片描述

结果很好理解,因为每次进入任务回调,写在前面的事件先执行,然后如果执行完毕置位了,下一次轮询到自己依旧还是执行写在前面的 。

我们可以想象,如果我们在每个事件处理程序中,我们不 return ,我们可以把事件全部执行,比如如下代码:

uint16_t Task_a_ProcessEvent(uint8_t task_id, uint16_t events)
{
    if(events & SYS_EVENT_MSG){
        return (events ^ SYS_EVENT_MSG);
    }

    if(events & a1_EVENT){
        PRINT("a1 run\n");
        tmos_set_event(TASK_a_ID,a1_EVENT);
        // return (events ^ a1_EVENT);
        events ^= a1_EVENT;
    }

    if(events & a2_EVENT){
        PRINT("a2 run\n");
        tmos_set_event(TASK_a_ID,a2_EVENT);
        // return (events ^ a2_EVENT);
        events ^= a2_EVENT;
    }
     return events; // 统一返回剩下未处理的事件比较合理
}

uint16_t Task_b_ProcessEvent(uint8_t task_id, uint16_t events)
{
    if(events & SYS_EVENT_MSG){
        return (events ^ SYS_EVENT_MSG);
    }

    if(events & b1_EVENT){
        PRINT("b1 run\n");
        tmos_set_event(TASK_b_ID,b1_EVENT);
        // return (events ^ b1_EVENT);
        events ^= b1_EVENT;
    }

    if(events & b2_EVENT){
        PRINT("b2 run\n");
        tmos_set_event(TASK_b_ID,b2_EVENT);
        // return (events ^ b2_EVENT);
        events ^= b2_EVENT;
    }
    return events; 
}

测试结果如下:

在这里插入图片描述

和说明的一样,通过这种方式,我们可以实现在一次轮询中实现多个事件同时执行(前提是这些事件都置位了),当然我们也可以选择性的只选众多事件中的某几个同时执行,也可以控制我们事件的先后执行顺序。

测试小结

通过这个测试结果,我们又可以来回答几个问题:

  1. 一次轮询能够执行的事件,是可以通过应用层的代码控制的,可以一次执行多个事件。
    但是一次轮询只会进一次任务的回调函数。

  2. 事件的执行循序也是纯应用层代码控制,哪个if(events & ) 写在前面,就先执行哪一个。

三、总结说明

本小节总结一下 测试的结论以及 一些对应的说明,如果对下面说明理解不了,还是需要去看一下上文的测试过程,上面每个测试后面都有小结说明,内容说明与上文小节会存在重复的说明,大家可以加深下印象(个人总结,有错误指出)。

  • TMOS 调度器只认任务,而不会去认事件,事件是任务自己的内部处理的:
    对调度器来说,每个任务只有一个 16 位的 events 变量。调度器看到这个变量不为 0,就直接把整个 events 传给该任务的注册回调函数,剩下的事全由任务自己内部处理。

  • 先注册的任务优先级更高(注册回调函数时返回 ID 值越小),TMOS 最开始执行的时候,按照优先级的顺序执行。

  • 程序中每执行一次 TMOS_SystemProcess(); TMOS 进行轮询一次, 每轮询一次只执行一个任务(只进一个任务的回调函数)。

  • TMOS 每次轮询,并不是遍历所有的任务,在执行完某个任务的时候,它的 ”指针 “ 指向了下一个有事件需要执行的任务。

  • TMOS 一次调度 执行事件数量和先后顺序取决于你自己的业务逻辑需求,可以一次执行多个事件。

  • 蓝牙任务内部也会有 任务 ID(ID 号最小,这个是库内部处理的事情,理论上来说应该是这样),蓝牙任务的执行也是和上面一样的 TMOS 轮询逻辑
    如果加上蓝牙做这个测试,可能需要考虑到 TMOS 轮询到蓝牙任务时候的一些情况。比如博主个人测试的时候,有时候连续打印了2个AAA,才执行应用层自己的测试任务。

  • TMOS 任务是阻塞操作,需要注意程序运行的时间。和之前说明的一样,不要在单个 TMOS 任务中执行超过连接间隔一半时长的任务,防止影响蓝牙通信。(这点通过我们上面的学习应该也很好理解,蓝牙操作也是 TMOS 的一个任务,其他任务阻塞,执行不到蓝牙任务。)

  • 主循环中加入其他操作,会在 TMOS 一次轮询结束之后,顺序执行,在实际应用中,可以把自己的业务逻辑放在主循环,但是这样不受 TMOS 管理,跑蓝牙相比之下容易影响蓝牙的时间线,需要特别注意自己业务的时间,不要阻塞太久,影响蓝牙通信 。

  • 实际应用中,如果有阻塞时间比较长的操作,可以拆分成多个小任务,同理,如果主循环中自己的其他业务有阻塞时间比较长的操作,也是拆分的思想,可以在自己其他业务逻辑中合理穿插 TMOS_SystemProcess(); 使得期间可以进行 TMOS 调度,保证蓝牙通信。

  • 还是为了避免阻塞时间过长,可以把代码放在 RAM 中运行,增加程序运行速率。
    在代码之前使用 __HIGH_CODE 修饰(但是也要注意使用芯片的 RAM 空间是否够用)。

  • 其他一些基础说明,请参考 沁恒微 蓝牙 芯片 TMOS 使用

结语

本文我们探讨了一下 TMOS 的调度机制,希望通过本文的一些测试,能让大家更加理解任务事件关系以及 TMOS 的调度机制,对大家今后使用 TMOS 有一定帮助。

好了,本文就到这里。谢谢大家!

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐