当前位置: 首页 > news >正文

快速设计简易嵌入式操作系统(5):贴近实际场景,访问多外设,进一步完善程序

引言

前面,我们已经基本完成具有实时操作系统核心功能的简单操作系统程序,但是呢,他其实还不够完善,因为对于切换的实现并不是操作系统管理的,还是由各个任务去自动控制,没有分离。正常情况的操作系统应该会去管理这些任务切换的时机,也就是所谓的时间片,这是后续需要进行改善的;其次,这里用MCU去做这种计算,实在没有那么顺眼,之所以想在MCU上弄操作系统不就是为了同时访问多个外设不冲突的吗?因此接下来,我们将真正用上STC8这个单片机,将这俩任务函数逻辑变成点灯的操作,比如一个任务点一个灯(因为示例比较简单,实际不用这个也可以实现)。

注:本文中搭建的LED电路并不算是标准的连接方式,理论上应该接个限流电阻的,所以大家尽量别模仿我,避免把了点烧了。。


一、初步思路建立

在前面的基础上,将累加的两个任务进行修改,变成实际的点灯的任务,为了体现多任务执行的特性,我们同时让它们闪烁,也就是两个不同led闪烁的任务。修改这俩任务思路应该容易想到:就是把两个任务函数内容分别改成控制LED闪烁的逻辑,本质上就是控制不同GPIO口输出的高低电平的来回变换

而亮灭的时间间隔可以使用延时的长短来控制,至于延时函数的编写,大家可能首先想到的就是那种死循环的逻辑了,当然,我们可以先从简单的方式开始,后续再根据实际情况不断完善。所以延时的处理咱就直接用这种循环计数的方式阻塞个1s来实现LED的闪烁,由于STC厂商提供了stc-isp软件,所以能够快速生成一个软件计时的延时函数。

然后其他的内容基本不用什么改变,所以总结一下当前做的事情,即

1、编写1s的延时函数

2、修改任务内容为控制LED闪烁


二、代码编写

那么接下来,就开始编写代码了,首先是延时函数,我们直接使用stc-isp生成一个1s的延时函数,需要注意的是其操作的步骤:

首先,打开stc-isp,选择当前使用的单片机芯片型号与时钟频率。(笔者使用的单片机为STC8H8K64U,时钟24.000MHz)其次,在软件界面右侧偏上一栏找到软件延时计数器单击,鸡泽选择系统频率为24.000MHz,定时长度为1000ms,8051指令集选择如下图所示,最后生成C代码复制即可。

然后粘贴到我们的代码中,如下图所示

void Delay1000ms(void)	//@24.000MHz
{unsigned char data i, j, k;i = 122;j = 193;k = 128;do{do{while (--k);} while (--j);} while (--i);
}

后面我们直接使用即可。

接着第二步,修改任务函数的内容为控制LED闪烁。笔者这里是使用P34和P64这两个端口来控制的两个不同LED闪烁,各位可自行选择。编写逻辑也很简单,首先是设置一下GPIO的模式为推挽输出或者准双向口模式,然后按照LED与端口连接方式给默认高低电平(笔者默认是给的熄灭状态),最后在while里面加个延时,然后翻转IO口电平即可。

示例代码如下:

void task0()
{// 0号任务,控制P34口的LED闪烁P3M0 &= ~0x10;P3M1 &= ~0x10;P34 = 1;// 死循环保持while(1){Delay1000ms();P34 = ~P34;task_switch();}
}void task1()
{// 1号任务,控制P64口的LED闪烁P6M0 &= ~0x10;P6M1 &= ~0x10;P64 = 1;// 死循环保持while(1){Delay1000ms();P64 = ~P64;task_switch();}
}

大家如果不太明白其中的逻辑的话,可以参考一下STC8H的技术手册,里面有详细介绍和示例代码学习,这里不再赘述。


这样,我们代码就修改完了,完整示例代码如下

