您好,欢迎来到测品娱乐。
搜索
您的当前位置:首页ucosii在STM32上的移植详解

ucosii在STM32上的移植详解

来源:测品娱乐
ucosii在stm32上的移植详解

虽然目前网上已经有不少关于ucosii在stm32上的移植版本,包括micrium也有官方移植版本。但这些版本具体是怎么移植出来的,又该怎么基于移植好的ucosii开发应用软件,网上介绍的并不多。这里介绍一下我的移植经历,希望对大家有所帮助。

我的移植基本上是从零开始的。首先想要做好移植,有两方面的内容是必须要了解。1.目标芯片;2.ucosii内核原理。

虽然我们移植的目标芯片是stm32,但操作系统的移植基本是针对Cortex-M3内核(以下简称CM3)而言的,所以我们只需了解CM3内核就好了。stm32芯片就是CM3内核加上各种各样的外设。

怎么才能了解CM3呢?看一本书<>(宋岩译,网上多的很)就好了,很多同学可能想,看完这本书移植的新鲜劲都没了,因此我把该书和移植有关的章节都列了出来,并对其中的重点内容进行介绍,我数了数相关章节还不到100页,就这点内容,总要看了吧。 相关章节如下:

chapter2 Cortex-M3概览 2.1 - 2.9

主要了解Cortex-M3的概貌。刚开始看时不用追求全部理解,后面会有详细介绍,很多内容多看几遍就明白。其中2.8 指令集,只要了解,CM3只使用thumb2就ok了。

chapter3 Cortex-M3基础 3.1 寄存器组

R0-R12: 通用寄存器 R13: 堆栈寄存器

有两个,MSP和PSP,同时只能看见一个 引用R13时,引用的是正在使用的那个 MSP:可用于异常服务和应用程序 PSP:只能用于应用程序

系统复位后,用的堆栈指针是MSP。

R14: 连接寄存器,又名LR,存储返回地址 R15: 程序计数寄存器,又名PC 3.2 特殊功能寄存器

程序状态字寄存器组(PSRs)

中断屏蔽寄存器组(PRIMASK, FAULTMASK, BASEPRI) 控制寄存器(CONTROL)

程序状态字寄存器组(PSRs)分为 应用程序 PSR(APSR) 中断号 PSR(IPSR) 执行 PSR(EPSR)

每个都是32位,由于这3个寄存器有效位是错开的,因此可以组合访问。 中断屏蔽寄存器组(PRIMASK, FAULTMASK, BASEPRI),这三个寄存器用于控制异常的使能和除能。

控制寄存器(CONTROL)它有两个作用: 1.定义级别

2.选择当前使用哪个堆栈指针 3.3 操作模式和极别

操作模式: 处理者模式和线程模式 异常处理:处理者模式 主程序:线程模式

ucosii不区分级和用户级,程序始终工作在级

这两个堆栈指针的切换是全自动的,就在出入异常服务例程时由硬件处理。 3.4 - 3.7

没什么好讲的,需要看。 3.8 复位序列

0x00000000 MSP初值

0x00000004 PC初值 复位向量 chapter7 异常 7.1 异常类型

分为系统异常(编号1-15)和外部中断(大于15) 7.2 优先级

CM3支持3个固定的高优先级和多达256级的可编程优先级。

在NVIC中,每个中断都有一个优先级配置寄存器(1个byte),用来配置该中断的优先级。但该寄存器并不是每个位都被使用,不同制造商生产的芯片不相同,譬如stm32使用4位,也就是说stm32支持16个可编程优先级(参考:chapter9)。

注意该寄存器是以MSB对齐的,因此stm32每个中断的优先级配置寄存器7:4位有效,3:0位无效。

对于优先级,CM3又分为抢占优先级和亚优先级,

NVIC中的应用程序中断及复位控制寄存器(AIRCR)的优先级分组(10:8)描述了如何划分抢占优先级和亚优先级。

什么意思?以stm32为例,优先级配置寄存器不是7:4位有效吗,如果AIRCR中的优先级分组值为4,则优先级配置寄存器的7:5位确定抢占优先级,位4确定亚优先级。此时所有中断有8个抢占优先级,每个抢占优先级有2个亚优先级。 抢占优先级高的中断可以抢占抢占优先级低的中断,即抢占优先级决定了中断是否可以嵌套。

相同抢占优先级的中断不能嵌套,但当抢占优先级相同的异常有不止一个到来时,就优先响应亚优先级最高的异常。 参考附录D

表D.9 中断优先级寄存器阵列 0xE000_E400 - 0xE000_E4EF 共240个。 表D.16系统异常优先级寄存器 0xE000_ED18 - 0xE000_ED23 共12个。 优先级相同,看中断号,中断号小的优先。

7.3 向量表

初始在0x00000000处,可以通过向量表偏移量寄存器(VTOR)(地址:0xE000_ED08)更改,一般无需更改。 7.4 中断输入及挂起行为

需要看。 7.5 Fault异常

可不看。

7.6 SVC和PendSV SVC

SVC主要用在分级和用户级的操作系统,ucosii不区分级和用户级,可以不管这个东西。

