uCOS-II系统移植

23.1 操作系统概述之前的实验都是利用单片机实现某个单一功能,但是有时候需要在两个功能同时运行,这时就需要引入操作系统的概念,操作系统(operating system,简称os)是一种管理电脑硬件与软件资源的程序,同时也是计算机系统的内核与基础,操作系统大致包括5种功能:进程管理,作业管理,存储管理,设备管理与文件管理。
23.1.1 操作系统分类操作系统有三种基本类型:多道程序系统,分时系统,实时系统,最初操作系统是不支持这种微型单片机的运行的,随着科技的发展才产生了针对于这种m系列内核的嵌入式操作系统,常见的嵌入式操作系统有freeertos,ucos,uc-linux(一种linux精简版本),在stm32中一般运用freertos和ucos这两种系统,linux由于必须有内存才能运行,一般linux系统需要大约200m的存储空间才能装下,我们这里采用ucos-ii系统为例来进行嵌入式操作系统的移植实验。
23.1.2 ucos简介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个状态转换关系如图
23.1.3 ucos-ii中与任务相关的函数(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:进程的优先级23.2 ucos-ii移植我们将下载好的ucos-ii的源代码解压出来如下图所示。
23.2.1 在工程中添加相应的文件(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分组中,否则会提示有重复定义错误。
23.2.2 文件修改我们编译工程后可以发现报了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 lrosintctxsw 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);}#endifvoid osctxsw(void);void osintctxsw(void);void osstarthighrdy(void);void ospendsv(void);#if os_critical_method == 3uos_cpu_sr os_cpu_sr_save(void);void os_cpu_sr_restore(os_cpu_sr cpu_sr);#endifos_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 &= ~( 1ctrl |= 1val ; //刚进入时的计数器值 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&( 1val = 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&( 1dr ; usart1_rx_count ++ ; }#if system_support_os osintexit() ;#endif}23.3 实验例程例程:利用移植完成的ucos-ii系统新建两个任务,并且在两个任务中打印自定义的任务名称。
#include sys.h#include delay.h#include usart1.h#include includes.h/****************************************************name :task01function :任务1paramater :nonereturn :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 :task02function :任务2paramater :nonereturn :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 :startfunction :开始任务paramater :nonereturn :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 :mainfunction :主函数paramater :nonereturn :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系统移植成功。

上海集成电路产业迎来发展机遇
在线研讨会 | 在 Jetson 上使用 Metropolis API 和微服务加速边缘人工智能开发
对于存储芯片需求,三星或将获得14.2万亿韩元利润
怎样用L293D控制直流电动机
软方电子:基于RK6570A的Android工业屏
uCOS-II系统移植
三极管放大电路原理详解
基于ACR/Tbit路由器的硬件抽象层的通用性软件结构设计
移植RT-Thread到STM32开发板的详细步骤例程
引起DC/DC电路电感啸叫的因素
次世代的全能型迷你播放器,谈谈最近大火的新品山灵M0
MC9S12系列16位单片机控制的EPS系统设计
未来十年我国正规化建设小型的国际月球科研站
英人工智能研讨会举行_硬件厂商成为AI研讨会要角
什么是调频(FM)、调幅(AM)、短波(SW)、长波(LW)
物联网有望为企业和员工提供重要的价值主张
iphone5采用nano-SIM卡 普通SIM卡不能剪成nano-SIM卡
GitHub上开源了个集众多数据源于一身的爬虫工具箱——InfoSpider
物联网芯片有哪些种类
交换机的基本配置