写了个最简单的操作系统,它跑在51单片机上!

您所在的位置:网站首页 自己编写操作系统怎么写 写了个最简单的操作系统,它跑在51单片机上!

写了个最简单的操作系统,它跑在51单片机上!

2024-03-04 03:56| 来源: 网络整理| 查看: 265

目录

0x00 先卖个关子 0x01 编写背景:为了满足好奇心 0x02 运行环境:51单片机或仿真软件 0x03 操作系统的功能 0x04 先展示效果 0x05 再说明源代码 0x06 测试代码

0x00 先卖个关子

什么是最简单的操作系统?我个人的理解是,在最简单硬件上运行的操作系统!

0x01 编写背景:为了满足好奇心

在多年前的一天,我对操作系统的任务调度很感兴趣,特别好奇两个死循环是怎么切换的,虽然操作系统原理的书上都讲了,是通过定时器中断,把时间切成时间片,每个任务执行一段时间片后,切换到下一个任务;道理我都懂,但是实际执行的细节是如何的呢——代码是如何写的呢?于是我萌生了一个自己写操作系统的念头,以满足自己的好奇心,利用工作之余编写代码与调试,终究实现了这个愿望;

0x02 运行环境:51单片机或仿真软件

硬件环境:51单片机(芯片型号AT89C52)或单片机仿真软件 编译环境:Keil

由于编写操作系统需要了解底层硬件相关的知识,而我本人最熟悉的硬件莫过于51单片机了,这应该也是很多嵌入式爱好者入门的第一款单片机吧,因此与很多网上搜索“自己编写操作系统”的结果不太一样,我这个系统是运行在51单片机上的;由于是在51单片机最小系统上运行的操作系统,代码量不大,比较易于理解,因此我把此操作系统命名为Easy OS;没有单片机硬件的朋友,也无需担心,可以使用仿真软件或Keil自带的软仿真来运行此系统;

0x03 操作系统的功能

由于主要是为了满足自己的好奇心,所以Easy OS的功能主要体现在任务的调度上,在config.h中可以设置相应的宏开关,将操作系统配置成不同的模式:分时系统、抢占式实时操作系统、非抢占式实时操作系统;同时还是实现了简单的任务间通信;

0x04 先展示效果

在说明源代码之前,先来看看效果吧! 创建几个任务:task0、task1、task2、task3;task0进行负责对计数器counter进行自加1,task1将counter的值输出到P1口,task2将(counter+10)的值输出到P2端口,task3将(counter+20)的值输出到P3端口,任务的处理代码如下:

