之前的实验都是利用单片机实现某个单一功能,但是有时候需要在两个功能同时运行,这时就需要引入操作系统的概念,操作系统(Operating System,简称OS)是一种管理电脑硬件与软件资源的程序,同时也是计算机系统的内核与基础,操作系统大致包括5种功能:进程管理,作业管理,存储管理,设备管理与文件管理。
操作系统有三种基本类型:多道程序系统,分时系统,实时系统,最初操作系统是不支持这种微型单片机的运行的,随着科技的发展才产生了针对于这种M系列内核的嵌入式操作系统,常见的嵌入式操作系统有FreeeRTOS,uCos,uC-Linux(一种Linux精简版本),在STM32中一般运用FreeRTOS和uCos这两种系统,Linux由于必须有内存才能运行,一般Linux系统需要大约200M的存储空间才能装下,我们这里采用uCos-II系统为例来进行嵌入式操作系统的移植实验。
uCos系统最早出自于1992年美国嵌入式专家Jean J.Labrosse发表在《嵌入式系统编程》上的,并在该杂志的BBS上发布了源码,发展到现在uCos-III已经出来,但是目前使用最广泛的还是uCos-II,本单元我们采用uCos-II来进行介绍。
uCos-II是一个可以基于ROM运行的,可裁剪的,抢占式,实时多任务内核,采用C语言进行编写,这是一种专门为计算机的嵌入式应用设计的,CPU硬件相关部分采用汇编语言编写,执行效率高,占用空间小,最小内核可编译至2Kbyte,uCos-II体系结构如下图所示。
从上图可以发现,我们移植系统的时候,只需要修改os_cpu.h,os_cpu_a.asm和os_cpu.c等三个文件即可,其中其中:os_cpu.h,进行数据类型的定义,以及处理器相关代码和几个函数原型;os_cpu_a.asm,是移植过程中需要汇编完成的一些函数,主要就是任务切换函数;os_cpu.c,定义一些用户HOOK函数。
图中定时器的作用是为UCOS-II提供系统时钟节拍,实现任务切换和任务延时等功能。这个时钟节拍由OS_TICKS_PER_SEC(在os_cfg.h中定义)设置,一般我们设置uCos-II的系统时钟节拍为1ms~100ms,具体根据你所用处理器和使用需要来设置。我们利用STM32F1的SYSTICK定时器来提供UCOS-II时钟节拍。
uCos-II早期版本只支持64个任务,但是从2.80版本开始,支持任务数提高到255个,不过对我们来说一般64个任务都是足够多了,一般很难用到这么多个任务。uCos-II保留了最高4个优先级和最低4个优先级的总共8个任务,用于拓展使用,但实际上,uCos-II一般只占用了最低2个优先级,分别用于空闲任务(倒数第一)和统计任务(倒数第二),所以剩下给我们使用的任务最多可达255-2=253个(V2.91)。
所谓的任务,其实就是一个死循环函数,该函数实现一定的功能,一个工程可以有很多这样的任务(最多255个),uCos-II对这些任务进行调度管理,让这些任务可以并发工作(不是同时工作,并发只是各任务轮流占用CPU,而不是同时占用,任何时候还是只有1个任务能够占用CPU),这就是uCos-II最基本的功能。
uCos-II的任何任务都是通过一个叫任务控制块(TCB)的东西来控制的,每个任务管理块有3个最重要的参数:1,任务函数指针;2,任务堆栈指针;3,任务优先级;任务控制块就是任务在系统里面的身份证(uCos-II通过优先级识别任务)
在uCos-II中,使用CPU的时候,优先级高(数值小)的任务比优先级低的任务具有优先使用权,即任务就绪表中总是优先级最高的任务获得CPU使用权,只有高优先级的任务让出CPU使用权(比如延时)时,低优先级的任务才能获得CPU使用权。uCos-II不支持多个任务优先级相同,也就是每个任务的优先级必须不一样。任务的调度其实就是CPU运行环境的切换
uCos-II的每个任务都是一个死循环。每个任务都处在以下5种状态之一的状态下,这5种状态是:睡眠状态、就绪状态、运行状态、等待状态(等待某一事件发生)和中断服务状态。
(1)睡眠状态:任务在没有被配备任务控制块或被剥夺了任务控制块时的状态。
(2)就绪状态:系统为任务配备了任务控制块且在任务就绪表中进行了就绪登记,任务已经准备好了,但由于该任务的优先级比正在运行的任务的优先级低,还暂时不能运行,这时任务的状态叫做就绪状态。
(3)运行状态:该任务获得CPU使用权,并正在运行中,此时的任务状态叫做运行状态。
(4)等待状态:正在运行的任务,需要等待一段时间或需要等待一个事件发生再运行时,该任务就会把CPU的使用权让给别的任务而使任务进入等待状态。
(5)中断服务状态:一个正在运行的任务一旦响应中断申请就会中止运行而去执行中断服务程序,这时任务的状态叫做中断服务状态。
uCos-II任务的5个状态转换关系如图
(1)创建进程:OSTaskCreate
函数原型:OSTaskCreate( void( *task )( void *pd ), void *pdata, OS_STK *ptos, INTU prio )
函数参数:
task:指向任务代码的指针
pdata:任务开始执行时,传递给任务的参数的指针
ptos:分配给任务的堆栈的栈顶指针
prio:分配给任务的优先级
每个任务都有自己的堆栈,堆栈必须申明为OS_STK类型,并且由连续的内存空间组成。可以静态分配堆栈空间,也可以动态分配堆栈空间。
(2)删除进程
函数原型:INT8U OSTaskDel( INT8U prio )
函数参数:
prio:进程的优先级,该函数是通过任务优先级来实现任务删除的
(3)请求删除进程
函数原型:INT8U OSTaskDelReq( INT8U prio )
函数参数:
prio:进程的优先级
(4)修改进程优先级
函数原型:INT8U OSTaskChangePrio( INT8U oldprio, INT8U newprio )
函数参数:
oldprio:进程的源优先级
newprio:进程的新优先级
(5)进程挂起
函数原型:INT8U OSTaskSuspend( INT8U prio )
函数参数:
prio:进程的优先级
任务挂起和任务删除有点类似,任务挂起只是将被挂起任务的就绪标志删除,并做任务挂起记录,并没有将任务控制块任务控制块链表里面删除,也不需要释放其资源,而任务删除则必须先释放被删除任务的资源,并将被删除任务的任务控制块也给删了。被挂起的任务,在恢复后可以继续运行。
(6)恢复进程
函数原型:INT8U OSTaskResume( INT8U prio )
函数参数:
prio:进程的优先级
我们将下载好的uCOS-II的源代码解压出来如下图所示。
(1)在工程目录下建立UCOSII文件夹,并在该文件夹内新建三个文件夹CONFIG,CORE和PORT
(2)将除了os_cfg_r.h和os_dbg_r.c这两个文件以外的所有文件全部复制到CORE文件夹下
(3)在CONFIG文件夹中新建includes.h文件和os_cfg.h文件
(4)在PORT文件夹中新建os_cpu.h,os_cpu_a.asm,os_cpu_c.c这3个文件
(5)在工程中添加这三个目录下的文件,如下图所示。
注:不要把ucos-ii.c文件添加到UCOS-CORE分组中,否则会提示有重复定义错误。
我们编译工程后可以发现报了11个错误,但都是同一个错误,如下图所示。
我们在移植的时候并没有发现这个文件,那是因为我们并没有用到这个文件,这个文件是在ucos-ii.h文件中引用的,我们跳转到这个文件将其屏蔽掉。
注 :我们可以发现在修改的时候,文件虽然可以打开,但是修改不了,这是因为我们下载的源码都被设置成了只读模式,在工程中只读文件会有一个钥匙的标志,这就需要我们将文件的只读属性去掉即可。
去掉只读属性之后,我们会发现项目中的文件上钥匙标志消失了,如下图所示。
此时,我们就可以对文件内容进行修改了。打开ucos_ii.h文件,屏蔽44行的文件引用,如下图所示。
此时会发现报更多的错误,此时我们进行新建文件的修改。
(1)os_cpu_a.asm文件详解
①这部分代码主要用于定义外部变量,IMPORT表示这是一个外部变量,不是在本程序内定义的,EXPORT则表示这些函数位于该文件内,供其他文件调用,类似于C语言中的extern关键字。
IMPORT OSRunning
IMPORT OSPrioCur
IMPORT OSPrioHighRdy
IMPORT OSTCBCur
IMPORT OSTCBHighRdy
IMPORT OSIntNesting
IMPORT OSIntExit
IMPORT OSTaskSwHook
EXPORT OSStartHighRdy
EXPORT OSCtxSw
EXPORT OSIntCtxSw
EXPORT OS_CPU_SR_Save
EXPORT OS_CPU_SR_Restore
EXPORT PendSV_Handler
②EQU和C语言中的define关键字一样,用于宏定义,定义了一些寄存器的地址
NVIC_INT_CTRL EQU 0xE000ED04 ;中断控制寄存器
NVIC_SYSPRI2 EQU 0xE000ED20 ;系统优先级寄存器
NVIC_PENDSV_PRI EQU 0xFFFF0000 ;PendSV中断和系统节拍中断
NVIC_PENDSVSET EQU 0x10000000 ;触发软件中断的值
PRESERVE8
AREA |.text|, CODE, READONLY
THUMB
③OS_CPU_SR_Save和OS_CPU_SR_Restore是用于开关中断的汇编函数,通过给PRIMASK写1来关闭中断,写0来开启中断,这里也可以使用CPS指令来快速开关中断
OS_CPU_SR_Save
MRS R0, PRIMASK ;读取PRIMASK到R0,R0为返回值
CPSID I ;PRIMASK=1,关中断(NMI和硬件FAULT可以响应)
BX LR ;返回
OS_CPU_SR_Restore
MSR PRIMASK, R0 ;读取R0到PRIMASK中,R0为参数
BX LR ;返回
④OSStartHighRdy是由OSStart()调用,用来开启多任务,如果多任务开启失败就会进入OSStartHang函数中
OSStartHighRdy
LDR R4, =NVIC_SYSPRI2 ;设置PendSV优先级
LDR R5, =NVIC_PENDSV_PRI
STR R5, [R4]
MOV R4, #0 ;设置PSP=0
MSR PSP, R4
LDR R4, =OSRunning ;设置OSRunning=1
MOV R5, #1
STRB R5, [R4]
;切换到最高优先级的任务
LDR R4, =NVIC_INT_CTRL ;R4=NVIC_INT_CTRL
LDR R5, =NVIC_PENDSVSET ;R5=NVIC_PENDSVSET
STR R5, [R4]
CPSIE I ;开启所有中断
OSStartHang
B OSStartHang ;死循环
⑤这两个函数都用于任务切换,它们的本质都是触发PendSV中断,具体切换过程在PendSV的中断函数中进行,其中OSCtxSw是任务级切换,OSIntCtxSw是中断级切换,是从中断退出时切换到一个任务中,从中断切换到任务的过程中,CPU的寄存器入栈工作已经完成。
OSCtxSw
PUSH {R4, R5}
LDR R4, =NVIC_INT_CTRL ;触发PendSV异常
LDR R5, =NVIC_PENDSVSET
STR R5, [R4] ;向NVIC_INT_CTRL写入NVIC_PENDSVSET触发PendSV中断
POP {R4, R5}
BX LR
OSIntCtxSw
PUSH {R4, R5}
LDR R4, =NVIC_INT_CTRL ;触发PendSV异常
LDR R5, =NVIC_PENDSVSET
STR R5, [R4] ;向NVIC_INT_CTRL写入NVIC_PENDSVSET触发PendSV中断
POP {R4, R5}
BX LR
NOP
⑥这部分代码才是真正的任务切换函数,通过触发PendSV中断来进入该函数内进行任务切换
PendSV_Handler
CPSID I ;任务切换过程中必须关闭所有中断
MRS R0, PSP ;如果在用PSP堆栈,则可以忽略保存寄存器
CBZ R0, PendSV_Handler_Nosave ;如果PSP为0就转移到PendSV_Handler_Nosave
SUBS R0, R0, #0x20 ;R0-=20H
STM R0, {R4-R11}
LDR R1, =OSTCBCur
LDR R1, [R1]
STR R0, [R1]
PendSV_Handler_Nosave
PUSH {R14} ;保存R14的值
LDR R0, =OSTaskSwHook ;调用OSTaskSwHook()
BLX R0
POP {R14}
LDR R0, =OSPrioCur
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R0, =OSTCBCur
LDR R1, =OSTCBHighRdy
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2] ;R0作为新任务的SP
LDM R0, {R4-R11} ;从堆栈中恢复R4-R11
ADDS R0, R0, #0x20
MSR PSP, R0 ;用新任务的SP加载PSP
ORR LR, LR, #0x04 ;确保LR的bit2为1,返回后使用进程堆栈
CPSIE I ;开启所有中断
BX LR ;中断返回
end
(2)os_cpu.h文件详解
①这部分主要用于定义一些数据类型,其中重点关注OS_STK这个数据类型,我们在定义任务堆栈的时候就是该类型数据,这是一个32位的数据类型,按字节算的话实际堆栈大小是我们定义的4倍。
typedef unsigned char BOOLEAN;
typedef unsigned char INT8U;
typedef signed char INT8S;
typedef unsigned short INT16U;
typedef signed short INT16S;
typedef unsigned int INT32U;
typedef signed int INT32S;
typedef float FP32;
typedef double FP64;
typedef unsigned int OS_STK;
typedef unsigned int OS_CPU_SR;
②这部分代码定义了堆栈的增长方向,任务机切换的宏定义OS_TASK_SW,如果OS_CRITICAL_METHOD被定义为3的话那么进出临界段的宏定义分别为OS_ENTER_CRITICAL和OS_EXIT_CRITICAL,这两个函数都是用汇编语言编写的
//OS_CRITICAL_METHOD = 1 :直接使用处理器的开关中断指令来实现宏
//OS_CRITICAL_METHOD = 2 :利用堆栈保存和恢复CPU的状态
//OS_CRITICAL_METHOD = 3 :利用编译器扩展功能获得程序状态字,保存在局部变量cpu_sr
#define OS_CRITICAL_METHOD 3 //进入临界段的方法
#if OS_CRITICAL_METHOD == 3
#define OS_ENTER_CRITICAL() {cpu_sr = OS_CPU_SR_Save();}
#define OS_EXIT_CRITICAL() {OS_CPU_SR_Restore(cpu_sr);}
#endif
void OSCtxSw(void);
void OSIntCtxSw(void);
void OSStartHighRdy(void);
void OSPendSV(void);
#if OS_CRITICAL_METHOD == 3u
OS_CPU_SR OS_CPU_SR_Save(void);
void OS_CPU_SR_Restore(OS_CPU_SR cpu_sr);
#endif
OS_CPU_EXT INT32U OSInterrputSum;
(3)sys.h文件修改
添加关于条件编译的定义,在文件中添加以下代码即可。
#define SYSTEM_SUPPORT_OS 1
当宏定义为1的时候,编译器在编译的时候会只编译满足条件的代码,当为0时,这部分代码不会被编译。
(4)delay.c文件修改
①添加Sys_Tick中断服务函数与函数定义
#include "includes.h"
//支持UCOSII
#ifdef OS_CRITICAL_METHOD
#define delay_osrunning OSRunning //OS是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec OS_TICKS_PER_SEC //OS时钟节拍,即每秒调度次数
#define delay_osintnesting OSIntNesting //中断嵌套级别,即中断嵌套次数
#endif
//systick中断服务函数,使用OS时用到
void SysTick_Handler()
{
//OS开始跑了,才执行正常的调度处理
if( delay_osrunning==1 )
{
OSIntEnter() ; //进入中断
OSTimeTick() ; //调用ucos的时钟服务程序
OSIntExit() ; //触发任务切换软中断
}
}
②时钟初始化函数修改
void SysTick_Init( u8 SYSCLK )
{
#if SYSTEM_SUPPORT_OS
u32 reload;
#endif
SysTick->CTRL &= ~( 1<<2 ) ; //SYSTICK使用外部时钟源
fac_us = SYSCLK/8 ; //fac_us都需要使用
#if SYSTEM_SUPPORT_OS
reload = SYSCLK/8 ; //每秒钟的计数次数,单位为K
reload *= 1000000/delay_ostickspersec ; //根据delay_ostickspersec设定溢出时间
fac_ms = 1000/delay_ostickspersec ; //代表OS可以延时的最少单位
SysTick->CTRL |= 1<<1 ; //开启SYSTICK中断
SysTick->LOAD = reload ; //每1/delay_ostickspersec秒中断一次
SysTick->CTRL |= 1<<0 ; //开启SYSTICK
#else
fac_ms = ( u16 )fac_us*1000 ; //代表每个ms需要的systick时钟数
#endif
}
首先要做根据UCOSII中定义的OS_TICKS_PER_SEC来计算出SysTick的装载值reload,开启SysTick中断,将reload值写进SysTick的LOAD寄存器中,最后开启SysTick,开启SysTick后还要编写其中断服务函数。
③微秒级别延时函数
void delay_us( u16 nus )
{
#if SYSTEM_SUPPORT_OS
u32 ticks, told, tnow, tcnt=0 ;
u32 reload = SysTick->LOAD ; //LOAD的值
ticks = nus*fac_us ; //需要的节拍数
OSSchedLock() ; //禁止调度,防止打断us延时
told = SysTick->VAL ; //刚进入时的计数器值
while( 1 )
{
tnow = SysTick->VAL ;
if( tnow!=told )
{
//这里注意一下SYSTICK是一个递减的计数器
if( tnow
④毫秒级别延时函数
void delay_ms( u16 nms )
{
#if SYSTEM_SUPPORT_OS
//如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度)
if( ( delay_osrunning==1 )&&( delay_osintnesting==0 ) )
{
//延时的时间大于OS的最少时间周期
if( nms>=fac_ms )
OSTimeDly( nms/fac_ms ) ; //UCOSII延时
nms %= fac_ms ; //延时太短,采用普通方式延时
}
delay_us( ( u32 )( nms*1000 ) ) ; //普通方式延时
#else
u32 temp ;
SysTick->LOAD = ( u32 )nms*fac_ms ; //时间加载(SysTick->LOAD为24bit)
SysTick->VAL = 0x00 ; //清空计数器
SysTick->CTRL = 0x01 ; //开始倒数
do
{
temp = SysTick->CTRL ;
}while( ( temp&0x01 )&&!( temp&( 1<<16 ) ) ) ; //等待时间到达
SysTick->CTRL = 0x00 ; //关闭计数器
SysTick->VAL = 0x00 ; //清空计数器
#endif
}
(5)usart1.c文件修改
①添加头文件定义
#if SYSTEM_SUPPORT_OS
#include "includes.h"
#endif
②修改串口中断服务函数
void USART1_IRQHandler()
{
#if SYSTEM_SUPPORT_OS
OSIntEnter() ;
#endif
//接收到数据
if( USART1->SR&( 1<<5 ) )
{
if( USART1->DR=='\\n' )
{
USART1_Data.Len = USART1_Rx_Count ;
USART1_Rx_Count = 0 ;
USART1_Data.State = 1 ;
}
USART1_Data.Buffer[ USART1_Rx_Count ] = USART1->DR ;
USART1_Rx_Count ++ ;
}
#if SYSTEM_SUPPORT_OS
OSIntExit() ;
#endif
}
例程:利用移植完成的ucos-ii系统新建两个任务,并且在两个任务中打印自定义的任务名称。
#include "sys.h"
#include "delay.h"
#include "usart1.h"
#include "includes.h"
/****************************************************
Name :Task01
Function :任务1
Paramater :None
Return :None
****************************************************/
#define TASK01_PRIO 7 //设置任务优先级
#define TASK01_SIZE 64 //设置任务堆栈大小
OS_STK TASK01_STK[ TASK01_SIZE ] ; //任务堆栈
void Task01( void *pdata )
{
while( 1 )
{
printf( "Task1 Run\\r\\n" ) ;
delay_ms( 1000 ) ;
}
}
/****************************************************
Name :Task02
Function :任务2
Paramater :None
Return :None
****************************************************/
#define TASK02_PRIO 6 //设置任务优先级
#define TASK02_SIZE 64 //设置任务堆栈大小
OS_STK TASK02_STK[ TASK02_SIZE ] ; //任务堆栈
void Task02( void *pdata )
{
while( 1 )
{
printf( "Task2 Run\\r\\n" ) ;
delay_ms( 2000 ) ;
}
}
/****************************************************
Name :Start
Function :开始任务
Paramater :None
Return :None
****************************************************/
#define START_PRIO 10 //开始任务的优先级设置为最低
#define START_SIZE 64 //设置任务堆栈大小
OS_STK START_STK[ START_SIZE ] ; //任务堆栈
void Start( void *pdata )
{
OS_CPU_SR cpu_sr=0 ;
pdata = pdata ;
OS_ENTER_CRITICAL() ; //进入临界区(无法被中断打断)
OSTaskCreate( Task01, ( void * )0, ( OS_STK* )&TASK01_STK[ TASK01_SIZE-1 ], TASK01_PRIO ) ;
OSTaskCreate( Task02, ( void * )0, ( OS_STK* )&TASK02_STK[ TASK02_SIZE-1 ], TASK02_PRIO ) ;
OSTaskSuspend( START_PRIO ) ; //挂起起始任务
OS_EXIT_CRITICAL() ; //退出临界区(可以被中断打断)
}
/****************************************************
Name :Main
Function :主函数
Paramater :None
Return :None
****************************************************/
int main()
{
STM32_Clock_Init( 9 ) ; //系统时钟设置
SysTick_Init( 72 ) ; //延时初始化
USART1_Init( 72, 115200 ) ; //串口初始化为115200
OSInit() ;
OSTaskCreate( Start, ( void * )0, ( OS_STK * )&START_STK[ START_SIZE-1 ], START_PRIO ) ; //创建起始任务
OSStart() ;
while( 1 ) ;
}
将程序下载进单片机,打开串口助手可以看到以下的效果。
通过时间可以看出,Task2的任务2s打印一次数据,Task1的任务1s打印一次数据,和我们程序所写一致,所以说明UCOS-II系统移植成功。
全部0条评论
快来发表一下你的评论吧 !