#include <stc8h.h> // 定义一些寄存器的地址#define MAX_TASKS	2		// 简化任务数为2
#define	MAX_TASK_DEPTH	32	// 堆栈深度// idata 表明信息定义在STC8访问最快的内部内存空间里面unsigned char idata task_sp[MAX_TASKS];	// 任务的堆栈指针
unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEPTH];  // 每个tasks任务的堆栈信息unsigned char idata task_id;		// 当前任务号,从0开始void Delay1000ms(void)	//@24.000MHz
{unsigned char data i, j, k;i = 122;j = 193;k = 128;do{do{while (--k);} while (--j);} while (--i);
}// 任务切换函数(任务调度器)
void task_switch()
{// 将当前任务执行位置(堆栈指针)记录到自定义的对应任务的堆栈指针数组位置中task_sp[task_id] = SP;// 后移任务id号,用于后续修改SP,切换执行的任务task_id = task_id + 1;// 避免任务编号溢出if(task_id == MAX_TASKS){task_id = 0;}// 更新堆栈指针SP,完成任务切换SP = task_sp[task_id];
}void task0()
{// 0号任务,控制P34口的LED闪烁P3M0 &= ~0x10;P3M1 &= ~0x10;P34 = 1;// 死循环保持while(1){Delay1000ms();P34 = ~P34;task_switch();}
}void task1()
{// 1号任务,控制P64口的LED闪烁P6M0 &= ~0x10;P6M1 &= ~0x10;P64 = 1;// 死循环保持while(1){Delay1000ms();P64 = ~P64;task_switch();}
}// fn 函数指针,注意数据类型int,16bit
// tid task id,8bit  0, 1
// 函数功能: 将一个task的函数指针放入对应的堆栈空间
void task_load(unsigned int fn, unsigned char tid)
{// 1. task的堆栈指针记录相应taskId堆栈信息地址task_sp[tid] = task_stack[tid] + 1;// 2. 使用两个空间存放task的函数指针task_stack[tid][0] = fn & 0xFF;		// 低8位task_stack[tid][1] = fn >> 8;		// 高8位
}void main()
{task_load(task0, 0);	// 装载任务0到对应堆栈内存task_load(task1, 1);	// 装载任务1到对应堆栈内存task_id = 0;SP = task_sp[0];		// 将当前的堆栈指针压入SP中
}

三、初步测试

理论上,该代码经过成功编译后运行的效果应该是两个LED灯同时以1s的亮灭间隔来变化。那么接下来我们看看实际效果是否符合预期。

首先编译一下看看

如上图,显然没有错误,编译成功了。

值得注意的是,如果编译前没有勾选生成hex文件的话,后续会不好烧录,因为一般烧录使用的就是编译时生成的hex16进制文件。勾选操作如下

接下来,我们把程序烧录到板子上看看实际效果:

初步多任务LED闪烁测试

经过观察,发现似乎现象并不符合我们的预期,正常情况下应该是1s会同时出现两个LED的闪烁,但是实际情况却只有一个LED,就相当于每个LED都隔了2s才闪一次。这是为什么呢?

检查代码,可以发现由于多任务执行,每个任务的LED变换一次后会触发切换函数去执行另一个任务的LED闪烁,而这个过程中,我们说所谓的多任务同时运行实际上还是单个任务在运行,只是时间片轮转较快仿佛在一起运行一样,这里因为任务0执行时任务1是没有执行的,然而任务0里面存在1s的延时,会大大拖慢轮转的频率,然后好不容易切换到另一个任务以后,因为任务1本身闪烁的延时又延时了1s,最后任务1的LED才出现闪烁的现象,整个过程算下来实际任务1的LED就是每2s闪烁一次了,任务0的LED闪烁情况也是同理。

实际上这种情况也挺常见的,使用裸机开发时出现多个任务的运行的时候很容易出现这种阻塞的常见,大多情况都是由于各个模块的优先级没处理好,或者是出现太多软件延时的逻辑,使MCU产生阻塞,影响了整个程序的运行情况。

