物联网实时内核 vnRTOS 免费开源 旗点云作品

本内核开源免费,欢迎大家下载使用学习,目前内核基础工作模块工作正常,有bug可以反馈给我。

内核源码下载链接:https://gitee.com/qidiyun/QDos 

此例程是基于 STM32F407ZG 芯片的,STM32F103 的也差不多,自己移植,或者我有空了再放上来。

 

自制国产实时内核——vnRTOS 所有文档: vnRTOS 文档

 

另外,欢迎有志之士一起加入到内核源码的开发中,一起维护。

 

一、前言

嵌入式系统是用来控制或者监视机器、装置、工厂等大规模设备的系统。大多数嵌入式系统都是由单个程序实现整个控制逻辑,但也有些嵌入式系统还包含操作系统

当前比较流行的嵌入式操作系统有:WinCE嵌入式LinuxVxwork、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);
		}
	}
}

 

发布了145 篇原创文章 · 获赞 54 · 访问量 21万+

猜你喜欢

转载自blog.csdn.net/aa120515692/article/details/104075823