从零开始写RTOS(一)
简介
基于周立功的rt1052开发板,模仿rt-thread写一个rtos操作系统,主要参考野火的《RT-Thread内核实现与实战应用开发》来进行开发。只有基本的调度功能,以及一些调度必备的部分,其余的部分大同小异,自己写逻辑添加就行。本项目使用的是IAR作为IDE来作为编译软件。本文主要是相当于作为自己的一个学习笔记吧,不算太难,有不对的地方请大家多指教。
第一步:
新建一个工程,这一部分太简单,篇幅有限不再详述。使用IAR的话,与KEIL有一定区别,我的建议是,直接在使用的官网下一个以IAR建立的SDK,省时省力,还可以专注于系统的开发。这里我使用的就是从NXP官网下载的SDK,并且完成了硬件初始化部分。
第二步:
考虑好测试方式,我是参考野火的资料中说的方式,使用逻辑分析仪抓取的测试IO的电平来观察实际线程运行时的情况。这部分可以自己选择更适合自己的测试方式。
第三步:
在工程的跟文件目录添加一个myRTOS文件夹,用来存放我们编写的系统相关代码文件。增加一个user的文件夹,用来存放main.c文件,以及应用线程的测试代码。这里先建好文件夹,在下面的教程里面我们会一步步在这两个文件夹里面补充代码文件。这里也要在IAR里面兴建两个组,以添加代码文件到其中,这部分的操作网上有很多资料,这里不再赘述。
第四步:
到这里呢,我们终于可以开始编写代码,首先我们在user文件夹里面新建main.c文件并添加到IAR工程中去,注意要删除原本的带main函数的文件,避免冲突嘛。然后在我们新建的main.c中增加一个main函数。然后添加硬件初始化代码,这部分代码可以使用原SDK的例程中的代码,注意针对不同单板,这部分单板初始化,需要进行适应性修改,这种基础部分就不再详述了。到现在为止,我们拥有了一个能够运行在自己单板的SDK基础工程代码。裸机代码就是在这个上面开发。main函数
int main(void)
{
BOARD_ConfigMPU();
BOARD_InitPins();
BOARD_BootClockRUN();
BOARD_InitDebugConsole();
}
第五步:
在裸机系统中,系统的主题就是一个在main函数里的while(1)无限循环。我们在这个循环中进行各种我们需要的操作。在多线程系统中,我们根据功能不同,把系统分割成一个个独立的且无法返回的函数,这个函数就是我们所说的线程。
1、定义一个线程,我们要先搞清楚如何存放它的环境变量。比如被中断之后的返回地址、子函数调用时局部变量放在哪里等等。裸机中,这些都放在栈上,那多系统之中,我们就需要人为的为线程创建一个线程栈,这其实就是一片连续的地址。
ALIGN(RT_ALIGN_SIZE)
/* 定义线程栈 */
u8 thread1_stack[512];
u8 thread2_stack[512];
2、这里可以看到线程栈实际上就是一个全局变量
应该注意的是这里的数据类型都是定义在mydef.h文件中。在文件夹myRTOS中新建这个文件,并添加到工程中的对应组中。
mydef.h文件内容:
#ifndef __MYDEF_H__
#define __MYDEF_H__
/*add user type definitions*/
typedef signed char s8; /**< 8bit integer type */
typedef signed short s16; /**< 16bit integer type */
typedef signed long s32; /**< 32bit integer type */
typedef signed long long s64; /**< 64bit integer type */
typedef unsigned char u8; /**< 8bit unsigned integer type */
typedef unsigned short u16; /**< 16bit unsigned integer type */
typedef unsigned long u32; /**< 32bit unsigned integer type */
typedef unsigned long long u64; /**< 64bit unsigned integer type */
/* 32bit CPU */
typedef long base_t; /**< Nbit CPU related date type */
typedef unsigned long ubase_t; /**< Nbit unsigned CPU related data type */
typedef base_t err_t; /**< Type for error number */
typedef u32 time_t; /**< Type for time stamp */
typedef u32 tick_t; /**< Type for tick count */
typedef base_t flag_t; /**< Type for flags */
typedef ubase_t os_size_t; /**< Type for size number */
typedef ubase_t dev_t; /**< Type for device */
typedef base_t off_t; /**< Type for offset */
#define my_inline static __inline
#define ALIGN(n) __attribute__((aligned(n)))
/* RT-Thread 错误码重定义 */
#define EOK 0 /**< There is no error */
#define ERROR 1 /**< A generic error happens */
#define ETIMEOUT 2 /**< Timed out */
#define EFULL 3 /**< The resource is full */
#define EEMPTY 4 /**< The resource is empty */
#define ENOMEM 5 /**< No memory */
#define ENOSYS 6 /**< No system */
#define EBUSY 7 /**< Busy */
#define EIO 8 /**< IO error */
#define EINTR 9 /**< Interrupted system call */
#define EINVAL 10 /**< Invalid argument */
#define OS_NULL 0
#define TURE 1
#define FLASH 0
#define RT_ALIGN_DOWN(size, align) ((size) & ~((align) - 1))
struct list_node
{
struct list_node *next; /* 指向后一个节点 */
struct list_node *prev; /* 指向前一个节点 */
};
typedef struct list_node list_t;
struct os_thread
{
void *sp; /* 线程栈指针 */
void *entry; /* 线程入口地址 */
void *parameter; /* 线程形参 */
void *stack_addr; /* 线程栈起始地址 */
u32 stack_size; /* 线程栈大小,单位为字节 */
list_t tlist; /* 线程链表节点 */
};
typedef struct os_thread *os_thread_t;
#endif
3、然后还是在myRTOS中新建一个my_os_config.h
my_os_config.h文件内容:
#ifndef __MY_OS_CONFIG__
#define __MY_OS_CONFIG__
#define RT_ALIGN_SIZE 4
#define THREAD_PRIORITY_MAX 4
#endif
4、根据自己想法定义一个线程函数,函数主体循环且不能返回:
在main.c中增加下面内容:
void thread_1(void* p)
{
while(1)
{
os_schedule();
}
}
void thread_2(void* p)
{
while(1)
{
os_schedule();
}
}
5、定义一个线程控制块,实际上这就是管理一个线程的结构体,我们可以先考虑一下要用到什么建立这个结构体的定义,后面有需要再添加。创建好之后定义一个线程控制块。
定义在mydef.h中,内容如下:
truct os_thread
{
void *sp; /* 线程栈指针 */
void *entry; /* 线程入口地址 */
void *parameter; /* 线程形参 */
void *stack_addr; /* 线程栈起始地址 */
u32 stack_size; /* 线程栈大小,单位为字节 */
list_t tlist; /* 线程链表节点 */
};
typedef struct os_thread *os_thread_t;
6、实现线程创建函数在thread,c文件中,在thread.h中声明。
thread_init内容如下:
err_t thread_init(struct os_thread *thread,
void (*entry)(void *parameter),
void *parameter,
void *stack_start,
u32 stack_size)
{
list_init(&(thread->tlist));
thread->entry = (void *)entry;
thread->parameter = parameter;
thread->stack_addr = stack_start;
thread->stack_size = stack_size;
thread->sp = hw_stack_init(thread->entry,
thread->parameter,
(void *)((char *)thread->stack_addr + thread->stack_size - 4));
return EOK;
}
7、实现链表相关函数,就是我们在线程块中增加的tlist这个元素,我们可以通过这个元素将线程挂在到链表,并且进行管理。关于链表的管理这里不讨论。
8、hw_stack_init,这个函数是用来初始化栈,它的第一个参数是线程入口,也就是之前建立的线程函数,第二个参数是这个线程函数的参数,第三个参数是栈顶地址-4,单位是字节。这里减4的原因,我理解是空栈或者满栈的兼容问题造成的,这里如果有不同意见,可以留言,或者私信我。
hw_stack_init的实现:
u8 *hw_stack_init(void *tentry,
void *parameter,
u8 *stack_addr)
{
struct stack_frame *stack_frame;
u8 *stk;
unsigned long i;
/* 获取栈顶指针
rt_hw_stack_init 在调用的时候,传给 stack_addr 的是(栈顶指针-4)*/
stk = stack_addr + sizeof(u32);
/* 让 stk 指针向下 8 字节对齐 */
stk = (u8 *)RT_ALIGN_DOWN((u32)stk, 8);
/* stk 指针继续向下移动 sizeof(struct stack_frame)个偏移 */
stk -= sizeof(struct stack_frame);
/* 将 stk 指针强制转化为 stack_frame 类型后存到 stack_frame */
stack_frame = (struct stack_frame *)stk;
/* 以 stack_frame 为起始地址,将栈空间里面的 sizeof(struct stack_frame)
个内存初始化为 0xdeadbeef */
for (i = 0; i < sizeof(struct stack_frame) / sizeof(u32); i ++)
{
((u32 *)stack_frame)[i] = 0xdeadbeef;
}
/* 初始化异常发生时自动保存的寄存器 */
stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; /* r0 : argument */
stack_frame->exception_stack_frame.r1 = 0; /* r1 */
stack_frame->exception_stack_frame.r2 = 0; /* r2 */
stack_frame->exception_stack_frame.r3 = 0; /* r3 */
stack_frame->exception_stack_frame.r12 = 0; /* r12 */
stack_frame->exception_stack_frame.lr = 0; /* lr:暂时初始化为 0 */
stack_frame->exception_stack_frame.pc = (unsigned long)tentry; /* entry point, pc */
stack_frame->exception_stack_frame.psr = 0x01000000L; /* PSR */
/* 返回线程栈指针 */
return stk;
}
9、我们在这里实验中,需要创建两个线程,这里我们将在main函数中创建这两个线程。
初始化线程实现代码:
/* 初始化线程 */
thread_init( &my_thread1, /* 线程控制块 */
thread_1, /* 线程入口地址 */
NULL, /* 线程形参 */
&thread1_stack[0], /* 线程栈起始地址 */
sizeof(thread1_stack) ); /* 线程栈大小,单位为字节*/
/* 将线程插入到就绪列表 */
list_insert_before( &(thread_priority_table[0]), &(my_thread1.tlist) );
/* 初始化线程 */
thread_init( &my_thread2, /* 线程控制块 */
thread_2, /* 线程入口地址 */
NULL, /* 线程形参 */
&thread2_stack[0], /* 线程栈起始地址 */
sizeof(thread2_stack) ); /* 线程栈大小,单位为字节 */
list_insert_before( &(thread_priority_table[1]), &(my_thread2.tlist) );
10、实现就绪列表,并在我们之前初始化线程完成后的地方将将线程添加到链表,上面代码中已体现。
list_t thread_priority_table[THREAD_PRIORITY_MAX];
11、调度器初始化,然后在main函数里面,线程初始化之前调用。
/* 初始化系统调度器 */
void system_scheduler_init(void)
{
register base_t offset;
/* 线程就绪列表初始化 */
for (offset = 0; offset < THREAD_PRIORITY_MAX; offset ++)
{
list_init(&thread_priority_table[offset]);
}
/* 初始化当前线程控制块指针 */
current_thread = OS_NULL;
}
12、启动调度器
/* 启动系统调度器 */
void system_scheduler_start(void)
{
register struct os_thread *to_thread;
/* 手动指定第一个运行的线程 */
to_thread = list_entry(thread_priority_table[0].next,
struct os_thread,
tlist);
current_thread = to_thread;
/* 切换到第一个线程,该函数在 context_rvds.S 中实现,
在 rthw.h 声明,用于实现第一次线程切换。
当一个汇编函数在 C 文件中调用的时候,如果有形参,
则执行的时候会将形参传人到 CPU 寄存器 r0。*/
hw_context_switch_to((u32)&to_thread->sp);
}
13、线程调度,第一次跟后面的从第二次调度开始就不一样了,注意看代码
代码使用汇编实现的,值得注意的是,如果你用的是不是IAR,那这些允许外部调用的接口定义方式需要做些改动,否则编译不过。
EXPORT hw_context_switch_to
hw_context_switch_to:
LDR r1, =interrupt_to_thread
STR r0, [r1]
; set from thread to 0
LDR r1, =interrupt_from_thread
MOV r0, #0x0
STR r0, [r1]
; set interrupt flag to 1
LDR r1, =thread_switch_interrupt_flag
MOV r0, #1
STR r0, [r1]
; set the PendSV exception priority
LDR r0, =NVIC_SYSPRI2
LDR r1, =NVIC_PENDSV_PRI
LDR.W r2, [r0,#0x00] ; read
ORR r1,r1,r2 ; modify
STR r1, [r0] ; write-back
LDR r0, =NVIC_INT_CTRL ; trigger the PendSV exception (causes context switch)
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
; enable interrupts at processor level
CPSIE F
CPSIE I
; never reach here!
END
14、下面就是真正切换线程的部分了,也是使用汇编实现的,这是一个异常处理函数
实现如下:
EXPORT PendSV_Handler
PendSV_Handler:
; disable interrupt to protect context switch
MRS r2, PRIMASK
CPSID I
; get thread_switch_interrupt_flag
LDR r0, =thread_switch_interrupt_flag
LDR r1, [r0]
CBZ r1, pendsv_exit ; pendsv already handled
; clear thread_switch_interrupt_flag to 0
MOV r1, #0x00
STR r1, [r0]
LDR r0, =interrupt_from_thread
LDR r1, [r0]
CBZ r1, switch_to_thread ; skip register save at the first time
MRS r1, psp ; get from thread stack pointer
STMFD r1!, {r4 - r11} ; push r4 - r11 register
LDR r0, [r0]
STR r1, [r0] ; update from thread stack pointer
switch_to_thread
LDR r1, =interrupt_to_thread
LDR r1, [r1]
LDR r1, [r1] ; load thread stack pointer
LDMFD r1!, {r4 - r11} ; pop r4 - r11 register
MSR psp, r1 ; update stack pointer
pendsv_exit
; restore interrupt
MSR PRIMASK, r2
ORR lr, lr, #0x04
BX lr
15、产生调度,使用的是os_schedule函数
实现如下:
void os_schedule(void)
{
struct os_thread *to_thread;
struct os_thread *from_thread;
/* 两个线程轮流切换 */
if ( current_thread == list_entry( thread_priority_table[0].next,
struct os_thread,
tlist) )
{
from_thread = current_thread;
to_thread = list_entry( thread_priority_table[1].next,
struct os_thread,
tlist);
current_thread = to_thread;
}
else
{
from_thread = current_thread;
to_thread = list_entry( thread_priority_table[0].next,
struct os_thread,
tlist);
current_thread = to_thread;
}
/* 产生上下文切换 */
hw_context_switch((u32)&from_thread->sp, (u32)&to_thread->sp);
}
16、实际实现调度的接口是,它由汇编实现,代码如下:
hw_context_switch:
; set thread_switch_interrupt_flag to 1
LDR r2, =thread_switch_interrupt_flag
LDR r3, [r2]
CMP r3, #1
BEQ _reswitch
MOV r3, #1
STR r3, [r2]
LDR r2, =interrupt_from_thread ; set interrupt_from_thread
STR r0, [r2]
_reswitch
LDR r2, =interrupt_to_thread ; set interrupt_to_thread
STR r1, [r2]
LDR r0, =NVIC_INT_CTRL ; trigger the PendSV exception (causes context switch)
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
BX LR
到这里我们就实现了一个简单的具有线程调度的系统了,它非常简单,仅仅实现了两个线程的上下文切换,并不包括优先级啊,定时器啊,时间片等等功能的实现,这部分功能想要实现,就在这个基础上增加就有,可以说到这里,我们这个系统的大致框架已经搭好了,下一步就是慢慢的丰富它。