其实拿到开发板的那一刻我就想好了,这次不从外设开始学习,而是先要把FreeRTOS移植好再把外设一个一个的学起来。而把FreeRTOS移植到S32K144的第一步就是创建几个周期性任务。
首先先大致介绍下FreeRTOS,一切资料都可以从如下官网获取(包括代码包,资料等):
FreeRTOS - Market leading RTOS (Real Time Operating System) for embedded systems with Internet of Things extensionsMarket-leading MIT licensed open source real-time operating system (RTOS) for microcontrollers and microprocessors. Includes IoT and general purpose libraries.https://www.freertos.org/
FreeRTOS和常说的uC/OS都属于是 RTOS 类操作系统,RTOS是什么?可以分为RT(Real Time)和OS(Operating System)两部分去理解,首先,这种操作系统是适合于汽车行业等对于“硬实时性”有严格要求的场合,而大家常说的windows和linux虽也是操作系统,但这类操作系统属于“软实时性”操作系统,什么是“软实时性”?“软实时性”是指响应时间若超出了期望的时间范围,并不会让user觉得这个功能失效了,而只是觉得这台电脑有点慢;“硬实时性”是指所有任务必须在期望时间内完成,没有完成意味着该功能的失效。为什么要有这个区分呢?举个例子,打开IE浏览器搜索CSDN,等了三秒才出来,你认为不过是慢了点,不影响后续操作,但汽车安全气囊或主动安全系统的触发即使比期望时间多了1ms,都有可能造成无法挽回的损失。因此,大多数嵌入式系统不仅能满足硬实时要求,也能满足软实时要求,当然这跟MCU性能是分不开的,一般Linux操作系统只会在拥有A核的SOC芯片上去完成搭建,而我们这个普普通通相貌平平的CortexM4还是算了。
话说回来,为什么要一上来就进行FreeRTOS的移植,移植好之后可以给我们带来什么?这个问题牵扯到太多,我只从代码架构的角度来简单讲下,假设没有那我们所有函数都要堆在main函数最后的while中去完成,且不说代码阅读的问题,一个更重要的问题是这样的话除了中断外就没有任何任务的时序了,即便是你通过counter实现了每个任务运行频率的控制,但也没办法实现不同任务之间的抢占和调度。
对于前面已经下载过S32DS的同学们就不必从FreeRTOS官网下载再copy到project这样做啦,因为NXP已经帮我们下载好了,而我们需要做的只是把FreeRTOS这个包导进来,具体做法如下:
在新建好工程之后找到ComponetsLib试图,双击FreeRTOS包,怎么找到这个试图参见我的上一篇文章:
双击OSs下的Freertos,出现Component Inspector - FreeRTOS ,这里面可以配置一些freertos的选项,这些选项在FreeRTOSConfig.h也可以修改,其实本质上是一样的,唯一有一点需要注意的是,我这里的开发板时钟主频为48MHz,后面的systick必须和这个频率一致,否则os会跑的不准:
配置好之后点击Generate Code:
当看到Project里面层级中自动导入了rtos就代表你已经完成了将FreeRTOS的原始包导入工程中的任务,可以开始真正集成的工作了。
集成之前有必要先对代码的运行进行一个简单的介绍,要对代码的运行有一个时间和空间上的初步概念。一般来说,正常完整的软件启动过程是这样的:
BootROM->Boot->APP
其中BootROM是芯片厂家对芯片写死的一部分代码,这部分代码的功能是完成芯片上电后的一些自检工作,然后会jump到Boot的“main”,Boot一般有两个主要作用,1是完成刷新(刷app或自刷新),2是完成跳转;APP是我们比较熟知的一块了,正常来说我们所做的绝大部分工作都是在App这个Block中完成的。以上三个Block可以理解为具备独立的三个hex。
而我们拿到的demo工程百分之九十九不会采取上述方式让code运行,我们可以先瞅一眼demo是如何做的?从startup.s开始看起:
M4属于armv7架构,然后在section isr_vector段定义了一个label叫_isr_vector(什么是section什么是段?这个我会在后面的文章中讲到,专门讲一下linker),然后在这个段中首先定义了一个中断向量表(中断向量表中全部为弱定义的handler,并不是中断服务函数本体),表头又定义了一个label叫__StackTop,目的是在map中告诉你这个是栈顶(但是和我理解的stack不太一样,难道不应该是放到最后?或者说stack放到最前最后都是可以的?),接下来是一个Reset_Handler,这个东西和普通的中断handler可不一样,他决定了我们芯片在reset之后去做什么,指针指向哪里,也就解答了我们demo中的code是如何开始跑的。
看下Reset_Handler函数的具体内容:
/* Reset Handler */
.thumb_func
.align 2
.globl Reset_Handler
.weak Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
cpsid i /* Mask interrupts */
/* Init the rest of the registers */
ldr r1,=0
ldr r2,=0
ldr r3,=0
ldr r4,=0
ldr r5,=0
ldr r6,=0
ldr r7,=0
mov r8,r7
mov r9,r7
mov r10,r7
mov r11,r7
mov r12,r7
#ifdef START_FROM_FLASH
/* Init ECC RAM */
ldr r1, =__RAM_START
ldr r2, =__RAM_END
subs r2, r1
subs r2, #1
ble .LC5
movs r0, 0
movs r3, #4
.LC4:
str r0, [r1]
add r1, r1, r3
subs r2, 4
bge .LC4
.LC5:
#endif
/* Initialize the stack pointer */
ldr r0,=__StackTop
mov r13,r0
#ifndef __NO_SYSTEM_INIT
/* Call the system init routine */
ldr r0,=SystemInit
blx r0
#endif
/* Init .data and .bss sections */
ldr r0,=init_data_bss
blx r0
cpsie i /* Unmask interrupts */
bl main
这里注释已经写的很清晰了,依次完成了r0到r13的内核系统寄存器初始化,然后是调用了一个叫SystemInit的函数进行系统初始化,最后会跳转到大家再熟悉不过的main函数。也就是说,你可以使他跳转到任意一个函数,并不一定非要从main开始。SystemInit函数的代码基本上都是对芯片system和看门狗寄存器的操作,大致看下函数的注释内容:
/*FUNCTION**********************************************************************
*
* Function Name : SystemInit
* Description : This function disables the watchdog, enables FPU
* and the power mode protection if the corresponding feature macro
* is enabled. SystemInit is called from startup_device file.
*
* Implements : SystemInit_Activity
*END**************************************************************************/
写的很明白,这个函数是被startup调用的,完成的功能有去看门狗,是能浮点运算单元等。
那么还有一个问题,这个Startup.s是如何被执行的呢?这个问题我的理解是,.s文件是MCU在上电或reset时均必定会执行的一段引导程序,类似于我们电脑中启动时候的BIOS,目的是完成环境的初始化工作,当然也不一定只有.s这一种方式去完成(类似于我上面讲到的BootROM->Boot->APP这个过程)。
了解这些基本知识后,正式开始os任务的创建。
我对于FreeRTOS任务的创建架构是这样的:
首先从main函数开始,完成必要的初始化后进行systick的配置,需要注意的是我这里并不是使用的外设timer而是内核的timer,入参ticks前面已经讲过了,使用48000。
uint16_t SysTick_Config(unsigned int ticks)
{
SysTick->LOAD = (uint32_t)(ticks - 1UL); /* set reload register */
SCB->SHP[(((uint32_t)-1) & 0xFUL)-4UL] = (uint8_t)((((1UL << 4) - 1UL) << (8U - 4)) & (uint32_t)0xFFUL);
SysTick->VAL = 0UL; /* Load the SysTick Counter Value */
SysTick->CTRL = (1<<2) |
(1<<1) |
(1<<0); /* Enable SysTick IRQ and SysTick Timer */
return (0UL); /* Function successful */
}
创建一个队列和一个任务:
/* Create the queue. */
xQueue = xQueueCreate( mainQUEUE_LENGTH, sizeof(uint8_t) );
/* create task, name app */
xTaskCreate((TaskFunction_t )Os_InitTsk,
(const char* )"Os_InitTsk",
(uint16_t )Os_InitTsk_StckSize,
(void* )NULL,
(UBaseType_t )u_OsShell_InitTsk_Prio,
(TaskHandle_t* )&os_InitTask_Handle);
然后在Os_InitTsk函数中创建callback函数挂载user想要初始化的任务,同时创建runnable任务:
static void Os_InitTsk(void)
{
BaseType_t xReturn = pdPASS;
Os_InitTsk_Callback();
taskENTER_CRITICAL();
xReturn = xTaskCreate((TaskFunction_t )Os_1msTsk,
(const char* )"Os_1msTsk",
(uint16_t )Os_1ms_StckSize,
(void* )NULL,
(UBaseType_t )Os_1ms_Prio,
(TaskHandle_t* )&os_1msTask_Handle);
xReturn = xTaskCreate((TaskFunction_t )Os_10msTsk,
(const char* )"Os_10msTsk",
(uint16_t )Os_10ms_StckSize,
(void* )NULL,
(UBaseType_t )Os_10ms_Prio,
(TaskHandle_t* )&os_10msTask_Handle);
xReturn = xTaskCreate((TaskFunction_t )Os_100msTsk,
(const char* )"Os_100msTsk",
(uint16_t )Os_100ms_StckSize,
(void* )NULL,
(UBaseType_t )Os_100ms_Prio,
(TaskHandle_t* )&os_100msTask_Handle);
vTaskDelete(os_InitTask_Handle);
taskEXIT_CRITICAL();
}
需要特别注意的是,前后要加上锁中断,然后在任务创建完成之后要加上一行vTaskDelete(os_InitTask_Handle);表示init_task再也不会被运行,只会执行一次。还有就是周期短的任务优先级要高于周期长的,初始化任务优先级最高。(优先级越高数字越大)
最后在Os_InitTsk函数完全执行出来之后,使能调度:vTaskStartScheduler();
至此,集成工作已经全部完成,开始调试:
我在每个callback里加了个counter,执行一次counter++。跑个10s左右,暂停,看到100ms任务大约跑了100次,并且每个ms任务的counter与任务周期倍数关系一致,至此,任务创建集成完毕,我的代码后续会上传上来。
还有一个问题就是这个S32DS里的FreeRTOS包中xTaskCreateStatic函数是可以创建自由分配stack大小的任务,而我现在使用的xTaskCreate函数这部分工作是自动分配的,后续会试试另一种。