面向A9的UCOS移植
一、预备知识
作为一个从来没移植过ucos的菜鸟,我能体会到新手接触ucos时的迷茫,在从官网下载到ucos的源码之后,我第一件事就是赶紧创建了一个c工程,然后去找程序的入口,结果,找了半天也没找到。后面发现,这样做其实是错误的,ucos其实并不是一个完整的运行程序,它只是一个函数库,这个函数库包含了时间管理、内存管理、多任务管理。。。等功能,也就是说我们在移植完ucos之后,可以直接通过调用他里面的函数来创建任务。就好像我们在刚学c语言时,老师告诉我们使用printf()函数,需要先调用stdio.h这个头文件,但是在调用printf()这个函数之前,我们需要建立一个主函数,然后在主函数中调用它,而这个主函数就是我们的程序入口。同样的道理,我们在调用ucos这个库之前,我们也需要为它建立一个程序入口。当然,我们现在不是在PC机上跑它,我们是要在S5P4418上面运行它,因此,除了建立程序入口之外,我们还需要将PC机上有的,而S5P4418上面没有的补全。那么,我们究竟需要补全哪些东西呢?我们来看一下。
1. 数据类型。我们在PC机下定义一个变量int i,这个i占的内存空间究竟是多少?很显然,我们一般都不去关心它,但是在4418上不关注可不行,我们必须告诉编译器一个整型所占的内存空间是多少。
2. 堆栈。在PC机下,我们往往会在程序中调用子程序,但是我们只管调用,不用去管参数怎么传递,怎样出栈,怎样入栈,同样,在4418下,参数的传递,出栈,入栈我们都得管。
3. 时间。首先,我这说的时间不是我们平时看到的时间,在这里是系统心跳时间,对于一个实时系统来说,没有心跳,它是不会干活的。
4. 临界段。新手可能有点迷糊,什么是临界段?其实说白了,进入临界段就是关中断,出临界段就是开中断。
到现在,我们的基本任务就明确了,如下:
设置数据类型、设置堆栈方法、设置心跳时间、出入临界段、设置程序入口并启动ucos。
在ucos移植之前,我们必须先对4418的硬件资源进行分析,在下面的章节中,我将先介绍在ucos移植过程中必须要用到的硬件资源。
二、硬件资源介绍
(一)、4418启动
我们知道电脑可以通过硬盘、U盘动、光驱等多种方式启动。 同样的也是有多种启动方式的,如 NAND FLASH、eMMC、SD卡等。在这里,我们使用SD卡来启动它。
下面来看一下它的SD卡启动流程。
图片不是很清楚,需要看到清晰图片的同学可以到4418用户手册的第99页查看,下面来分析一下这张图。
1.芯片上电后,最开始执行的代码ROM中的代码,这一段程序在芯片出厂的时候就由厂家固化在芯片内部,用户无法更改。
2.ROM中的代码会根据外部引脚的电平选择启动设备,在选择SD卡启动时,这3个引脚应为101。
3.在确定启动设备为SD卡启动后,ROM中的程序会从SD卡的第一个bank开始拷贝,拷贝的长度为32个bank(是不是32个bank我也没细看),超过32个bank的部分由用户自己的程序进行代码搬移
4.这32个bank的程序需不需要我们自己写呢?答案是否定的,这32个bank的程序三星也给我们做好了,它有一个名字叫2nboot,在这一段程序中,三星公司帮我们设置好了程序运行环境。同时,它也对内存和串口进行了初始化。
5.接下来的程序就需要我们自己来编写了,自己编写代码的步骤如下如下:
A.设置异常向量表
B.设置处理器到超级管理员模式,关闭中断
C.关闭MMU
D.重置异常向量表
E.初始化堆栈
F.如果代码长度过长,则需要将剩余代码拷贝到内存
G.跳转到主函数,执行C语言代码
(二)、关于4418的中断
在一个系统中,中断的地位不言而喻,4418是一个四核处理器,所以,它的中断部分比一般的处理器要复杂的多,4418有专门的中断控制器(GIC),并且有专门的手册进行说明,通过4418的手册,我们可以去ARM官网下载需要的文档。
1. 多核处理器的中断分类
打开ARM官网下载的手册,找到中断控制的篇章,找到63页,我们可以看到如下内容:
翻译过来之后,也就是公共的外围中断,简称SPI,除了SPI之外,还有PPI和SGI中断,由于这俩种我们都没有用到,所以,我们只介绍SPI中断。那么,什么是SPI中断呢?对于ARM核心来说,4418这个芯片中除了ARM核之外的设备都叫外围设备,这些设备所产生的中断就叫SPI中断。
SPI中断又可以分为我们熟知的FIQ和IRQ中断,所以,4418芯片手册330页的中断源描述表中内容全部属于SPI中断。
2. 怎样将复杂的多核中断系统变为熟知的单核中断
多核的中断系统对于新手来说,有点过于复杂,不利于学习,在这里,我们就需要屏蔽掉这些复杂的东西,转而变成容易上手的单核中断。怎么做呢?
A. 去ARM官网下载该手册
打开手册的第35页,可以看到如下内容
我们的ARM 是支持中断绕过的,也就是说,我们可以直接绕过GIC部分。
这下好办,去ARM官网下载如下手册
打开124页,可以看到如下内容
看到这个寄存器的描述,我们马上知道,只要往这个寄存器的第0位写0就可以绕过GIC 。
很自然,我们就去找这个寄存器的地址,找到第200页,GICC_CTLR在以前的版本中是叫ICCICR.
那么,我们现在来找ICCICR的地址。
打开Cortex-A9 MPCore Technical Reference Manual,第73页
手册上说它的base为0,其实我个人觉得这样描述是有偏差的,这里应该是它的偏移量为0.那它的基地址又是多少呢?继续往下看,在第26页,有一个关于A9的私有内存描述表
在这个表中,我们可以看到,中断控制接口的偏移从0x100开始,同时,它的基地址为
PERIPHBASE[31:13]的值,这个值我们需要都取协处理器cp15中c15的值,读写基地址,可以按照如下方法操作。
我读取出来的值为0XF0000000.
因此,ICCICR寄存器的地址为0XF0000000+0X100,往这个地址写0则绕过GIC,这样,多核处理器的中断就跟我们以前用的单核没有区别了。
3. 中断处理
在操作完以上步骤后,我们就可以像操作单核一样来操作多核中断,处理器在发生异常后,会跳转到相应的异常向量地址,如果说,发生了IRQ中断,程序就会跳转到我们前面设置好的IRQ异常地址处执行程序,在跳转到IRQ异常地址后,我们需要做以下事:
A. 保存现场
B. 判断中断源
C. 清除中断标志
D. 执行中断处理函数
E. 恢复现场
具体程序我会在UCOS移植时进行介绍。
(三)、关于时钟
对于一个处理器来说,时钟就是动力的来源,没有时钟处理器是不会干活的,干活速度有多快,这就跟时钟速度有关系了,因此,在我们使用4418的其他功能之前,我们必须先设置好它的时钟,不过,时钟的初始化不需要我们自己动手,在2nboot中,已经为我们做好了,设置如下:
在这里,我们只进行简单介绍,在4418用户手册第4章对时钟进行了基本描述
4418使用了4个PLL用来提升频率,通过P、M、V三个参数,可以设置PLL的值, PLL的计算方法如下:
PLL x = (MDIV × Fin)/(PDIV × 2^SDIV)
MDIV 64<=MDIV<=1023
PDIV 1<=PDIV<=63
SDIV = 0,1,2,3
x = 0,1,2,3
关于M, P , S三个值的设置,可以参考4418用户手册112页三星的推荐参数。
关于PLL的寄存器,借用别人的一张图进行说明
当然,以上只是关于PLL的寄存器描述,另外,4418还有很多时钟,不过这些时钟大多是通过4个PLL分频得到,这些时钟可以单独关闭,以降低4418的功耗
这些时钟分为3个时钟级别,每个级别有独立的寄存器分别用来控制不同模块的时钟,关于这些时钟寄存器的介绍,大家可以查阅4418用户手册第5章的内容。这里不再进行介绍。
1, 串口时钟
在4418成功启动后,串口将作为我们的主要调试手段,因此,在初始化时钟之后,我们就应该初始化串口,同样的,2nboot也为我们初始化好了串口,在这里只做简单介绍。详细内容请大家阅读4418用户手册第24章的内容。4418的串口有独立的时钟模块,大家可以仔细阅读数据手册5.1.2章节。
2, 串口波特率
在4418用户手册823页介绍了波特率的设置
串口时钟频率/(16 × 波特率)=整数部分+小数部分
设置波特率等需要的 4个寄存器
UARTCR 控制寄存器,用于使能串口和发送接收
UARTLCR_H 设置停止位校验位等
UARtiBRD 存放整数部分,直接把结果的整数放进去就行 UARTFBRD 存放计算过的小数部分,计算公式如下
(四)、关于4418的定时器
4418作为一个多核处理器,定时器资源非常丰富,每个处理器有自己的私有定时器,看门狗定时器,以及处理器共用的全局定时器,关于这些定时器,在Cortex -A9 MPCore Technical Reference Manual的第四章有介绍,另外4418也为我们提供了5个PWM定时器,timer0到timer4,其中,timer0到timer3都具备PWM输出功能,timer4不具备输出功能,关于这些定时器的描述,在4418的用户手册中第723页有详细介绍。
1、 PWMtimer的时钟
由图可知,它的时钟采用的是PCLK。值得注意的是,在使用定时器0,1时,我们需要先唤醒4418的定时器模块,在4418中,模块都是可以单独关闭的,它们由3个IP REST寄存器进行控制,关于寄存器的描述在4418手册的第226页
在对应位写1即为唤醒模块
2、 寄存器
在使用定时器时,我们需要配置以下寄存器
其中TCFG0是时钟预定标和死区空间配置寄存器,在使用timer4时,预定标值为TCHG0[15:8],死区值为[23:16];
TCFG1为时钟功能寄存器,通过设置这个寄存器可以对时钟进行分频和对外部时钟进行计数。
TCON为定时器控制寄存器,它可以控制timer的启动、数据更新方式等。
TCNTB0为计数值寄存器,TCMTB0为计数比较寄存器。
另外,还有定时器共用的中断控制和中断状态寄存器TINT_CSTAT
3、 定时器的计数方式
定时器对分频后的时钟进行减计数,当计数值到达0时,将产生周期中断。
到这里,在移植UCOS时必须要用到的资源介绍完毕。
三、Ucos_II移植
(一)、UCOS源码
Ucos的源码有俩种,一种是对应平台上的ucos,一种是存粹的源代码,在这里,我们从官网下载S3C2410平台的源代码。下载解压后内容如下:
我们只需要common文件夹和source文件夹的内容,S3C2410文件夹的内容不需要,在接下来的篇幅中,如果不是我们自己编写的文件,都来自于这俩个文件夹,大家只需要直接复制便可。
新建我们自己的工程,新建文件夹include和source,其中,include为头文件目录,source为源文件目录。在include目录下新建common目录和ucos_ii目录,在source目录下新建common目录和ucos_ii目录。
(二)、添加文件
在common文件夹中,我们存放与平台相关的一些公用函数,与平台相关的头文件存放在include目录下的common文件夹下,c语言文件和汇编语言文件存放在source目录下的common文件夹下。在接下来的移植过程中,我们都将代码文件夹分为头文件目录和源文件目录,不再赘述。
首先,在common头文件目录下新建def.h文件,并编写以下内容:
在这一段代码中,主要是讲ucos中的数据类型与编译器的数据类型一一对应,和对数据的位操作方式定义。
在common源文件目录下新建frmwrk.c文件,我们只需要编写一个函数
在这个函数中,do_irqs为我们自己的IRQ中断入口函数,在中断发生后,pc将会跳转到do_irqs处执行程序。
编写完后,在common头文件目录下编写frmwrk.h文件对函数进行声明。
在common头文件目录下编写includes.h文件,编写以下内容
将从官网下载文件中的os_cpu_a.s,os_cpu_c.c复制到common源文件目录下。
将从官网下载文件中的 os_cpu.h复制到common头文件目录下。
到这里,common目录下的工作完成。
将从官网下载文件中以下文件添加到ucos_ii源文件目录下。
打开os_core.c文件,在土中对应位置添加蓝色部分内容,添加这部分内容的作用是使我们的系统能够及时的切换到优先级最高的任务。
将从官网下载文件中以下文件添加到ucos_ii头文件目录下。
(三)、堆栈方法
在ucos中,堆栈方法分俩类,一类是中断对栈,一类是任务堆栈,先介绍中断堆栈。
中断堆栈的功能主要是在发生中断时保存现场和恢复现场,4418上的代码如下:
以上截图只是任务堆栈的一部分内容,大家找到位置后可仔细查看。
任务堆栈主要是在任务切换时保存和恢复现场,任务堆栈的函数在os_cpu_a.s中,内容如下
以上代码中,以蓝色部分为界,上半部分是保存现场,下半部分是恢复现场,蓝色部分是中断处理函数。
(四)、时钟心跳
时钟心跳依赖于4418的定时器,因此,想要初始化ucos的心跳我们首先要设置4418的定时器,并使定时器按时发出中断,然后在中断处理函数中调用ucos的心跳函数。
图中的蓝色部分函数就是UCOS的心跳函数,每调用一次,系统的心跳就会累加一次。
(五)、临界段
前面也说了,进入临界段就是关闭中断,出临界段就是开中断,用函数实现就是以下内容,这俩个函数在os_cpu_a.s中,如果没有,就添加以下内容。
(六)、设置程序入口
在4418成功启动后,我们会跳转到C语言函数执行代码,这个时候,我们就可以调用ucos的代码来创建进程了,初始化过程如下:
首先,调用Osinit()对ucos进行初始化,然后创建了一个LED进程,再然后启动系统心跳,最后,就是启动我们的UCOS了。其中,关于ucos进程的创建,大家可以直接去百度。
四、提高CPU效率
由于内存访问速度远远跟不上CPU的速度,如果直接运行的话CPU的效率不到10%,这样就特别浪费资源,而如果为CPU配置好了CACHE,CPU的效率可以达到%90,运算速度可以提高几十倍,下面介绍CACHE的配置。
在配置CACHE之前,首先要配置好内存属性,才能让CACHE访问,也就是说,我们需要先配置好MMU,关于MMU的配置大家可以自己百度,在这里我只介绍内存的属性该如何配置,在MMU配置中,页表内容的低12位标识内存属性,具体每个位的内容如下
11:10位,内存的A、P校验位
8:5 位,域的编号
C位,表示运行CACHE访问
B位,表示允许回写
在设置内存属性时,我们要使DDR3范围内的内存允许CHCHE访问和回写,如果不在DDR范围之内,为了安全起见,我们只允许CACHE访问,不允许回写。
用函数实现如下:
代码中,tb是页表地址,为了方便,我们采用的是平行映射的方式,也就是说,物理地址就是等于虚拟地址,同时,4418的DDR范围是1G~3G,因此,我们只允许1G~3G的内容回写。
设置好映射表后,将映射表的基地址写入CP15的C2中,同时,在CP15的c3中写入内存权限。
最后,通过设置CP15的C1寄存器,打开cache,和MMU。