这里说点题外话,一开始我很奇怪为什么会提供这种中断,因为这种中断一般都是用在大型的操作系统上,如linux系统上,可CM3又不提供MMU,应该是无法移植linux系统。后来我才知道uclinux是针对没有MMU的嵌入式系统而设计的,不过还是很怀疑有人会在像stm32这种芯片上用uclinux。

PendSV

PendSV中断主要做上下文切换,也就是任务切换,是ucosii移植过程中最重要的中断。

主要有两点:

1.PendSV中断是手工往NVIC的PendSV悬起寄存器中写1产生的(由OS写)。

2.PendSV中断优先级必须设为最低。 在讲移植代码时会介绍具体是如何做的。 对于7.6的PendSV部分应认真研读一下。 chapter8 NVIC与中断控制

NVIC负责芯片的中断管理,它和CM3内核紧密相关。

如果对于CM3中断配置不是很了解,可以看看8.1, 8.2, 8.3, 8.4节。 8.7节讲述了SysTick定时器,需要看。 chapter9 中断的具体行为 9.1 中断/异常的响应序列 当CM3开始响应一个中断时

1.xPSR, PC, LR, R12以及R3‐R0入栈 2.取向量

3.选择堆栈指针MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC

对移植ucosii来说,需要注意1,3 9.2 异常返回

在CM3中,进入中断时,LR寄存器的值会被自动更新。9.6节对更新后的值进行说明。这里统称EXC_RETURN。返回时通过把EXC_RETURN往PC里写来识别返回动作的。因为EXC_RETURN是一个特殊值,所以对于CM3,汇编语言就不需要类似reti这种指令,而用C语言开发时,不需要特殊编译器命令指示一个函

数为中断服务程序。实际上,中断服务程序如果是c代码编写,汇编成汇编代码,函数结尾一般是reti。 9.3 嵌套的中断

只要注意:中断嵌套不能过深即可。 9.4和9.5

这两节说明CM3对中断的响应能力大大提高了,主要是硬件机制的改进。但对移植来说,并不需要关注。 9.6 异常返回值

对不同状态进入中断时,LR寄存器的值进行说明,需要看。这里有一点需要注意,该点在讲移植代码时再介绍。 9.7和9.8

对移植来说,并不需要关注。 chapter10 Cortex-M3的低层编程

这一章仅需关注10.2节,因为对移植来说汇编与C的接口是必须面对的。 10.2 汇编与C的接口 有两点需要知道:

1.当主调函数需要传递参数(实参)时,它们使用R0‐R3。其中R0传递第一个,R1传递第2个……在返回时,把返回值写到R0中。

2.在函数中,用汇编写代码时,R0-R3, R12可以随便使用,而使用R4‐R11,则必须先PUSH,后POP。

以上内容和移植多少都有些关系,刚开始看,可能不太明白,多看几遍就好了。

ucosii在stm32上的移植详解2

在详解1中主要讲了移植需要用到的CM3内核知识,本文讲一讲ucosii的原理和代码组成。ucosii最经典的学习资料莫过于邵贝贝老师的<<嵌入式实时操作系统uc/os-ii(第2版)>>,我想这本书对学ucosii已经足够了,因为他把ucosii V2.55代码都讲了一遍。移植前应该好好看看此书。

下面说说我对ucosii的理解。应该说ucosii这个内核还是比较简单的,基本可以分为任务调度,任务同步和内存管理三个部分。 任务调度

ucosii为保证实时性,给每个任务分配一个不同的优先级。当发生任务切换时,总是切换到就绪的最高优先级任务。有2种情况会发生任务切换。 1.任务等待资源就绪或自我延时; 2.退出中断;

情况1可以理解为任务主动放弃cpu的使用权。

情况2可以理解为中断后,某种资源可能就绪了,需要任务切换。

需要注意的是SysTick中断,这个中断是os的“心跳”,必须得有。这样就使得cpu会发生周期性地做任务切换。由于ucosii不支持时间片轮转调度,因此在该中断中必须做的工作仅有os的时间管理。也就是调用OSTimeTick()。 任务同步

任务同步和大多数操作系统的做法差不多,如果学过操作系统或是有多线程编程经验的话,应该很好理解。无非是任务A因为某个资源未就绪,就放弃cpu使用权,等任务B或是中断使该资源就绪,当再次任务进行切换时如果任务A优先级最高,则任务A继续执行。具体怎么实现就看邵老师的书吧。 内存管理

ucosii的内存管理比较简单,就不说了。 下面看看ucosii代码组成:

os_core.c是ucosii的核心,它包含了内核初始化,任务切换,事件块管理等,其中事件块是各个同步量(这里我把互斥量,信号量,邮箱,队列统称为同步量,不是很科学,图个方便。事件标志组不是以事件块为基础的,不过原理也差不多)的基础。

os_task.c 任务管理代码。 os_flag.c os_mbox.c os_mutex.c os_q.c

os_sem.c 各个同步量管理代码。 os_mem.c 内存管理代码。

os_time.c 时间管理代码,主要做各种延时。 os_tmr.c

