FreeRTOS任务之深入篇

目录

  • 1.Tick
    • [1.1 Tick的概念](#1.1 Tick的概念)
    • [1.2 Tick与任务调度](#1.2 Tick与任务调度)
    • [1.3 Tick与延时函数](#1.3 Tick与延时函数)
  • 2.任务状态
    • [2.1 运行状态 (Running)](#2.1 运行状态 (Running))
    • [2.2 就绪状态 (Ready)](#2.2 就绪状态 (Ready))
    • [2.3 阻塞状态 (Blocked)](#2.3 阻塞状态 (Blocked))
    • [5.4 暂停状态 (Suspended)](#5.4 暂停状态 (Suspended))
    • [2.5 特殊状态:删除状态 (Deleted)](#2.5 特殊状态:删除状态 (Deleted))
    • [5.6 任务状态转换](#5.6 任务状态转换)
    • [2.7 实验](#2.7 实验)
  • 3.Delay函数
    • [3.1 两个函数](#3.1 两个函数)
    • [3.2 实验](#3.2 实验)
  • 4.空闲任务及其钩子函数
    • [4.1 空闲任务](#4.1 空闲任务)
    • [4.2 钩子函数](#4.2 钩子函数)
    • [4.3 实验](#4.3 实验)
  • 5.任务调度算法
    • [5.1 可抢占调度与合作调度](#5.1 可抢占调度与合作调度)
    • [5.2 时间片轮转 (Time Slicing)](#5.2 时间片轮转 (Time Slicing))
    • [5.3 空闲任务的让步策略 (Idle Task Yield)](#5.3 空闲任务的让步策略 (Idle Task Yield))
    • [5.4 调度策略的组合](#5.4 调度策略的组合)
    • [5.5 实验](#5.5 实验)
      • [5.5.1 抢占](#5.5.1 抢占)
      • [5.5.2 时间片轮流](#5.5.2 时间片轮流)
      • [5.5.3 空闲任务让步](#5.5.3 空闲任务让步)
  • 6.保存现场
  • 7.栈的创建
  • 8.任务的运行(调度)
  • 9.任务的切换
  • 疑问

1.Tick

1.1 Tick的概念

在FreeRTOS中,Tick是任务调度的基准,它提供了一种类似"时间片"的机制来切换任务。每个任务的执行时间通过Tick来划分,并根据Tick的到来来决定任务的切换。Tick的主要作用是进行任务调度、延时管理等。

  • Tick周期(Time Slice) :每个Tick的时间是由configTICK_RATE_HZ来控制的,单位是赫兹(Hz),表示每秒发生多少次Tick中断,Tick周期的长度为1 / configTICK_RATE_HZ秒。例如,如果configTICK_RATE_HZ=100,那么每个Tick周期的长度就是10ms每10毫秒就会触发一次Tick中断
  • 时间片(Time Slice):对于同一优先级的任务来说,操作系统会给它们分配一定的时间片。当Tick到来时,操作系统会检查当前的任务是否已经执行完它的时间片,如果没有,就会继续执行该任务;如果时间片已用完,操作系统会将当前任务挂起,切换到另一个任务。
  • t1处就发生了一次tick中断

1.2 Tick与任务调度

任务调度是基于时间片的,每个任务的运行时间被分割成多个Tick周期。当发生Tick中断时,系统会进行任务调度。

假设有两个任务,任务A和任务B,它们的优先级相同,Tick周期为10ms。如果configTICK_RATE_HZ=100,那么每个时间片的长度是10ms。任务A从t0开始执行,到t1时(即10ms后),发生了一个Tick中断。

  • Tick中断发生时,操作系统会选择下一个要运行的任务。如果任务A还没有用完它的时间片,操作系统不会立即切换任务,而是继续执行任务A直到时间片用完(即下一次Tick中断到来)。如果任务A已经执行完了,它就会进入挂起状态,操作系统会切换到任务B执行。
  • 任务切换 :假设在t1时发生了Tick中断,操作系统会检查任务A的执行情况。如果任务A已经用完它的时间片,操作系统就会切换到任务B。在t2时(下一次Tick中断),如果任务B还没有完成它的时间片,系统会再次切换到任务A,依此类推。

1.3 Tick与延时函数

经常需要让任务等待一段时间才能继续执行。这种等待通常是通过Tick来实现的。比如,vTaskDelay()函数就是基于Tick来进行延时的。

c 复制代码
vTaskDelay(2);  // 等待2个Tick

假设configTICK_RATE_HZ=100,那么每个Tick的周期是10ms,因此上面这个延时调用实际上会让任务暂停20ms。

为什么延时函数可能不精确

由于FreeRTOS的时间片是基于Tick中断的,任务的切换通常会在Tick中断的边缘发生,因此延时的精度受到Tick周期的限制。在某些情况下,任务可能会在时间片结束后略微延迟(例如,任务刚好在Tick的中断发生时运行,任务可能会比计划的时间晚一点被挂起)。因此,延时不一定会精确到预期的时间片长度,但这种误差通常较小。

2.任务状态

FreeRTOS中的任务状态主要有四种:

  • 运行状态 (Running):任务正在CPU上执行。
  • 就绪状态 (Ready):任务已经准备好,等待调度器分配CPU资源执行。
  • 阻塞状态 (Blocked):任务正在等待某个事件发生(例如等待时间结束、等待信号量、等待队列消息等),在此期间不占用CPU。
  • 暂停状态 (Suspended):任务被主动挂起,不会参与调度,直到被其他任务恢复。

2.1 运行状态 (Running)

任务处于运行状态 时,表示它正在执行,即正在被调度器分配CPU时间。任务只有在就绪状态 时才会进入运行状态。当任务的时间片到达(即Tick中断发生时),或者有更高优先级的任务准备好时,当前任务会被暂停,任务调度器会切换任务。

  • 任务A处于运行状态,并且没有被其他任务抢占。

2.2 就绪状态 (Ready)

任务处于就绪状态时,它已经准备好,可以被调度器选择执行,但可能因为其他任务正在执行或任务优先级较低而未被立即执行。处于就绪状态的任务会在系统空闲时或高优先级任务完成后被调度执行。

任务从阻塞状态暂停状态 转到就绪状态,通常是因为事件发生、超时结束、资源可用等。

示例

  • 任务B在完成了某些任务后进入就绪状态,等待调度器分配CPU资源。

2.3 阻塞状态 (Blocked)

阻塞状态 表示任务正在等待某个事件的发生,这个事件可以是时间事件 (如超时、延时)或同步事件(如信号量、队列数据、任务通知等)。任务处于阻塞状态时,它不占用CPU资源,直到等待的事件发生或超时。

常见的阻塞事件包括:

  • 时间相关的事件 :任务等待一段时间,或等待某个绝对时间点,例如使用vTaskDelay()延时一段时间。
  • 同步事件:任务等待其他任务或中断发出的信号。例如,任务A等待任务B给它发送数据,或者等待某个按钮按下。

阻塞状态下的任务会在事件发生时重新进入就绪状态,准备再次被调度。

示例

  • 任务C调用了vTaskDelay(100),等待100ms,期间进入阻塞状态,直到100ms到达。
  • 任务D在等待一个队列中的数据,如果队列为空,任务D会阻塞直到有数据可用。

其中针对同步事件,等待的某些信息可以如下

  • 队列(queue)
  • 二进制信号量(binary semaphores)
  • 计数信号量(counting semaphores)
  • 互斥量(mutexes)
  • 递归互斥量、递归锁(recursive mutexes)
  • 事件组(event groups)
  • 任务通知(task notifications)

其实和线程同步的概念是差不多的。

5.4 暂停状态 (Suspended)

暂停状态 是任务被主动暂停执行的状态。任务被暂停时,它不会再参与调度,直到被恢复。任务可以通过vTaskSuspend()函数进入暂停状态,暂停状态的任务不能再被调度执行,除非通过vTaskResume()或中断恢复操作来恢复。

任务从暂停状态转到就绪状态,通常需要其他任务或中断来触发。

  • 任务E调用了vTaskSuspend()进入暂停状态,直到其他任务调用vTaskResume()恢复它的执行。
  • 任务F在某些条件下不需要继续执行,程序通过vTaskSuspend()将其暂停,直到条件变化,调用vTaskResume()恢复。
c 复制代码
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
//参数xTaskToSuspend表示要暂停的任务,如果为NULL,表示暂停自己。

2.5 特殊状态:删除状态 (Deleted)

除了上述四种基本状态外,还有一种特殊状态是删除状态 。当一个任务被删除时,它不再存在于任务调度器中。可以通过vTaskDelete()函数删除任务,删除后的任务无法再恢复,系统会回收该任务占用的资源。

5.6 任务状态转换

  • 从就绪状态到运行状态:当任务被调度器选择执行时,任务进入运行状态。
  • 从运行状态到阻塞状态:任务在执行过程中由于等待某个事件(如等待信号量、队列消息或时间)而进入阻塞状态。
  • 从阻塞状态到就绪状态:当阻塞条件(如超时或等待的同步事件)满足时,任务会从阻塞状态返回到就绪状态,准备重新执行。
  • 从就绪状态到运行状态:当调度器选择一个就绪状态的任务并分配CPU时,任务进入运行状态。
  • 从运行状态到就绪状态:任务完成时间片或被高优先级任务抢占时,任务回到就绪状态,等待再次被调度执行。
  • 从运行状态到删除状态 :任务完成后或通过vTaskDelete()被删除时,任务会进入删除状态,系统会回收其资源。
  • 从就绪状态到暂停状态 :任务通过调用vTaskSuspend()进入暂停状态,暂停状态下的任务不会再被调度。
  • 从暂停状态到就绪状态 :任务通过调用vTaskResume()恢复后,任务返回到就绪状态。

2.7 实验

📎08_freertos_example_task_status.zip

c 复制代码
TaskHandle_t xHandleTask1;
TaskHandle_t xHandleTask3;

static int task1flagrun = 0;
static int task2flagrun = 0;
static int task3flagrun = 0;

void Task1Function(void * param)
{
	TickType_t tStart = xTaskGetTickCount();
	TickType_t t;
	int flag = 0;
	
	while (1)
	{
		t = xTaskGetTickCount();
		
		task1flagrun = 1;
		task2flagrun = 0;
		task3flagrun = 0;
		printf("1");

		if (!flag && (t > tStart + 10))
		{
			vTaskSuspend(xHandleTask3);
			flag = 1;
		}

		if (t > tStart + 20)
		{
			vTaskResume(xHandleTask3);
		}
	}
}

void Task2Function(void * param)
{
	while (1)
	{
		task1flagrun = 0;
		task2flagrun = 1;
		task3flagrun = 0;
		printf("2");

		vTaskDelay(10);
	}
}

void Task3Function(void * param)
{
	while (1)
	{
		task1flagrun = 0;
		task2flagrun = 0; 
		task3flagrun = 1;
		printf("3");
	}
}


/*-----------------------------------------------------------*/

StackType_t xTask3Stack[100];
StaticTask_t xTask3TCB;

StackType_t xIdleTaskStack[100];
StaticTask_t xIdleTaskTCB;


/*
 * The buffers used here have been successfully allocated before (global variables)
 */
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
                                    StackType_t ** ppxIdleTaskStackBuffer,
                                    uint32_t * pulIdleTaskStackSize )
{
    *ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
    *ppxIdleTaskStackBuffer = xIdleTaskStack;
    *pulIdleTaskStackSize = 100;
}


int main( void )
{
		
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello, world!\r\n");

	xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
	xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
	xHandleTask3 = xTaskCreateStatic(Task3Function, "Task3", 100, NULL, 1, xTask3Stack, &xTask3TCB);

	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

3.Delay函数

3.1 两个函数

c 复制代码
void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少个Tick */

/* pxPreviousWakeTime: 上一次被唤醒的时间
 * xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)
 * 单位都是Tick Count
*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
                           const TickType_t xTimeIncrement );

vTaskDelay:

  • 指定等待多少个tick中断才变为就绪状态

xTaskDelayUntil:

  • 返回pdTRUE表示确实延迟了,返回pdFALSE表示没有发生延迟(因为延迟的时间点早就过了)
  • 等待到指定的绝对时刻,才能变为就绪态。
  • 参数 xLastWakeTime 记录上一次退出延时的时间点,确保 两次唤醒间隔严格为 N 个 Tick,属于绝对时间。

来看下代码:

c 复制代码
void vTask1( void *pvParameters )
{
    const TickType_t xDelay50ms = pdMS_TO_TICKS( 50UL );
    TickType_t xLastWakeTime;
    int i;
    /* 获得当前的Tick Count */
    xLastWakeTime = xTaskGetTickCount();
    for( ;; )
    {
        flag = 1;
        /* 故意加入多个循环,让程序运行时间长一点 */
        for (i = 0; i <5; i++)
            printf( "Task 1 is running\r\n" );
##if 1
        vTaskDelay(xDelay50ms);
##else
        vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
##endif
    }
}
void vTask2( void *pvParameters )
{
    for( ;; )
    {
        flag = 0;
        printf( "Task 2 is running\r\n" );
    }
}
int main( void )
{
    prvSetupHardware();
    /* Task1的优先级更高, Task1先执行 */
    xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
    xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
    /* 启动调度器 */
    vTaskStartScheduler();
    /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
    return 0;
}

vTaskDelay() 函数是相对延时,指每次延时都是从执行函数vTaskDelay()开始,直到延时指定的时间结束。在这个过程中,如果vTaskDelay() 函数前面的内容执行了一段时间(这里假设是:10ms) ,我们通过vTaskDelay() 函数延时了50个系统节拍,那么这个任务实际运行的间隔就是60ms。

vTaskDelayUntil() 和 xTaskDelayUntil() 函数是绝对延时,指将整个任务的运行周期看成一个整体,适用于需要按照一定频率运行的任务,比如这里使用xTaskDelayUntil()函数延时50ms,那么整个任务的运行间隔就是50ms(前提是任务里面的代码整体运行实际不能超过50ms)。


vTaskDelay:指定的是阻塞的时间,任务从调用 vTaskDelay(N) 的时刻开始,阻塞当前任务 至少 N 个 Tick 周期:

  • vTaskDelay()的精度受到Tick中断频率的影响。如果configTICK_RATE_HZ的值较低(例如50),延迟的精度就会较差,因为每个Tick代表的时间较长,如果ti和ti+1,之间间隔时间大(configTICK_RATE_HZ的值很低),那么就可能导致延时的时间实际上误差比较大,因为它是以ti处的tick中断作为衡量基础的。

  • 示例:

    • 若 configTICK_RATE_HZ = 50(Tick 周期 20ms),调用 vTaskDelay(2):
    • 若在 t1(Tick 中断刚触发)时调用,实际延时为 2*20ms = 40ms。
    • 若在 t1 + 18ms 时调用(接近下一个 Tick),实际延时可能仅为 2ms(等待到 t2 触发) + 20ms = 22ms,误差高达 18ms。
  • 例如下图中,设定的是延迟两个tick,可以看出vTaskDelay延迟t2和t3、t5和t6实际上是不同的长度,如果时间周期不是很大可以忽略不记,如果时间周期很大那么效果就很明显了。

vTaskDelayUntil:指定的是任务执行的间隔、周期

  • 而对于xTaskDelayUntil,使用xTaskDelayUntil(&Pre, n)时,前后两次退出xTaskDelayUntil的时间至少是n个Tick中断。

  • 上图下半部分,当第1次调用xTaskDelayUntil时,Pre为1,也就是上一次退出Delay的时间为t1;而参2为2,即两个tick中断,也就是说这次Delay退出的时间和上一次退出Delay的时间至少是两个tick中断,图中可以看出这次Delay退出的时间是t3,确实是两个tick。至于后的还有绿线,那是因为xTaskDelayUntil函数后面是还有代码要执行的,如果xTaskDelayUntil函数后面没有代码,t3到t5的图形基本是和t1到t3一致的。

  • 示例

    • 若 configTICK_RATE_HZ = 100(Tick 周期 10ms),调用 xTaskDelayUntil(&xLastWakeTime, 2)
    • 第一次唤醒在 t1,下次唤醒在 t1 + 2*10ms = t3。
    • 即使任务在 t1 到 t3 之间执行了 5ms,xLastWakeTime 会自动更新为 t3,下一周期从 t3 开始,保证总周期为 20ms。

3.2 实验

📎09_freertos_example_delay.zip

4.空闲任务及其钩子函数

回顾:FreeRTOS在xTaskCreate中分配TCB和栈,但并不一定 就是在vTaskDelete中释放TCB和栈。可以尝试去不断轮流调用xTaskCreatevTaskDelete,最终的结果是内存被消耗殆尽(自杀的情况下)。

如果是在一个任务中去杀死另外一个任务,然后再继续循环创建,再继续杀死,这种并不会造成内存被消耗殆尽,这种情况就是vTaskDelete函数去实现了内存的清理

而自杀的情况下,抽象点就死了无法处理自己的尸体,这时候就得想办法将优先级为0的空闲任务上号来清理自杀掉的"尸体"

4.1 空闲任务

在使用 vTaskStartScheduler() 函数来创建、启动调度器时,这个函数内部会创建空闲任务

空闲任务的作用:

  • 在FreeRTOS中,空闲任务常常被用来回收已经被删除任务的内存空间。例如,当调用vTaskDelete()删除一个任务时,空闲任务会负责清理和释放删除任务占用的内存(自杀的情况下)。
  • 空闲任务保证了FreeRTOS系统始终有任务运行,哪怕在没有任何用户任务就绪时。空闲任务的优先级最低(0级),只有在所有其他任务都不能运行时,才会被调度器选中并执行。这样,即使没有任务在运行,系统也不会空闲,避免了资源浪费。
  • 空闲任务永远不会阻塞。即使所有的用户任务都被阻塞,空闲任务也会保持运行,确保系统能有效工作。空闲任务的执行周期通常非常短,不会占用太多CPU时间,确保用户任务有机会执行。

空闲任务的优先级:

  • 空闲任务的优先级为0,也就是最低的优先级。这个优先级设置意味着当有其他任务就绪时,空闲任务不会占用CPU资源。空闲任务仅在系统没有其他任务可执行时才会运行。比如,当所有的高优先级任务处于阻塞状态,或者用户任务的优先级都低于空闲任务时,空闲任务才会被调度。
  • 如果某个任务变为就绪状态且优先级高于0,它会抢占空闲任务的执行,空闲任务会被调度器切换出去,允许用户任务继续执行。

4.2 钩子函数

空闲任务钩子函数是一个可选功能,它允许用户在空闲任务执行时执行自定义代码。每当空闲任务运行时,FreeRTOS会调用这个钩子函数。钩子函数的用途有很多,最常见的应用包括:

  • 执行低优先级后台任务:如果系统中没有高优先级任务需要执行,可以使用空闲任务钩子函数执行一些低优先级的后台任务,比如维护一些周期性任务或执行日志记录。
  • 测量CPU使用率:空闲任务的执行意味着系统处于空闲状态,所以通过统计空闲任务的执行时间,可以计算出CPU的占用率。这对于性能分析和调试非常有用。
  • 省电模式:空闲任务的执行也可以用来控制系统的省电模式。因为空闲任务的执行意味着系统没有重要的任务要做,所以可以让系统进入低功耗状态,降低功耗。

空闲任务的钩子函数的实现需要满足以下几点:

  1. 高效执行:钩子函数必须非常高效,因为它会在空闲任务的每个周期执行。如果钩子函数执行时间过长,可能导致空闲任务无法及时运行,影响系统的稳定性,甚至阻碍资源的回收(比如无法释放已删除任务的内存)。
  2. 不能导致阻塞或暂停:空闲任务是系统中唯一不能被阻塞或暂停的任务。空闲任务的钩子函数不能使空闲任务进入阻塞状态或暂停状态,否则会导致系统出现异常。

空闲任务钩子函数的名称是vApplicationIdleHook,它需要在应用程序代码中实现,并且启用钩子函数功能。在FreeRTOS配置文件中,需要将configUSE_IDLE_HOOK宏定义为1才能启用空闲任务钩子函数功能。


启用和实现空闲任务钩子函数

  1. 启用空闲任务钩子函数
    通过在FreeRTOS配置文件中设置configUSE_IDLE_HOOK宏为1来启用空闲任务的钩子函数。默认情况下,这个宏是设置为0的,这意味着空闲任务钩子函数被禁用。
c 复制代码
#define configUSE_IDLE_HOOK 1  // 启用空闲任务钩子函数
  1. 实现钩子函数
    在应用程序中,你需要实现vApplicationIdleHook()函数。每当空闲任务运行时,FreeRTOS会调用这个钩子函数。
c 复制代码
void vApplicationIdleHook(void)
{
    // 执行低优先级的后台任务
    // 比如:监控系统空闲时间、控制省电模式等
}

注意事项:

  • 不要进行耗时操作:空闲任务钩子函数需要尽量简洁高效,避免进行复杂的操作或长时间的计算。耗时的操作会导致空闲任务无法及时完成,影响内存释放和任务调度。
  • 避免阻塞操作 :空闲任务钩子函数不能调用会导致阻塞或暂停的API,例如vTaskDelay()vTaskSuspend()等,这会导致系统的空闲任务挂起,无法正常运行。

4.3 实验

📎10_freertos_example_idletask.zip

c 复制代码
/*-----------------------------------------------------------*/

/*
 * Configure the clocks, GPIO and other peripherals as required by the demo.
 */
static void prvSetupHardware( void );



/*
 * Retargets the C library printf function to the USART.
 */
int fputc( int ch, FILE *f );


/*
 * Configures the timers and interrupts for the fast interrupt test as
 * described at the top of this file.
 */
extern void vSetupTimerTest( void );
void Task2Function(void * param);

/*-----------------------------------------------------------*/
static int task1flagrun = 0;
static int task2flagrun = 0;
static int taskidleflagrun = 0;

void Task1Function(void * param)
{
	TaskHandle_t xHandleTask2;
	BaseType_t xReturn;
	
	while (1)
	{
		task1flagrun = 1;
		task2flagrun = 0;
		taskidleflagrun = 0;
		printf("1");
		xReturn = xTaskCreate(Task2Function, "Task2", 1024, NULL, 2, &xHandleTask2);
		if (xReturn != pdPASS)
			printf("xTaskCreate err\r\n");
		//vTaskDelete(xHandleTask2);
			
	}
}

void Task2Function(void * param)
{
	while (1)
	{
		task1flagrun = 0;
		task2flagrun = 1;
		taskidleflagrun = 0;
		printf("2");
		//vTaskDelay(2);
		vTaskDelete(NULL);
	}
}

void vApplicationIdleHook( void )
{
	task1flagrun = 0;
	task2flagrun = 0;
	taskidleflagrun = 1;	
	printf("0");
}


/*-----------------------------------------------------------*/

int main( void )
{
	TaskHandle_t xHandleTask1;
		
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello, world!\r\n");

	xTaskCreate(Task1Function, "Task1", 100, NULL, 0, &xHandleTask1);

	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

5.任务调度算法

任务调度算法是确定哪个就绪态的任务被选择并切换为运行状态的核心机制。在FreeRTOS中,调度算法的行为主要受三个配置项的控制,这些配置项定义了系统的调度方式。调度算法的目标是确保任务的高优先级执行顺序以及同优先级任务的合理调度,具体分为以下几个方面:

  • 可抢占性:决定是否允许高优先级任务在当前任务执行时被立即切换。
  • 时间片轮转:确定同一优先级任务的执行方式,即同优先级的任务如何轮流执行。
  • 空闲任务的让步策略:决定空闲任务是否会主动让出CPU时间给其他用户任务。

5.1 可抢占调度与合作调度

可抢占调度(Preemptive Scheduling):

  • 可抢占调度 模式下,调度器允许高优先级的任务在任何时候打断正在运行的低优先级任务,并立即占用CPU资源。当前任务被挂起,并回到就绪状态,等待高优先级任务执行完毕后,才继续执行。
  • 配置项configUSE_PREEMPTION,当该配置项为1时,系统启用可抢占调度,即高优先级任务可以随时中断当前任务的执行。
  • 这种调度方式能够保证高优先级任务的及时响应,是多任务实时系统中最常用的调度方式。但是由于任务在执行时可能会被高优先级任务打断,这会导致任务的执行不够连续,可能出现中断和上下文切换的开销。

合作调度(Co-operative Scheduling):

  • 合作调度 模式下,任务不会主动抢占当前正在执行的任务。即使有更高优先级的任务变为就绪态,它也只能等到当前任务主动让出CPU资源时才能开始执行。
  • 配置项configUSE_PREEMPTION,当该配置项为0时,系统启用合作调度,只有当前任务主动让出控制权时,其他任务才能被执行。
  • 这种调度方式的开销较小,因为任务之间不会因为抢占而频繁切换。但缺点是高优先级任务可能会被延迟执行,直到当前任务完全执行完,不适合实时性要求高的系统。

5.2 时间片轮转 (Time Slicing)

可抢占调度 模式下,如果系统设置为时间片轮转 ,同优先级的任务将在时间片结束后轮流执行。时间片是指一个任务连续执行的最大时间,每个任务的执行时间由调度器分配,直到时间片耗尽,调度器才会切换到下一个同优先级的任务。

使用时间片轮转(Time Slicing):

  • 配置项configUSE_TIME_SLICING,当该配置项为1时,启用时间片轮转。也就是说,同优先级的任务会轮流执行,每个任务都会获得一个时间片,任务之间的切换基于时间片的结束。
  • 通过时间片轮转,系统能确保同优先级的任务轮流执行,避免某个任务占用CPU过长时间。如果某些任务执行时间较长,可能会导致频繁的任务切换,增加上下文切换的开销。

不使用时间片轮转(Without Time Slicing):

  • 配置项configUSE_TIME_SLICING,当该配置项为0时,系统禁用时间片轮转。同优先级的任务会一直执行,直到其主动让出CPU或被更高优先级任务抢占。
  • 降低了上下文切换的开销,适用于任务的执行时间非常确定且连续的情况。但是同优先级的任务可能会被一个任务"独占",导致其他任务的执行延迟。

不使用时间片轮转的话且不设置抢占,假设同优先级的两个任务,task1执行的时间需要相当长的时间,这样就会导致task2需要等待task1很久才能执行到,而如果使用时间片的话,规定了task1的最大执行时间,时间到了不管任务task1是否执行结束都得让出CPU,让task2有机会去执行。

当然如果设置的可抢占(同样不使用时间片轮流),高优先级任务就绪时会引起任务切换,高优先级任务不再运行时也会引起任务切换,也就是说高优先级任务结束,task2抢到资源也是能执行的,但是如果后续没有高优先级任务前来抢占然后结束引起任务切好,task1也将没机会执行

5.3 空闲任务的让步策略 (Idle Task Yield)

空闲任务是否让步给用户任务取决于配置项configIDLE_SHOULD_YIELD

空闲任务让步(Idle Task Yield):

  • 配置项configIDLE_SHOULD_YIELD,当该配置项为1时,空闲任务会在每次循环结束时主动检查是否有更高优先级的任务可以执行。如果有的话,就主动让出CPU时间给这些任务。
  • 空闲任务不会长时间占用CPU,能够让用户任务更快地被调度。如果空闲任务过于频繁地让步,可能会导致调度器过于频繁地进行任务切换,增加上下文切换的开销。

空闲任务不让步(Idle Task Not Yield):

  • 配置项configIDLE_SHOULD_YIELD,当该配置项为0时,空闲任务不会主动让步,它将一直运行,直到系统进入空闲状态或者其他任务被调度执行。
  • 空闲任务不会因为让步而频繁切换上下文,减少了开销。但是可能会导致空闲任务占用CPU时间过长,其他任务有时得不到及时执行。
  • 但是需要注意的是,如果设置的是不可抢占,那么即使设置不让步,也会让步

configIDLE SHOULD YIELD的实质是什么?

  • 在空闲任务的循环中,主动触发调度器,让出CPU
  • 空闲任务每循环一次,就调用taskYIELD0) 函数一次
  • 不配置configIDLE SHOULD_YIELD为0的话,空闲任务的while循环会执行多次,跟用户任务一样
c 复制代码
static portTASK_FUNCTION( prvIdleTask, pvParameters )
{
    /* 为避免编译器警告,将传入参数 pvParameters 强制转换为 void 类型,
     * 因为在该任务中没有使用 pvParameters。 */
    ( void ) pvParameters;

    /** 
     * 这是 RTOS 的空闲任务(Idle Task),当调度器启动后自动创建。
     * 空闲任务主要在系统无其他任务可运行时执行,同时也负责系统维护工作。
     */

    /* 如果任务在安全上下文中删除了自己,空闲任务负责清理其安全上下文。
     * 这里为空闲任务分配一个安全上下文区域,大小由 configMINIMAL_SECURE_STACK_SIZE 定义。 */
    portALLOCATE_SECURE_CONTEXT( configMINIMAL_SECURE_STACK_SIZE );

    /* 无限循环,空闲任务会一直运行 */
    for( ; ; )
    {
        /* 检查是否有任务自我删除了:
         * 如果有任务调用了删除函数,但未能释放它们的 TCB 和任务栈,
         * 空闲任务负责清理这些被删除任务所占用的资源。 */
        prvCheckTasksWaitingTermination();

        #if ( configUSE_PREEMPTION == 0 )
            {
                /* 当系统不使用抢占式调度时,必须强制进行任务切换,
                 * 以便检查是否有其他任务已就绪。
                 * 因为在非抢占模式下,任务不会自动切换,所以调用 taskYIELD() 强制让出 CPU。 */
                taskYIELD();
            }
        #endif /* configUSE_PREEMPTION */

        #if ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) )
            {
                /* 当系统使用抢占式调度且配置了 idle task 应该 yield 时,
                 * 如果空闲优先级就绪列表中存在其他与空闲任务同级别的任务,
                 * 则空闲任务在每个时间片内主动调用 taskYIELD(),
                 * 让出 CPU,以便同等优先级的其他任务获得执行机会。 */
                if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > ( UBaseType_t ) 1 )
                {
                    taskYIELD();
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        #endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) ) */

        #if ( configUSE_IDLE_HOOK == 1 )
            {
                /* 如果启用了空闲钩子(Idle Hook),调用用户定义的 vApplicationIdleHook() 函数。
                 * 这允许开发者在空闲任务中执行一些背景工作,
                 * 但 vApplicationIdleHook() 必须保证不能阻塞。 */
                extern void vApplicationIdleHook( void );
                vApplicationIdleHook();
            }
        #endif /* configUSE_IDLE_HOOK */

        #if ( configUSE_TICKLESS_IDLE != 0 )
            {
                TickType_t xExpectedIdleTime;

                /* 在进入低功耗模式前,先调用 prvGetExpectedIdleTime() 预先获取
                 * 预计的空闲时间。注意,此时调度器未被挂起,结果可能不完全准确。 */
                xExpectedIdleTime = prvGetExpectedIdleTime();

                /* 如果预计空闲时间大于或等于预定义的最小空闲时间(configEXPECTED_IDLE_TIME_BEFORE_SLEEP),
                 * 则可以考虑进入低功耗模式。 */
                if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP )
                {
                    /* 挂起调度器,防止在处理低功耗模式期间发生任务切换 */
                    vTaskSuspendAll();
                    {
                        /* 当调度器挂起后,再次采样空闲时间,因为此时数据更准确。
                         * 此处断言确保下一个任务解除阻塞时间不早于当前 Tick Count。 */
                        configASSERT( xNextTaskUnblockTime >= xTickCount );
                        xExpectedIdleTime = prvGetExpectedIdleTime();

                        /* 应用程序可以通过 configPRE_SUPPRESS_TICKS_AND_SLEEP_PROCESSING() 宏,
                         * 修改 xExpectedIdleTime 的值,控制是否调用低功耗函数。 */
                        configPRE_SUPPRESS_TICKS_AND_SLEEP_PROCESSING( xExpectedIdleTime );

                        /* 如果重新采样后的空闲时间仍满足条件,则进入低功耗模式:
                         * trace 宏用于跟踪低功耗状态开始和结束,
                         * portSUPPRESS_TICKS_AND_SLEEP() 函数实现进入低功耗状态。 */
                        if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP )
                        {
                            traceLOW_POWER_IDLE_BEGIN();
                            portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime );
                            traceLOW_POWER_IDLE_END();
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                    /* 恢复调度器,允许任务切换继续 */
                    ( void ) xTaskResumeAll();
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        #endif /* configUSE_TICKLESS_IDLE */
    }
}

上面就是空闲任务的运行函数,具体如何创建的在后续会说,先来看这个空闲任务函数的关键部分:

c 复制代码
            #if ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) )
            {
                /* 当系统使用抢占式调度且配置了 idle task 应该 yield 时,
                 * 如果空闲优先级就绪列表中存在其他与空闲任务同级别的任务,
                 * 则空闲任务在每个时间片内主动调用 taskYIELD(),
                 * 让出 CPU,以便同等优先级的其他任务获得执行机会。 */
                if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > ( UBaseType_t ) 1 )
                {
                    taskYIELD();
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }

其实就是在pxReadyTasksLists[ tskIDLE_PRIORITY ]链表当中(这一部分再后续也会讲解)去查找是否有优先级和空闲任务一样的任务,也就是优先级为tskIDLE_PRIORITY的数量是否大于1,如果是就调用taskYIELD(),空闲任务退让,并将其放到pxReadyTasksLists[ tskIDLE_PRIORITY ]链尾,启动调度器进行任务切换

5.4 调度策略的组合

配置项 A B C D E
configUSE_PREEMPTION 1 1 1 1 0
configUSE_TIME_SLICING 1 1 0 0 X
configIDLE_SHOULD_YIELD 1 0 1 0 X
说明 常用 很少用 很少用 很少用 几乎不用
  • A:可抢占 + 时间片轮转 + 空闲任务让步
  • B:可抢占 + 时间片轮转 + 空闲任务不让步
  • C:可抢占 + 非时间片轮转 + 空闲任务让步
  • D:可抢占 + 非时间片轮转 + 空闲任务不让步
  • E:合作调度

5.5 实验

c 复制代码
static volatile int flagIdleTaskrun = 0; // 空闲任务运行时flagIdleTaskrun=1
static volatile int flagTask1run = 0; // 任务1运行时flagTask1run=1
static volatile int flagTask2run = 0; // 任务2运行时flagTask2run=1
static volatile int flagTask3run = 0; // 任务3运行时flagTask3run=1
void vTask1( void *pvParameters )
{
    /* 任务函数的主体一般都是无限循环 */
    for( ;; ){
        flagIdleTaskrun = 0;
        flagTask1run = 1;
        flagTask2run = 0;
        flagTask3run = 0;
        /* 打印任务的信息 */
        printf("T1\r\n");
    }
}
void vTask2( void *pvParameters )
{
    /* 任务函数的主体一般都是无限循环 */
    for( ;; ){
        flagIdleTaskrun = 0;
        flagTask1run = 0;
        flagTask2run = 1;
        flagTask3run = 0;
        /* 打印任务的信息 */
        printf("T2\r\n");
    }
}
void vTask3( void *pvParameters )
{
    const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL );
    /* 任务函数的主体一般都是无限循环 */
    for( ;; ){
        flagIdleTaskrun = 0;
        flagTask1run = 0;
        flagTask2run = 0;
        flagTask3run = 1;
        /* 打印任务的信息 */
        printf("T3\r\n");
        
        // 如果不休眠的话, 其他任务无法得到执行
        vTaskDelay( xDelay5ms );
    }
}
void vApplicationIdleHook(void)
{
    flagIdleTaskrun = 1;
    flagTask1run = 0;
    flagTask2run = 0;
    flagTask3run = 0;
    /* 故意加入打印让flagIdleTaskrun变为1的时间维持长一点 */
    printf("Id\r\n");
}
int main( void )
{
    prvSetupHardware();
    xTaskCreate(vTask1, "Task 1", 1000, NULL, 0, NULL);
    xTaskCreate(vTask2, "Task 2", 1000, NULL, 0, NULL);
    xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);
    /* 启动调度器 */
    vTaskStartScheduler();
    /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
    return 0;
}

后续实验在FreeRTOSConfig.h 中,定义宏

5.5.1 抢占

基于 时间片轮转 + 空闲任务让步

抢占:

c 复制代码
##define configUSE_PREEMPTION 1
##define configUSE_TIME_SLICING 1
##define configIDLE_SHOULD_YIELD 1
  • 当task3休眠的时候(调用了vTaskDelay),CPU让出,让剩下的task1、task2、idle任务优先级为0,由于设置的是时间片轮流加空闲任务让步,这三个同优先级轮流执行
  • 当task3休眠完毕,设置的可抢占,task3任务就绪,抢占正在执行的低优先级任务,开执行执行任务。
  • 这就是抢占:即高优先级任务就绪后,正在执行的低优先级要将资源让出,给高优先级的任务去执行。

不抢占:

c 复制代码
##define configUSE_PREEMPTION 0
##define configUSE_TIME_SLICING 1
##define configIDLE_SHOULD_YIELD 1
  • 设置的不可抢占,即使高优先级的任务休眠完毕,也无法执行,资源一直被task1占用

5.5.2 时间片轮流

基于可抢占和空闲任务让步

时间片轮流:

  • 回顾:
c 复制代码
##define configUSE_PREEMPTION 1
##define configUSE_TIME_SLICING 1
##define configIDLE_SHOULD_YIELD 1
  • 可以看出使用时间片轮流的话,并不会出现一个任务占用太长时间的情况

不使用时间片轮流:

c 复制代码
##define configUSE_PREEMPTION 1
##define configUSE_TIME_SLICING 0
##define configIDLE_SHOULD_YIELD 1
  • 设置的可抢占,高优先级任务就绪时会引起任务切换,高优先级任务不再运行时也会引起任务切换
  • 可以看出每次高优先级任务task3休眠不再运行时,引起任务切换,同优先级的task1和task2争夺,抢到了一直执行直到task3休眠结束,task3休眠期间并不会出现task1和task2轮流执行的情况
  • 这就是时间片不轮流
  • 如果设置的不可抢占,task3没机会引起任务切换,抢到资源的task1或task2将会一直执行,直到资源耗尽(但是这里的任务设置的都是无限循环,另外一个任务根本没机会执行到)

不支持时间片轮转时,空闲任务什么时候才可以执行?

  • Task3调用vTaskDelay导致任务切换时
  • 如果没有高优先级的Task3,空闲任务永远没有机会执行
  • 不支持时间片轮转的话,当前任务何时放弃CPU资源?只有这两种情况:被高优先级任务抢占、自己主动放弃,如果把Task3去掉,那么Task1会永远执行,所以空闲任务没有机会执行。

5.5.3 空闲任务让步

让步:

c 复制代码
##define configUSE_PREEMPTION 1
##define configUSE_TIME_SLICING 1
##define configIDLE_SHOULD_YIELD 1
  • 空闲任务的每个循环中,会主动让出处理器,从图中可以看到flagIdelTaskrun的波形很小

不让步:

c 复制代码
##define configUSE_PREEMPTION 1
##define configUSE_TIME_SLICING 1
##define configIDLE_SHOULD_YIELD 0
  • 空闲任务跟任务1、任务2同等待遇,它们的波形宽度是差不多的

6.保存现场

假设有这么一个任务,进行a+b的操作:

c 复制代码
void vTask1( void *pvParameters )
{
    int a = 1;
    //这中间可能有其它东西
    int b = 2;
    a = a + b;
}
void vTask2( void *pvParameters )
{
    //抢占任务
    for(int i = 0;i <= 100000; i++)

}

int main( void )
{
    prvSetupHardware();
    xTaskCreate(vTask1, "Task 1", 1000, NULL, 0, NULL);
    for(int i = 0;i <= 100000; i++)
    xTaskCreate(vTask2, "Task 2", 1000, NULL, 2, NULL);
    /* 启动调度器 */
    vTaskStartScheduler();
    /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
    return 0;
}

ARM 芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:

  • 对内存只有读、写指令
  • 对于数据的运算是在 CPU 内部实现
  • 使用 RISC 指令的 CPU 复杂度小一点,易于设计

就上面对于 a=a+b 这样的算式,需要 4 个步骤才可以实现:

CPU 运行时,先去取得指令,再执行指令:

  • 把内存 a 的值读入 CPU 寄存器 R0
  • 把内存 b 的值读入 CPU 寄存器
  • R1把 R0、R1 累加,存入 R0
  • 把 R0 的值写入内存 a

这时候问题来了,在上面给出的代码中,当 for(int i = 0;i <= 100000; i++);执行完了,而我的task1任务并还没有执行完,也就是a=a+b的操作还没有结束,假设只运行到了第1步:把内存 a 的值读入 CPU 寄存器 R0,就被高优先级的task2任务给抢占呢。

那如果我task2的也执行一些操作,需要从task2的内存(或者可以说是task2在内存中的栈区)取出值也是存放到逻辑运算单元ALU中的R0寄存器,假设值是100,那我原先R0里存放的是任务task1的a的值1,变成了100,后续task2结束了,R0仍然是100,task1继续运行,岂不是实际上原本是1+2变成了100+2。

因此为了防止这一现象,在任务切换的时候有一个保存现场的作用,在中断的时候也有这么一个操作。那他怎么保存???----保存进内存里(或者说是所被抢占的任务的栈区中,这里是将CPU中寄存器的值都保存进task1的栈区当中)

在后续重新运行的task1,只需要将栈区的关于寄存器的值恢复到CPU当中,即可继续进行接下来的2、3、4步骤

7.栈的创建

来看看之前提到过的TCB结构体,这里只是提取出关键部分:

c 复制代码
typedef struct tskTaskControlBlock       /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
    volatile StackType_t * pxTopOfStack; /*< Points to the location of the last item placed on the tasks stack.  THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */

    #if ( portUSING_MPU_WRAPPERS == 1 )
        xMPU_SETTINGS xMPUSettings; /*< The MPU settings are defined as part of the port layer.  THIS MUST BE THE SECOND MEMBER OF THE TCB STRUCT. */
    #endif

    ListItem_t xStateListItem;                  /*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */
    ListItem_t xEventListItem;                  /*< Used to reference a task from an event list. */
    UBaseType_t uxPriority;                     /*< The priority of the task.  0 is the lowest priority. */
    StackType_t * pxStack;                      /*< Points to the start of the stack. */
    char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< Descriptive name given to the task when created.  Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
}

再来看看创建任务的函数,以动态创建为例子:

c 复制代码
BaseType_t xTaskCreate( 
    TaskFunction_t pxTaskCode,                    // 任务函数
    const char * const pcName,                    // 任务名字
    const configSTACK_DEPTH_TYPE usStackDepth,    // 栈的深度
    void * const pvParameters,                    // 传递给任务的参数
    UBaseType_t uxPriority,                       // 任务优先级
    TaskHandle_t * const pxCreatedTask            // 任务句柄
);

可以看出函数的参数中,任务的名字、任务的优先级都在TCB结构体中有所体现,至于栈的深度肯定也是和TCB结构体中指向栈的底部pxStack和顶部pxTopOfStack挂钩,那参数中关于任务函数pxTaskCode及其函数参数pvParameters又在哪里体现?或者说是存放在哪里了???

来研究一下xTaskCreate函数的源代码:

c 复制代码
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
                            const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
                            const configSTACK_DEPTH_TYPE usStackDepth,
                            void * const pvParameters,
                            UBaseType_t uxPriority,
                            TaskHandle_t * const pxCreatedTask )
{
    //前面部分代码是申请一个TCB结构体:pxNewTCB
    if( pxNewTCB != NULL )
        {
            prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
            prvAddNewTaskToReadyList( pxNewTCB );
            xReturn = pdPASS;
        }
}

主要是看一下prvInitialiseNewTask函数,继续往里面看:

c 复制代码
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode,
                                  const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
                                  const uint32_t ulStackDepth,
                                  void * const pvParameters,
                                  UBaseType_t uxPriority,
                                  TaskHandle_t * const pxCreatedTask,
                                  TCB_t * pxNewTCB,
                                  const MemoryRegion_t * const xRegions )
{
    StackType_t * pxTopOfStack;
    UBaseType_t x;
    //这里也只提取出关键
    /* Initialize the TCB stack to look as if the task was already running,
     * but had been interrupted by the scheduler.  The return address is set
     * to the start of the task function. Once the stack has been initialised
     * the top of stack variable is updated. */
    #if ( portUSING_MPU_WRAPPERS == 1 )
        {
            /* If the port has capability to detect stack overflow,
             * pass the stack end address to the stack initialization
             * function as well. */
            #if ( portHAS_STACK_OVERFLOW_CHECKING == 1 )
                {
                    #if ( portSTACK_GROWTH < 0 )
                        {
                            pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxNewTCB->pxStack, pxTaskCode, pvParameters, xRunPrivileged );
                        }
                    #else /* portSTACK_GROWTH */
                        {
                            pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxNewTCB->pxEndOfStack, pxTaskCode, pvParameters, xRunPrivileged );
                        }
                    #endif /* portSTACK_GROWTH */
                }
            #else /* portHAS_STACK_OVERFLOW_CHECKING */
                {
                    pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters, xRunPrivileged );
                }
            #endif /* portHAS_STACK_OVERFLOW_CHECKING */
        }
    #else /* portUSING_MPU_WRAPPERS */
        {
            /* If the port has capability to detect stack overflow,
             * pass the stack end address to the stack initialization
             * function as well. */
            #if ( portHAS_STACK_OVERFLOW_CHECKING == 1 )
                {
                    #if ( portSTACK_GROWTH < 0 )
                        {
                            pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxNewTCB->pxStack, pxTaskCode, pvParameters );
                        }
                    #else /* portSTACK_GROWTH */
                        {
                            pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxNewTCB->pxEndOfStack, pxTaskCode, pvParameters );
                        }
                    #endif /* portSTACK_GROWTH */
                }
            #else /* portHAS_STACK_OVERFLOW_CHECKING */
                {
                    pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
                }
            #endif /* portHAS_STACK_OVERFLOW_CHECKING */
        }
    #endif /* portUSING_MPU_WRAPPERS */
}
    

从上面可以看到,是调用到了pxPortInitialiseStack函数,继续看:

c 复制代码
StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,
                                     TaskFunction_t pxCode,
                                     void * pvParameters )
{
    /* Simulate the stack frame as it would be created by a context switch
     * interrupt. */
    pxTopOfStack--;                                                      /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */
    *pxTopOfStack = portINITIAL_XPSR;                                    /* xPSR */
    pxTopOfStack--;
    *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
    pxTopOfStack--;
    *pxTopOfStack = ( StackType_t ) prvTaskExitError;                    /* LR */

    pxTopOfStack -= 5;                                                   /* R12, R3, R2 and R1. */
    *pxTopOfStack = ( StackType_t ) pvParameters;                        /* R0 */
    pxTopOfStack -= 8;                                                   /* R11, R10, R9, R8, R7, R6, R5 and R4. */

    return pxTopOfStack;
}

那这里就可以很容易看到了,实际上,任务的函数名以及函数的参数,是被存在了栈区当中,并且R0到R1X的寄存器也就被存放到寄存器当中,为什么?

因为在调用创建任务的函数的过程中,咱们任务任务实际上是还没有运行的,因其这个任务函数想要运行的话,是和保存现场、恢复现场是类似的,运行就要恢复CPU当中寄存器的现场。

8.任务的运行(调度)

在前面讲到过任务的调度以及状态,介绍了任务与任务之间的关系以及大概的切换规则。那么,有疑问了,已经就绪的任务又是如何被取出来然后运行的,就比如第一个刚创建好的任务,是如何从就绪到运行的:

  • 找到最高优先级的运行态、就绪态任务,运行它
  • 如果大家平级,轮流执行:排队,链表前面的先运行,运行1个tick后乖乖地去链表尾部排队

老样子,来看看任务创建函数xTaskCreate中的部分实现代码:

c 复制代码
 BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
                            const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
                            const configSTACK_DEPTH_TYPE usStackDepth,
                            void * const pvParameters,
                            UBaseType_t uxPriority,
                            TaskHandle_t * const pxCreatedTask )
 {

     //前面省略掉了一些关于TCB结构体的开辟内容,主要看看下面部分的函数
     if( pxNewTCB != NULL )
        {
            #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e9029 !e731 Macro has been consolidated for readability reasons. */
                {
                    /* Tasks can be created statically or dynamically, so note this
                     * task was created dynamically in case it is later deleted. */
                    pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
                }
            #endif /* tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE */

            prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
            prvAddNewTaskToReadyList( pxNewTCB );
            xReturn = pdPASS;
        }
 }

prvAddNewTaskToReadyList主要看这个函数,看名字就可以大概猜出了, 将一个新创建的任务加入就绪列表(Ready List)

c 复制代码
static void prvAddNewTaskToReadyList( TCB_t * pxNewTCB )
{
    /* 进入临界区:确保在修改任务列表期间,中断不会访问这些数据 */
    taskENTER_CRITICAL();
    {
        /* 全局变量 uxCurrentNumberOfTasks 记录当前系统中任务的数量,
         * 新任务加入时任务数量加 1。 */
        uxCurrentNumberOfTasks++;

        /* 如果当前没有运行的任务(pxCurrentTCB == NULL),即系统中没有其他任务,
         * 或者所有任务都处于挂起状态,则将新任务设为当前任务。 */
        if( pxCurrentTCB == NULL )
        {
            /* 将新任务设为当前任务 */
            pxCurrentTCB = pxNewTCB;

            /* 如果这是系统中创建的第一个任务 */
            if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 )
            {
                /* 进行初步初始化,初始化任务列表、等待列表等。
                 * 如果此调用失败,系统无法恢复,但会记录失败情况。 */
                prvInitialiseTaskLists();
            }
            else
            {
                /* 测试覆盖标记,用于代码覆盖率测试 */
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            /* 如果系统中已经存在任务 */
            /* 检查调度器是否已经启动 */
            if( xSchedulerRunning == pdFALSE )
            {
                /* 如果调度器还未启动,则比较当前任务和新任务的优先级:
                 * 如果当前任务的优先级小于或等于新任务的优先级,
                 * 则更新 pxCurrentTCB 为新任务,确保在启动调度器时,
                 * 当前任务是最高优先级任务之一。 */
                if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
                {
                    pxCurrentTCB = pxNewTCB;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
                /* 如果调度器已经运行,则不会更改当前运行任务 */
                mtCOVERAGE_TEST_MARKER();
            }
        }

        /* 全局任务编号递增,用于唯一标识任务 */
        uxTaskNumber++;

        #if ( configUSE_TRACE_FACILITY == 1 )
            {
                /* 如果启用了跟踪功能,则把当前任务编号存入 TCB 中,
                 * 用于系统监控和调试。 */
                pxNewTCB->uxTCBNumber = uxTaskNumber;
            }
        #endif /* configUSE_TRACE_FACILITY */

        /* 调用跟踪宏,记录任务创建事件,方便调试和系统监控 */
        traceTASK_CREATE( pxNewTCB );

        /* 将新任务添加到就绪列表中 */
        prvAddTaskToReadyList( pxNewTCB );  // 这里⚡⚡

        /* 调用平台相关的宏或函数,进行任务控制块(TCB)的必要设置,
         * 如初始化任务栈、设置任务上下文等 */
        portSETUP_TCB( pxNewTCB );
    }
    /* 退出临界区 */
    taskEXIT_CRITICAL();

    /* 如果调度器已经启动 */
    if( xSchedulerRunning != pdFALSE )
    {
        /* 如果新创建的任务优先级高于当前运行任务,则触发任务切换 */
        if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority )
        {
            taskYIELD_IF_USING_PREEMPTION();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
}

这个函数不仅实现了任务的创建,而且还通过调用prvAddTaskToReadyList函数将已经创建的任务加入链表当中,再来看看prvAddTaskToReadyList的内部实现,其实他就是一个宏:

c 复制代码
/*
 * 将 pxTCB 所表示的任务放入其对应优先级的就绪列表中,
 * 并插入到列表的末尾。
 */
#define prvAddTaskToReadyList( pxTCB )                                                                 \
    traceMOVED_TASK_TO_READY_STATE( pxTCB );      /* 调用跟踪宏,记录任务即将进入就绪状态 */         \
    taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );   /* 记录任务的就绪优先级(用于统计和调试) */     \
    listINSERT_END( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ),                                   \
                    &( ( pxTCB )->xStateListItem ) );   /* 将任务的状态列表项插入到对应优先级的就绪列表末尾 */ \
    tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )    /* 调用跟踪宏,记录任务已经进入就绪状态 */

其实就是将创建的任务根据其优先级加入到pxReadyTasksLists数组当中,比如pxReadyTasksLists[0]存放的是优先级为0的任务链表,注意,是链表,或者将他理解为一个二维数组也行。内部是这样定义:

c 复制代码
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; /*< Prioritised ready tasks. */

这样就很好理解了,已经就绪的任务会存放到这个二维数组当中,调度器根据实际情况在这个二维数组当中去将已经就绪的任务取出,然后运行

当然,又有个疑问?既然将就绪的任务放进了pxReadyTasksLists当中,又是谁来触发调度,让调度器将当中的任务取出然后进行运行:

  • tick中断,我猜测是没经过一个tick中断就会去触发查看, 系统会更新 Tick Count,然后检查就绪任务列表,判断是否有高优先级任务已经就绪,如果有则触发上下文切换。同时,任务调用诸如 taskYIELD() 或者 API 导致任务阻塞时,也会触发调度。

上面通过对任务的创建函数xTaskCreateprvAddNewTaskToReadyList进行了讲解,那么这里再来看一个现象:

c 复制代码
int main( void )
{
    prvSetupHardware();
    
    xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL );
    xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
    xTaskCreate( vTask3, "Task 3", 1000, NULL, 1, NULL );

    /* 启动调度器 */
    vTaskStartScheduler();
    /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
    return 0;
}

三个任务优先级都是0,会是哪个任务先运行???任务3会先运行:

具体的讲解在上面介绍该函数的时候也已经讲清楚了, 如果调度器还未启动,则比较当前任务和新任务的优先级:如果当前任务的优先级小于或等于新任务的优先级,则更新 pxCurrentTCB 为新任务,确保在启动调度器时,当前任务是最高优先级任务之一。

那如果是3个优先级都改为0呢??答案会是空闲任务先运行,然后再运行任务1

c 复制代码
int main( void )
{
    prvSetupHardware();
    
    xTaskCreate( vTask1, "Task 1", 1000, NULL, 0, NULL );
    xTaskCreate( vTask2, "Task 2", 1000, NULL, 0, NULL );
    xTaskCreate( vTask3, "Task 3", 1000, NULL, 0, NULL );

    /* 启动调度器 */
    vTaskStartScheduler();
    /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
    return 0;
}

xTaskCreate函数在上面也讲解了:xTaskCreate-->prvAddNewTaskToReadyList( pxNewTCB ); --> prvAddTaskToReadyList( pxNewTCB ); 将任务添加进链表,那么pxReadyTasksLists[0]链表的指向是:task1 -> task2 -> task3,此时pxCurrentTCB仍然是任务3,按理说调度器启动后运行的应该会是pxCurrentTCB指向的任务3才是,然而确实空闲任务。来看看启动调度器函数vTaskStartScheduler()

c 复制代码
void vTaskStartScheduler( void )
{
    BaseType_t xReturn;

    /* Add the idle task at the lowest priority. */
    #if ( configSUPPORT_STATIC_ALLOCATION == 1 )
        {
            StaticTask_t * pxIdleTaskTCBBuffer = NULL;
            StackType_t * pxIdleTaskStackBuffer = NULL;
            uint32_t ulIdleTaskStackSize;

            /* The Idle task is created using user provided RAM - obtain the
             * address of the RAM then create the idle task. */
            vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
            xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
                                                 configIDLE_TASK_NAME,
                                                 ulIdleTaskStackSize,
                                                 ( void * ) NULL,       /*lint !e961.  The cast is not redundant for all compilers. */
                                                 portPRIVILEGE_BIT,     /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
                                                 pxIdleTaskStackBuffer,
                                                 pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */

            if( xIdleTaskHandle != NULL )
            {
                xReturn = pdPASS;
            }
            else
            {
                xReturn = pdFAIL;
            }
        }
    #else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */
        {
            /* The Idle task is being created using dynamically allocated RAM. */
            xReturn = xTaskCreate( prvIdleTask,
                                   configIDLE_TASK_NAME,
                                   configMINIMAL_STACK_SIZE,
                                   ( void * ) NULL,
                                   portPRIVILEGE_BIT,  /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
                                   &xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
        }
    //省略
}

清晰的看到,也是调用到了任务创建函数,以xTaskCreate为例,那么最后的走向还是和一开始介绍这个函数的时候一样,xTaskCreate-->prvAddNewTaskToReadyList( pxNewTCB ); 中,会将空闲任务prvIdleTsk赋值给了pxNewTCB,因此先运行的是Idle,而接下来便是从pxReadyTasksLists[0]取出的链头task1

9.任务的切换

在查看存放就绪任务的二维数组pxReadyTasksLists时,还发现了另外两个链表:

c 复制代码
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;              /*< Points to the delayed task list currently being used. */

PRIVILEGED_DATA static List_t xPendingReadyList;                         /*< Tasks that have been readied while the scheduler was suspended.  They will be moved to the ready list when the scheduler is resumed. */

这里取出关键的两个:

  • 延迟任务链表:指向当前正在使用的延迟任务列表。
  • 等待就绪任务链表:在调度程序挂起时已准备好的任务。当调度程序恢复时,它们将被移动到就绪列表中。

先来看一个任务函数:

c 复制代码
void vTask1( void *pvParameters )
{
    //前面进行了一些简单的操作后执行下面这行代码
    vTaskDelay(xDelay50ms);
    //后面也有其它的一些代码
}

void vTask2(void *pvParameters)
{
    //省略
}

void vTask23(void *pvParameters)
{
    省略
}

int main( void )
{
    prvSetupHardware();

    xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
    xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
    xTaskCreate( vTask3, "Task 3", 1000, NULL, 1, NULL );

    /* 启动调度器 */
    vTaskStartScheduler();
    /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
    return 0;
}

主要就是查这个任务函数调用了vTaskDelay函数会发现什么??

首先要知道的是,task1的优先级是最高的,如果不调用vTaskDelay的话,task2和3根本没机会运行到的。原本调用完xTaskCreate创建好的vTask1任务是存放在了pxReadyTasksLists[2]的链表头,优先级最高会将其从pxReadyTasksLists[2]链表中取出来运行,而调用了vTaskDelay延迟函数,这个vTask1任务是否还会放到pxReadyTasksLists[2]链表???

并不会,而是放到了上面所说的:pxDelayedTaskList链表,这样下一次tick中断发生后调度器在pxReadyTasksLists中取出已经就绪的任务是才不会是vtask1,而是其它任务

当然,任务1等待5个tick(一般1个tick是1ms)后又可以重新运行了,也就是会重新放到了pxReadyTasksLists[2]的链表当中,等待tick中断发生调度

疑问

疑问1

相同优先级情况下,后面创建的任务反而会先运行

  • 后面的任务插入链表时,pxCurrentTCB会执行它

疑问2

使用vTaskDelay时,如何延时若干毫秒?

  • 使用宏或者自己将毫秒换算成tick数
  • 假设配置项configTICK_RATE_HZ等于1000,则tick周期为1ms,想延时N毫秒,就使用vTaskDelay(N)。有一个:pdMS TO TICKS(ms),可以把毫秒转换为Tick数

疑问3

使用Keil模拟器中的逻辑分析仪时,时间不准确

  • 确认代码中设置时钟时用的频率(在prvSetupHardware函数当中)
  • 确认Options中Xtal的频率
  • 保证上述两者相同,比如代码中使用的时钟所属的晶振频率是8MHz,就要将Xtal的频率同样设置为8

根据实际进行更改

疑问4

相关推荐
Flag- L18 分钟前
STM32标准库-TIM定时器
stm32·单片机·嵌入式硬件
2301_775602381 小时前
STM32什么是寄存器
stm32·单片机·嵌入式硬件
GenCoder3 小时前
Keil开发STM32生成hex文件/bin文件
stm32·bin文件生成·keil开发
lixzest4 小时前
STM32开发中,线程启动异常问题排查简述
stm32·嵌入式硬件
scoone5 小时前
四款主流物联网操作系统(FreeRTOS、LiteOS、RT-Thread、AliOS)的综合对比分析
嵌入式
Evan_ZGYF丶5 小时前
【PCIe总线】 -- PCI、PCIe相关实现
linux·嵌入式·pcie·pci
学习噢学个屁5 小时前
基于STM32语音识别柔光台灯
c语言·stm32·单片机·嵌入式硬件·语音识别
欢乐熊嵌入式编程7 小时前
欢乐熊大话蓝牙知识14:用 STM32 或 EFR32 实现 BLE 通信模块:从0到蓝牙,你也能搞!
stm32·单片机·嵌入式硬件
傍晚冰川8 小时前
FreeRTOS任务调度过程vTaskStartScheduler()&任务设计和划分
开发语言·笔记·stm32·单片机·嵌入式硬件·学习