个人原创,请勿抄袭。
转载请注明出处,谢谢!
个人邮箱 737355415@qq.com
------------------------------------------------------华丽的分割线--------------------------------------------------------
好久没上CSDN了,今天给大家带来点干货。
目录
2、第二重要的当然是任务链表,任务链表分普通任务链表,延时任务链表,就绪任务链表和运行任务链表,还有挂起链表,我没有加
RTOS和前后台系统最大的区别在于:RTOS可以改变任务堆栈的走向。
举个例子,现在有任务函数1、2、3、4和5,前后台系统只能按12345顺序指行,而RTOS可以随意组合。
目前只写了任务切换的函数,之前写了一个PendSV中断切换的函数,发现只能在母函数里切换,一旦进入子函数返回就会出错,后来弄了几天, 疯狂得找堆栈知识,了解到子函数会开辟新的函数栈,什么意思呢? 意思是子函数例如 delay函数 会把母函数得栈顶替换为子函数的栈顶,这样一来就不能保存母函数的栈顶,得保存子函数的,否则返回母函数栈顶,就会出现硬件错误,原因是 栈顶地址和寄存器不匹配。
1、RTOS最底层的就是PendSV中断切换函数
//-------------------------------------------------------------------------
// 配置 PendSV_Handler
//-------------------------------------------------------------------------
__asm static void PendSV_Set(void)
{
NVIC_SYSPRI14 EQU 0xE000ED20 // M0 M3 -> 0XE000ED22
NVIC_PENDSV_PRI EQU 0x00FF0000 // M0 M3 -> 0X000000FF
LDR R1, =NVIC_PENDSV_PRI
LDR R0, =NVIC_SYSPRI14
STRB R1, [R0]
BX LR
}
//-------------------------------------------------------------------------
// 触发一次PendSV_Handler
//-------------------------------------------------------------------------
__asm static void PendSV_Trig(void)
{
NVIC_INT_CTRL EQU 0xE000ED04
NVIC_PENDSVSET EQU 0x10000000
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
}
这个中断函数写的久了,有些记不太清了,专用于MDK的,汇编.S文件不怎么会写,大家凑合着看吧,如果是M3的板子好像要改点地方,因为M3和M0的指令集不一样,这个就各位自己去研究下啦,话说我为什么不用M3,因为M0便宜呀。
//-------------------------------------------------------------------------
// 不用管进栈的位置,只需出栈切换任务时 R0指向 初始创建任务时R0的位置即可 -
//-------------------------------------------------------------------------
__asm void PendSV_Handler(void)
{
PRESERVE8
CPSID I // 禁止中断
// *p = 栈顶
// p = 栈顶的地址
// &p = 栈顶的地址的地址
LDR R1,=__cpp(&mPSPCurrTsk) // 当前任务的栈顶的地址的地址
LDR R1,[R1] // 当前任务的栈顶的地址
LDR R3,=__cpp(&mPSPNextTsk) // 下个任务的栈顶的地址的地址
LDR R3,[R3] // 下个任务的栈顶的地址
cmp R1,R3 // 当前任务和下个任务
BEQ out // == 退出
//------------------------------------------
// 拷贝子函数栈基地址到母函数基地址
//------------------------------------------
MRS R0, PSP // 保存PSP指针 xPSR, PC, LR, R12, R0-R3自动保存
SUBS R0,#0x20 // 往前偏移8个字
CMP R1,#0
BEQ COPY // 栈顶的地址存在则直接保存
STR R0,[R1] // 更新当前任务的栈顶
COPY
//-----------------------------------------------------------
//------------------------------------------
// 跳转母函数基地址直接回到子函数执行,再返回
//------------------------------------------
//------------------------------------------
// 加载下个任务的栈顶 1
//------------------------------------------
// 保存R4-R11 1
STM R0!,{R4-R7}
MOV R4,R8
MOV R5,R9
MOV R6,R10
MOV R7,R11
STM R0!,{R4-R7} // 这里STM等效STMIA
// 保存R4-R11 2
LDR R1,=__cpp(&mPSPCurrTsk) // 当前任务的栈顶的地址的地址
LDR R3,=__cpp(&mPSPNextTsk) // 下个任务的栈顶的地址的地址
LDR R3,[R3] // 下个任务的栈顶的地址
STR R3,[R1] // 当前任务的栈顶的地址指向下个任务的栈顶的地址
LDR R0,[R3] // 加载下个任务的栈顶
//------------------------------------------
// 加载下个任务的栈顶 2
//------------------------------------------
// 准备出栈
//------------------------------------------
// 出栈顺序更新 1
//------------------------------------------
ADDS R0,#0X10 // 移到R8
STM R0!,{R4-R7}
MOV R8,R4
MOV R9,R5
MOV R10,R6
MOV R11,R7
SUBS R0,#0X20 // 移到R4
STM R0!,{R4-R7}
ADDS R0,#0X10 // 移到R0
//------------------------------------------
// 出栈顺序更新 2
//------------------------------------------
MSR PSP, R0 // 更新PSP指针
out // 任务不变,直接跳出去
CPSIE I // 恢复中断
BX LR // 返回 xPSR, PC, LR, R12, R0-R3自动恢复
ALIGN 4
}
2、第二重要的当然是任务链表,任务链表分普通任务链表,延时任务链表,就绪任务链表和运行任务链表,还有挂起链表,我没有加
这是链表种类的定义
//---------------------------------
// TASK LIST -
// INCLUDE: -
// 优先级 mPriority -
// 延时值 mTick -
// 状态 mStatus -
// 栈顶 mPtrStackTop -
// 栈底 mPtrStackBottom -
// 栈顶最大值 mPtrStackTopMax -
// 事件标志组 mEvent -
// 上一个节点 mPtrTskLast -
// 下一个节点 mPtrTskNext -
//---------------------------------
typedef struct _TSK_LIST_
{
u32 mStatus : 3; // 3位任务状态
u32 mPriority : 5; // 5位优先级 0~31
u32 mEvent : 8; // 8位事件标志 最大8个事件
u32 mTick : 16; // 16位延时 最大65S
u32 mPtrStackTop; // 栈顶 SP Pointer
u32 mPtrStackBottom; // 栈底
u32 mPtrStackTopMax; // 栈顶最大值
struct _TSK_LIST_* mPtrTskLast; // 上一个节点任务
struct _TSK_LIST_* mPtrTskNext; // 下一个节点任务
}TSK_LST;
// 延时列表 以优先级为纽带
typedef struct _TSK_BLK_
{
u32 prio; // 优先级
u32 tick; // 延时值
struct _TSK_BLK_ *next; // 指向下一个延时任务
}TSK_BLK;
// 就绪链表 以优先级为纽带
typedef struct _TSK_RDY_
{
u32 prio; // 优先级
struct _TSK_RDY_ *next; // 指向下一个就绪任务
}TSK_RDY;
typedef struct _TSK_RUN_
{
u32 pri ; // 4位优先级 0~15
struct _TSK_RUN_* next; //
}TSK_RUN;
typedef enum
{
TSK_Run, // 运行
TSK_Rdy, // 就绪
TSK_Blk, // 阻塞
TSK_Sus // 挂起
}TSK_STATUS;
typedef enum
{
F, // 错误
T = !F // 正确
}STATUS;
这是链表的初始化
TSK_LST* tsk_lst = NULL; // 任务链表
TSK_RDY* tsk_rdy = NULL; // 就绪链表
TSK_BLK* tsk_blk = NULL; // 阻塞链表
TSK_RUN* tsk_run = NULL; // 运行链表
void TSK_Init(void)
{
// tsk_lst = (TSK_LST*)&mem32buf[0]; // 每个占据6字 允许添加8个 任务(包含表头和系统空闲任务)
// tsk_rdy = (TSK_RDY*)&mem32buf[128]; // 每个占据2字 允许添加6个就绪任务(包含表头)
// tsk_blk = (TSK_BLK*)&mem32buf[256]; // 每个占据2字 允许添加6个阻塞任务(包含表头)
// tsk_run = (TSK_RUN*)&mem32buf[384]; // 每个占据2字 允许添加2个运行任务(包含表头) 不支持时间片轮渡
tsk_lst = (TSK_LST*)MEM_Alloc(sizeof(TSK_LST)); // 每个占据6字 任务(包含表头和系统空闲任务)
tsk_rdy = (TSK_RDY*)MEM_Alloc(sizeof(TSK_RDY)); // 每个占据2字 就绪任务(包含表头)
tsk_blk = (TSK_BLK*)MEM_Alloc(sizeof(TSK_BLK)); // 每个占据2字 阻塞任务(包含表头)
tsk_run = (TSK_RUN*)MEM_Alloc(sizeof(TSK_RUN)); // 每个占据2字 运行任务(包含表头) 不支持时间片轮渡
tsk_lst->mPtrTskNext = tsk_lst; // 初始化循环双向链表
tsk_rdy->next = tsk_rdy; //
tsk_blk->next = tsk_blk;
tsk_run->next = tsk_run;
}
大家会发现上面用了MEM_ALLOC(int)函数,咦怎么不用malloc,听说mallo函数容易造成内存碎片和内存泄露,加上一名合格的嵌入式程序员必须要会内存管理机制,所以我自己写了个内存管理的函数,MEM_Free函数会自动合并相邻的内存碎片,达到更高的内存利用率,内存管理机制有三种,以分配内存优先,以内存利用率优先的,还有一种不记得了
3、内存分配
这是内存分配链表的定义
#define NULL 0
#define MEM_LEN_ALL 512
#define MEM_LEN_STRUCT sizeof(TS_MEM_NODE)
#define MEM_DATA_TYPE u8
//---------------------------------------------
// 内存块链表
//---------------------------------------------
typedef struct _TS_MEM_NODE_
{
u32 len : 31; // 当前内存块的长度
u32 status : 1 ; // 是否被使用
struct _TS_MEM_NODE_* plast; // 指向上个内存块的地址
struct _TS_MEM_NODE_* pnext; // 指向下个内存块的地址
}TS_MEM_NODE;
typedef enum _TS_MEM_STATUS_
{
USED, // 使用
FREE, // 空闲
}TS_MEM_STATUS;
void MEM_Init(void) // 内存初始化
{
// 头索引不许动
MEM_HEAD = (TS_MEM_NODE*)MEM_ALLOCATED; // 指向数组第一个位置
//MEM_NODE = (TS_MEM_NODE*)&MEM_ALLOCATED[MEM_LEN_STRUCT]; // 指向数组第12个位置
MEM_NODE = MEM_HEAD + 1;
MEM_HEAD->len = 0;
MEM_HEAD->status = USED;
MEM_HEAD->plast = MEM_NODE;
MEM_HEAD->pnext = MEM_NODE;
MEM_NODE->len = MEM_LEN_ALL - MEM_LEN_STRUCT - MEM_LEN_STRUCT;
MEM_NODE->status = FREE;
MEM_NODE->plast = MEM_HEAD;
MEM_NODE->pnext = MEM_HEAD;
}
这里有个很重要的点就是4字节对齐,因为内存分配链表是4字节对其的,当把某个地址强制转化为内存分配结构体指针时,如果地址不能被4整除,就会出现错误。
void* MEM_Alloc(u32 memlen) // 内存分配
{
TS_MEM_NODE* mem_node = MEM_HEAD; // 这个的功能主要是 用链表 寻找可用的 块
TS_MEM_NODE* mem_new = NULL; // 新内存块的地址
TS_MEM_NODE* mem_free = NULL;
u8* mem8free = NULL;
u32 lenfree=0;
while (1)
{
while (mem_node->status == USED)
{
if (mem_node->pnext == MEM_HEAD) // 如果最后一个节点都被使用中
return NULL; // 没有申请空间了
mem_node = mem_node->pnext; //
}
// 能运行到这里,肯定是有空闲块存在
if (mem_node->len < memlen) // 如果该内存块长度不够
{
if (mem_node->pnext == MEM_HEAD) // 如果已经是最后一块了
return NULL; // 申请空间太大
mem_node = mem_node->pnext; // 继续找下一块
continue;
}
lenfree = mem_node->len; // 当前块空闲长度 以字节为单位
mem_new = mem_node; // 新节点
// 这里涉及到一个 字节对齐 4字节
if (memlen%4 != 0)
{
memlen = memlen + 4 - memlen%4;
}
mem8free = ((u8*)mem_new) + MEM_LEN_STRUCT + memlen; // 以字节单位移动节点
mem_free = (TS_MEM_NODE*)mem8free;
mem_free->pnext = mem_new->pnext;
mem_free->plast = mem_new;
mem_new->pnext->plast = mem_free;
mem_new->pnext = mem_free;
mem_new->len = memlen;
mem_new->status = USED;
mem_free->len = lenfree - memlen - MEM_LEN_STRUCT;
mem_free->status = FREE;
return (mem_new+1);
}
}
u32 MEM_Free(void* memnode)
{
TS_MEM_NODE* mem_node = MEM_HEAD; // 这个的功能主要是 用链表 寻找可用的 块
TS_MEM_NODE* mem_del = ((TS_MEM_NODE*)memnode) - 1;
while (mem_node != mem_del)
{
if (mem_node->pnext == MEM_HEAD)
return 1; // 不存在
mem_node = mem_node->pnext;
}
mem_node->status = FREE;
// free节点
if (mem_node->pnext != MEM_HEAD)
{
// 下个节点是否空闲
if (mem_node->pnext->status == FREE)
{
mem_node->len += (mem_node->pnext->len + MEM_LEN_STRUCT);
mem_node->status = FREE;
mem_node->pnext = mem_node->pnext->pnext;
mem_node->pnext->plast = mem_node;
}
}
// 上个节点是否空闲
if (mem_node->plast != MEM_HEAD)
{
if (mem_node->plast->status == FREE)
{
mem_node->plast->len += (mem_node->len + MEM_LEN_STRUCT);
mem_node->plast->pnext = mem_node->pnext;
mem_node->pnext->plast = mem_node->plast;
}
}
mem_del = NULL;
return 0; // 成功
代码我放百度网盘
链接:https://pan.baidu.com/s/1_AH-fKPmc3Q5jtlw7bJpeg 提取码:f68x
还有信号量,等组件还没有开始写,实际上这些信号量等组件和延时一样,都是在中断中判断下个任务,所以有能力的你们肯定自己也能写好。
下一期会讲如何用ESP8266和STM32搭建家庭宽带MQTT服务器,实现低成本智能家居环境。
欢迎各路大神指点!