u8 counter = 0; void task0() { u16 i,j; while (1) { counter++; for (i = 0; i P1 = counter; } } void task2() { while (1) { P2 = counter + 10; } } void task3() { while (1) { P3 = counter + 20; } }

以下是仿真软件运行时,随机选取的三个不同时刻的截图,仿真软件中,芯片引脚上的蓝点表示0(低电平),红点表示1(高电平),因此下面各图的数据分别如下: 图1:当P1输出0x11时,P2输出0x1b,P3输出0x25; 图2:当P1输出0x08时,P2输出0x12,P3输出0x1c; 图3:当P1输出0x0b时,P2输出0x15,P3输出0x1f;

图1 图1 图2 图2 图3 图3 可以看到每个时刻都满足 P2等于P1+10,P3等于P1+20;这说明多任务确实“同时”运行了!

0x05 再说明源代码

本文主要用于说明源代码的结构与重要逻辑,不进行代码的罗列,如果需要下载完整源代码,可以点击Easy OS源码下载链接

头文件 types.h

基本数据类型的定义

config.h

内核配置相关的宏定义

system.h

系统函数的定义

task.h

任务相关的定义,这里重点介绍一下程序控制块struct __tcb,见注释;

// 任务控制块,task control block struct __tcb { // 为了节省资源id,status,signal使用位段定义 u8 id:4; // 任务id,用户任务从0开始递增,实时系统中id越小优先级越高 u8 status:2; // 任务状态,READY/RUNNING/SLEEP/DELAY u8 signal:2; // 信号,用于任务之间进行通信 u8 sp; // 堆栈指针 u8 sp_top; // 栈顶 #if (OS_SCHED_ALGO == OS_REAL_TIME) u8 dly; // 实时系统中的准备延时,当dly为0时,才允许进入REAYD状态 #endif struct __tcb * next; //指向下一个__tcb }; asm文件及C文件 boot.asm

这个上电后跑的第一段代码,这个文件相当于很多大型操作系统的bootloader了,这个文件的作用是设置了SP指向系统堆栈,然后跳转到C程序的main函数;

由于提到了系统堆栈,所以也说明一下系统堆栈;在本操作系统中,所谓系统堆栈,是为了便于对堆栈的管理,也是为了对51单片机上的资源斤斤计较,特意划分出来的一块固定地址内存,当main函数(后续称为主任务)、中断处理函数调用其他函数时,造成的PC指针入栈,使用的就是系统堆栈;由于系统初始化完毕后主任务进入死循环,后续将不用使用系统堆栈;而中断函数不会进行嵌套,因此保证了各个中断函数只会分别地在不同时机使用系统堆栈,不会造成程序跑飞;

如果您想实现中断嵌套,或者在系统正常运作后主任务依然可以执行业务逻辑代码,则建议给主任务和中断函数设置分别设置独立的堆栈,这样的堆栈管理会比较清晰,但是会牺牲很多内存;

相对于系统堆栈,后续介绍的任务中有自己独立的堆栈,称之为任务堆栈;

isr_a.asm

中断处理的汇编函数,定义了每个中断发生时回调的哪个C函数,此文件定义的宏CALL_ISR_HANDLER,封装了一般中断的处理流程,每个中断发生时均按照这个流程进行处理;中断处理流程可以参照后续介绍的任务切换流程,搞懂了任务切换流程自然就懂中断处理流程了;

isr_c.c

中断处理C程序,这个文件的函数均为Isr_a.asm的回调,在实际项目的应用过程中,如果要处理中断的话,在这个文件里面实现即可;当然咯,内核代码已经占用了51单片机的很多资源,要应用到实际项目中的话,有可能需要进行资源扩展,能够在51最小系统上跑起自己写的一个操作系统已经满足了我的代码欲!

system.c

系统初始化函数,初始化系统时钟,并创建空任务;

task_a.asm task_c.c

这两个文件重点介绍

1 任务切换

先介绍任务切换中最核心的逻辑,文字说明之前先来个示意图; 任务切换流程 task_a.asm和task_c.c分别为任务调度相关的汇编和C代码文件,这里有几个比较重要的地方;SAVE_TASK_CONTEXT、RESUME_TASK_CONTEXT、__timer_isr0、__os_swtich_task;

__timer_isr0为定时器0的中断入口点,任务切换的工作从这里开始;

SAVE_TASK_CONTEXT用于保存任务上线文,即将当前CPU寄存器的值压入堆栈,RESUME_TASK_CONTEXT用于恢复上下文,即将堆栈里面的数据恢复到CPU寄存器中,要注意出栈和入栈的顺序是相反的;

可以粗略的想一下,当任务切换时,将旧任务的上下文保存起来(保存到旧任务tcb->sp指向的堆栈),再将新任务的上下文(新任务的tcb->sp指向的堆栈)恢复到CPU,这样CPU的当前运行状态就变成了新任务的状态了;有了新状态还不够,还需要将PC指针切换到新任务中,PC切换怎么做到的呢?先不考虑任务调度的情况,当中断发生时,程序进入中断处理程序前,会自动将PC入栈,压入SP指向的堆栈中,当中断处理程序结束,调用RETI指令时,会从SP指向的堆栈里面弹出数据作为PC指针,从而确保中断结束后可以会到原来的位置上继续执行程序。如果要实现任务切换,则需要加以处理,发生中断时,PC自动入栈(SP指向当前任务的堆栈,这个不需要处理),在中断处理函数中,将SP修改为指向新任务的堆栈,那么出栈时,PC就被赋予了新任务的执行地址了;

上面提到新旧任务SP切换,这个处理是在__os_swtich_task完成的,在汇编中,SP的值被复制到了OS_SP_BK,而在C函数__os_swtich_task中将OS_SP_BK的值保存在当前任务current->sp中,然后切换任务(即根据调度算法,将current指向下一个任务),再将新任务的current->sp赋给OS_SP_BK,此时进入汇编程序,汇编程序在将OS_SP_BK的值赋给SP,从而完成了新旧堆栈指针的切换;

整个过程再理一理,当定时器0中断发生时,程序进入汇编入口__timer_isr0,进入时自动将PC压入current->sp(指向的堆栈),调用SAVE_TASK_CONTEXT将上下文也压入current->sp中,SP复制到OS_SP_BK,调用__os_swtich_task将OS_SP_BK保存到current->sp,__os_swtich_task进行调度,使current指向新的任务,然后将新任务的current->sp赋给OS_SP_BK,在汇编中将OS_SP_BK复制到SP,则SP就指向了新任务堆栈,调用RESUME_TASK_CONTEXT恢复上下文,调用RETI使PC指向新任务执行地址;

__timer_isr0处理程序中,还出现了将系统堆栈OS_SYSTEM_STK_SP赋给SP的情况,这个主要是为了在__timer_isr0中调用C函数时,将返回地址压入系统堆栈而非任务堆栈中,这样可以将任务上下文的出入栈,与中断程序调用函数的出入栈区别开来,便于区分与处理;

2 创建任务

任务的创建是使用__os_create_task实现的,也可以使用宏定义os_create_task创建,第一个任务由system.c的os_init代码创建,该任务称其为空任务或系统任务吧,用以区分在主任务中创建的任务,主任务创建的任务称为用户任务;创建空任务时,current指针被指向了空任务,在任务切换的过程中,current指针始终指向当前任务;

任务被创建后使用单向循环链表进行管理,不同状态的任务均使用同一个链表,创建第1个用户任务后,它会排在空任务后面,创建第2个用户任务排在第1个用户任务后面,依次类推;在全部任务创建完毕后,且调用os_start之前,各个任务的tcb及其对应的堆栈数据结构示意图如下,其中tcb仅列出部分字段; 任务链表

3 加载第一个任务

在任务切换的介绍中,提到CPU在旧任务和新任务之间进行上线文和PC的切换,那么第一个旧任务是谁呢?答案是os_load_current_task,此函数将current->sp的值通过__os_load_current_task间接赋给了SP,又调用__os_load_current_task执行了一次RESUME_TASK_CONTEXT和RET将当前任务的上下文和PC“强制“恢复了,从而使CPU进入current指向的地址执行指令;

4 调度算法

调度算法的实现见__os_switch_task和os_timer_tick函数;

如果OS_SCHED_ALGO定义为OS_TIME_SHARED,则系统被编译为分时操作系统,每个任务的运行统一长度时间片,时间片用完后,顺序切换到任务链表的下一个任务执行;

如果OS_SCHED_ALGO定义为OS_REAL_TIME,则系统被编译为实时操作系统,实时操作系统又有一个子宏定义OS_PREEMPTIVE_EN,如果OS_PREEMPTIVE_EN为1,则表示抢占式系统,如果OS_PREEMPTIVE_EN为0则表示非抢占式系统;

实时系统中以任务的id作为运行优先级,空任务的优先级最低,为了算法的简单,在用户任务中,越早创建的任务id越低,即优先级越高;实时系统在每次调度中,均执行优先级最高的就绪状态的任务;

不同调度算法对应的任务状态迁移图如下:

分时操作系统 分时系统的任务状态 非抢占式实时系统 非抢占式系统的任务状态 抢占式实时系统 抢占式系统的任务状态 三种调度方式的获得CPU使用权的区别:抛开os_start不说,分时操作系统全靠时间片的到来;非抢占式操作系统,靠优先级获得,且一旦获取CPU后,如果不是任务主动调用os_delay释放CPU的话,会一直占用CPU,即使高优先级的任务处于就绪态,也无法抢占低优先级任务的CPU;抢占式操作系统,靠优先级获得,如果有更高优先级的任务进入就绪状态,则高优先级的任务会抢占CPU,高优先级的任务如果不通过os_delay主动释放CPU,则低优先级的任务始终无法获取CPU使用权;

0x06 测试代码

main.c编写了几段测试代码,用于测试与观察不同调度模式时的表现,读者可以修改宏定义TEST_MODE选择使用哪段测试代码,修改TEST_MODE后请记得相应修改config.h中的内核配置;对于实时系统,建议多尝试不同的delay值,以及多观察屏蔽os_delay后的表现,可以使用在线仿真的方式进行调试跟踪任务的切换情况,这样可以更加深入的理解;

Easy OS源码下载链接

感谢您的阅读,欢迎在留言区交流!



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3