因此,为了避免这种阻塞,我们下面就采取硬件计时的办法来进行延时的操作,同时优化一下整个多任务运行的操作逻辑,改善任务自身来切换任务执行的逻辑,让整个简易操作系统变得更加贴近真实的嵌入式实时操作系统!


四、改善思路

接下来,我们优化代码,首先就是对于延时的优化,采取硬件的方式来实现。因为单片机芯片中除了处理器以外,其实还集成了很多片上外设,如常见的GPIO,还有一种就是所谓的定时器了,该外设可以帮助我们脱离对MCU的阻塞,在MCU处理的同时帮助我们计时计数,这样就可以降低阻塞带来的一些影响了。

大致的思路就是采用定时器中断,即定时器帮忙计时,每计时1ms的时候产生一个中断,然后计数+1,计数器到1000时意味着达到1s,此时执行相应操作。

那么这个LED闪烁这个延时具体是咋实现呢?想一想,LED闪烁本质上就是两种状态的切换,即亮和灭,如果把这两张状态看做工作与否就相当于是运行或挂起这两种状态,所以首先我们可以定义这两种工作状态,当然其实状态对应不太重要,重点在于按时切换。关于状态的定义,使用枚举常量最为合适;

其次,两种状态都是关联到这俩任务上的,由于任务逻辑较为简单,所以可以作为是各任务此时的状态,而区分任务的量是其id号,然后状态的变化在于规定的时间,时间的变化在于计数的多少,也就是说:通过对id号的判断,容易获取任务的状态;通过设定指定的时间可以控制状态维持的时长;通过计数器与设定时间的比较,可以获取切换状态的时机。那么这里涉及到的四个量,分别是任务id、任务状态、任务延时周期与任务延时计数器,巧妙的是,这四个量都是每个任务的共有属性,因此我们可以采用结构体定义他们,然后如果有多个任务,直接定义该结构体类型的数组即可记录。

那么延时的核心逻辑肯定就和延时相关的量密切联系了,定时器中断是核心的,然后初始状态下,两个任务应该是正常的运行状态,当出现了延时则变成挂起,经过1s后还原任务状态。所以这里应该还需要一个控制延时的函数,用于挂起对应任务和设置挂起的时长。

最后在中断处理函数中,就是核心的状态控制逻辑了,即遍历所有任务的状态,如果是挂起则维持对应任务的计数器计数,然后如果对应任务的计数器超过设定周期就清除置位,同时清除任务挂起状态,恢复运行状态。

以上就是整个关于多个LED闪烁任务执行的逻辑优化了,整理总结一下即:

1、定义状态枚举:运行/挂起(用于处理led状态转换)

2、定义任务结构体变量,包含id、状态、延时周期以及计数器(便于维护各任务相关的变量)

3、创建定时器中断,初始化配置,用于1ms计时中断

4、定义“挂起”函数,设置任务挂起,类似延时不处理

5、完善中断处理函数,编写状态切换控制逻辑,如状态保持、切换时机控制等

同时,任务挂起时,相当于不用执行对应任务,此时检测到挂起状态后可以切换到其他任务去执行。


五、程序优化

优化的思路已经相对成熟,接下来就开始代码编写。

首先是状态的枚举定义,笔者定义运行/挂起两种状态,分别用TASK_RUNNING和TASK_SUSPENDED表示,示例代码如下:

// 状态定义: 0 - 运行 1 - 挂起
typedef enum
{TASK_RUNNING,TASK_SUSPENDED,
}TaskStatus;

其次,任务共有属性的结构体定义,笔者就是定义了Task结构体,包含了id、状态、延时周期与延时计数器这几个量,然后创建一个该类型数组,用于存放多个任务信息,且默认均为运行状态无挂起延时。示例代码如下:

// 任务属性结构体定义
typedef struct
{unsigned char id;		// 任务idTaskStatus status;		// 任务状态int delay_duration;		// 延时周期int delay_count;		// 延时计数器
}Task;// 存储任务的数组
Task idata tasks[MAX_TASKS] = {{0, TASK_RUNNING, 0, 0},{1, TASK_RUNNING, 0, 0},
};

