STM32理论 —— μCOS-Ⅲ

文章目录

μCos-Ⅲ全称是Micro C OS Ⅲ,由Micriμm公司发布的一个操作系统。

  • μCOS-Ⅲ与FreeRTOS的区别:μCOS-III 的源码可读性比较强,相比 RTX 和 FreeRTOS 都要好很多,代码写的非常规范。国内资料较多。 一般芯片厂商推出一款新的处理器芯片后,都会做一个评估板,很多 RTOS 厂商都会在这个评估板的基础上做一些相关的DEMO,比如 Micrium 公司(μCOS-Ⅲ的公司)为 ST 出的评估板配套的例子如下:
    STM32理论 —— μCOS-Ⅲ

有一本时间触发嵌入式系统的模式(patterns for time-triggered embedded systems)的书,作者是 Michael J. Pont,他在这个领域深有研究,而且有很多相关的论文发表,有兴趣的可以搜索一些他的文章进行深入的了解。作为入门,大家可以看一下时间触发嵌入式系统这本书的第 11 页,1.7 Time-triggered systems(一定要读)对时间触发做了入门性的介绍,讲的非常好。如果读英文有点吃力的话,可以选择读中文版。

1. 基于时间触发的合作式调度器

调度器就是使用相关的调度算法来决定当前需要执行哪个任务。所有的调度器有一个共同的特性:调度器可以区分就绪态任务挂起任务。调度器选择就绪态中的其中一个任务,然后激活执行它。不同调度器之间最大的区别就是如何分配就绪态任务间的完成时间。

嵌入式实时操作系统的核心就是调度器任务切换,调度器的核心就是调度算法。任务切换的实现在各个 RTOS 中区别不大。调度算法就有区别了,比如μCOS-III 和 FreeRTOS 在抢占式调度器算法上就是两种不同的调度方法。


合作式调度器就是根据用户的设置时刻(周期或者单次)来执行相应的任务,每个时刻只有一个任务可以执行,这些任务间不支持被强占,直到该任务自愿放弃 CPU 的控制权,再轮到下一个排队的任务。下面是简介关于合作式调度器的特点:


  • 合作式调度器
    • 合作式调度器提供了一种单任务的的系统结构
  • 操作
    • 在特定的时刻调度任务(以周期性或者单次方式)
    • 当任务需要运行时,将被添加到等待队列。
    • 当 CPU 空闲的时候,运行等待队列中的下一个(如果有的话)。
    • CPU运行任务直到完成,然后由调度器来控制。
  • 实现
    • 这种调度器很简单,用少量代码即可实现。
    • 该调度器必须一次只为一个任务分配存储器。
    • 该调度器通常完全由高级语言(比如“C”)实现。
    • 该调度器不是一种独立的系统,它是开发人员代码的一部分。
  • 新能
    • 设计阶段需要小心以快速响应外部事件。
  • 可靠性和安全性
    • 合作式调度简单,可预测,可靠并且安全。

1.1 合作式调度器设计

合作式调度器提供了一种简单而且可预测性非常高的平台。存储器的开销为每个任务 16 个字节,对 CPU 的要求很低(随时标间隔而变,也就是嘀嗒定时器的周期)。合作式调度器主要由以下几部分组成:

  • 调度器数据结构
  • 初始化函数
  • 嘀嗒定时器中断,用来以一定的时间间隔刷新调度器
  • 向调度器增加任务函数
  • 使任务在应当运行的时候被执行的调度函数
  • 从调度器删除任务的函数(未涉及)

有上面的五步,一个简单的合作式调度器就算设计完成了,下面进行简单讲解。

1.1.1 调度器数据结构

调度器的数据结构如下:

typedef unsigned char tByte; // 两个字节
typedef unsigned int tWord; // 两个字
typedef struct
{
	void (*pTask)(); /*指向任务的指针必须是一个*void(void)*函数;*/
	tWord Delay; /*延时(时标)知道下一个函数的运行*/
	tWord Period; /*连续运行之间的间隔*/
	tByte RunMe; /*当任务需要运行的时候由调度器加 1*/
}sTask;

这个结构体需要占用 16 个字节,为什么是 16 个字节而不是 13 个字节,指针函数占用 4 个字节 + Delay 占用 4 个字节 + Period 占用 4 个字节 + RunMe 占用 1 个字节 = 13 个字节。实际测试中发现做了结构体会以组员中占用字节最多的类型做字节对齐,比如上面这个就是按照 16 个字节来算。

1.1.2 初始化函数

