从零开始写RTOS(一)

  • Post author:
  • Post category:其他



从零开始写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

到这里我们就实现了一个简单的具有线程调度的系统了,它非常简单,仅仅实现了两个线程的上下文切换,并不包括优先级啊,定时器啊,时间片等等功能的实现,这部分功能想要实现,就在这个基础上增加就有,可以说到这里,我们这个系统的大致框架已经搭好了,下一步就是慢慢的丰富它。



版权声明:本文为w375073907原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。