接着,初始化定时器,这里也是直接在stc-isp中生成即可,当然,注意开启中断使能。

void Timer0_Init(void)		//1毫秒@24.000MHz
{AUXR |= 0x80;			//定时器时钟1T模式TMOD &= 0xF0;			//设置定时器模式TL0 = 0x40;				//设置定时初始值TH0 = 0xA2;				//设置定时初始值TF0 = 0;				//清除TF0标志ET0 = 1;                //使能定时器0中断EA = 1;                 //使能总中断TR0 = 1;				//定时器0开始计时
}

然后,创建和定义挂起延时函数,即将任务状态变成挂起状态,然后初始化延时计数器,设定延时周期。传入的参数自然是任务id和指定的延时周期了。示例代码如下:

// 延时挂起函数
void Sleep(unsigned char tid, int delay_ms)
{tasks[tid].status = TASK_SUSPENDED;tasks[tid].delay_duration = delay_ms;tasks[tid].delay_count = 0;
}

最后再完善一下定时器0的中断处理函数,笔者命名为void Timer0_ISR(void) interrupt 1{},注意这里的interrupt 1 不要忘了,这是8051单片机中的中断处理函数相关的一个关键字,不了解的可以去看看其技术手册关于中断的描述。

按前面的思路,这里面我们首先是要遍历一遍各个任务的状态,如果还是挂起状态就继续去自增对应的计数器,然后计数器如果要超过其延时周期了就清除置零,然后恢复任务运行状态。示例代码如下:

// 定时器0中断处理函数
void Timer0_ISR(void)	interrupt 1
{unsigned char i;TL0 = 0x40;				//设置定时初始值TH0 = 0xA2;				//设置定时初始值for(i = 0; i < MAX_TASKS; i++){// 任务为挂起状态则正常递增计数器if(tasks[i].status == TASK_SUSPENDED){tasks[i].delay_count++;}// 计数器超过延时周期则计数器置零并恢复任务运行状态if(tasks[i].delay_count >= tasks[i].delay_duration){tasks[i].delay_count = 0;tasks[i].status = TASK_RUNNING;}}
}

这样,关于延时的优化就结束了,接着main函数中调用定时器0初始化,然后在修改原来任务中的延时逻辑为当前逻辑即可,同时检测任务挂起就切换到另一个任务区运行。修改后的任务函数代码示例如下:

void task0()
{// 0号任务,控制P34口的LED闪烁P3M0 &= ~0x10;P3M1 &= ~0x10;P34 = 1;// 死循环保持while(1){if(tasks[0].status == TASK_SUSPENDED){task_switch();continue;}Sleep(0, 1000);		// 挂起1sP34 = ~P34;task_switch();}
}void task1()
{// 1号任务,控制P64口的LED闪烁P6M0 &= ~0x10;P6M1 &= ~0x10;P64 = 1;// 死循环保持while(1){	if(tasks[1].status == TASK_SUSPENDED){task_switch();continue;}Sleep(1, 1000);		// 挂起1sP64 = ~P64;task_switch();}
}

六、优化后的测试

关于延时的部分优化完毕,接下来就是测试时间。编译代码

没有错误,编译成功。然后直接烧录程序到板子上看看实际效果:

多任务LED闪烁测试

可以看到,LED闪烁正常了,说明优化成功。

当然,其实还有一个地方有待优化,就是任务切换函数的调用,即任务调度器,这个正常情况应该是由操作系统管理的,而不是任务自身去调用,所以这里我们还可以继续完善一下任务切换函数的调用机制:使用定时器中断去执行,每10ms就自动切换一次(至于为什么是10ms,因为测试过发现受限于人眼视觉暂流特性,至少10ms左右才能大概分辨这俩任务交替运行时出现的LED闪烁状态),逻辑比较简单,这里不再赘述。直接附上修改后的完整代码了:

