0x00先卖个关子什么是最简单的操作系统

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

0x00 我们先卖掉吧

0x01 写作背景:满足好奇心

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

0x03 操作系统功能

0x04 先展示效果

0x05 再解释一下源码

0x06 测试代码

0x00 我们先卖掉吧

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

0x01 写作背景:满足好奇心

很多年前的一天,我对操作系统的任务调度非常感兴趣。 我特别好奇如何在两个无限循环之间切换。 虽然操作系统原理的书籍告诉我,时间是通过定时器中断来切分的。 每个任务执行一段时间后,切换到下一个任务; 原理我明白了,但是实际执行的细节是什么——代码是怎么写的? 于是我萌生了编写自己的操作系统来满足我的好奇心的想法。 我利用业余时间写代码、调试,终于实现了这个愿望;

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

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

编译环境:Keil

由于编写操作系统需要了解底层硬件,所以我最熟悉的硬件是51单片机。 这也应该是很多嵌入式爱好者的第一款微控制器。 因此,类似于网上很多搜索“自己写操作”。 “系统”的结果并不相同。 我的系统运行在51单片机上。 由于是运行在51单片机最小系统上的操作系统,代码量并不大,也比较容易理解,所以我将这个操作系统命名为Easy。 操作系统; 如果你没有单片机硬件,不用担心,你可以使用仿真软件或者Keil自己的软仿真来运行这个系统;

0x03 操作系统功能

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

0x04 先展示效果

在讲解源码之前,我们先来看看效果!

创建多个任务:task0、task1、task2、task3; task0负责将计数器加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 < 200; i++)
            for (j = 0; j < 200; j++);
    }
}
void task1()
{
    while (1) 
    {
        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

图2

图3

可见,每一时刻P2都等于P1+10,P3都等于P1+20; 这说明多任务确实是“同时”运行的!

0x05 再解释一下源码

本文主要用于讲解源码的结构和重要逻辑。 它没有列出代码。 如果您需要下载完整的源代码,可以点击Easy OS源代码下载链接。

头文件类型.h

基本数据类型定义

配置文件

与内核配置相关的宏定义

系统.h

系统功能定义

任务.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单片机上的资源而专门划分的一块固定地址的内存。 当主函数(后续称为主任务)和中断处理函数调用其他函数时,将PC指针压入堆栈,使用系统堆栈; 由于系统初始化后主任务进入死循环,以后不会再使用系统堆栈; 中断函数不会嵌套,从而保证每个中断函数只会在不同时刻使用系统堆栈,不会导致程序跑飞;

如果要实现中断嵌套,或者系统正常运行后主任务仍然可以执行业务逻辑代码,建议为主任务和中断函数设置设置独立的堆栈。 这样的堆栈管理会更加清晰,但是会牺牲很多。 记忆;

与系统栈相比,后面介绍的任务都有自己独立的栈,称为任务栈;

isr_a.asm

中断处理的汇编函数定义了每次中断发生时回调哪个C函数。 该文件中定义的宏CALL_ISR_HANDLER封装了一般的中断处理流程。 每个中断都按照这个流程进行处理; 中断处理过程可以参考后面介绍的任务切换过程。 了解了任务切换流程,自然也就了解了中断处理流程;

isr_c.c

中断处理C程序,该文件的函数都是Isr_a.asm的回调。 在实际项目的应用过程中,如果想要处理中断,可以在该文件中实现; 当然,内核代码已经占用了51单片机的很多资源,如果要应用到实际项目中,可能还需要扩展很多资源。 能够在51最小系统上运行自己编写的操作系统已经满足了我的编码愿望!

系统.c

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

任务_a.asm 任务_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指令时,PC将被压入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 代码创建的。 该任务称为空任务或系统任务,以区别于主任务。 主任务创建的任务称为用户任务; 创建空任务时,当前指针指向空任务。 任务切换过程中,当前指针始终指向当前任务;

任务创建后,使用单向循环链表进行管理。 不同状态的任务都使用同一个链表。 第一个用户任务创建后,将排在空任务后面。 创建的第二个用户任务将排在第一个用户任务后面。 完成任务后,依此类推; 所有任务创建完成后,调用os_start之前,每个任务的tcb及其对应的栈数据结构如下,其中tcb只列出了部分字段;

3 加载第一个任务

在任务切换的介绍中,提到了CPU在旧任务和新任务之间切换在线文本和PC。 那么第一个老任务是谁呢? 答案是os_load_current_task。 该函数通过__os_load_current_task间接将current->sp的值赋给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中的内核配置; 对于实时系统,建议尝试不同的延迟值,观察阻塞os_delay后的性能。 您可以使用在线模拟来调试和跟踪任务切换,从而获得更深入的了解;

Easy OS源代码下载链接

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

单片机

门禁电子密码锁操作详解157.课程设计总结(157)

2024-5-11 11:03:48

单片机

南京理工大学机械工程学院毕业实习2018.03.07

2024-5-11 12:07:32

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索