自己写基于Cortex-M0的RTOS

个人原创,请勿抄袭。

转载请注明出处,谢谢!

个人邮箱 737355415@qq.com

------------------------------------------------------华丽的分割线-------------------------------------------------------- 

好久没上CSDN了,今天给大家带来点干货。

 

目录

1、RTOS最底层的就是PendSV中断切换函数

2、第二重要的当然是任务链表,任务链表分普通任务链表,延时任务链表,就绪任务链表和运行任务链表,还有挂起链表,我没有加

3、内存分配

 

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服务器,实现低成本智能家居环境。

 

欢迎各路大神指点!

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值