本内核开源免费,欢迎大家下载使用学习,目前内核基础工作模块工作正常,有bug可以反馈给我。
内核源码下载链接:https://gitee.com/qidiyun/QDos
此例程是基于 STM32F407ZG 芯片的,STM32F103 的也差不多,自己移植,或者我有空了再放上来。
自制国产实时内核——vnRTOS 所有文档:
第 1 节 内核介绍
第 2 节 代码结构
第 3 节 让内核跑起来
第 4 节 线程的同步与互斥——资源
第 5 节 线程的异步通知
问:当前市场上有ucOS、freeRTOS、RT-thread 等内核、还有开源的linux等,为什么我们还要自己在开发一套内核呢?
ucOS 是商业收费的,freeRTOS 是一个免费的开源的内核,非常好用。RT-thread也是一款国产的实时内核,非常好用,强烈推荐大家使用 RT-thread。至于为什么还要自己开发一套实时内核呢?
一个是源于技术的追求。vnRTOS 是我大学的毕业设计,当时就觉得写内核是一件很 cool 的事情。接下来会有几篇文章详细讲解如何自己写内核,有兴趣的同学可以关注一波。
另外一个是技术的储备。我们写这个 vnRTOS 不是为了去取代 ucOS或者 FreeRTOS、RT-thread等。相反,我们觉得其它实时内核做得非常好,很值得我们去学习。而我们开发 vnRTOS 是为了将来有一天,当我们被别人卡脖子的时候(参考 某为 和 某米国的故事)。当我们没有内核可用时,我们有自己的技术,我们有自己的储备。
简而言之。。。这个 vnRTOS 就是个备胎。。。(不过备胎虽备胎,但是目前我们用这个 vnRTOS做了个物联网项目,目前还算稳定,比较适合一些微小型场合)
一、前言
嵌入式系统是用来控制或者监视机器、装置、工厂等大规模设备的系统。大多数嵌入式系统都是由单个程序实现整个控制逻辑,但也有些嵌入式系统还包含操作系统。
当前比较流行的嵌入式操作系统有:WinCE、嵌入式Linux、Vxwork、ucos II 等。但他们都有着各自的缺陷。WinCE、嵌入式Linux内核较为庞大,不适应一些低端的、资源较少的场合,而Vxwork、ucos II虽然都具有微内核这个特点,但版权费较高。
本文试图自己构建一个简单微小内核,以便在一些低端的、对系统资源要求严格、且成本不高的场合中使用。
本文设计的 vn Kernel采用的是基于优先级的时间片的任务调度思想。而vn 即 John von Neumann。
John von Neumann是一位伟大的数学家。他设计的“冯·诺依曼架构”是计算机架构的一个经典。正是有了这个架构,才有了今天的计算机,也才会有了现今嵌入式系统中的核心——微控制器。
本人在此表示深深的敬意。
二、内核框架:
2.1内核定义:
内核是操作系统最基本的部分。它是为众多应用程序提供对计算机硬件的安全访问的一部分软件,这种访问是有限的,并且内核决定一个程序在什么时候对某部分硬件操作多长时间。内核的分类可分为单内核和双内核以及微内核。
显然,一个微小的内核更适合嵌入式领域。因为在嵌入式系统中,ROM、RAM等资源都特别宝贵,尤其是在一些低端的领域,ROM都只有一百多K,RAM不到一百K,无法运行Win CE、嵌入式Linux这类操作系统。
然而,采用单个程序控制的思路,则在一些对实时性要求特别高的场合下行不通,因而,一个简单有效的微小内核对系统的性能、成本有着重要的影响。
2.2任务调度:
2.1优先级调度法:
Vn Kernel采用任务的思想,支持任意多个任务,这取决于系统的RAM等资源。同时,每个任务都有自身的优先级,总共有 64 个优先级可选。其中、最高优先级和最低两个优先级是内核专有的,用户可用到的优先级有 61 个。优先级数值越大,优先级越低。
实时内核的一个标准就是当前执行的任务是否可被抢占。本文设计的Vn Kernel属于可抢占式内核,高优先级的任务会立即抢占当前任务,获得CPU的执行权。
2.2 时间片调度法:
时间片调度法是一个经典的任务调度策略,许多操作系统都采用该调度法,例如windows、linux等。
Vn kernel可选的优先级有64个,不同优先级的任务采用优先级高抢占低优先级的调度方式,同时,允许有多个任务有着相同的优先级。
相同优先级的任务将会在内核中以双向链表的形式存在,并采用时间片调度方式。时间片长度可设置,考虑到系统频繁地切换任务会带来更多的资源消耗,故而时间片长度一般为10ms。
2.3 任务块链表:
内核会为每个任务创建一个任务块以便描述该任务。在内核中,所有准备就绪的任务块会以数组链表的形式存在。所谓数组链表,即在内核中,有一个指针数组,该数组的元素指向任务链表。所有任务将根据自身的优先级,添加到对应的链表中。其结构大致如下:
图中第一行为数组元素,A~G为任务编号。内核会根据任务的优先,将任务添加到相应的链表中,并且,会根据数组的下标,使数组的元素指向相应的任务链表头。
如此,内核便可以根据该指针数组访问到所有准备就绪的任务。例如上图有许多任务都准备就绪。内核会找到最高优先级的任务,即 2 所指向的任务 A。由于与任务A相同优先级的任务还有 B 和 C。所以内核将会采用时间片调度的方法,轮流执行这三个任务。
假设这个时候,系统创建了任务 X 该任务的优先级为 1 。那么此时,任务X将抢占当前任务,获得CPU执行权。此时内核只会执行任务X,除非有更高的优先级任务或者任务X放弃CPU执行权。
三、引导代码:
bootloader:
考虑到实际产品的升级问题,本文设计了一套基于STM32的bootloader。该bootloader支持启动内核、系统升级、烧写flash、参数设置、命令行等功能。对于STM32而言,复位后系统一般都会从0x8000000 处启动。
故而、bootloader存放在0x8000000处,预留大小为 10 K。参数存放地址为0x8003000 ~ 0x8003800 ,大小为 2 K。内核存在0x8003800处。
关于BootLoader的源码见这篇文章:BootLoader 源码链接
Bootlader实际演示结果如下图:
四、内核源码分析:
4.1任务块结构体:
/***************************************************************
定义一个task_tcb结构体,用于记录任务的相关信息
***************************************************************/
typedef struct task_tcb *PT_task_tcb;
//#pragma pack(1)
typedef struct task_tcb{
INT32U *task_sp; //任务堆栈指针
/*
*以下两个元素看似必须,实则不需要。
*那么系统是怎么根据TCB找到执行函数 和 参数指针的呢?
*实际上,当我们为TCB初始化任务栈的时候,就已经将这两个参数
*传递进入了。想想看, 任务栈是什么?所有寄存器,也就是
*有 R0 (ARM架构函数的第一个参数存放在R0) PC
*现在知道为什么TCB没有指明任务的执行函数,却能找到该函数了吧。
*/
//void (*task_fun)(void *pd); //任务的执行函数
//void *pdata; //任务处理函数的参数指针
INT32U task_id; //任务的ID,由系统统一分配
#if TASK_IF_NAME
INT8U task_name[TASK_NAME_LEN]; //任务的名字,由用户指定
#endif
INT8U task_prio; //任务优先级
INT8U task_state; //任务状态
INT32U task_runtime; //任务的时间片长度
INT32U task_delaytime; //任务如果需要等待,那么等待的时间长度
/* 以下这个元素,用于就绪表中优先级相同的情况下 */
PT_task_tcb task_rdy_next;
PT_task_tcb task_rdy_prev;
// PT_task_tcb task_waitlist;
/* 以下两个仅用于构成双向链表,实际中作用不大 */
PT_task_tcb task_prev; //指向上一个
PT_task_tcb task_next; //指向下一个
struct list_head list; //链表,用于任务等待
/* 每个任务都可以获取资源,下面是任务已经申请到的资源
当任务被删除时,要释放资源
*/
struct os_resource *task_resource;
#if STACK_ADD
INT32U *pdata;
#else
//INT32U *pdata;
#endif
//
}T_task_tcb;
其中最重要的是任务堆栈指针。系统调用task_create 这个函数来创建一个任务,同时为该任务分配一块内存,用以存放任务块、此外,还将额外多分配一块内存,用以任务的堆栈。
4.2任务抢占:
当有一个更高优先级的任务发生时,内核将会触发一次软件中断。Cortex-M3架构中,提供一个可悬起中断——pendSV_handler。内核的实际任务切换工作是在该中断完成的。
内核首先将当前所有寄存器压栈。并找到最高优先级的任务的任务栈,并将里面的数据出栈。
对于Cortex-M3架构,其经典的任务栈操作汇编代码如下:
;********************************************************************
; *
; 第一次任务调度 *
; *
;********************************************************************
; 状态分析:
; *
; 当系统第一次调度任务之前,很显然,此时系统还不算存于多任务系统
; 可以看成裸机状态,那么此时系统是在运行那个任务呢?
; 显然,没有任务,可以理解为 bootloader 阶段。显然,当我们进入到
; 多任务阶段后,是不想系统再回到 之前的阶段。
; 而且,最重要的是,第一次任务切换时,触发 pendSV 中断时,系统会
; 自动将 xPSR,PC,LR,R12,R0~R3 压栈。
; 此时,系统进入中断之前不是存于多任务系统状态,那么就不需要再
; 将 R8 ~ R11 入栈。
; 之后,系统存于多任务系统状态,那么就需要对 R8 ~ R11 入栈
;
;**********************************************************************
;
;*********************************************************************
; 特权级-用户级 分析:
;
; 此外,当系统刚复位时,系统是处于 线程特权模式 请参考
; Cortex-M3 权威指南.pdf 25页
; 此时,系统缺省值的 是 MSP ,主程序堆栈。但是,当我们运行任务
; 时,希望系统使用的是 PSP ,线程堆栈。
; 当系统进入异常时,处于 特权级handler 模式,使用的一定是 MSP
; 如何从 告诉系统要使用 psp 呢?
; 方法一:
; 在中断退出时,修改LR
; ORR LR, LR, #0x04
; 方法二:
; 请参考 Cortex-M3 权威指南.pdf 40页
;
;*********************************************************************
; 实现步骤
; 1. 设置 pendSV 中断的优先级
; 2. 设置 PSP 为 0 ,告诉系统,这是第一次调用
; 3. 触发中断
;********************************************************************
__cpu_start_shced
;设置 pendSV 中断优先级
LDR R0, =NVIC_SYSPRI14 ;取中断优先级寄存器地址
LDR R1, =NVIC_PENDSV_PRI ;去中断优先级 0xff
STRB R1, [R0] ;将 R1 写入到 [r0] 中
;需要注意的是 strb 是写入一个字节
;也就是写入 0xf 8位,因为中断优先级寄存器
;都是 8 位的。
;请参考 Cortex-M3 权威指南.pdf 404 页
;设置 psp 为 0 。是否记得前面说过, msp psp 的区别?
;那么现在的问题是:当 stm32 执行到这里的时候,stm32 处于何种状态?
;显然,前面说过复位后是 特权级线程模式 ,那么可见的是 msp 。那是否意味着我们不能使用
; psp 呢? 不是,所谓可见是对于 push pop 操作而言的,那么复位后 psp 的值时多少呢?
;我不知道,没去查,但由于一般的程序,复位后都没有去更改stm的特权级,也没去修改 sp 是哪个。
;所以就没有用过 psp
;但是现在,我们希望系统第一次任务调度后,使用的是 psp 。而该 psp 指向当前任务的栈。为何?
;因为前面说过,当系统执行异常时,使用的一定是 msp 。这样就能把 系统栈 跟 任务栈很好的分开了。
;设置 psp 为0 ,告诉系统,这是第一次调度,之后再任务切换函数里头,会去修改 psp 使其指向任务栈
MOVS R0, #0
MSR PSP, R0 ;MSR 是特殊指令,用于操作特殊寄存器的
;系统刚启动时,是不允许任务调度的, if_task_run 为 0
LDR R0, =if_task_run ;将 if_task_run 的地址写入到 R0 中,这是一个伪指令
MOVS R1, #1 ;
STRB R1, [R0] ;将 R1 的值写入到地址为 R0 的内存中
;触发一次 pendSV 中断,只要往中断控制寄存器中的第 28 位写入 1 ,即可触发一次软件中断
LDR R0, =NVIC_INT_CTRL ;把 NVIC_INT_CTRT 展开,可以得到
; ldr r0, =0xE000ED04
;如果等于号后面是一个数值,则表示 r0 = 0xE000ED04
;如果等于号后面是一个变量名,或标号,则 取其地址
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
;当执行到指令时,系统已经触发了 pendSV 中断了,那么系统应该跳到中断处理函数哪里了。
;理论上是如此,不过我们得保证中断时允许的啊,可能前面禁了中断后忘记打开了。
;开中断 关中断和开中断可以分别由指令CPSID i和CPSIE i实现,
CPSIE I
;----------------------------------------------------------------------------------------------------
LDR R0, =task_shced_user
BLX R0
;接下来是一个死循环,防止系统跑飞。不过系统一般是不会到这里的。
__cpu_err
B __cpu_err
;************************************************************************
; *
; 任务切换函数 __cpu_shced *
; *
;************************************************************************
; 前面已经说过了,只需简单地触发一次 pendSV 中断即可,真正的任务
; 切换在 中断处理函数 中完成
;********************************************************************
__cpu_shced
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
;************************************************************************
; *
; 中断退出调度函数 __cpu_int_shced *
; *
;************************************************************************
; 当一个中断退出时, os_int_exit 要调用这个函数,确认是否需要从新调度
__cpu_int_shced
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
;************************************************************************
; *
; pensSV 中断处理函数 __cpu_pendSV_handler *
; *
;************************************************************************
; 真正的任务切换函数
; 由于 CM3 在中断时会有一般的寄存器自动保存到任务堆栈里头、所以
; OS_CPU_PendSVHandler 只需要保存 R4-R11 并调节堆栈指针即可
__cpu_pendSV_handler
CPSID I ;任务切换需要关中断
MRS R0, PSP ;读取 psp 的值
;如果 psp 为 0 说明是第一次任务调度,则跳过下面的步骤
CBZ R0, __cpu_pendSV_handler_nosave
;if enable the FPU
SUBS R0, R0, #0X40
VSTM R0, {S16-S31}
;如果不是 0 ,那么保存 R4 ~ R11 到任务栈
;为什么要减去 0x20呢? 0x20 是32,也就是 8 个寄存器(一个寄存器4个字节)因为还要入栈
;数数看, R4 ~ R11 是不是 8 个寄存器
SUBS R0, R0, #0X20 ;后缀 S 是要求更新 APSR 中的相关标志
STM R0, {R4-R11} ;将 {R4-R11} 压入到 地址为 R0 的内存中,注意不是压栈操作
;所以要先把 R0 - 0x32 ,之后低地址是 r4 高地址是 r11
;那么 R0 是多少呢? 显然,前面已经令其为 psp 了
;第一次任务调度时,是不会执行这段的,但是当任务开始
;调度后,psp 不再是0 ,而是当前任务的 任务栈
;修改任务的 TCB 的栈指针,请注意,TCB 结构体得第一个元素就是该任务栈的指针
;task_tcb_cur->task_ps = r0 (r0 是 psp 偏移后的值)
LDR R1, =task_tcb_cur ;当前任务 tcb 的地址
LDR R1, [R1] ;从地址中读出值
;读出来的值时什么呢? 就是 tcb 的第一个元素
;这是一个指针,任务栈指针
STR R0, [R1] ;将 R0 写入到 地址为 R1 的内存中
;这一段比较难理解,我们可以转换成 C 语言来看
;首先, task_tcb_cur 是一个指针,指向当前 任务 TCB 的内存地址
;上面 3 句等价与下面 3 句
; 1. r1 = &tcb
; 2. r1 = *(&tcb) = tcb
; 3. *(r1) = r0
;将 2 代入到 3 式中
; 4. **(&tcb) = r0
;也就是 *tcb = r0
;前面已经所过了, tcb 是执行当前任务块得指针 假设当前任务块 是 TCB
;那么:代入到 4 中
; 5. *(&TCB) = r0 也就是:
; TCB = r0 (这样写不恰当,应该是 TCB 的第一个元素)
;也就是 tcb->sp = r0 = psp
__cpu_pendSV_handler_nosave
;调用用户的函数,不过一般都置空
PUSH {R14}
LDR R0, =task_shced_user
BLX R0
POP {R14}
;修改 task_tcb_cur
; task_tcb_cur = task_tcb_high
LDR R0, =task_tcb_cur ; r0 = &task_tcb_cur
LDR R1, =task_tcb_high ; r1 = &task_tcb_high
LDR R2, [R1] ; r2 = *(&task_tcb_high)
STR R2, [R0] ; *(&task_tcb_cur) = *(&task_tcb_high)
; 约掉 * 和 & 得到
; task_tcb_cur = task_tcb_high
;接下来要出之前压入的 r4 ~ r11
;当对于第一次调度而言,之前根本就没有压入过 r4 ~ r11 啊
;请参考 os_cpu.c 中的 task_init_ptop 任务栈初始化函数
;在任务第一次创建时,就已经初始化栈了,r4 ~ r11 被手工放入,所以要先出手工放入的值
LDR R0, [R2] ; R0 = *(*(&task_tcb_high))
; 这句相当于 r0 = task_tcb_high 指向的 TCB 的第一个元素
; 也就是 r0 = task_tcb_high->task_sp
LDM R0, {R4-R11} ; 从地址为 R0 的内存中读出内容
ADDS R0, R0, #0X20 ; 知道为什么要加 20 不?栈是从高往低增长的
; 我们出了 r4-r11 这 0x20 个字节后,要从新调整栈指针
;if enable FPU
VLDM R0, {S16-S31}
ADDS R0, R0, #0X40
MSR PSP, R0 ; psp = r0
ORR LR, LR, #0X04
;打开中断
CPSIE I
BX LR
END
4.3:双向链表的操作
内核会将相同优先级的任务放到同一个链表中。其双向链表的操作函数如下:
void add_tcb_list(struct task_tcb *head, struct task_tcb *ptcb)
{
head->task_rdy_prev->task_rdy_next = ptcb;
ptcb->task_rdy_next = head;
ptcb->task_rdy_prev = head->task_rdy_prev;
head->task_rdy_prev = ptcb;
}
void del_tcb_list(struct task_tcb *ptcb)
{
ptcb->task_rdy_next->task_rdy_prev = ptcb->task_rdy_prev;
ptcb->task_rdy_prev->task_rdy_next = ptcb->task_rdy_next;
}
4.4:时间片调度算法
Cortex-M3内核提供一个系统时基定时器——Tick定时器。可以作为10ms定时功能。当发生中断时,内核会找到当前正在运行的任务,将其时间片长度减并判断其值,如果为0,在当前任务的链表中中找到下一个任务块,并执行任务切换,其代码如下:
if(task_tcb_cur->task_state == TASK_STATE_RUNING)
{
task_tcb_cur->task_runtime --; //时间片长度--
if(task_tcb_cur->task_runtime == 0)
{
task_tcb_cur->task_runtime = TASK_RUNTIME;
//task_tab[task_tcb_cur->task_prio] = task_tcb_cur->task_rdy_next;
flag = 1;
}
}
4.5:任务休眠
任务除了上面说的就绪态和运行态,任务有时候还需要休眠,让出CPU执行权,内核中的休眠函数的源码如下:
/*******************************************************************************
任务休眠
*******************************************************************************/
void task_sleep(INT32U ms)
{
INT32U cpu_sr;
sys_interrupt_disable();
tcb_tab_del(task_tcb_cur,task_tcb_cur->task_prio); //删除
task_tcb_cur->task_delaytime = ms; //休眠时间
list_add((struct list_head *)&task_tcb_cur->list,&sleep_list); //添加到休眠链表
task_shced(); //任务调度
/* 在这里更改状态 */
task_tcb_cur->task_state = TASK_STATE_SLEEP; //更改状态
sys_interrupt_enable();
}
休眠的任务将会从就绪链表中删除,并加入到一个休眠链表中——sleep_list 。定时器周期产生中断,并对休眠链表中的所有任务进行休眠时间查询。如果该任务的休眠时间到了,则将任务从休眠链表中删除,并加入到就绪链表中,再做一次任务调度。
/* 休眠链表 */
list_for_each_entry_safe_reverse(pos,n,&sleep_list,list,struct task_tcb)
{
pos->task_delaytime --;
if(pos->task_delaytime == 0)
{
pos->task_state = TASK_STATE_READY;
list_del((struct list_head *)&pos->list); //从休眠链表中删除
task_tab_add(pos,pos->task_prio); //加入到就绪链表中
flag = 1;
}
}
if(flag == 1)
{
__task_shced_timer();
//不相等时才要做切换.
if(task_tcb_high != task_tcb_cur)
{
__cpu_int_shced();
}
}
五、Cortex-M3处理器:
5.1简介:
任何内核,都是要在具体的CPU上运行才有意义。本文设计的 vn kernel 是基于STM32F103ZET6这款芯片。
而该芯片采用的ARM公司的Cortex-M3内核架构。
Cortex-M3处理器采用ARMv7-M架构,它包括所有的16位Thumb指令集和基本的32位Thumb-2指令集架构,Cortex-M3处理器不能执行ARM指令集。
5.2工作模式:
Cortex-M3处理器支持2种工作模式:线程模式和处理模式。当处理器复位时,处理器处于 “特权级线程模式”,而发生异常时,处理将会进入“特权级handle”模式,异常返回时回到“特权级线程模式”。
然而,不管是“handle”还是“线程”模式,只要处理器处于“特权级”,那么处理器将使用的程序主堆栈——MSP。
除此之外,处理器还支持“用户线程级模式”,在该模式下,处理器将使用线程堆栈——PSP。显然,我们希望任务时处于“线程级模式”,内核是处于“特权级模式”。
六、实验结果:
本文编写了一个简单的测代码。基于 STM32F407 内核工程文件如下:
Main函数如下:
一开始时处理器相关的一些初始化工作,之后调用 core_init() ,对内核进行初始化。Debug_1() 则是创建两个任务 led1 led2 。分别控制两个LED灯闪烁。 core_start() 则是开始启动内核。
两个任务的代码大致相同,如下所示:
int main(void)
{
find_stack_direction();
SystemInit();
LED_Init(); //初始化LED端口
/* 重定义向量表 */
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0000);
core_init();
debug_1();
core_start();
/*-------------------------------------------------------------------*/
// led1_task((void *)0);
while(1)
{
u8 t;
t++;
}
}
debug1函数内如下:
void debug_1(void)
{
led1_id = task_create(led1_task, (void *)0, 50, 24, "led1");
led2_id = task_create(led2_task, (void *)0, 50, 25, "led2");
res_id1 = task_create(debug_resource1, (void *)0, 50, 26, "debug1");
res_id2 = task_create(debug_resource2, (void *)0, 50, 27, "debug2");
// res_id3 = task_create(debug_resource3, (void *)0, 5, 24, "debug3");
}
void led1_task(void *p)
{
volatile INT32U i;
LED_Init();
for(;;)
{
//task_change_prio(TASK_SELF, 25);
for(i = 0; i < 2; i++)
{
res_id5 = task_create(debug_resource1, (void *)0, 50, 26, "debug1");
res_id6 = task_create(debug_resource2, (void *)0, 50, 27, "debug2");
GPIO_WriteBit(GPIOE, GPIO_Pin_3, Bit_SET);
task_sleep(100); //delay(1000); //task_sleep(100);
GPIO_WriteBit(GPIOE, GPIO_Pin_3, Bit_RESET);
task_sleep(200); //task_sleep(100);
task_delete(res_id5);
task_delete(res_id6);
}
}
}
void led2_task(void *p)
{
int i;
LED_Init();
for(;;)
{
//task_change_prio(TASK_SELF, 20);
for(i = 0; i < 2; i++)
{
res_id3 = task_create(debug_resource1, (void *)0, 50, 26, "debug1");
res_id4 = task_create(debug_resource2, (void *)0, 50, 27, "debug2");
GPIO_WriteBit(GPIOE, GPIO_Pin_4, Bit_SET);
task_sleep(300); //task_sleep(100); //
GPIO_WriteBit(GPIOE, GPIO_Pin_4, Bit_RESET);
task_sleep(200); //delay(1000); //
task_delete(res_id3);
task_delete(res_id4);
}
}
}