定时器管理代码,这部分代码时从V2.81版才开始有的,邵老师的书讲的是V2.55版的代码,是没有这部分内容的。如果前面的代码都理解的话,这部分代码也是不难理解的。一个定时器大体由3部分组成:定时时间,回调函数和属性。当定时时间到了的话,就进行一次回调函数的处理,定时器属性说明定时器是周期性的定时还是只做一次定时。如果用户使能了OS_TMR_EN,ucosii会在内部创建一个定时器任务,负责处理各个定时器。这个任务一般应该由硬件定时器的中断函数中调用OSTmrSignal()去激活。所以从本质上说os_tmr.c中的定时器是由一个硬件定时器分化出来的。

默认情况下是由SysTick中断里通过OSTimeTickHook()去激活定时器任务的。 移植相关文件

os_cpu.h: 进行数据类型定义,处理器相关代码和几个函数原型。 os_cpu_c.c: 定义一些用户hook函数。

os_cpu_a.asm: 移植需要用汇编代码完成的函数,主要就是任务切换函数。 os_dbg.c: 内核调试相关数据和函数,可以不改。 ucosii内核就介绍到这里。

ucosii在stm32上的移植详解3

移植详解1和2中主要讲了移植需要用到的基础知识,本文则对具体的移植过程进行介绍。

首先从micrium网站上下载官方移植版本(编译器使用ARM/Keil的,V2.86版本,V2.85有问题)。

下载地址:http://micrium.com/page/downloads/ports/st/stm32 解压缩后得到如下文件夹和文件: Micrium\\ AppNotes Licensing Software ReadMe.pdf

AppNotes包含ucosii移植说明文件。这两个文件中我们仅需关心

Micrium\\AppNotes\\AN1xxx-RTOS\\AN1018-uCOS-II-Cortex-M3\\AN-1018.pdf。因为这个文件对ucosii在CM3内核移植过程中需要修改的代码进行了说明。

Licensing包含ucosii使用许可证。

Software下有好几个文件夹,在本文的移植中仅需关心uCOS-II即可。 CPU: stm32标准外设库

EvalBoards: micrium官方评估板相关代码

uc-CPU: 基于micrium官方评估板的ucosii移植代码 uC-LCD:micrium官方评估板LCD驱动代码 uc-LIB: micrium官方的一个库代码 uCOS-II: ucosii源代码

uC-Probe: 和uC-Probe相关代码 ReadMe.pdf就不说了。

