经过前一篇文章 固件库的移植,我们的处理器就能完成从上电复位到进入main函数的过程,在main函数中初始化我们要用到的外设,并完成外设的中断处理函数。如果我们需要处理器完成的任务比较单一,可以不使用操作系统,比如BLE协议栈应用开发就是用了状态机模型,但如果需要处理器完成的任务比较多,为了便于管理可以引入轻巧的实时操作系统,下面以UCOS-II为例介绍下实时操作系统的移植。
1. UCOS的底层支持要素
前面有一篇文章介绍任务调度时谈到任务调度的核心是通过触发PendSV中断,在其中断处理函数中执行任务切换工作,所以PendSV应该算系统移植的一个重点。任务周期性切换的驱动源是SYSTICK系统滴答时钟,这个号称操作系统心跳的滴答定时器SYSTICK的配置和中断处理同样应该是系统移植的一个重点。对照前面提到的系统启动三要素:时钟系统、中断系统、存储系统,SYSTICK与PendSV应该算时钟与中断部分的底层支持要素。
操作系统一般涉及到多任务间的同步,虽然有互斥量与信号量这类偏上层的事件控制块保证任务间同步,也有临界区这种偏底层的工具保证任务内的原子操作,既然是偏底层的,临界区应该也是移植的一个关注点,而且临界区涉及到开关中断,也属于时钟与中断部分的底层支持要素。
接下来看看存储方面有哪些跟处理器直接相关的要素,我们学习单片机或编程语言时应该了解过相同的数据类型在不同的处理器平台或编译器上可能会表现出不同的位宽,例如整型int可能在16位单片机上占16位宽度也即2字节而在32位单片机上占32位宽度也即4字节,所以根据实际的处理器型号重新定义每种数据类型的位宽算是操作系统移植的一个重点。对堆栈的操作一般涉及寄存器的功能配置(比如xPSR状态寄存器),所以堆栈初始化、堆栈生长方向等也应该是操作系统移植的一个重点。数据类型位宽重定义与堆栈初始化算是存储部分的底层支持要素。
2. UCOSⅡ的移植
先下载UCOS源码,UCOS开发者Micrium除了提供单纯的UCOS源码,也针对很多平台提供了适配或移植后的源码,本文基于STM32F10X硬件平台,针对该平台适配后的源码下载网址如下:
https://www.micrium.com/downloadcenter/download-results/?searchterm=mi-stm32f107&supported=true
下载Micrium_uC-Eval-STM32F107_uCOS-II系统源码,解压后的文件结构如下:
- EvalBoards:支持评估(开发)板STM32F107的相关文件(可以理解为工程模板示例文件),包含了BSP与UCOSⅡ两个文件夹,BSP目录保存底层板级启动相关的文件,这里的启动文件较旧,我们使用前面介绍的更新的ST V3.5版固件库文件作为启动文件;UCOSⅡ目录保存系统配置文件,我们移植时会借用部分配置文件;
- uC-CPU:跟CPU架构相关的文件,我们依然使用ST标准外设固件库提供相关的功能,不使用该文件夹下的文件;
- uC-LIB:这个是Micrium官方的库,可以部分替代C-LIB,KEIL MDK还提供了Micro-LIB微库,我们可以选择使用C-LIB或Micro-LIB,当然也可以选择这个uC-LIB,如果不想使用uC-LIB,可以忽略这个文件夹;
- uCOS-II:这个文件夹是我们移植的关键,其中Source保存了UCOSⅡ的源码,移植时不需要更改;Port保存了与处理器平台相关的接口文件,是移植时的重点,需要根据处理器型号做出部分更改。
下面用一个简图展示下不同目录模块在移植过程中的关系:
其中BSP由上一节介绍的STM32F10x_StdPeriph_Lib_V3.5.0提供板级支持,如果前面ST固件库已移植完成,可以忽略BSP这部分。然后就是UCOSⅡ Port与Source目录下文件的移植。最后是应用层的配置文件来自…\EvalBoards\Micrium\uC-Eval-STM32F107\uCOS-II目录,这个等下层移植完成后再介绍。下面先看下UCOSⅡ目录下的文件结构:
其中Source目录下的源码不需要任何更改,在前面的文章中也介绍过,下面再简单总结下该目录下各文件功能:
文件名 | 功能描述 |
---|---|
os_core.c | 内核数据结构管理,ucos-ii的核心,涵盖内核的初始化、任务切换、事件块管理、事件标志组管理等功能 |
os_flag.c | 事件标志组的管理代码 |
os_mbox.c | 消息邮箱的管理代码 |
os_mem.c | 存储分区的管理代码 |
os_mutex.c | 互斥量的管理代码 |
os_q.c | 消息队列的管理代码 |
os_sem.c | 信号量的管理代码 |
os_task.c | 任务的管理代码 |
os_time.c | 时间管理,主要实现任务延时 |
os_tmr.c | 定时器管理,设置定时时间,超时则调用超时函数 |
ucos_ii.h | 定义了全局变量、内核数据结构。函数声明等,也是ucos-ii的核心 |
下面重点看下Port目录下各文件的功能:
文件名 | 功能描述 |
---|---|
oc_cpu_a.asm | 与处理器相关的汇编代码,主要是与任务切换相关,比如状态寄存器保存恢复、SYSTICK与PendSV配置与中断处理 |
os_cpu_c.c | 定义用户钩子函数,提供扩充软件功能的的接口,任务堆栈初始化函数也在这里定义 |
os_cpu.h | 定义数据类型、处理器相关代码、声明函数原型 |
os_dbg.c | 内核调试相关数据和相关函数 |
先看os_cpu.h,里面数据类型位宽及堆栈生长方向已经针对STM32F107做了调整,所以不需要变更,代码如下:
// uCOS-II\Ports\os_cpu.h
/*
*********************************************************************************************************
* DATA TYPES
* (Compiler Specific)
*********************************************************************************************************
*/
typedef unsigned char BOOLEAN;
typedef unsigned char INT8U; /* Unsigned 8 bit quantity */
typedef signed char INT8S; /* Signed 8 bit quantity */
typedef unsigned short INT16U; /* Unsigned 16 bit quantity */
typedef signed short INT16S; /* Signed 16 bit quantity */
typedef unsigned int INT32U; /* Unsigned 32 bit quantity */
typedef signed int INT32S; /* Signed 32 bit quantity */
typedef float FP32; /* Single precision floating point */
typedef double FP64; /* Double precision floating point */
typedef unsigned int OS_STK; /* Each stack entry is 32-bit wide */
typedef unsigned int OS_CPU_SR; /* Define size of CPU status register (PSR = 32 bits) */
#define OS_STK_GROWTH 1u /* Stack grows from HIGH to LOW memory on ARM */
涉及到的函数如下:
// uCOS-II\Ports\os_cpu.h
/*
*********************************************************************************************************
* PROTOTYPES
*********************************************************************************************************
*/
#if OS_CRITICAL_METHOD == 3u /* See OS_CPU_A.ASM */
OS_CPU_SR OS_CPU_SR_Save(void);
void OS_CPU_SR_Restore(OS_CPU_SR cpu_sr);
#endif
void OSCtxSw(void);
void OSIntCtxSw(void);
void OSStartHighRdy(void);
void OS_CPU_PendSVHandler(void);
/* See OS_CPU_C.C */
void OS_CPU_SysTickHandler(void);
void OS_CPU_SysTickInit(INT32U cnts);
#endif
上面的函数中,涉及到底层SYSTICK与PendSV的函数需要调整,由于底层BSP部分不使用UCOS代码直接使用STM32F10x固件库的代码,所以二者需要做好匹配。在STM32F10x固件库的中断向量表(见startup_stm32f10x_hd.s文件)中SysTick与PendSV的异常处理函数名分别为SysTick_Handler与PendSV_Handler,跟UCOS源码中的不一致,这里把startup_stm32f10x_hd.s文件中的SysTick_Handler与PendSV_Handler分别全部替换为UCOS源码中(见os_cpu.h文件)的函数名OS_CPU_SysTickHandler与OS_CPU_PendSVHandler(当然也可以反过来把UCOS源码中的处理函数名全部替换为ST固件库中的处理函数名),SysTick与PendSV异常触发后就可以通过中断向量表调用真正的异常处理函数了。既然这里选择了修改ST固件库中断向量表中的文件名,UCOS中的oc_cpu_a.asm文件(里面的代码在前篇任务调度器中介绍过)就不需要变更了。
下面再看下os_cpu_c.c中的代码,主要是一些钩子函数定义,下面只展示部分重要的代码(堆栈初始化、SYSTICK初始化及异常处理函数):
// uCOS-II\Ports\os_cpu_c.c
/*
*********************************************************************************************************
* SYS TICK DEFINES
*********************************************************************************************************
*/
#define OS_CPU_CM3_NVIC_ST_CTRL (*((volatile INT32U *)0xE000E010uL)) /* SysTick Ctrl & Status Reg. */
#define OS_CPU_CM3_NVIC_ST_RELOAD (*((volatile INT32U *)0xE000E014uL)) /* SysTick Reload Value Reg. */
#define OS_CPU_CM3_NVIC_ST_CURRENT (*((volatile INT32U *)0xE000E018uL)) /* SysTick Current Value Reg. */
#define OS_CPU_CM3_NVIC_ST_CAL (*((volatile INT32U *)0xE000E01CuL)) /* SysTick Cal Value Reg. */
#define OS_CPU_CM3_NVIC_PRIO_ST (*((volatile INT8U *)0xE000ED23uL)) /* SysTick Handler Prio Reg. */
#define OS_CPU_CM3_NVIC_ST_CTRL_COUNT 0x00010000uL /* Count flag. */
#define OS_CPU_CM3_NVIC_ST_CTRL_CLK_SRC 0x00000004uL /* Clock Source. */
#define OS_CPU_CM3_NVIC_ST_CTRL_INTEN 0x00000002uL /* Interrupt enable. */
#define OS_CPU_CM3_NVIC_ST_CTRL_ENABLE 0x00000001uL /* Counter mode. */
#define OS_CPU_CM3_NVIC_PRIO_MIN 0xFFu /* Min handler prio. */
/*
*********************************************************************************************************
* SYS TICK HANDLER
*
* Description: Handle the system tick (SysTick) interrupt, which is used to generate the uC/OS-II tick
* interrupt.
*
* Arguments : none.
*
* Note(s) : 1) This function MUST be placed on entry 15 of the Cortex-M3 vector table.
*********************************************************************************************************
*/
void OS_CPU_SysTickHandler (void)
{
OS_CPU_SR cpu_sr;
OS_ENTER_CRITICAL(); /* Tell uC/OS-II that we are starting an ISR */
OSIntNesting++;
OS_EXIT_CRITICAL();
OSTimeTick(); /* Call uC/OS-II's OSTimeTick() */
OSIntExit(); /* Tell uC/OS-II that we are leaving the ISR */
}
/*
*********************************************************************************************************
* INITIALIZE SYS TICK
*
* Description: Initialize the SysTick.
*
* Arguments : cnts is the number of SysTick counts between two OS tick interrupts.
*
* Note(s) : 1) This function MUST be called after OSStart() & after processor initialization.
*********************************************************************************************************
*/
void OS_CPU_SysTickInit (INT32U cnts)
{
OS_CPU_CM3_NVIC_ST_RELOAD = cnts - 1u;
/* Set prio of SysTick handler to min prio. */
OS_CPU_CM3_NVIC_PRIO_ST = OS_CPU_CM3_NVIC_PRIO_MIN;
/* Enable timer. */
OS_CPU_CM3_NVIC_ST_CTRL |= OS_CPU_CM3_NVIC_ST_CTRL_CLK_SRC | OS_CPU_CM3_NVIC_ST_CTRL_ENABLE;
/* Enable timer interrupt. */
OS_CPU_CM3_NVIC_ST_CTRL |= OS_CPU_CM3_NVIC_ST_CTRL_INTEN;
}
/*
*********************************************************************************************************
* INITIALIZE A TASK'S STACK
*
* Description: This function is called by either OSTaskCreate() or OSTaskCreateExt() to initialize the
* stack frame of the task being created. This function is highly processor specific.
*
* Arguments : task is a pointer to the task code
*
* p_arg is a pointer to a user supplied data area that will be passed to the task
* when the task first executes.
*
* ptos is a pointer to the top of stack. It is assumed that 'ptos' points to
* a 'free' entry on the task stack. If OS_STK_GROWTH is set to 1 then
* 'ptos' w