b. 创建了任务之后,如何触发任务执行
使用vTaskStartScheduler() 启动任务
/* Start FreeRTOS scheduler */
vTaskStartScheduler();
{
/* The Idle task is being created using dynamically allocated RAM. */
xReturn = xTaskCreate( prvIdleTask,
configIDLE_TASK_NAME,
configMINIMAL_STACK_SIZE,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
&xIdleTaskHandle );
#endif /* configSUPPORT_STATIC_ALLOCATION */
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */
if( xReturn == pdPASS )
{
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE;
xTickCount = ( TickType_t ) 0U;
/* If configGENERATE_RUN_TIME_STATS is defined then the following
macro must be defined to configure the timer/counter used to generate
the run time counter time base. NOTE: If configGENERATE_RUN_TIME_STATS
is set to 0 and the following line fails to build then ensure you do not
have portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() defined in your
FreeRTOSConfig.h file. */
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
/* Setting up the timer tick is hardware specific and thus in the
portable interface. */
if( xPortStartScheduler() != pdFALSE )
{
/* Should not reach here as if the scheduler is running the
function will not return. */
}
else
{
/* Should only reach here if a task calls xTaskEndScheduler(). */
}
}
else
{
/* This line will only be reached if the kernel could not be started,
because there was not enough FreeRTOS heap to create the idle task
or the timer task. */
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
/* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
meaning xIdleTaskHandle is not used anywhere else. */
( void ) xIdleTaskHandle;
}
第一步: 创建空闲任务和创建一个软件定时器
空闲任务(Idle Task)
空闲任务是调度器在vTaskStartScheduler函数调用启动后自动创建的一个任务。之前的任务调度演示图处于简化的目的并没有列出空闲任务。空闲任务具有最低的优先级0,当用户创建的任务都在阻塞状态或挂起状态时,空闲任务便得以执行。最低的优先级确保空闲任务不会抢占用户任务。同时空闲任务负责清理内核的资源,所以当有任务被删除后,应该保证空闲任务能运行清理和回收内核的资源。
空闲任务可以绑定一个钩子任务(Task Hook),当空闲任务运行的时候钩子任务也会被自动调用。钩子任务里可以添加测量空闲任务运行时间的函数或者把系统放入低功耗模式的函数。空闲任务运行的时间反映出了系统的可用计算资源,可以用于推算CPU的占用率。CPU的占用率过高的话可能会对系统的实时性有影响。
钩子任务是提供给开发者的函数,实现自己想要实现的东西, 比如说,我想要计数,每计数1w下就打印出来,它的函数原型是vApplicationIdleHook( void );
空闲任务执行过程:
1、释放内存
2、检查是否使用抢占内核,如果没使用就调用taskYIELD
3、如果使用抢占式内核,而且configIDLE_SHOULD_YIELD等于1,那么空闲任务就把CPU使用权让给同优先级的其他任务。
4、是否使能钩子函数,使能的话就调用
5、是否使能Tickless模式,使能的话就做相应的处理
钩子函数(Hook)
钩子函数会被空闲任务每循环一次就自动调用一次。
通常空闲任务钩子函数被用于:
1、 执行低优先级,后台或需要不停处理的功能代码。
2、 测试出系统处理裕量(空闲任务只会在所有其它任务都不运行时才有机会执行,所以测量出空闲任务占用的处理时间就可以清楚的知道系统有多少富余的处理时间)。
3、将处理器配置到低功耗模式——提供一种自动省电方法,使得在没有任何应用功能
需要处理的时候,系统自动进入省电模式。
https://blog.csdn.net/qq_51963216/article/details/122788392
软件定时器
在MCU中有硬件定时器,由外部晶振提供时钟输入源,经过时钟模块寄存器的配置,得到中断时间,一般都是中断模式触发时间中断,在中断服务函数中设置中断操作。硬件中断时间很精准,最小能到ns级别。缺点是在MCU的外设中一般timer的数量是很少的,有限的。
而软件定时器事项功能和硬件定时器的功能是一样的,都是定时操作执行对应的服务,区别是软件定时器定时到了后执行的是回调函数,且在回调函数中不能执行阻塞任务的操作,如vTaskDelay()等。
当调用vTaskStartScheduler()来开启调度器后,这个函数内部使用xTimerCreateTimerTask()来创建软件定时器任务。
软件定时器任务的任务优先级和栈深度是在FreeRTOSConfig.h中通configTIMER_TASK_PRIORITY和configTIMER_TASK_STACK_DEPTH这两个宏配置的。
https://blog.csdn.net/langtao1/article/details/126448743
第二步: xPortStartScheduler()
这个函数的实现方式是不同的芯片有不同的实现方式,放在port.c(这个文件用于实现平台依赖的函数(不同平台之间的实现方式不同))
走进一个ARM的函数定义:
/*
* See header file for description.
*/
BaseType_t xPortStartScheduler( void )
{
/* Make PendSV, CallSV and SysTick the same priroity as the kernel. */
*(portNVIC_SYSPRI2) |= portNVIC_PENDSV_PRI;
*(portNVIC_SYSPRI2) |= portNVIC_SYSTICK_PRI;
/* Start the timer that generates the tick ISR. Interrupts are disabled
here already. */
prvSetupTimerInterrupt();
/* Initialise the critical nesting count ready for the first task. */
uxCriticalNesting = 0;
/* Start the first task. */
vPortStartFirstTask();
/* Should never get here as the tasks will now be executing! Call the task
exit error function to prevent compiler warnings about a static function
not being called in the case that the application writer overrides this
functionality by defining configTASK_RETURN_ADDRESS. Call
vTaskSwitchContext() so link time optimisation does not remove the
symbol. */
vTaskSwitchContext();
prvTaskExitError();
/* Should not get here! */
return 0;
}
a) 配置PendSV 和SysTick 的中断优先级为最低
b) prvSetupTimerInterrupt() 设置定时器周期,使能定时器的中断
/*
* Setup the systick timer to generate the tick interrupts at the required
* frequency.
*/
void prvSetupTimerInterrupt( void )
{
/* Stop and reset the SysTick. */
*(portNVIC_SYSTICK_CTRL) = 0UL;
*(portNVIC_SYSTICK_CURRENT_VALUE) = 0UL;
/* Configure SysTick to interrupt at the requested rate. */
*(portNVIC_SYSTICK_LOAD) = ( configCPU_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
*(portNVIC_SYSTICK_CTRL) = portNVIC_SYSTICK_CLK | portNVIC_SYSTICK_INT | portNVIC_SYSTICK_ENABLE;
}
c) prvPortStartFirstTask: 触发svc 中断,进入svc中断服务函数里面。
static void prvPortStartFirstTask( void )
{
/* Start the first task. This also clears the bit that indicates the FPU is
in use in case the FPU was used before the scheduler was started - which
would otherwise result in the unnecessary leaving of space in the SVC stack
for lazy saving of FPU registers. */
__asm volatile(
" ldr r0, =0xE000ED08 \n" /* Use the NVIC offset register to locate the stack. */
" ldr r0, [r0] \n"
" ldr r0, [r0] \n"
" msr msp, r0 \n" /* Set the msp back to the start of the stack. */
" mov r0, #0 \n" /* Clear the bit that indicates the FPU is in use, see comment above. */
" msr control, r0 \n"
" cpsie i \n" /* Globally enable interrupts. */
" cpsie f \n"
" dsb \n"
" isb \n"
" svc 0 \n" /* System call to start first task. */
" nop \n"
);
}
SVC(系统服务调用)
svc用于任务启动。所以只被调用一次。
有些操作系统不允许应用程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用SVC发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件,它就会产生一个SVC异常。
在该异常回调里启动第一个任务。
当svc被调用的时候,那么它就会进入到启动时在向量列表里面注册的中断服务函数里面。
中断向量列表
简单来说就是每个中断都有它的中断服务函数,放在一起形成一种表,在系统初始化的时候注册,它长这样:
/* Address Vector IRQ Source module Source description */
#define VECTOR_000 (pointer*)__BOOT_STACK_ADDRESS /* ARM core Initial Stack Pointer */
#define VECTOR_001 __startup /* 0x0000_0004 1 - ARM core Initial Program Counter */
#define VECTOR_002 default_isr /* 0x0000_0008 2 - ARM core Non-maskable Interrupt (NMI) */
#define VECTOR_003 default_isr /* 0x0000_000C 3 - ARM core Hard Fault */
#define VECTOR_004 default_isr /* 0x0000_0010 4 - ARM core MemManage Fault */
#define VECTOR_005 default_isr /* 0x0000_0014 5 - ARM core Bus Fault */
#define VECTOR_006 default_isr /* 0x0000_0018 6 - ARM core Usage Fault */
#define VECTOR_007 default_isr /* 0x0000_001C 7 - */
#define VECTOR_008 default_isr /* 0x0000_0020 8 - */
#define VECTOR_009 default_isr /* 0x0000_0024 9 - */
#define VECTOR_010 default_isr /* 0x0000_0028 10 - */
#define VECTOR_011 SVC_Handler /* 0x0000_002C 11 - ARM core Supervisor call (SVCall) */
#define VECTOR_012 default_isr /* 0x0000_0030 12 - ARM core Debug Monitor */
#define VECTOR_013 default_isr /* 0x0000_0034 13 - */
#define VECTOR_014 PendSV_Handler /* 0x0000_0038 14 - Pendable request for system service (PendableSrvReq) */
#define VECTOR_015 SysTick_Handler /* 0x0000_003C 15 - System tick timer (SysTick) */
#define VECTOR_016 default_isr /* 0x0000_0040 16 0 */
#define VECTOR_017 default_isr /* 0x0000_0044 17 1 */
#define VECTOR_018 default_isr /* 0x0000_0048 18 2 */
#define VECTOR_019 default_isr /* 0x0000_004C 19 3 */
#define VECTOR_020 default_isr /* 0x0000_0050 20 4 */
#define VECTOR_021 default_isr /* 0x0000_0054 21 5 */
#define VECTOR_022 default_isr /* 0x0000_0058 22 6 */
d) vPortSVCHandler()函数
1. 找到当前需要跑的TCB (pxCurrentTCB)
2. 找到栈顶指针,把该TCB栈中内容load到cpu 寄存器中。在pxPortInitialiseStack() 函数初始化任务栈。
3. 找到PC (Program counter)指针,进入任务函数(不太理解这里,不知道怎么找到的)
void vPortSVCHandler( void )
{
__asm volatile (
" ldr r3, pxCurrentTCBConst2 \n" /* Restore the context. */ " ldr r1, [r3] \n" /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
" ldr r0, [r1] \n" /* The first item in pxCurrentTCB is the task top of stack. */
" ldmia r0!, {r4-r11, r14} \n" /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
" msr psp, r0 \n" /* Restore the task stack pointer. */
" isb \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" bx r14 \n"
" \n"
" .align 4 \n"
"pxCurrentTCBConst2: .word pxCurrentTCB \n"
);
}
Note: 关于如何找到PC的,我之前在初始化TCB的时候,忽略了一个函数pxPortInitialiseStack(), 里面从栈顶填入要执行的任务的地址,这个就是PC值
/*
* See header file for description.
*/
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; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS; /* LR */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
pxTopOfStack -= 8; /* R11..R4. */
return pxTopOfStack;
}