好了,官方的东西介绍完了,该我们自己建立工程着手移植了。关于建立工程,并使用stm32标准外设库在我之前的文章《stm32标准外设库使用详解》已有介绍,这里请大家下载其中模板代码(http://download.csdn.net/source/3448543),本文的移植是基于这个工程的。

建立文件夹 template\\src\i template\\src\i\\src template\\src\i\\port;

把Micrium\\Software\II\\Source下的文件拷贝至template\\src\i\\src;把Micrium\\Software\II\\Ports\\ARM-Cortex-M3\\Generic\\RealView下的文 件拷贝至template\\src\i\\port;

ucosii\\src下的代码是ucosii中无需修改部分

ucosii\\port下的代码是移植时需要修改的。为防止对源码的误改动造成移植失败,可以把ucosii\\src下的代码文件设为只读。

这里根据AN-1018.pdf和移植详解1、2中介绍的移植基础知识,对ucosii\\port下的代码解释一下。 os_cpu.h

#ifdef OS_CPU_GLOBALS #define OS_CPU_EXT #else

#define OS_CPU_EXT extern #endif

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 FP; 就不解释了。

typedef unsigned int OS_STK; typedef unsigned int OS_CPU_SR;

因为CM3是32位宽的,所以OS_STK(堆栈的数据类型)被类型重定义为unsigned int。

因为CM3的状态寄存器(xPSR)是32位宽的,因此OS_CPU_SR被类型重定义为unsigned int。OS_CPU_SR是在OS_CRITICAL_METHOD方法3中保存cpu状态寄存器用的。在CM3中,移植OS_ENTER_CRITICAL(),OS_EXIT_CRITICAL()选方法3是最合适的。 #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

具体定义宏OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL(),其中OS_CPU_SR_Save()和OS_CPU_SR_Restore()是用汇编代码写的,代码在os_cpu_a.asm中,到时再解释。 #define OS_STK_GROWTH 1

CM3中,栈是由高地址向低地址增长的,因此OS_STK_GROWTH定义为1。 #define OS_TASK_SW() OSCtxSw()

定义任务切换宏,OSCtxSw()是用汇编代码写的,代码在os_cpu_a.asm中,到时再解释。

#if OS_CRITICAL_METHOD == 3 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);

void OS_CPU_SysTickHandler(void); void OS_CPU_SysTickInit(void); INT32U OS_CPU_SysTickClkFreq(void);

申明几个函数,这里要注意最后三个函数需要注释掉,为什么呢?

OS_CPU_SysTickHandler()定义在os_cpu_c.c中,是SysTick中断的中断处理函数,而stm32f10x_it.c,中已经有该中断函数的定义SysTick_Handler(),这里也就不需要了,是不是很奇怪官方移植版为什么会这样弄吧,后面我会解释的。

OS_CPU_SysTickInit()定义在os_cpu_c.c中,用于初始化SysTick定时器,它依赖于OS_CPU_SysTickClkFreq(),而此函数我们自己会实现,所以注释掉。

OS_CPU_SysTickClkFreq()定义在BSP.C (Micrium\\Software\\EvalBoards)中,而本文移植中并未用到BSP.C,后面我们会自己实现,因此可以把它注释掉。

os_cpu_c.c

ucosii移植时需要我们写10个相当简单的C函数。 OSInitHookBegin() OSInitHookEnd() OSTaskCreateHook() OSTaskDelHook() OSTaskIdleHook() OSTaskStatHook() OSTaskStkInit() OSTaskSwHook() OSTCBInitHook() OSTimeTickHook()

这些函数除了OSTaskStkInit(),都是一些hook函数。这些hook函数如果不使能的话,都不会用上,也都比较简单,看看就应该明白了,所以就不介绍。

下面就说一说OSTaskStkInit()。说之前还是得先说一下任务切换,因为初始化任务堆栈,是为任务切换服务的。代码在正常运行时,一行一行往下执行,怎么才能跑到另一个任务(即函数)执行呢?首先大家可以回想一下中断过程,当中断发生时,原来函数执行的地方(程序计数器PC、处理器状态寄存器及所有通用寄存器,即当前代码的现场)被保存到栈里面去了,然后开始取中断向量,跑到中断函数里面执行。执行完了呢,想回到原来函数执行的地方,该怎么办呢,只要把栈中保存的原来函数执行的信息恢复即可(把栈中保存的代码现场重新赋给cpu的各个寄存器),一切就都回去了,好像什么事都没发生一样。这个过程大家应该都比较熟悉,任务切换和这有什么关系,试想一下,如果有3个函数foo1(), foo2(), foo3()像是刚被中断,现场保存到栈里面去了,而中断返回时做点手脚(调度程序的作用),想回哪个回哪个,是不是就做了函数(任务)切换了。看到这里应该有点明白OSTaskStkInit()的作用了吧,它被任务创建函数调用,所以要在开始时,在栈中作出该任务好像刚被中断一样的假象。(关于任务切换的原理邵老师书中的3.06节有介绍)。

那么中断后栈中是个什么情形呢,<>中9.1.1有介绍,xPSR,PC,LR,R12,R3-R0被自动保存到栈中的,R11-R4如果需要保存,只能手工保存。因此OSTaskStkInit()的工作就是在任务自己的栈中保存cpu的所有寄存器。这些值里R1-R12都没什么意义,这里用相应的数字代号(如R1用0x01010101)主要是方便调试。

其他几个:

xPSR = 0x01000000L,xPSR T位(第24位)置1,否则第一次执行任务时Fault,

PC肯定得指向任务入口,

R14 = 0xFFFFFFFEL,最低4位为E,是一个非法值,主要目的是不让使用R14,即任务是不能返回的。

R0用于传递任务函数的参数,因此等于p_arg。

OS_STK *OSTaskStkInit (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt) {

OS_STK *stk;

(void)opt; /* 'opt' is not used, prevent warning */ stk = ptos; /* Load stack pointer */ /* Registers stacked as if auto-saved on exception */ *(stk) = (INT32U)0x01000000L; /* xPSR */ *(--stk) = (INT32U)task; /* Entry Point */ /* R14 (LR) (init value will cause fault if ever used)*/ *(--stk) = (INT32U)0xFFFFFFFEL; *(--stk) = (INT32U)0x12121212L; /* R12 */ *(--stk) = (INT32U)0x03030303L; /* R3 */ *(--stk) = (INT32U)0x02020202L; /* R2 */ *(--stk) = (INT32U)0x01010101L; /* R1 */ *(--stk) = (INT32U)p_arg; /* R0 : argument */ /* Remaining registers saved on process stack */ *(--stk) = (INT32U)0x11111111L; /* R11 */ *(--stk) = (INT32U)0x10101010L; /* R10 */ *(--stk) = (INT32U)0x09090909L; /* R9 */ *(--stk) = (INT32U)0x08080808L; /* R8 */ *(--stk) = (INT32U)0x07070707L; /* R7 */ *(--stk) = (INT32U)0x06060606L; /* R6 */ *(--stk) = (INT32U)0x05050505L; /* R5 */ *(--stk) = (INT32U)0x04040404L; /* R4 */ return (stk); }

把OS_CPU_SysTickHandler(), OS_CPU_SysTickInit()注释掉。

#define OS_CPU_CM3_NVIC_ST_CTRL (*((volatile INT32U *)0xE000E010)) #define OS_CPU_CM3_NVIC_ST_RELOAD (*((volatile INT32U *)0xE000E014))

#define OS_CPU_CM3_NVIC_ST_CURRENT (*((volatile INT32U *)0xE000E018))

#define OS_CPU_CM3_NVIC_ST_CAL (*((volatile INT32U *)0xE000E01C)) #define OS_CPU_CM3_NVIC_ST_CTRL_COUNT #define OS_CPU_CM3_NVIC_ST_CTRL_CLK_SRC #define OS_CPU_CM3_NVIC_ST_CTRL_INTEN #define OS_CPU_CM3_NVIC_ST_CTRL_ENABLE OS_CPU_SysTickInit()。 os_cpu_a.asm

这个文件包含着必须用汇编写的代码。

EXTERN OSRunning ; External references EXTERN OSPrioCur EXTERN OSPrioHighRdy EXTERN OSTCBCur EXTERN OSTCBHighRdy EXTERN OSIntNesting EXTERN OSIntExit EXTERN OSTaskSwHook

申明这些变量是在其他文件定义的,本文件只做引用(有几个好像并未引用,不过没有关系)。

EXPORT OS_CPU_SR_Save ; Functions declared in this file EXPORT OS_CPU_SR_Restore EXPORT OSStartHighRdy EXPORT OSCtxSw EXPORT OSIntCtxSw

EXPORT OS_CPU_PendSVHandler 申明这些函数是在本文件中定义的。

NVIC_INT_CTRL EQU 0xE000ED04 ;中断控制及状态寄存器ICSR的地址 NVIC_SYSPRI14 EQU 0xE000ED22 ;PendSV优先级寄存器的地址

0x00010000 0x00000004 0x00000002 0x00000001

把上面这些宏定义也注释掉,因为它们都用于OS_CPU_SysTickHandler(),

NVIC_PENDSV_PRI EQU NVIC_PENDSVSET EQU OS_CPU_SR_Save

0xFF ;PendSV中断的优先级为255(最低) 0x10000000 ;位28为1

定义几个常量,类似C语言中的#define预处理指令。

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()调用,用来启动最高优先级任务,当然任务必须在OSStart()前已被创建。 OSStartHighRdy

;设置PendSV中断的优先级 #1

LDR R0, =NVIC_SYSPRI14 ;R0 = NVIC_SYSPRI14 LDR R1, =NVIC_PENDSV_PRI ;R1 = NVIC_PENDSV_PRI STRB R1, [R0] ;*(uint8_t *)NVIC_SYSPRI14 = NVIC_PENDSV_PRI ;设置PSP为0 #2

MOVS R0, #0 ;R0 = 0 MSR PSP, R0 ;PSP = R0 ;设置OSRunning为TRUE

LDR R0, =OSRunning ;R0 = OSRunning MOVS R1, #1 ;R1 = 1 STRB R1, [R0] ;OSRunning = 1 ;触发PendSV中断 #3

LDR R0, =NVIC_INT_CTRL ;R0 = NVIC_INT_CTRL LDR R1, =NVIC_PENDSVSET ;R1 = NVIC_PENDSVSET STR R1, [R0] ;*(uint32_t *)NVIC_INT_CTRL = NVIC_PENDSVSET CPSIE I ;开中断

OSStartHang ;死循环,应该不会到这里 B OSStartHang

#1.PendSV中断的优先级应该为最低优先级,原因在<>的7.6节已有说明。

#2.PSP设置为0,是告诉具体的任务切换程序(OS_CPU_PendSVHandler()),这是第一次任务切换。做过切换后PSP就不会为0了,后面会看到。

#3.往中断控制及状态寄存器ICSR(0xE000ED04)第28位写1即可产生PendSV中断。这个<>8.4.5 其它异常的配置寄存器有说明。

当一个任务放弃cpu的使用权,就会调用OS_TASK_SW()宏,而

OS_TASK_SW()就是OSCtxSw()。OSCtxSw()应该做任务切换。但是在CM3中,所有任务切换都被放到PendSV的中断处理函数中去做了,因此OSCtxSw()只需简单的触发PendSV中断即可。OS_TASK_SW()是由OS_Sched()调用。 void OS_Sched (void) {

#if OS_CRITICAL_METHOD == 3 OS_CPU_SR cpu_sr = 0; #endif

OS_ENTER_CRITICAL(); if (OSIntNesting == 0) { if (OSLockNesting == 0) { OS_SchedNew();

if (OSPrioHighRdy != OSPrioCur) {

OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; #if OS_TASK_PROFILE_EN > 0

OSTCBHighRdy->OSTCBCtxSwCtr++; #endif

OSCtxSwCtr++;

OS_TASK_SW(); /* 触发PendSV中断 */ } } }

/* 一旦开中断,PendSV中断函数会执行(当然要等更高优先级中断处理完) */ OS_EXIT_CRITICAL(); }

OSCtxSw

;触发PendSV中断

LDR R0, =NVIC_INT_CTRL ;R0 = NVIC_INT_CTRL LDR R1, =NVIC_PENDSVSET ;R1 = NVIC_PENDSVSET

STR R1, [R0] ;*(uint32_t *)NVIC_INT_CTRL = NVIC_PENDSVSET BX LR ;返回

当一个中断处理函数退出时,OSIntExit()会被调用来决定是否有优先级更高的任务需要执行。如果有OSIntExit()对调用OSIntCtxSw()做任务切换。 OSIntCtxSw

;触发PendSV中断

LDR R0, =NVIC_INT_CTRL LDR R1, =NVIC_PENDSVSET STR R1, [R0] BX LR

看到这里有些同学可能奇怪怎么OSCtxSw()和OSIntCtxSw()完全一样,事实上,这两个函数的意义是不一样的,OSCtxSw()做的是任务之间的切换,如任务A因为等待某个资源或是做延时切换到任务B,而OSIntCtxSw()则是中断退出时,由中断状态切换到另一个任务。由中断切换到任务时,CPU寄存器入栈的工作已经做完了,所以无需做第二次了(参考邵老师书的3.10节)。这里只不过由于CM3的特殊机制导致了在这两个函数中只要做触发PendSV中断即可,具体切换由PendSV中断来处理。

前面已经说过真正的任务切换是在PendSV中断处理函数里做的,由于CM3在中断时会有一半的寄存器自动保存到任务堆栈里,所以在PendSV中断处理函数中只需保存R4-R11并调节堆栈指针即可。 PendSV中断处理函数伪代码如下: OS_CPU_PendSVHandler() {

if (PSP != NULL) {

Save R4-R11 onto task stack; OSTCBCur->OSTCBStkPtr = SP; }

OSTaskSwHook();

OSPrioCur = OSPrioHighRdy; OSTCBCur = OSTCBHighRdy;

PSP = OSTCBHighRdy->OSTCBStkPtr; Restore R4-R11 from new task stack; Return from exception; }

OS_CPU_PendSVHandler ;xPSR, PC, LR, R12, R0-R3已自动保存 CPSID I ;任务切换期间需要关中断 MRS R0, PSP ;R0 = PSP

;如果PSP == 0,跳到OS_CPU_PendSVHandler_nosave执行 #1 CBZ R0, OS_CPU_PendSVHandler_nosave ;保存R4-R11到任务堆栈

SUBS R0, R0, #0x20 ;R0 -= 0x20 STM R0, {R4-R11} ;保存R4-R11到任务堆栈 ;OSTCBCur->OSTCBStkPtr = SP;

LDR R1, =OSTCBCur ;R1 = &OSTCBCur LDR R1, [R1] ;R1 = *R1 (R1 = OSTCBCur)

STR R0, [R1] ;*R1 = R0 (*OSTCBCur = SP) #2 OS_CPU_PendSVHandler_nosave ;调用OSTaskSwHook()

PUSH {R14} ;保存R14,因为后面要调用函数 LDR R0, =OSTaskSwHook ;R0 = &OSTaskSwHook BLX R0 ;调用OSTaskSwHook() POP {R14} ;恢复R14 ;OSPrioCur = OSPrioHighRdy;

LDR R0, =OSPrioCur ;R0 = &OSPrioCur LDR R1, =OSPrioHighRdy ;R1 = &OSPrioHighRdy LDRB R2, [R1] ;R2 = *R1 (R2 = OSPrioHighRdy) STRB R2, [R0] ;*R0 = R2 (OSPrioCur = OSPrioHighRdy) ;OSTCBCur = OSTCBHighRdy;

LDR R0, =OSTCBCur ;R0 = &OSTCBCur LDR R1, =OSTCBHighRdy ;R1 = &OSTCBHighRdy LDR R2, [R1] ;R2 = *R1 (R2 = OSTCBHighRdy) STR R2, [R0] ;*R0 = R2 (OSTCBCur = OSTCBHighRdy)

LDR R0, [R2] ;R0 = *R2 (R0 = OSTCBHighRdy), 此时R0是新任务的SP

;SP = OSTCBHighRdy->OSTCBStkPtr #3

LDM R0, {R4-R11} ;从任务堆栈SP恢复R4-R11 ADDS R0, R0, #0x20 ;R0 += 0x20 MSR PSP, R0

;PSP = R0,用新任务的SP加载PSP

ORR LR, LR, #0x04 ;确保LR位2为1,返回后使用进程堆栈 #4 CPSIE I ;开中断 BX LR ;中断返回 END

#1 如果PSP == 0,说明OSStartHighRdy()启动后第一次做任务切换,而任务刚创建时R4-R11已经保存在堆栈中了,所以不需要再保存一次了。

#2 OSTCBStkPtr是任务控制块结构体的第一个变量,所以*OSTCBCur = SP(不是很科学)就是OSTCBCur->OSTCBStkPtr = SP; #3 和#2类似。

#4 因为在中断处理函数中使用的是MSP,所以在返回任务后必须使用PSP,所以LR位2必须为1。 os_dbg.c

用于系统调试,可以不管。

需要修改的代码就介绍到这里,如果还有不明白之处,就再看看AN-1018.pdf,邵老师的书和<>。

ucosii在stm32上的移植详解4

详解3中有一个问题还没解释,就是stm32f10x_it.c中已经有SysTick中断函数的定义SysTick_Handler(),为什么官方版非要弄个OS_CPU_SysTickHandler()。答案就在启动文件上,一般我们自己开发基于stm32芯片的软件,都会使用标准外设库CMSIS中提供的启动文件,而官方移植的启动文件却是自己写的,在两个文件init.s,vectors.s中

(Micrium\\Software\\EvalBoards\\ST\\STM3210B-EVAL\\RVMDK)。init.s负责进入main(),vectors.s设置中断向量。OS_CPU_SysTickHandler和OS_CPU_PendSVHandler就是在vectors.s中被设置的。

我的移植是使用标准外设库CMSIS中startup_stm32f10x_hd.s作为启动文件的,那该怎么在这个文件中设置OS_CPU_SysTickHandler呢,事实上在startup_stm32f10x_hd.s文件中,PendSV中断向量名为PendSV_Handler,所以只需用OS_CPU_PendSVHandler把所有出现PendSV_Handler的地方替换掉就可以了。

那么为什么OS_CPU_SysTickHandler不用这种方式处理呢,这样也就不用注释os_cpu.c中的OS_CPU_SysTickHandler(),这主要是基于两个原因:

1. startup_stm32f10x_hd.s尽量少该,能不改就不改。

2. 如果保留OS_CPU_SysTickHandler(),在以后开发过程中,改动

OS_CPU_SysTickHandler()中的内容可能性是非常大的,如果一不小把该文件其他部分改了造成了问题,这个bug就非常难查了,所以我一般移植好后就把ucosii的这些文件设置为只读。

对于上面的原因1,一开始移植时,我曾做过在PendSV_Handler()中调用OS_CPU_PendSVHandler(),后来发现这样不行,这是为什么呢?问题出在LR寄存器上。

PendSV_Handler() {

OS_CPU_PendSVHandler(); }

汇编出来的代码会是这样: PendSV_Handler PROC PUSH {r4,lr}

BL OS_CPU_PendSVHandler POP {r4,pc} ENDP

这样在进入OS_CPU_PendSVHandler之后,LR寄存器中存放的是指令POP {r4,pc}的地址+1。在OS_CPU_PendSVHandler中的ORR LR, LR, #0x04就不会起作用,也就无法使用PSP,移植因此失败。其实在AN-1018.pdf的3.04.06中也有强调OS_CPU_PendSVHandler必须被放置在中断向量表中。一开始我也没注意。

到这里移植的大部分工作都做完了,下面剩下的就是把工程配置好,SysTick中断处理好。

在工程中建立ucosii组,把ucosii下的文件都加进该组。这里别忘了把os_cpu_a.asm加入。

在工程的Options中,c/c++选项卡的Include Paths中添加.\\src\i\\src;.\\src\i\\port。

编译工程,会发现缺少app_cfg.h和os_cfg.h文件,app_cfg.h是用来配置应用软件的,主要是任务的优先级和堆栈大小,中断优先级等信息。目前还没有基于ucosii开发应用软件,所以只需在include文件夹中创建一个空的app_cfg.h文件即可。os_cfg.h是用来配置ucosii系统的。拷贝

Micrium\\Software\\EvalBoards\\ST\\STM3210B-EVAL\\RVMDK\\OS-Probe\\os_cfg.h到template\\include,对其做如下修改: #define OS_APP_HOOKS_EN 0 #define OS_DEBUG_EN 0 #define OS_EVENT_MULTI_EN 0 #define OS_SCHED_LOCK_EN 0 #define OS_TICK_STEP_EN 0 #define OS_TASK_CHANGE_PRIO_EN 0 #define OS_TASK_QUERY_EN 0 #define OS_TASK_STAT_EN 0 #define OS_TASK_STAT_STK_CHK_EN 0 #define OS_TASK_SUSPEND_EN 0 #define OS_FLAG_EN 0 #define OS_MBOX_EN 0 #define OS_TIME_DLY_HMSM_EN 0 #define OS_TIME_DLY_RESUME_EN 0 #define OS_TIME_GET_SET_EN 0 #define OS_TIME_TICK_HOOK_EN 0

所做的修改主要是把一些功能给去掉,减少内核大小,也利于调试。等移植完成后,如果需要该功能,再做开启。

接下来就剩下处理好SysTick中断和启动任务了。SysTick是系统的“心跳”,本质上来说就是一个定时器。先把原来main.c中的内容删除,添加如下代码: #include \"ucos_ii.h\" #include \"stm32f10x.h\"

static OS_STK startup_task_stk[STARTUP_TASK_STK_SIZE]; static void systick_init(void) {

RCC_ClocksTypeDef rcc_clocks; RCC_GetClocksFreq(&rcc_clocks);

SysTick_Config(rcc_clocks.HCLK_Frequency / OS_TICKS_PER_SEC); }

static void startup_task(void *p_arg) {

systick_init(); /* Initialize the SysTick. */ #if (OS_TASK_STAT_EN > 0)

OSStatInit(); /* Determine CPU capacity. */ #endif

/* TODO: create application tasks here */

OSTaskDel(OS_PRIO_SELF); }

int main(void) {

OSInit();

OSTaskCreate(startup_task, (void *)0,

&startup_task_stk[STARTUP_TASK_STK_SIZE - 1], STARTUP_TASK_PRIO); OSStart(); return 0; }

systick_init()用来初始化并启动SysTick定时器。 RCC_GetClocksFreq()用来获取系统时钟。 SysTick_Config()初始化并使能SysTick定时器。

这里要注意的是OS_TICKS_PER_SEC,它是每秒钟的ticks数,如果为1000,就是1s中1000个ticks,也就是说1ms就会产生一个SysTick中断。系统的时间片为1ms。

在邵老师的书中3.11节已有明确说明,必须在调用OSStart()之后,才能开启时钟节拍器(SysTick)。一般会把它放在第一个任务(启动任务)中。

startup_task()用来创建其他应用任务,创建完其他任务后,就会自己删除自己。 文件中的STARTUP_TASK_STK_SIZE,STARTUP_TASK_PRIO需要在app_cfg.h中定义。代码如下: /* task priority */

#define STARTUP_TASK_PRIO 4 /* task stack size */

#define STARTUP_TASK_STK_SIZE 80

在stm32f10x_it.c中,还需要添加SysTick中断的处理代码: void SysTick_Handler(void) {

OSIntEnter(); OSTimeTick(); OSIntExit(); }

这个代码是仿照OS_CPU_SysTickHandler()中代码的,在邵老师书的3.11节亦有说明。这里就不解释。

至此ucosii在stm32上的移植已全部完成。

ucosii在stm32上的移植详解5

详解1-4把移植过程都已经介绍了。接下来的工作是验证移植是否ok以及如何基于移植好的ucosii开发应用程序。前一个问题可以说是后一个问题的特殊情况,一般我们会创建两个简单的任务,看看任务切换是否成功来验证移植是否ok,因为任务切换可以说是ucosii最核心的功能。

任务代码(main.c):

static void task1(void *p_arg) {

for (;;) {

led_on(LED_0); OSTimeDly(500); led_off(LED_0); OSTimeDly(500); } }

static void task2(void *p_arg) {

for (;;) {

led_on(LED_1); OSTimeDly(500); led_off(LED_1); OSTimeDly(500); } }

在startup_task()创建任务: err = OSTaskCreate(task1, (void *)0,

&task1_stk[TASK1_STK_SIZE-1], TASK1_PRIO); err = OSTaskCreate(task2, (void *)0,

&task2_stk[TASK2_STK_SIZE-1], TASK2_PRIO);

把任务的堆栈大小和优先级写入app_cfg.h,定义任务堆栈,编译调试。 在任务中打断点,用模拟器调试可以发现已经可以做任务切换了。如果有板子,烧到板子中运行,可以看到两个灯会以1Hz的频率闪烁。 可以认为移植初步成功,内核其他功能有待在应用中继续验证。 如何基于移植好的ucosii开发应用程序呢?

开发应用程序大部分都是为了处理或控制一个真实的物理系统,而真实的物理系统往往都是模拟系统,为了方便计算机处理,首先需要对系统做离散化处理。针对ucosii,离散化过程是通过系统“心跳”(SysTick)来实现的。一般应用程序都有多个任务(不多任务谁用ucosii啊),任务可以分为周期任务和非周期任务。周期任务是周期性循环地处理事情的任务,而非周期任务一般是某个条件触发才执行的任务。这里有一个问题,SysTick的时间是多少合适。SysTick的时间一般取周期性任务中周期最短的时间值。譬如说,系统里有3个周期性任务:系统主任务(如处理pid等,任务周期4ms),键盘扫描任务(任务周期16ms),通信任务(任务周期128ms),SysTick时间就取4ms。当然在SysTick时间较小时,要注意系统负荷问题,这时最好测一下cpu使用率及各个任务的时间等。

周期性任务的开发套路是怎么样的呢?看看定时器任务的做法就知道了,代码在os_tmr.c。首先在OSTmr_Init()中初始化OSTmrSemSignal,然后OSTmr_Task()任务会一直等待OSTmrSemSignal,等到OSTmrSemSignal后去处理各个定时器。那么谁在释放OSTmrSemSignal呢?OSTmrSignal(),这个函数要求放在一定频率的时钟中断里,默认是在SysTick中断中(如果使能OS_TIME_TICK_HOOK_EN)。好了,现在我们可以总结总结周期性任务的一般套路了。

首先在任务初始化函数中初始化一个信号量(一般会用信号量),伪代码如下: void task_init(void) {

task_sem = OSSemCreate(0); }

在任务中等待信号量 void task (void *p_arg) {

for (;;) {

OSSemPend(task_sem, 0, &err);

/* TODO: task handle here */ } }

周期性的释放信号量 OSSemPost(task_sem);

对于上面所说系统主任务,OSSemPost(task_sem)可以放在SysTick_Handler()中。所以一般来说OS_CPU_SysTickHandler()改动的可能性是非常大的。

非周期任务的开发套路又是怎样的呢?其实和周期性任务是差不多的,只是信号量不是周期性地释放,而是按需释放。

其他内核功能就不多介绍了,大家按需使用,不是很难。 本文代码:http://download.csdn.net/source/3472653

该移植代码在我自己开发的一个小玩意上已得到一段时间的验证,未发现问题。但由于水平所限,并不敢保证该移植是没有任何问题的,殷切希望大家批评指正。

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- cepb.cn 版权所有 湘ICP备2022005869号-7

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务