这里的初始化函数主要是指滴答定时器的初始化,以此来产生调度器所需要的时标。一般情况下把时标间隔都设置为 1ms。

/*
*******************************************************************************************
* 函 数 名: bsp_InitTimer
* 功能说明: 配置 systick 中断,并初始化软件定时器变量
* 形 参: 无 * 返 回 值: 无
*******************************************************************************************
*/
void bsp_InitTimer(void)
{
	uint8_t i;
	/* 清零所有的软件定时器 */
	for (i = 0; i < TMR_COUNT; i++)
	{
		s_tTmr[i].Count = 0;
		s_tTmr[i].PreLoad = 0;
		s_tTmr[i].Flag = 0;
		s_tTmr[i].Mode = TMR_ONCE_MODE; /* 缺省是 1 次性工作模式 */
	}
	/*
	配置 systic 中断周期为 1ms,并启动 systick 中断。
	 SystemCoreClock 是固件中定义的系统内核时钟,对于 STM32F4XX,一般为 168MHz
	 SysTick_Config() 函数的形参表示内核时钟多少个周期后触发一次 Systick 定时中断.
	 -- SystemCoreClock / 1000 表示定时频率为 1000Hz, 也就是定时周期为 1ms
	 -- SystemCoreClock / 500 表示定时频率为 500Hz, 也就是定时周期为 2ms
	 -- SystemCoreClock / 2000 表示定时频率为 2000Hz, 也就是定时周期为 500us
	 对于常规的应用,我们一般取定时周期 1ms。对于低速 CPU 或者低功耗应用,可以设置定时周期为 10ms
	 */
	SysTick_Config(SystemCoreClock / 1000);
}

一个中断原则:如果用户使用时间触发模式,强烈建议只用一个中断,这个中断用于调度器的时标,如果用户还使用了其它中断,那么基于时间触发模式的可预测性和可靠的系统结构将被破坏。

1.1.3 刷新函数

刷新函数的主要功能是:每个时标中断执行一次。在嘀嗒定时器中断里面执行。当刷新函数确定某个任务要执行的时候,将这个任务结构体的成员 RunMe 加 1,要注意的是刷新任务不执行任何函数,这里只是设置 一下 RunMe 标志,由调度函数根据此标志执行相应任务。

/*
*******************************************************************************************
* 函 数 名: SCH_Update(void)
* 功能说明: 调度器的刷新函数,每个时标中断执行一次。在嘀嗒定时器中断里面执行。
* 当刷新函数确定某个任务要执行的时候,将 RunMe 加 1,要注意的是刷新任务
* 不执行任何函数,需要运行的任务有调度函数激活。
* 形 参:无
* 返 回 值: 无
*******************************************************************************************
*/
void SCH_Update(void)
{
	tByte index;
	/*注意计数单位是时标,不是毫秒*/
	for(index = 0; index < SCH_MAX_TASKS; index++)
	{
		/*检测这里是否有任务*/
		if(SCH_task_G[index].pTask) 
		{
			if(SCH_task_G[index].Delay == 0)
			{
				/*任务需要运行 将 RunMe 置 1*/
				SCH_task_G[index].RunMe += 1; 
				if(SCH_task_G[index].Period)
				{ 
					/*调度周期性的任务再次执行*/
					SCH_task_G[index].Delay = SCH_task_G[index].Period;
				} 
			}
			else
			{
				/*还有准备好运行*/
				SCH_task_G[index].Delay -= 1;
			} 
		} 
	} 
}

1.1.4 添加任务函数

添加任务函数的主要功能是将任务添加到任务队列上,下面主要是说一下这个函数中参数的功能:

tByte SCH_Add_Task(void (*pFuntion)(void), tWord DELAY, tWord PERIOD)
1. void (*pFuntion)(void) :表示函数的地址,也就是将函数名填进去就行了。
2. DELAY :表示函数第一次运行需要等待的时间。
3. PERIOD:表示函数周期性执行的时间间隔

举四个例子说明一下:

  • SCH_Add_Task(DOTASK, 1000, 0) —— DOTASK 是函数的运行地址,1000 是 1000 个时标以后开始运行,只运行一次。
  • SCH_Add_Task(DOTASK, 0, 1000) —— DOTASK 是函数的运行地址,每个 1000 个时标周期性的运行一次。
  • SCH_Add_Task(DOTASK, 300, 1000) —— DOTASK 是函数的运行地址,300 是 300 个时标以后开始运行,以后就是每 1000 个时标周期运行一次,也就是说运行时刻是 300,1300,2300,3300,4300 等等。
  • Task_ID = SCH_Add_Task(DOTASK,1000,0); —— Task_ID 是任务标示符,将任务标示符保存 以便以后删除任务。