#include <stc8h.h> // 定义一些寄存器的地址#define MAX_TASKS	2		// 简化任务数为2
#define	MAX_TASK_DEPTH	32	// 堆栈深度// idata 表明信息定义在STC8访问最快的内部内存空间里面unsigned char idata task_sp[MAX_TASKS];	// 任务的堆栈指针
unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEPTH];  // 每个tasks任务的堆栈信息unsigned char idata task_id;		// 当前任务号,从0开始// 状态定义: 0 - 运行 1 - 挂起
typedef enum
{TASK_RUNNING,TASK_SUSPENDED,
}TaskStatus;// 任务属性结构体定义
typedef struct
{unsigned char id;		// 任务idTaskStatus status;		// 任务状态int delay_duration;		// 延时周期int delay_count;		// 延时计数器
}Task;// 存储任务的数组
Task idata tasks[MAX_TASKS] = {{0, TASK_RUNNING, 0, 0},{1, TASK_RUNNING, 0, 0},
};unsigned int change_count;void Timer0_Init(void)		//1毫秒@24.000MHz
{AUXR |= 0x80;			//定时器时钟1T模式TMOD &= 0xF0;			//设置定时器模式TL0 = 0x40;				//设置定时初始值TH0 = 0xA2;				//设置定时初始值TF0 = 0;				//清除TF0标志ET0 = 1;                //使能定时器0中断EA = 1;                 //使能总中断TR0 = 1;				//定时器0开始计时
}// 用于自动切换任务
void Timer1_Init(void)		//1毫秒@24.000MHz
{AUXR |= 0x40;			//定时器时钟1T模式TMOD &= 0x0F;			//设置定时器模式TL1 = 0x40;				//设置定时初始值TH1 = 0xA2;				//设置定时初始值TF1 = 0;				//清除TF1标志ET1 = 1;                //使能定时器1中断TR1 = 1;				//定时器1开始计时
}// 延时挂起函数
void Sleep(unsigned char tid, int delay_ms)
{tasks[tid].status = TASK_SUSPENDED;tasks[tid].delay_duration = delay_ms;tasks[tid].delay_count = 0;
}// 任务切换函数(任务调度器)
void task_switch()
{// 将当前任务执行位置(堆栈指针)记录到自定义的对应任务的堆栈指针数组位置中task_sp[task_id] = SP;// 后移任务id号,用于后续修改SP,切换执行的任务task_id = task_id + 1;// 避免任务编号溢出if(task_id == MAX_TASKS){task_id = 0;}// 更新堆栈指针SP,完成任务切换SP = task_sp[task_id];
}void task0()
{// 0号任务,控制P34口的LED闪烁P3M0 &= ~0x10;P3M1 &= ~0x10;P34 = 1;// 死循环保持while(1){if(tasks[0].status == TASK_SUSPENDED){task_switch();continue;}Sleep(0, 1000);		// 挂起1sP34 = ~P34;}
}void task1()
{// 1号任务,控制P64口的LED闪烁P6M0 &= ~0x10;P6M1 &= ~0x10;P64 = 1;// 死循环保持while(1){	if(tasks[1].status == TASK_SUSPENDED){task_switch();continue;}Sleep(1, 1000);		// 挂起1sP64 = ~P64;}
}// 定时器0中断处理函数
void Timer0_ISR(void)	interrupt 1
{unsigned char i;TL0 = 0x40;				//设置定时初始值TH0 = 0xA2;				//设置定时初始值for(i = 0; i < MAX_TASKS; i++){// 任务为挂起状态则正常递增计数器if(tasks[i].status == TASK_SUSPENDED){tasks[i].delay_count++;}// 计数器超过延时周期则计数器置零并恢复任务运行状态if(tasks[i].delay_count >= tasks[i].delay_duration){tasks[i].delay_count = 0;tasks[i].status = TASK_RUNNING;}}
}// 定时器1中断处理函数
void Timer1_ISR(void)	interrupt 3
{TL1 = 0x40;				//设置定时初始值TH1 = 0xA2;				//设置定时初始值// 每10ms切换一次,切换速度太快会影响任务执行状态change_count++;if(change_count >= 10000){task_switch();change_count = 0;}}// fn 函数指针,注意数据类型int,16bit
// tid task id,8bit  0, 1
// 函数功能: 将一个task的函数指针放入对应的堆栈空间
void task_load(unsigned int fn, unsigned char tid)
{// 1. task的堆栈指针记录相应taskId堆栈信息地址task_sp[tid] = task_stack[tid] + 1;// 2. 使用两个空间存放task的函数指针task_stack[tid][0] = fn & 0xFF;		// 低8位task_stack[tid][1] = fn >> 8;		// 高8位
}void main()
{Timer0_Init();Timer1_Init();task_load(task0, 0);	// 装载任务0到对应堆栈内存task_load(task1, 1);	// 装载任务1到对应堆栈内存task_id = 0;SP = task_sp[0];		// 将当前的堆栈指针压入SP中
}

经过测试,效果与前面相同。


七、总结

本次完成了设计简单嵌入式实时操作系统的最后一篇,单说这一篇的话,我们完成了在具有实时操作系统核心功能的条件下多个LED闪烁任务的运行,改善了任务手动调度任务的局限,利用定时器更真实地反映了实时操作系统的任务调度特性。

整个基于8051单片机的简易实时操作系统的程序设计到这里也是告一段落,整个过程,我们依次学习了嵌入式操作系统的基本常见概念、8051单片机内存结构的基本知识与程序切换执行顺序的核心方式以及耗时最长的基于STC8单片机的简易实时操作系统的程序设计。通过这一过程,应该能够在真正学习嵌入式操作系统前对其有一个基本而较为深刻的认知,为后续深入学习打下基础。


笔者小白,能力有限,以上内容难免存在不足和纰漏,仅供参考,各位阅读时请带着批判性思维学习,遇到问题多查查。同时欢迎各位评论区批评指正。谢谢。


 

http://www.dtcms.com/a/333436.html

相关文章:

  • WPF 监控CPU、内存性能
  • python math数学运算模块
  • 【AI论文】Story2Board:一种无需训练的富有表现力故事板生成方法
  • Numerical Difference between vLLM logprobs and huggingface logprobs
  • windows下hashcat使用gpu破解execl打开密码
  • 深入Amazon DynamoDB:高效、无缝的数据存储解决方案
  • 项目生命周期
  • Python爬虫大师课:HTTP协议深度解析与工业级请求封装
  • k8s环境使用Operator部署Seaweedfs集群(一)
  • STM32传感器模块编程实践(十四)DIY语音+满溢检测智能垃圾桶模型
  • SD-WAN核心特点有哪些,适用哪些场景?
  • Rust 入门 泛型和特征-深入特征 (十五)
  • 【Cuda 编程思想】LinearQaunt-分块量化矩阵乘法计算过程
  • 关系型数据库核心组件:视图、函数与存储引擎详解
  • 分布式锁那些事
  • 机器学习中的PCA降维
  • ubuntu 20.04 搭建多用户远程桌面共享及同时在线使用
  • langGraph--2--langServe+langGraph示例
  • 云原生俱乐部-k8s知识点归纳(3)
  • Spark03-RDD01-简介+常用的Transformation算子
  • Rust 中 Box 的深度解析:作用、原理与最佳实践
  • 图解软件知识库体系
  • MiniSetupGetCdType函数分析之CDTYPE三种零售版oem版vol版
  • MMU 的资料收集
  • 【DDIA】第九章:一致性与共识
  • IDEA插件选择和设置优化指南(中英双版)
  • 永磁同步电机控制 第一篇、认识电机
  • 【原创理论】Stochastic Coupled Dyadic System (SCDS):一个用于两性关系动力学建模的随机耦合系统框架
  • STM32如何定位HardFault错误,一种实用方法
  • 进程和线程 (线程)