目录
1前言
用了很久的单片机操作系统,却不知道操作系统是怎么运行的,最近参考野火ucos方面的书,算是自己手写了一个操作系统。权当记录下学习的过程。
2准备
操作系统的优越性有哪些就不多赘述,自己手写一个操作系统才能更明白操作系统如何在单片机里面运行的。在操作上系统上用了大量的链表,如果c语言掌握的还不够扎实,应该先从熟悉链表开始。
3汇编
操作系统里面会涉及到汇编,本人没什么经历深究汇编,就直接把野火汇编语言拿了过来。汇编怎么跑的不要问我,我也不懂。
4过程
4.1工程文件
用cubemx生成工程文件,设置好时钟等基本参数就不多加赘述了。
4.2汇编语言
汇编是能在单片机上运行操作系统的最重要的东西,可以理解为汇编实现了我们主动从程序上触发了中断不需要依赖单片机的外设功能。汇编语言的文件名"os_cpu_s.s"
;********************************************************************************************************
; 全局变量&函数
;********************************************************************************************************
IMPORT OSTCBCurPtr ; 外部文件引人的参考
IMPORT OSTCBHighRdyPtr
EXPORT OSStartHighRdy ; 该文件定义的函数
EXPORT OS_PendSV_Handler
;********************************************************************************************************
; 常量
;********************************************************************************************************
;--------------------------------------------------------------------------------------------------------
;有关内核外设寄存器定义可参考官方文档:STM32F10xxx Cortex-M3 programming manual
;系统控制块外设SCB地址范围:0xE000ED00-0xE000ED3F
;--------------------------------------------------------------------------------------------------------
NVIC_INT_CTRL EQU 0xE000ED04 ; 中断控制及状态寄存器 SCB_ICSR。
NVIC_SYSPRI14 EQU 0xE000ED22 ; 系统优先级寄存器 SCB_SHPR3:bit16~23
NVIC_PENDSV_PRI EQU 0xFF ; PendSV 优先级的值(最低)。
NVIC_PENDSVSET EQU 0x10000000 ; 触发PendSV异常的值 Bit28:PENDSVSET。
;********************************************************************************************************
; 代码产生指令
;********************************************************************************************************
PRESERVE8
THUMB
AREA CODE, CODE, READONLY
;********************************************************************************************************
; 开始第一次上下文切换
; 1、配置PendSV异常的优先级为最低
; 2、在开始第一次上下文切换之前,设置psp=0
; 3、触发PendSV异常,开始上下文切换
;********************************************************************************************************
OSStartHighRdy
LDR R0, = NVIC_SYSPRI14 ; 设置 PendSV 异常优先级为最低
LDR R1, = NVIC_PENDSV_PRI
STRB R1, [R0]
MOVS R0, #0 ; 设置psp的值为0,开始第一次上下文切换
MSR PSP, R0
LDR R0, =NVIC_INT_CTRL ; 触发PendSV异常
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
CPSIE I ; 开中断
OSStartHang
B OSStartHang ; 程序应永远不会运行到这里
;********************************************************************************************************
; PendSVHandler异常
;********************************************************************************************************
OS_PendSV_Handler
; 任务的保存,即把CPU寄存器的值存储到任务的堆栈中
CPSID I ; 关中断,NMI和HardFault除外,防止上下文切换被中断
MRS R0, PSP ; 将psp的值加载到R0
CBZ R0, OS_CPU_PendSVHandler_nosave ; 判断R0,如果值为0则跳转到OS_CPU_PendSVHandler_nosave
; 进行第一次任务切换的时候,R0肯定为0
; 在进入PendSV异常的时候,当前CPU的xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0会自动存储到当前任务堆栈,同时递减PSP的值
STMDB R0!, {R4-R11} ; 手动存储CPU寄存器R4-R11的值到当前任务的堆栈
LDR R1, = OSTCBCurPtr ; 加载 OSTCBCurPtr 指针的地址到R1,这里LDR属于伪指令
LDR R1, [R1] ; 加载 OSTCBCurPtr 指针到R1,这里LDR属于ARM指令
STR R0, [R1] ; 存储R0的值到 OSTCBCurPtr->OSTCBStkPtr,这个时候R0存的是任务空闲栈的栈顶
; 任务的切换,即把下一个要运行的任务的堆栈内容加载到CPU寄存器中
OS_CPU_PendSVHandler_nosave
; OSTCBCurPtr = OSTCBHighRdyPtr;
LDR R0, = OSTCBCurPtr ; 加载 OSTCBCurPtr 指针的地址到R0,这里LDR属于伪指令
LDR R1, = OSTCBHighRdyPtr ; 加载 OSTCBHighRdyPtr 指针的地址到R1,这里LDR属于伪指令
LDR R2, [R1] ; 加载 OSTCBHighRdyPtr 指针到R2,这里LDR属于ARM指令
STR R2, [R0] ; 存储 OSTCBHighRdyPtr 到 OSTCBCurPtr
LDR R0, [R2] ; 加载 OSTCBHighRdyPtr 到 R0
LDMIA R0!, {R4-R11} ; 加载需要手动保存的信息到CPU寄存器R4-R11
MSR PSP, R0 ; 更新PSP的值,这个时候PSP指向下一个要执行的任务的堆栈的栈底(这个栈底已经加上刚刚手动加载到CPU寄存器R4-R11的偏移)
ORR LR, LR, #0x04 ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1
CPSIE I ; 开中断
BX LR ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
; 同时PSP的值也将更新,即指向任务堆栈的栈顶。在STM32中,堆栈是由高地址向低地址生长的。
NOP ; 为了汇编指令对齐,不然会有警告
END
这里直接把野火讲ucos的汇编拿了过来,这个汇编也是ucos上的实现主动中断的汇编。中间的寄存器是属于M3的内核寄存器,M3能实现操作的系统的原因是因为R13和R14两个寄存器可以一个保存函数现场,一个运行函数,说深层次点就是出栈入栈,说浅显易懂就是中断嵌套的实现。说下汇编中几个需要和c语言沟通的参数。
// 外部参考,从c中引进的变量
IMPORT OSTCBCurPtr ; 现在运行的任务
IMPORT OSTCBHighRdyPtr ;就绪的高优先级任务
// 需要在c中运行的函数
EXPORT OSStartHighRdy ; 进入系统调度需要运行的函数
EXPORT OS_PendSV_Handler ;用户中断服务函数
与之对应的c语言申明。
首先我们先建立"os.h" "os_type.h"两个头文件。
"os_type.h"主要定义了结构类型,为了和ucos一样而建立的。
#include "stdint.h"
typedef uint16_t CPU_INT16U;
typedef uint32_t CPU_INT32U;
typedef uint32_t CPU_INT08U;
typedef CPU_INT32U CPU_ADDR;
typedef CPU_INT32U CPU_STK;
typedef CPU_ADDR CPU_STK_SIZE;
typedef volatile CPU_INT32U CPU_REG32;
typedef CPU_INT16U OS_OBJ_QTY;
typedef CPU_INT08U OS_PRIO;
typedef CPU_INT08U OS_STATE;
typedef CPU_INT32U OS_IDLE_CTR;
typedef CPU_INT32U OS_TICK;
紧接着在 "os.h"建立一个结构体
typedef struct os_tcb OS_TCB;
struct os_tcb
{
CPU_STK *StkPtr;
OS_TCB *NextPtr;//就绪列表上一个指针
OS_TCB *PrevPtr;//就绪列表下一个指针
OS_TCB *TimeNextPtr;//时间片上一个指针
OS_TCB *TimePrevPtr;//时间片下一个指针
OS_TICK TimeQuanta;//需要多少时间片
OS_TICK TimeQuantaCtr;//时间片计时
OS_STATE TaskState;//任务状态(挂起)
CPU_STK_SIZE StkSize;
OS_TICK TaskDelayTicks;//任务延时周期个数
OS_PRIO Prio;//优先级
};
void OSStartHighRdy(void);//调度之前需要运行的函数
void OS_PendSV_Handler(void);//用户中断服务函数
结构体里面的内容到目前为止一个都不需要。主要有一个结构体类型 OS_TCB就行了。同时声明两个汇编中的函数。
接着建立 "os_core.c"
OS_TCB *OSTCBCurPtr;//现在正在执行的任务
OS_TCB *OSTCBHighRdyPtr;//就绪的高优先任务
到目前为止,建立好了c语言和汇编的联系,当触发用户中断,就绪的高优先任务会取代正在执行的任务。
4.3OS系统的初始化
typedef enum
{
OS_ERR_NONE = 0u,
OS_ERR_FATAL_RETURN = 15001u,
}OS_ERR;
void OSInit (OS_ERR *p_err)
{
OSRunning = OS_STATE_OS_STOPPED;
OSTCBCurPtr = (OS_TCB *)0;
OSTCBHighRdyPtr = (OS_TCB *)0;
*p_err = OS_ERR_NONE;
}
错误枚举引用的ucos,实际上需要注意的的就两行代码,把两个任务初始化为。
OSTCBCurPtr = (OS_TCB *)0;
OSTCBHighRdyPtr = (OS_TCB *)0;
4.3.1任务初始化函数
typedef void (*OS_TASK_PTR)(void *p_arg);
CPU_STK *OSTaskStkInit (OS_TASK_PTR p_task,
void *p_arg,
CPU_STK *p_stk_base,
CPU_STK_SIZE stk_size)
{
CPU_STK *p_stk;
p_stk = &p_stk_base[stk_size];
/* 异常发生时自动保存的寄存器 */
*--p_stk = (CPU_STK)0x01000000u; /* xPSR的bit24必须置1 */
*--p_stk = (CPU_STK)p_task; /* 任务的入口地址 */
*--p_stk = (CPU_STK)0x14141414u; /* R14 (LR) */
*--p_stk = (CPU_STK)0x12121212u; /* R12 */
*--p_stk = (CPU_STK)0x03030303u; /* R3 */
*--p_stk = (CPU_STK)0x02020202u; /* R2 */
*--p_stk = (CPU_STK)0x01010101u; /* R1 */
*--p_stk = (CPU_STK)p_arg; /* R0 : 任务形参 */
/* 异常发生时需手动保存的寄存器 */
*--p_stk = (CPU_STK)0x11111111u; /* R11 */
*--p_stk = (CPU_STK)0x10101010u; /* R10 */
*--p_stk = (CPU_STK)0x09090909u; /* R9 */
*--p_stk = (CPU_STK)0x08080808u; /* R8 */
*--p_stk = (CPU_STK)0x07070707u; /* R7 */
*--p_stk = (CPU_STK)0x06060606u; /* R6 */
*--p_stk = (CPU_STK)0x05050505u; /* R5 */
*--p_stk = (CPU_STK)0x04040404u; /* R4 */
return (p_stk);
}
要开启一个任务,首先要做的就是任务初始化,为什么会有0x14141414什么的数值,那代表的是R14的寄存器,除了PSR寄存器和另外两个赋值的寄存器,其他的都是可以随意填数字的。至于为啥这样写我还没太深究过。讲也将不清楚。只能跟着注释勉强理解,
4.3.2创建任务函数
void OSTaskCreate (OS_TCB *p_tcb,
OS_TASK_PTR p_task,
void *p_arg,
CPU_STK *p_stk_base,
CPU_STK_SIZE stk_size,
OS_PRIO Prio,
OS_TICK TimeQuanta,
OS_ERR *p_err)
{
CPU_STK *p_sp;
p_sp = OSTaskStkInit(p_task,p_arg,p_stk_base,stk_size);
p_tcb->StkPtr = p_sp;
p_tcb->StkSize = stk_size;
p_tcb->TaskDelayTicks=0;
p_tcb->Prio=Prio;
p_tcb->NextPtr=(OS_TCB *)0;
p_tcb->PrevPtr=(OS_TCB *)0;
p_tcb->TimeNextPtr=(OS_TCB *)0;
p_tcb->TimePrevPtr=(OS_TCB *)0;
p_tcb->TimeQuanta=TimeQuanta;
p_tcb->TimeQuantaCtr=0;
p_tcb->TaskState=0;
OS_RdyListadd(p_tcb,p_tcb->Prio);//加入就绪列表
if(p_tcb->TimeQuanta>0)//加入时间片
OS_RdyTimeadd(p_tcb,p_tcb->Prio);
*p_err = OS_ERR_NONE;
}
任务建立就是对任务初始化,然后对OS_TCB类型的变量初始化。删繁就简,只提取出目前我们需要的部分。
void OSTaskCreate (OS_TCB *p_tcb,
OS_TASK_PTR p_task,
void *p_arg,
CPU_STK *p_stk_base,
CPU_STK_SIZE stk_size,
OS_PRIO Prio,
OS_TICK TimeQuanta,
OS_ERR *p_err)
{
p_sp = OSTaskStkInit(p_task,p_arg,p_stk_base,stk_size);
*p_err = OS_ERR_NONE;
}
这样看起来是不是很简单。那么其中的形参各自意义是什么,接下来我们慢慢来看。
4.3.3空闲任务
在ucos中初始化完成以后会有默认任务,也就是空闲任务,空闲任务可以保证在你没有创建任何任务的情况下,都会有一个任务在跑,不会因为你一个任务都没创建就奔溃。
OS_IDLE_CTR OSIdleTaskCtr;//全局变量
OS_TCB OSIdleTaskTCB;//空闲任务块
#define OS_CFG_IDLE_TASK_STK_SIZE 128u//分配栈大小
CPU_STK OSCfg_IdleTaskStk[OS_CFG_IDLE_TASK_STK_SIZE];//分配的栈
CPU_STK * const OSCfg_IdleTaskStkBasePtr = (CPU_STK *)&OSCfg_IdleTaskStk[0];//栈的基地址
CPU_STK_SIZE const OSCfg_IdleTaskStkSize = (CPU_STK_SIZE)OS_CFG_IDLE_TASK_STK_SIZE;//栈的大小
void OS_IdleTask(void *p_arg);
void OS_IdleTaskInit(OS_ERR *p_err)
{
/* 初始化空闲任务计数器 */
OSIdleTaskCtr = (OS_IDLE_CTR)0;
/* 创建空闲任务 */
OSTaskCreate((OS_TCB *)&OSIdleTaskTCB,
(OS_TASK_PTR )OS_IdleTask,
(void *)0,
(CPU_STK *)OSCfg_IdleTaskStkBasePtr,
(CPU_STK_SIZE)OSCfg_IdleTaskStkSize,
(OS_PRIO )OS_CFG_PRIO_MAX-1,
0,
(OS_ERR *)p_err);
}
void OS_IdleTask(void *p_arg)
{
p_arg=p_arg;
while(1)
{
OSIdleTaskCtr++;
OSTimDly(0);
}
}
任何程序(子程序)在跑的时候都会分配一定的内存。这个是自己生成的,叫做栈。那么以上的意思rom就是分配了一块128b的空间(以数组的形式)给开辟的空闲任务,任务的起点就是数组地址,也就是数组第一个元素的地址。OSIdleTaskTCB这个东西呢,就叫他任务块。
删繁就简我们需要的暂时就只有他。
void OS_IdleTask(void *p_arg)
{
p_arg=p_arg;
while(1)
{
OSIdleTaskCtr++;
}
}
回过头来看任务初始化部分
CPU_STK *OSTaskStkInit (OS_TASK_PTR p_task,
void *p_arg,
CPU_STK *p_stk_base,
CPU_STK_SIZE stk_size)
{
CPU_STK *p_stk;
p_stk = &p_stk_base[stk_size];
/* 异常发生时自动保存的寄存器 */
*--p_stk = (CPU_STK)0x01000000u; /* xPSR的bit24必须置1 */
*--p_stk = (CPU_STK)p_task; /* 任务的入口地址 */
*--p_stk = (CPU_STK)p_arg; /* R0 : 任务形参 */
return (p_stk);
}
我们在任务初始化里面需要设置只有XPSR 24bit置1,惹怒的名称(入口地址),任务的基地址,任务的形参。
那么可以完成的创建一个空闲任务了,代码引用上文的直接用。
OS_IDLE_CTR OSIdleTaskCtr;//全局变量
OS_TCB OSIdleTaskTCB;//空闲任务块
#define OS_CFG_IDLE_TASK_STK_SIZE 128u//分配栈大小
CPU_STK OSCfg_IdleTaskStk[OS_CFG_IDLE_TASK_STK_SIZE];//分配的栈
CPU_STK * const OSCfg_IdleTaskStkBasePtr = (CPU_STK *)&OSCfg_IdleTaskStk[0];//栈的基地址
CPU_STK_SIZE const OSCfg_IdleTaskStkSize = (CPU_STK_SIZE)OS_CFG_IDLE_TASK_STK_SIZE;//栈的大小
void OS_IdleTask(void *p_arg);
void OS_IdleTaskInit(OS_ERR *p_err)
{
/* 初始化空闲任务计数器 */
OSIdleTaskCtr = (OS_IDLE_CTR)0;
/* 创建空闲任务 */
OSTaskCreate((OS_TCB *)&OSIdleTaskTCB,
(OS_TASK_PTR )OS_IdleTask,
(void *)0,
(CPU_STK *)OSCfg_IdleTaskStkBasePtr,
(CPU_STK_SIZE)OSCfg_IdleTaskStkSize,
(OS_PRIO )OS_CFG_PRIO_MAX-1,
0,
(OS_ERR *)p_err);
}
void OS_IdleTask(void *p_arg)
{
p_arg=p_arg;
while(1)
{
OSIdleTaskCtr++;
}
}
由此就可以完成一次OS系统初始了。
void OSInit (OS_ERR *p_err)
{
OSRunning = OS_STATE_OS_STOPPED;
OSTCBCurPtr = (OS_TCB *)0;
OSTCBHighRdyPtr = (OS_TCB *)0;
OS_IdleTaskInit(p_err);
*p_err = OS_ERR_NONE;
}
4.3.4OS启动
OS启动的目的是为了把程序阻碍,权限全部OS系统去处理,在主循环里面会永远阻塞在这。
void OSStart (OS_ERR *p_err)
{
if( OSRunning == OS_STATE_OS_STOPPED )
{
OSTCBHighRdyPtr =&OSIdleTaskTCB;
OSStartHighRdy();
*p_err = OS_ERR_FATAL_RETURN;
}
else
*p_err = OS_STATE_OS_RUNNING;//程序运行到这里说明报错了
}
代码做的事情非常简单,就是把空闲任务设置为就绪任务,然后用OSStartHighRdy(),汇编函数执行。
到这里就已经完成了把程序的运行条件交给OS系统。
void os_app(void)
{
OS_ERR err;
OSInit(&err);
OSStart(&err);
}
仅仅是这样还不够,我们需要OS系统的目的是为了实现通过中断来实现程序的实时调度,那么我们还需要做两件最关键的事情,中断和调度。
4.4多任务的实时调度
4.4.1中断
中断就是停止做一件事情去干一件优先级更高的事情,做完了再回头来做这件没做完的事情。此前我们已经有ucos写的汇编中断服务函数。
OS_PendSV_Handler
; 任务的保存,即把CPU寄存器的值存储到任务的堆栈中
CPSID I ; 关中断,NMI和HardFault除外,防止上下文切换被中断
MRS R0, PSP ; 将psp的值加载到R0
CBZ R0, OS_CPU_PendSVHandler_nosave ; 判断R0,如果值为0则跳转到OS_CPU_PendSVHandler_nosave
; 进行第一次任务切换的时候,R0肯定为0
; 在进入PendSV异常的时候,当前CPU的xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0会自动存储到当前任务堆栈,同时递减PSP的值
STMDB R0!, {R4-R11} ; 手动存储CPU寄存器R4-R11的值到当前任务的堆栈
LDR R1, = OSTCBCurPtr ; 加载 OSTCBCurPtr 指针的地址到R1,这里LDR属于伪指令
LDR R1, [R1] ; 加载 OSTCBCurPtr 指针到R1,这里LDR属于ARM指令
STR R0, [R1] ; 存储R0的值到 OSTCBCurPtr->OSTCBStkPtr,这个时候R0存的是任务空闲栈的栈顶
并且声明在h文件里面
void OS_PendSV_Handler(void);
那么接下来我们需要做的事情就清晰了:第一步触发中断,第二步在中断向量表里面给我们的中断服务函数找到位置。
4.4.1.1触发中断
#define NVIC_INT_CTRL *((CPU_REG32 *)0xE000ED04) /* 中断控制及状态寄存器 SCB_ICSR */
#define NVIC_PENDSVSET 0x10000000 /* 触发PendSV异常的值 Bit28:PENDSVSET */
#define OS_TASK_SW() NVIC_INT_CTRL = NVIC_PENDSVSET//主动触发中断
4.4.1.2中断向量表中中断服务函数的位置
HAL库里面专门为用户中断服务函数里面留了一个位置,这就是为什么本篇文章是基于HAL写的。
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
这是启动文件里面的中断服务函数。
在对应的c文件里面找到PendSV_Handler中断服务函数的位置。
把我们的中断服务函数写进去就行了。
/**
* @brief This function handles Pendable request for system service.
*/
void PendSV_Handler(void)
{
/* USER CODE BEGIN PendSV_IRQn 0 */
OS_PendSV_Handler();
/* USER CODE END PendSV_IRQn 0 */
/* USER CODE BEGIN PendSV_IRQn 1 */
/* USER CODE END PendSV_IRQn 1 */
}
4.4.2调度
4.4.2.1简单调度
那我们如何实现任务调度呢。先来一个简单的例子。
#define TASK1_STK_SIZE 128
#define TASK2_STK_SIZE 128
static CPU_STK Task1Stk[TASK1_STK_SIZE];
static CPU_STK Task2Stk[TASK2_STK_SIZE];
OS_TCB Task1TCB;
OS_TCB Task2TCB;
void Task1( void *p_arg );
void Task2( void *p_arg );
void os_app(void)
{
OS_ERR err;
OSInit(&err);
OSTaskCreate ((OS_TCB *) &Task1TCB,
(OS_TASK_PTR ) Task1,
(void *) 0,
(CPU_STK *) &Task1Stk[0],
(CPU_STK_SIZE) TASK1_STK_SIZE,
(OS_PRIO ) 0,//优先级
0,//时间片
(OS_ERR *) &err);
OSTaskCreate ((OS_TCB*) &Task2TCB,
(OS_TASK_PTR ) Task2,
(void *) 0,
(CPU_STK*) &Task2Stk[0],
(CPU_STK_SIZE) TASK2_STK_SIZE,
(OS_PRIO ) 1,//优先级
0,//时间片
(OS_ERR *) &err);
OSStart(&err);
}
uint8_t flag1=0;
uint8_t flag2=0;
void Task1(void *p_arg)
{
while(1)
{
if(flag1 == 1)
flag1=0;
else
flag1=1;
OSSched();//启动任务调度
}
}
void Task2(void *p_arg)
{
while(1)
{
if(flag2 == 1)
flag2=0;
else
flag2=1;
OSSched();//启动任务调度
}
}
任务调度函数
void OSSched (void)
{
if(OSTCBCurPtr==Task1TCB)
OSTCBHighRdyPtr==Task2TCB;//改变最高等级的就绪任务
else if((OSTCBCurPtr==Task2TCB))
OSTCBHighRdyPtr==Task1TCB;//改变最高等级的就绪任务
OS_TASK_SW();//主动触发中断,触发中断后会跳转到中断服务函数
}
这样我们就实现一个简单的任务调度了。
4.4.3.2任务延时
调度讲完以前得先讲讲延时,从这里开始,已经逐步脱离了ucos的形式,因为后面的东西太复杂我也一时半会理解不了,只能通过了解它要做的事情,去实现相同的功能。首先我对os_tcb做一点点改变。
typedef struct os_tcb OS_TCB;
struct os_tcb
{
CPU_STK *StkPtr;
CPU_STK_SIZE StkSize;
OS_TICK TaskDelayTicks;//任务延时周期个数
};
void OSTaskCreate (OS_TCB *p_tcb,
OS_TASK_PTR p_task,
void *p_arg,
CPU_STK *p_stk_base,
CPU_STK_SIZE stk_size,
OS_PRIO Prio,
OS_TICK TimeQuanta,
OS_ERR *p_err)
{
CPU_STK *p_sp;
p_sp = OSTaskStkInit(p_task,p_arg,p_stk_base,stk_size);
p_tcb->StkPtr = p_sp;
p_tcb->StkSize = stk_size;
p_tcb->TaskDelayTicks=0;
*p_err = OS_ERR_NONE;
}
添加新函数,设置任务延时。
void OSTimDly(OS_TICK dly)
{
OSTCBCurPtr->TaskDelayTicks=dly;
OSSched();
}
void OSTimeTick (void)
{
if(Task1TCB.TaskDelayTicks>0)
Task1TCB.TaskDelayTicks--;
if(Task2TCB.TaskDelayTicks>0)
Task2TCB.TaskDelayTicks--;
}
void OS_SysTick_handler(void)
{
OSTimeTick();
}
找到stm32systick的中断服务函数
/**
* @brief This function handles System tick timer.
*/
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
OS_SysTick_handler();
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
cubemx生成的文件里面会用systick进行毫秒计时,所以这个中断你不去管就是开着在。主任务函数里面再做一点点修改。这样就做好了毫秒计延时的准备。到这里,就结束了和cubemx的对接,以后的内容都不需要附着于cubemx。
#define TASK1_STK_SIZE 128
#define TASK2_STK_SIZE 128
static CPU_STK Task1Stk[TASK1_STK_SIZE];
static CPU_STK Task2Stk[TASK2_STK_SIZE];
OS_TCB Task1TCB;
OS_TCB Task2TCB;
void Task1( void *p_arg );
void Task2( void *p_arg );
void os_app(void)
{
OS_ERR err;
OSInit(&err);
OSTaskCreate ((OS_TCB *) &Task1TCB,
(OS_TASK_PTR ) Task1,
(void *) 0,
(CPU_STK *) &Task1Stk[0],
(CPU_STK_SIZE) TASK1_STK_SIZE,
(OS_PRIO ) 0,//优先级
0,//时间片
(OS_ERR *) &err);
OSTaskCreate ((OS_TCB*) &Task2TCB,
(OS_TASK_PTR ) Task2,
(void *) 0,
(CPU_STK*) &Task2Stk[0],
(CPU_STK_SIZE) TASK2_STK_SIZE,
(OS_PRIO ) 1,//优先级
0,//时间片
(OS_ERR *) &err);
OSStart(&err);
}
uint8_t flag1=0;
uint8_t flag2=0;
void Task1(void *p_arg)
{
while(1)
{
if(flag1 == 1)
flag1=0;
else
flag1=1;
OSTimDly(10);
}
}
void Task2(void *p_arg)
{
while(1)
{
if(flag2 == 1)
flag2=0;
else
flag2=1;
OSTimDly(10);
}
}
接下来我们对 OSSched()做一点点修改
void OSSched (void)
{
if(OSTCBCurPtr==Task1TCB && Task2TCB.TaskDelayTicks==0)
OSTCBHighRdyPtr==Task2TCB;//改变最高等级的就绪任务
else if(OSTCBCurPtr==Task2TCB && Task1TCB.TaskDelayTicks==0)
OSTCBHighRdyPtr==Task1TCB;//改变最高等级的就绪任务
esle
OSTCBHighRdyPtr==OSIdleTaskTCB;//延时未到,继续执行空闲任务
OS_TASK_SW();//主动触发中断,触发中断后会跳转到中断服务函数
}
任务1执行一次,延时10ms,任务2执行一次,延时10ms,没任务就跑空闲任务。
4.4.3.3通过就绪列表实现任务调度
可以说是基于延时的多优先级的任务调度,到这里,跨度比较大。
typedef struct os_rdy_list OS_RDY_LIST;
typedef struct os_tcb OS_TCB;
struct os_rdy_list
{
OS_TCB *HeadPtr;
OS_TCB *TailPtr;
OS_OBJ_QTY NbrEntries;
};
struct os_tcb
{
CPU_STK *StkPtr;
OS_TCB *NextPtr;//就绪列表上一个指针
OS_TCB *PrevPtr;//就绪列表下一个指针
CPU_STK_SIZE StkSize;
OS_TICK TaskDelayTicks;//任务延时周期个数
OS_PRIO Prio;//优先级
};
就绪列表,本身我们可以想象成一个数组,假如我们设置32个优先级,数组的每个元素是任务块。每我从数组的第一个元素开始轮询,发现该任务延时结束,即TaskDelayTicks==0;就把该任务赋值给OSTCBHighRdyPtr。那么如果同优先级的不止一个呢?那就建立一个二维数组,把同优先级的任务轮询完了再轮询下一个优先级。为了便于以后对任务增减的操作,我们不用二维数组,链表代替同优先级的任务排序。
#define OS_CFG_PRIO_MAX 32u//最大优先级
OS_RDY_LIST OSRdyList[OS_CFG_PRIO_MAX];
我们把这个链表类型的数组就叫就绪列表,我们可以通过它调度任务。通过封装好的函数实现初始任务的添加删除
void OS_RdyListInit(void)//就绪列表初始化
{
OS_PRIO i;
OS_RDY_LIST *p_rdy_list;
for( i=0u; i<OS_CFG_PRIO_MAX; i++ )
{
p_rdy_list = &OSRdyList[i];
p_rdy_list->NbrEntries = (OS_OBJ_QTY)0;
p_rdy_list->HeadPtr = (OS_TCB *)0;
p_rdy_list->TailPtr = (OS_TCB *)0;
}
}
void OS_RdyListadd(OS_TCB *tcb,OS_PRIO Prio)//插入一个任务
{
OS_RDY_LIST *p_rdy_list;
p_rdy_list = &OSRdyList[Prio];
OS_TCB *tcb_new;
if(p_rdy_list->HeadPtr==(OS_TCB *)0)//头部为空
{
p_rdy_list->HeadPtr=tcb;
p_rdy_list->TailPtr =tcb;
p_rdy_list->NbrEntries++;
}
else
{
tcb_new=p_rdy_list->HeadPtr;
while(1)
{
if(tcb_new->NextPtr==(OS_TCB *)0)
break;
tcb_new=tcb_new->NextPtr;
}
tcb_new->NextPtr=tcb;
p_rdy_list->TailPtr =tcb;
p_rdy_list->NbrEntries++;
}
}
void OS_RdyDelint(OS_TCB *tcb)//删除一个任务
{
OS_PRIO Prio=0;
OS_RDY_LIST *p_rdy_list;
OS_TCB *tcb_ptr;
p_rdy_list = &OSRdyList[Prio];
for(Prio=0;Prio<OS_CFG_PRIO_MAX;Prio++)
{
if(p_rdy_list->NbrEntries!=0)
{
tcb_ptr=p_rdy_list->HeadPtr;
if(tcb_ptr==tcb)
{
p_rdy_list->HeadPtr=tcb_ptr->NextPtr;
p_rdy_list->HeadPtr->PrevPtr=(OS_TCB *)0;
tcb->NextPtr=(OS_TCB *)0;
tcb->PrevPtr=(OS_TCB *)0;
p_rdy_list->HeadPtr--;
return;
}
while(1)
{
tcb_ptr=tcb_ptr->NextPtr;
if(tcb_ptr==(OS_TCB *)0)//到表尾没有找到就跳出
break;
if(tcb_ptr==tcb)
{
if(tcb_ptr==p_rdy_list->TailPtr)
{
p_rdy_list->TailPtr=tcb_ptr->PrevPtr;
p_rdy_list->TailPtr->PrevPtr=(OS_TCB *)0;
}
else
{
tcb_ptr->NextPtr->PrevPtr=tcb_ptr->PrevPtr;
tcb_ptr->PrevPtr->NextPtr=tcb_ptr->NextPtr;
}
tcb->NextPtr=(OS_TCB *)0;
tcb->PrevPtr=(OS_TCB *)0;
p_rdy_list->HeadPtr--;
return;
}
}
}
}
}
然后实现对已经就绪的最高优先级任务查询
OS_TCB *OSTaskPrior(void)//寻找就绪的最高优先级任务
{
OS_PRIO Prio=0;
OS_RDY_LIST *p_rdy_list;
OS_PRIO Prio_new=0;
if(OSTCBCurPtr==&OSIdleTaskTCB)//正在执行空闲任务
Prio_new=OS_CFG_PRIO_MAX;//32
else if(OSTCBCurPtr->TaskDelayTicks==0)//任务未结束,之轮询比他高的优先级
Prio_new=OSTCBCurPtr->Prio;//获取当前执行任务优先级
else if(OSTCBCurPtr->TaskDelayTicks>0)//任务已经结束,开始遍历下一次的任务
Prio_new=OS_CFG_PRIO_MAX;//32
for( Prio=0u; Prio<Prio_new; Prio++ )
{
p_rdy_list = &OSRdyList[Prio];//从最高优先级开始轮询
if(p_rdy_list->NbrEntries!=(OS_OBJ_QTY)0)
{
OS_TCB *tcb_new;
tcb_new=p_rdy_list->HeadPtr;
while(1)
{
if(tcb_new->TaskDelayTicks==0)//延时时间到说明任务就绪
return tcb_new;//找到一个最高优先级的任务
if(tcb_new->NextPtr==(OS_TCB *)0)//下一个任务地址为0
break;
tcb_new=tcb_new->NextPtr;
}
}
}
return OSTCBCurPtr;//继续执行当前任务
}
OS初始化的时候初始化就序列表
void OSInit (OS_ERR *p_err)
{
OSRunning = OS_STATE_OS_STOPPED;
OSTCBCurPtr = (OS_TCB *)0;
OSTCBHighRdyPtr = (OS_TCB *)0;
OS_RdyListInit();
OS_IdleTaskInit(p_err);
*p_err = OS_ERR_NONE;
}
开启一个任务的时候同时把该任务放进就序列表
void OSTaskCreate (OS_TCB *p_tcb,
OS_TASK_PTR p_task,
void *p_arg,
CPU_STK *p_stk_base,
CPU_STK_SIZE stk_size,
OS_PRIO Prio,
OS_TICK TimeQuanta,
OS_ERR *p_err)
{
CPU_STK *p_sp;
p_sp = OSTaskStkInit(p_task,p_arg,p_stk_base,stk_size);
p_tcb->StkPtr = p_sp;
p_tcb->StkSize = stk_size;
p_tcb->TaskDelayTicks=0;
p_tcb->Prio=Prio;
p_tcb->NextPtr=(OS_TCB *)0;
p_tcb->PrevPtr=(OS_TCB *)0;
OS_RdyListadd(p_tcb,p_tcb->Prio);//加入就绪列表
*p_err = OS_ERR_NONE;
}
同样的方式修改延时计时函数,该函数放在systick里面。
void TaskDelayTicksDecrement(void)//活跃任务时间递减-1ms
{
OS_PRIO Prio=0;
OS_RDY_LIST *p_rdy_list;
for( Prio=0u; Prio<OS_CFG_PRIO_MAX; Prio++ )
{
p_rdy_list = &OSRdyList[Prio];//从最高优先级开始轮询
if(p_rdy_list->NbrEntries!=(OS_OBJ_QTY)0)
{
OS_TCB *tcb_new;
tcb_new=p_rdy_list->HeadPtr;
if(tcb_new->TaskDelayTicks>0 && tcb_new->TaskState==0)//处于延时阶段
p_rdy_list->HeadPtr->TaskDelayTicks--;
while(1)
{
if(tcb_new->NextPtr==(OS_TCB *)0)//下一个任务地址为0
break; ;
if(tcb_new->NextPtr->TaskDelayTicks>0)//开始延时
tcb_new->NextPtr->TaskDelayTicks--;
tcb_new=tcb_new->NextPtr;
}
}
}
}
void OSTimeTick (void)
{
TaskDelayTicksDecrement();
OSSched();
}
void OS_SysTick_handler(void)
{
OSTimeTick();
}
进一步修改调度函数
void OSSched (void)
{
OS_TCB *tcbptr;
tcbptr=OSTaskPrior();//获取当前就绪的最高优先级任务
if(tcbptr==&OSIdleTaskTCB && OSTCBCurPtr==&OSIdleTaskTCB)//返回是空闲任务,且正在执行空闲任务
return;
if(tcbptr==OSTCBCurPtr)
return;
OS_TASK_SW();
}
这样就完成了对多优先级多任务的调度。
当然实现的方法有很多,可以分成两个链表,一个判断计时为0的任务(就绪任务),一个把计时为0的任务放进去,按照优先级依次调用,这样做就是在程序的时间复杂度和空间复杂度上面抉择,我选择了时间复杂度更高的写法。
这里也讲了下对任务的删除,把任务从就绪列表里面删除就行了。后面有时间再专门说下任务的删除。其实到这些地方已经没有再大量的参考ucos的写法。ucos的写法很好,但我写的出来才是我自己最熟悉最了解的。
4.5时间片的分配和任务的挂起
时间片在就是对同优先级任务进行时间分配。假如我任务1任务2任务3都是同一优先级0。如果我不对任务进行阻塞延时,那么就会一直运行第一个创建的任务,比如我第一个创建注册到就绪列表的是任务1,那么就会一直运行任务1。为了三个任务同时运行,就比如对三个任务进行分配,比如300ms运行任务1,300ms运行任务2,300ms运行任务3,任务3运行300ms再去运行任务1,此反复循环。任务没跑完不要紧,主动触发中断去运行下一个任务。这种东西在liumx里面用的很多,根据这个需求,我自己写的时间片函数。
至于任务挂起就更好理解了,暂时不执行这个任务,除非重新唤醒。
4.5.1OS结构体
只需要再结构体里面在添加一点点元素,然后重新建立一个链表用于时间片计时。
typedef struct os_time_list OS_TIME_LIST;
typedef struct os_rdy_list OS_RDY_LIST;
typedef struct os_tcb OS_TCB;
typedef void (*OS_TASK_PTR)(void *p_arg);
struct os_rdy_list//就绪列表
{
OS_TCB *HeadPtr;
OS_TCB *TailPtr;
OS_OBJ_QTY NbrEntries;
};
struct os_time_list//时间片
{
OS_TCB *HeadPtr;
OS_TCB *TailPtr;
OS_OBJ_QTY NbrEntries;
};
struct os_tcb
{
CPU_STK *StkPtr;
CPU_STK_SIZE StkSize;
OS_TICK TaskDelayTicks;//任务延时周期个数
OS_PRIO Prio;//优先级
OS_TCB *NextPtr;//就绪列表上一个指针
OS_TCB *PrevPtr;//就绪列表下一个指针
OS_TCB *TimeNextPtr;//时间片上一个指针
OS_TCB *TimePrevPtr;//时间片下一个指针
OS_TICK TimeQuanta;//需要多少时间片
OS_TICK TimeQuantaCtr;//时间片计时
OS_STATE TaskState;//任务状态(挂起)
};
4.5.2时间片的增减
时间片的增减直接套用任务增减少的函数
void OS_RdyTimeadd(OS_TCB *tcb,OS_PRIO Prio)//插入一个时间片
{
OS_TIME_LIST *p_time_list;
p_time_list = &OSTimeList[Prio];
OS_TCB *tcb_new;
if(p_time_list->HeadPtr==(OS_TCB *)0)//头部为空
{
p_time_list->HeadPtr=tcb;
p_time_list->TailPtr =tcb;
p_time_list->NbrEntries++;
}
else
{
tcb_new=p_time_list->HeadPtr;
while(1)
{
if(tcb_new->TimeNextPtr==(OS_TCB *)0)
break;
tcb_new=tcb_new->TimeNextPtr;
}
tcb_new->TimeNextPtr=tcb;
p_time_list->TailPtr =tcb;
p_time_list->NbrEntries++;
}
}
void OS_timeDelint(OS_TCB *tcb)//删除一个时间片
{
OS_PRIO Prio=0;
OS_TIME_LIST *p_time_list;
OS_TCB *tcb_ptr;
p_time_list = &OSTimeList[Prio];
for(Prio=0;Prio<OS_CFG_PRIO_MAX;Prio++)
{
if(p_time_list->NbrEntries!=0)
{
tcb_ptr=p_time_list->HeadPtr;
if(tcb_ptr==tcb)
{
p_time_list->HeadPtr=tcb_ptr->TimeNextPtr;
p_time_list->HeadPtr->TimePrevPtr=(OS_TCB *)0;
tcb->TimeNextPtr=(OS_TCB *)0;
tcb->TimePrevPtr=(OS_TCB *)0;
return;
}
while(1)
{
tcb_ptr=tcb_ptr->TimeNextPtr;
if(tcb_ptr==(OS_TCB *)0)//到表尾没有找到就跳出
break;
if(tcb_ptr==tcb)
{
if(tcb_ptr==p_time_list->TailPtr)
{
p_time_list->TailPtr=tcb_ptr->TimePrevPtr;
p_time_list->TailPtr->TimePrevPtr=(OS_TCB *)0;
}
else
{
tcb_ptr->TimeNextPtr->TimePrevPtr=tcb_ptr->TimePrevPtr;
tcb_ptr->TimePrevPtr->TimeNextPtr=tcb_ptr->TimeNextPtr;
}
tcb->TimeNextPtr=(OS_TCB *)0;
tcb->TimePrevPtr=(OS_TCB *)0;
return;
}
}
}
}
}
对时间片计时和轮询
OS_TCB *TimeControl(OS_TCB *tcb)//时间片轮询
{
OS_TCB *tcb_new;
OS_TIME_LIST *p_time_list;
p_time_list = &OSTimeList[tcb->Prio];
if(p_time_list->HeadPtr>0)
{
if(tcb->TimeQuantaCtr > tcb->TimeQuanta)
{
tcb_new=tcb->TimePrevPtr;
while(1)
{
if(tcb_new->TimeQuanta==0 && tcb_new->TaskDelayTicks==0 && tcb_new->TaskState==0)
{
OS_timeDelint(tcb);
tcb->TimeQuantaCtr=0;
OS_RdyTimeadd(tcb,tcb->Prio);
return tcb_new;
}
if(tcb_new==p_time_list->TailPtr)
return tcb;
tcb_new=tcb_new->TimePrevPtr;
}
}
}
return tcb;
}
void TimeQuantaSystick(void)//时间片计时
{
OS_TCB *tcb=OSTCBCurPtr;
if(tcb->TimeQuanta>0 &tcb->TaskState==0)
tcb->TimeQuantaCtr++;
}
分别放在任务调度函数和1ms计时函数里面,这里做一个小优化。判定没有就绪的高优先级任务。
void OSSched (void)
{
OS_TCB *tcbptr;
tcbptr=OSTaskPrior();//获取当前就绪的最高优先级任务
if(tcbptr==&OSIdleTaskTCB && OSTCBCurPtr==&OSIdleTaskTCB)//返回是空闲任务,且正在执行空闲任务
return;
if(tcbptr==OSTCBCurPtr)
return;
OSTCBHighRdyPtr=TimeControl(tcbptr);// //时间片的判定
OS_TASK_SW();
}
void OSTimeTick (void)
{
if(OSTCBHighRdyPtr!=0)
{
TaskDelayTicksDecrement();
TimeQuantaSystick();
OSSched();
}
}
void OS_SysTick_handler(void)
{
OSTimeTick();
}
初始化和任务的创建
void OSInit (OS_ERR *p_err)
{
OSRunning = OS_STATE_OS_STOPPED;
OSTCBCurPtr = (OS_TCB *)0;
OSTCBHighRdyPtr = (OS_TCB *)0;
OS_RdyListInit();
OS_RdyTimeInit();
OS_IdleTaskInit(p_err);
*p_err = OS_ERR_NONE;
}
void OSTaskCreate (OS_TCB *p_tcb,
OS_TASK_PTR p_task,
void *p_arg,
CPU_STK *p_stk_base,
CPU_STK_SIZE stk_size,
OS_PRIO Prio,
OS_TICK TimeQuanta,
OS_ERR *p_err)
{
CPU_STK *p_sp;
p_sp = OSTaskStkInit(p_task,p_arg,p_stk_base,stk_size);
p_tcb->StkPtr = p_sp;
p_tcb->StkSize = stk_size;
p_tcb->TaskDelayTicks=0;
p_tcb->Prio=Prio;
p_tcb->NextPtr=(OS_TCB *)0;
p_tcb->PrevPtr=(OS_TCB *)0;
p_tcb->TimeNextPtr=(OS_TCB *)0;
p_tcb->TimePrevPtr=(OS_TCB *)0;
p_tcb->TimeQuanta=TimeQuanta;
p_tcb->TimeQuantaCtr=0;
p_tcb->TaskState=0;
OS_RdyListadd(p_tcb,p_tcb->Prio);//加入就绪列表
if(p_tcb->TimeQuanta>0)
OS_RdyTimeadd(p_tcb,p_tcb->Prio); //如果有有效时间片就加入时间片
*p_err = OS_ERR_NONE;
}
这里详细说下时间片调度逻辑。当前在运行的每ms+1,当时间运行满后就把该任务函数,然后添加到链表的尾部。
调度任务也很简单,先找到优先级最高的就绪任务,然后再在该优先级任务里面寻找是时间片和阻塞延时都满足的任务(还有挂起的状态位,但不碰它默认都是0不挂起)。
4.5.3任务的挂机和运行
值得注意的是,任务可以把自己挂起,但不可以把自己启动,任务挂起了多少次任务就要启动多少次。
void OSTaskSusPend(OS_TCB *tcb)//任务挂起
{
if(tcb->TaskState<0xFF)
tcb->TaskState++;
OSSched ();
}
void OSTaskStart(OS_TCB *tcb)//任务启动
{
if(tcb->TaskState>0)
tcb->TaskState--;
OSSched ();
}
5主函数函数形式
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
os_app();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
#define TASK1_STK_SIZE 20
#define TASK2_STK_SIZE 20
static CPU_STK Task1Stk[TASK1_STK_SIZE];
static CPU_STK Task2Stk[TASK2_STK_SIZE];
OS_TCB Task1TCB;
OS_TCB Task2TCB;
void Task1( void *p_arg );
void Task2( void *p_arg );
void os_app(void)
{
OS_ERR err;
OSInit(&err);
OSTaskCreate ((OS_TCB *) &Task1TCB,
(OS_TASK_PTR ) Task1,
(void *) 0,
(CPU_STK *) &Task1Stk[0],
(CPU_STK_SIZE) TASK1_STK_SIZE,
(OS_PRIO ) 0,//优先级
0,//时间片
(OS_ERR *) &err);
OSTaskCreate ((OS_TCB*) &Task2TCB,
(OS_TASK_PTR ) Task2,
(void *) 0,
(CPU_STK*) &Task2Stk[0],
(CPU_STK_SIZE) TASK2_STK_SIZE,
(OS_PRIO ) 1,//优先级
0,//时间片
(OS_ERR *) &err);
OSStart(&err);
}
uint8_t flag1=0;
uint8_t flag2=0;
void Task1(void *p_arg)
{
while(1)
{
if(flag1 == 1)
flag1=0;
else
flag1=1;
OSTimDly(10);
}
}
void Task2(void *p_arg)
{
while(1)
{
if(flag2 == 1)
flag2=0;
else
flag2=1;
OSTimDly(10);
}
后记
至于什么信号量,互斥量,消息队列,二值信号量提前优先级,什么的就以后再说了,或许某一天我想起来的时候会不定时更新,关全局中断开全局中断hal库都有对应的宏。自己手写一个操作系统,最重要的还是加深对操作系统的调度方式的理解,我也怕我某一天忘了,所以把这些东西给记录下来。