下面是函数的源代码:

/*
*******************************************************************************************
* 函 数 名: SCH_Add_Task
* 功能说明: 添加任务。
* 形 参:void (*pFuntion)(void) tWord DELAY tWord PERIOD
* 返 回 值: 返回任务的 ID 号
*******************************************************************************************
*/
tByte SCH_Add_Task(void (*pFuntion)(void),tWord DELAY,tWord PERIOD)
{
	tByte index = 0; /*首先在队列中找到一个空隙,(如果有的话)*/
	while((SCH_task_G[index].pTask != 0) && (index <SCH_MAX_TASKS))
	{
		index ++;
	}
	if(index == SCH_MAX_TASKS)/*超过最大的任务数目 则返错误信息*/
	{
		Error_code_G = ERROR_SCH_TOO_MANY_TASKS;/*设置全局错误变量*/
		return SCH_MAX_TASKS;
	}
	SCH_task_G[index].pTask = pFuntion; /*运行到这里说明申请的任务块成功*/
	SCH_task_G[index].Delay = DELAY;
	SCH_task_G[index].Period = PERIOD;
	SCH_task_G[index].RunMe =0;
	return index; /*返回任务的位置,以便于以后删除*/
}

1.1.5 调度函数

刷新函数不执行任何函数任务,需要运行的任务由调度函数激活。下面是调度函数的源码:

/*
*******************************************************************************************
* 函 数 名: SCH_Dispatch_Tasks
* 功能说明: 在主任务里面执行的调度函数。
* 形 参:无
* 返 回 值: 无
*******************************************************************************************
*/
void SCH_Dispatch_Tasks(void)
{
	tByte index;
	/*运行下一个任务,如果下一个任务准备就绪的话*/
	for(index = 0; index < SCH_MAX_TASKS; index++)
	{
		if(SCH_task_G[index].RunMe >0)
		{
			/*执行任务 */
			(*SCH_task_G[index].pTask)(); 
			/* 执行任务完成后,将 RunMe 减一 */
			SCH_task_G[index].RunMe -= 1;
			/*如果是单次任务的话,则将任务删除 */ 
			if(SCH_task_G[index].Period == 0) 
			{
				SCH_Task_Delete(index);
			} 
		} 
	} 
}

1.2 合作式调度器的三个问题

使用合作式调度器要特别的注意下面三个问题,这三个问题也是做时间触发嵌入式模式研究的主要方向,研究时间触发要解决的就是这三个问题。

1.2.1 只有一个中断的原则

这个原则应该在很大程度上限制了基于时间触发的合作式调度器使用范围,比如想使用按键中断来及时的响应外部事件。如果不使用中断,那么外部事件将无法得到及时的响应。

1.2.2 任务重叠的问题

2. 基于时间触发的抢占式调度器

如果某些任务事件需要及时响应,那么他在合作式的调度器中就不适用。这时就需要用到抢占式调度器。使得最高优先级的任务什么时候可以执行,可以得到 CPU 的控制权是可知的,同时使得任务级响应时间得以最优化。

如果使用抢占式调度,最高优先级的任务一旦就绪,总能得到 CPU 的控制权。运行着的任务在运行中时,若有另一个比它优先级更高的任务进入了就绪态,那么该任务的 CPU 使用权就被剥夺,或者说它被暂时挂起了,那个更高优先级的任务立刻获得了 CPU 的控制权。如果在中断服务子程序中使一个高优先级的任务进入就绪态,那么在中断完成时,被中断了的任务会被挂起,优先级高的那个任务开始运行。

3 基于时间触发的时间片调度器

在小型的嵌入式 RTOS 中,最常用的时间片调度算法就是 Round-robin 调度算法。这种调度算法可以用于抢占式或者合作式的多任务中,时间片调度适合用于不要求任务实时响应的情况下。

实现 Round-robin 调度算法需要给同优先级的任务分配一个专门的列表,用于记录当前就绪的任务,并为每个任务分配一个时间片(即任务需要运行的时间长度,时间片用完了就进行任务切换)。

目前 embOS,FreeRTOS,μCOS-III 和 RTX 都支持 Round-robin 调度算法。

上一篇:MIGO采购订单收货批次增强


下一篇:6-12 使用函数求余弦函数的近似值 (15 分)