FreeRTOS笔记(二)

最新更新时间:2022-05-28来源: eefocus关键字:FreeRTOS  STM32 手机看文章 扫描二维码
随时随地手机看文章

一、任务定义

在多任务系统中,我们根据功能的不同,把整个系统分割 成一个个独立的且无法返回的函数,这个函数我们称为任务。


二、任务创建

2.1 定义任务栈

写一个 RTOS,对于全局变量与局部变量这些种种环境参数,我们必须弄清楚他们是如何存储的。


在裸机系统 中,他们统统放在一个叫栈的地方,栈是单片机 RAM 里面一段连续的内存空间,栈的大小一般在启动文件或者链接脚本里面指定,最后由C库函数_main进行初始化。


在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于RAM中。


多任务系统中,有多少个任务就需要定义多少个任务栈。


示例 1 定义任务栈


/* 

 #define portSTACK_TYPE uint32_t

 typedef portSTACK_TYPE StackType_t;

*/

#define TASK1_STACK_SIZE 128 

StackType_t Task1Stack[TASK1_STACK_SIZE];

#define TASK2_STACK_SIZE 128

StackType_t Task2Stack[TASK2_STACK_SIZE];


2.2 定义任务函数

任务是一个独立的函数,函数主体无限循环且不能返回。


示例 2 任务函数定义


/* 软件延时 */

void delay (uint32_t count)

{

for(; count!=0; count--);

}

/* 任务1 */

void Task1_Entry( void *p_arg )

{

for( ;; )

{

flag1 = 1;

delay( 100 );

flag1 = 0;

delay( 100 );


}

}


/* 任务2 */

void Task2_Entry( void *p_arg )

{

for( ;; )

{

flag2 = 1;

delay( 100 );

flag2 = 0;

delay( 100 );

}

}


2.3 定义任务控制块

在裸机系统中,程序的主体是 CPU 按照顺序执行的。而在多任务系统中,任务的执行是由系统调度的。


系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块, 这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针, 任务名称,任务的形参等。


有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现。


示例 3 任务控制块数据类型声明


typedef struct tskTaskControlBlock

{

volatile StackType_t    *pxTopOfStack;    /* 栈顶 */


ListItem_t     xStateListItem;   /* 任务节点 一个内置在 TCB 控制块中的链表节点,通过 这个节点,可以将任务控制块挂接到各种链表中。这个节点就类似晾衣架的钩子,TCB 就是衣服。*/

    

    StackType_t             *pxStack;         /* 任务栈起始地址 */

                                          

char                    pcTaskName[ configMAX_TASK_NAME_LEN ]; /* 任务名称,字符串形式 */

} tskTCB;

typedef tskTCB TCB_t;


2.4 实现任务创建函数

示例 4 任务创建函数xTaskCreateStatic()函数


/*

//任务句柄void指针

typedef void * TaskHandle_t;


//TaskFunction_t类型的pxTaskCode为函数指针,指向任务函数的入口。任务永远不会返回(位于死循环内)。

typedef void (*TaskFunction_t)( void * );//参数为void指针类型并返回void类型。


//

#define portSTACK_TYPE uint32_t

typedef portSTACK_TYPE StackType_t;

*/


TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,           /* 任务入口 */

            const char * const pcName,           /* 任务名称,字符串形式 ,字符串的最大长度(包括字符串结束字符)由宏configMAX_TASK_NAME_LEN指定,该宏位于FreeRTOSConfig.h文件中。*/

            const uint32_t ulStackDepth,         /* 任务栈大小,单位为字,不是字节数,在16位宽度的堆栈下,usStackDepth定义为100,则实际使用200字节堆栈存储空间。1字=2字节=16位 */

            void * const pvParameters,           /* 任务形参,当任务创建时,作为一个参数传递给任务。*/

            StackType_t * const puxStackBuffer,  /* 任务栈起始地址 */

            TCB_t * const pxTaskBuffer )         /* 任务控制块指针 */

{

TCB_t *pxNewTCB;

TaskHandle_t xReturn;//定义一个任务句柄 xReturn,任务句柄用于指向任务的 TCB。

    /*任务控制块指针与任务栈起始地址均非空时,形参赋值*/

if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )

{

pxNewTCB = ( TCB_t * ) pxTaskBuffer; 

pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;


/* 创建新的任务 */

prvInitialiseNewTask( pxTaskCode,        /* 任务入口 */

                              pcName,            /* 任务名称,字符串形式 */

                              ulStackDepth,      /* 任务栈大小,单位为字 */ 

                              pvParameters,      /* 任务形参 */

                              &xReturn,          /* 任务句柄 */ 

                              pxNewTCB);         /* 任务栈起始地址 */      


}

else

{

xReturn = NULL;

}


/* 返回任务句柄,如果任务创建成功,此时xReturn应该指向任务控制块 */

    return xReturn;

}


示例 5 创建新任务prvInitialiseNewTask()函数


/*

typedef unsigned long UBaseType_t;

*/

static void prvInitialiseNewTask( TaskFunction_t pxTaskCode,              /* 任务入口 */

const char * const pcName,              /* 任务名称,字符串形式 */

const uint32_t ulStackDepth,            /* 任务栈大小,单位为字 */

void * const pvParameters,              /* 任务形参 */

TaskHandle_t * const pxCreatedTask,     /* 任务句柄 */

TCB_t *pxNewTCB )                       /* 任务控制块指针 */


{

StackType_t *pxTopOfStack;

UBaseType_t x;

/* 获取栈顶地址 */

pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );

/* 向下做8字节对齐 ,在 Cortex-M3(Cortex-M4 或 Cortex-M7)内核的单片机中,因为总线宽度是 32 位的,通常只要栈保持 4 字节对齐就行, 可这样为啥要 8字节?难道有哪些操作是 64位的?确实有,那就是浮点运算,所以要8字节对齐(但是目前都还没有涉及到浮点运算,只是为了后续兼容浮点运行的考虑)。如果栈顶指针是8字节对齐的,在进行向下8字节对齐的时候,指针不会移动,如果不是8字节对齐的,在做向下8字节对齐的时候,就会空出几个字节,不会使用,比如当 pxTopOfStack是 33,明显不能整除 8,进行向下 8字节对齐就是 32,那么就会空出一个字

节不使用。*/

pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );


/* 将任务的名字存储在TCB中 */

for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )

{

pxNewTCB->pcTaskName[ x ] = pcName[ x ];


if( pcName[ x ] == 0x00 )

{

break;

}

}

/* 任务名字的长度不能超过configMAX_TASK_NAME_LEN */

pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '';


    /* 初始化TCB中的xStateListItem节点,即初始化该节点所在的链 表为空,表示节点还没有插入任何链表。*/

    vListInitialiseItem( &( pxNewTCB->xStateListItem ) );

    /* 设置xStateListItem节点的拥有者 */

listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );

    

    

    /* 调用 pxPortInitialiseStack()函数初始化任务栈,并更新栈顶指针, 任务第一次运行的环境参数就存在任务栈中。 */

pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );   



/* 让任务句柄指向任务控制块 */

    if( ( void * ) pxCreatedTask != NULL )

{

*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;

}

}


示例 6 初始化任务栈pxPortInitialiseStack()函数


#define portINITIAL_XPSR         ( 0x01000000 )

#define portSTART_ADDRESS_MASK ( ( StackType_t ) 0xfffffffeUL )


/*

*************************************************************************

*                              任务栈初始化函数

*************************************************************************

*/


static void prvTaskExitError( void )

{

    /* 函数停止在这里 */

    for(;;);

}


StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )

{

    /* 异常发生时,CPU自动从栈中加载到CPU寄存器的内容,包括 8 个寄存器,分别为R0、R1、R2、R3、R12、R14、R15和 xPSR的位 24,且顺序不能变。 */

pxTopOfStack--;

*pxTopOfStack = portINITIAL_XPSR;                                     /* xPSR的bit24必须置1 */

pxTopOfStack--;

*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC,即任务入口函数 */

pxTopOfStack--;

*pxTopOfStack = ( StackType_t ) prvTaskExitError;                     /* LR,函数返回地址,通常任务是不会返回的,如果返回了就跳转到 prvTaskExitError,该函数是一个无限循环。*/

pxTopOfStack -= 5; /* R12, R3, R2 and R1 默认初始化为0 */

*pxTopOfStack = ( StackType_t ) pvParameters;                         /* R0,任务形参 */

    

    /* 异常发生时,手动加载到CPU寄存器的内容 */    

pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4默认初始化为0 */


/* 返回栈顶指针,此时pxTopOfStack指向空闲栈,此时 pxTopOfStack指向具体见图 1。任务第一次运行时,就是从这个栈指针开始手动加载 8 个字的内容到 CPU 寄存器:R4、R5、R6、R7、 R8、R9、R10和 R11,当退出异常时,栈中剩下的 8个字的内容会自动加载到 CPU寄存器: R0、R1、R2、R3、R12、R14、R15和 xPSR的位 24。此时 PC指针就指向了任务入口地址,从而成功跳转到第一个任务。*/

    return pxTopOfStack;

}


图 1 任务栈初始化完后栈空间分布图

image-20200726094055221

三、实现就绪列表

3.1 定义就绪列表

任务创建好之后,我们需要把任务添加到就绪列表里面,表示任务已经就绪,系统随时可以调度。


/* 任务就绪列表 */

List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

/* 就绪列表实际上就是一个 List_t 类型的数组,数组的大小由决定 最大任务优先级的宏 configMAX_PRIORITIES 决定, configMAX_PRIORITIES 在 FreeRTOSConfig.h 中默认定义为 5,最大支持 256 个优先级。数组的下标对应了任务的优 先级,同一优先级的任务统一插入到就绪列表的同一条链表中。一个空的就绪列表具体见图 2。 */


图 2 空的就绪列表

image-20200726094638369

3.2 就绪列表初始化

就绪列表在使用前需要先初始化,就绪列表初始化的工作在函数 prvInitialiseTaskLists() 里面实现。


示例 7 就绪列表初始化prvInitialiseTaskLists()函数


/* 初始化任务相关的列表(遍历初始化链表)*/

void prvInitialiseTaskLists( void )

{

    UBaseType_t uxPriority;

    

    for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )

{

vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );

}

}


**图 3 就绪列表初始化完毕之后示意图 **

image-20200726095643001

3.3 将任务插入到就绪列表

任务控制块里面有一个 xStateListItem 成员,数据类型为 ListItem_t,我们将任务插入 到就绪列表里面,就是通过将任务控制块的 xStateListItem 这个节点插入到就绪列表中来实现的。


如果把就绪列表比作是晾衣架,任务是衣服,那 xStateListItem 就是晾衣架上面的钩子,每个任务都自带晾衣架钩子,就是为了把自己挂在各种不同的链表中。


示例 8 将任务插入到就绪列表


/* 初始化与任务相关的列表,如就绪列表 */

    prvInitialiseTaskLists();

    

    /* 创建任务 */

    Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry,   /* 任务入口 */

                  (char *)"Task1",               /* 任务名称,字符串形式 */

                  (uint32_t)TASK1_STACK_SIZE ,   /* 任务栈大小,单位为字 */

                  (void *) NULL,                 /* 任务形参 */

                  (StackType_t *)Task1Stack,     /* 任务栈起始地址 */

[1] [2] [3]
关键字:FreeRTOS  STM32 编辑:什么鱼 引用地址:FreeRTOS笔记(二)

上一篇:STM32笔记(六)---GPIO中断EXTI
下一篇:FreeRTOS笔记(一)

推荐阅读

STM32入门系列-开发工具keil5安装
主要介绍如下三部分内容:keil5软件获取keil5安装安装STM32芯片包软件获取可以通过搜索引擎搜索关键字“KEIL5下载”,找到其官方网站http://www.keil.com。我们这里使用MDK5.14版本,如果后面出了更高的版本选择性升级即可,不过也没有必要什么都追求最新的,这一个软件用着习惯就行。登录armkeil官方网站,网站首页有软件下载连接,在这里我们选择MDK-Arm。会让你先注册,注册完成之后就可以开始下载了。软件安装安装此软件时一定要注意以下几点:安装路径不能带中文,必须是英文路径。安装目录不能跟51的KEIL或者KEIL4冲突,三者目录必须分开。KEIL5的安装比起KEIL4多了一个步骤,必须添加芯片包,不
发表于 2022-11-18
<font color='red'>STM32</font>入门系列-开发工具keil5安装
STM32入门系列-存储器与寄存器介绍
介绍两部分内容:什么是存储器映射什么是寄存器及寄存器映射为了让大家对存储器与寄存器有一个更清楚的认识,并且为之后使用 C 语言来访问 STM32 寄存器内容打下基础。等明白了如何使用 C 语言封装底层寄存器,也就为后面学习库函数的开发做好了铺垫。什么是存储器映射程序存储器、数据存储器、寄存器和I/O端口排列在同一顺序的4GB地址空间内。这就是我们曾提到过的被控总线的连接部分,而编程时就是操作这一块地方。存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程称为存储器映射,如果再分配一个地址就叫重映射。 STM32F103ZET6 数据手册中存储器映射图如下所示。从上图中可以看到 ARM 把这 4GB 的存储
发表于 2022-11-18
STM32入门系列-GPIO概念介绍
GPIO(general purpose intput output)是通用输入输出端口的简称,可以通过软件来控制其输入和输出。STM32 芯片的 GPIO 引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能。不过 GPIO 最简单的应用还属点亮 LED 灯了,只需通过软件控制 GPIO 输出高低电平即可。当然 GPIO 还可以作为输入控制,比如在引脚上接入一个按键,通过电平的高低判断按键是否按下。其中STM32F103xC、STM32F103xD和STM32F103xE有144个引脚的芯片如下图所示。那么是不是所有引脚都是 GPIO 呢?当然不是,STM32 引脚可以分为这么几大类:电源引脚:引脚图中的 VDD、V
发表于 2022-11-18
STM32入门系列-GPIO结构
已经了解了STM32 GPIO的基本概念及引脚分类。现在来看下STM32 GPIO内部的结构是怎样的。IO端口位的基本结构如下图所示。从图中可以看出GPIO内部结构还是比较复杂的,只要将这张GPIO结构图理解好,那么关于GPIO的各种应用模式将非常清楚。图中最右端I/O端口就是STM32芯片的引脚,其它部分都在STM32芯片内部。上图中我们将每部分都用红色数字标号了,按照顺序我们逐一讲解。保护二极管引脚内部加上这两个保护二级管可以防止引脚外部过高或过低的电压输入,当引脚电压高于VDD_FT或VDD时,上方的二极管导通吸收这个高电压,当引脚电压低于VSS时,下方的二极管导通,防止不正常电压引入芯片导致芯片烧毁。尽管STM32芯片内部有
发表于 2022-11-18
<font color='red'>STM32</font>入门系列-GPIO结构
STM32入门系列-创建寄存器模板
介绍如何使用 KEIL5 软件创建寄存器模板, 方便之后使用寄存器方式来操作STM32开发板上的LED,让大家创建属于自己的寄存器工程模板。获取工程模板的基础文件首先我们在电脑任意位置创建一个文件夹,命名为“寄存器模板创建”,然后在其下面新建 2 个文件夹,如下:Obj 文件夹: 用于存放编译产生的 c/汇编/链接的列表清单、 调试信息、hex文件、预览信息、封装库等文件。User 文件夹: 用于存放用户编写的 main.c、 STM32F1 启动文件、stm32f10x.h头文件。我们需要将寄存器工程模板所需的基础文件 main.c、STM32F1 启动文件及stm32f10x.h 头文件放入到 User 文件夹内。在工程实例的U
发表于 2022-11-18
<font color='red'>STM32</font>入门系列-创建寄存器模板
STM32入门系列-使用C语言封装寄存器
前面介绍了存储器映射、寄存器和寄存器映射,这些都是为了介绍使用 C语言封装寄存器做铺垫。这里我们通过一个实例来对 C 语言封装寄存器进行介绍。具体实例:控制 GPIOC 端口的第 0 管脚输出一个低电平。首先我们需要知道GPIOC 端口外设是挂接在哪个总线上的,然后根据总线基地址和本身的偏移地址得到 GPIOC 外设基地址,最后通过这个外设基地址得到里面各种寄存器基地址。总线和外设基地址封装根据寄存器的概念,我们可以使用 C 语言中的宏定义对寄存器进行定义。具体代码如下://定义外设基地址#define PERIPH_BASE ((unsigned int)0x40000000) 1)//定义 APB2 总线基地址#define A
发表于 2022-11-18

推荐帖子

MC9S12DJ256串口接收多个字节出错,帮忙分析下
MC9S12DJ256,串口跟PC机通信,当PC机发一个字节的时候,是对的,但是当发多个字节的时候,单片机就会出错,有时是接收的字节少与发的字节,有时是字节内容接收错误。但单片机向PC机上报多个字节是对的。如果将单片机的两个串口互联,对发,通信时也会出现接收的字节少于发的字节。请各位大侠指点下!不甚感激!MC9S12DJ256串口接收多个字节出错,帮忙分析下
lihao_123 NXP MCU
nios2 教程
有资料大家多多分享啊nios2教程
wall_e FPGA/CPLD
msp430的com怎连????
msp430f437有4个lcd的com引脚,可不知怎连。 请各位高手帮忙,多谢。msp430的com怎连????
BITCH 微控制器 MCU
串口这是怎么回事???
接上arm板,可以接收到armlinux启动的信息,但是arm却无法接受键盘的信息!再其他电脑测试下,arm正常 就是串口无法接受信息 这是什么问题??会是串口烧掉了么?烧掉了为什么还可以发送信息?? 需要换串口芯片??串口这是怎么回事???
jackedison 嵌入式系统
TI 专家: LM5017 的反相升降压电路支持负电源
如图1a和1b所示,只需对降压转换器原理图进行简单修改,便可将同步降压转换器IC用于反相升降压配置。反相升降压转换器可生成负极输出电压,计算公式如下:VOUT=-D/(1-D)xVIN 反相升降压转换器的工作情况如图2a和2b所示。在TON(Q1:导通;Q2:关闭)期间,电感器储存能量;而在TOFF(Q1:关闭;Q2:导通)期间,电感器为输出电容器充电。 反相升降压配置下降压IC的最大VIN及IOUT当在反相配置中使用降压稳压器IC时,
qwqwqw2088 模拟与混合信号
小白刚刚学习FPGA,打算用FPGA产生ofdm信号,还请有了解这方面研究的前辈能指导指导我
FPGA用altera公司的cycloneⅤ 1.我想知道ofdm是不是只是简单地将需要处理的数据进行快速傅里叶变换然后输出? 2.如果不是,那在做快速傅里叶变换之前需要做什么处理? 3.quarterⅱ可以直接用fft的IP核,调用设置参数后,接下来该怎么做? 4.fft的IP核处理的数据是从哪里来的?是它本身随机产生的吗?那输出又从哪管脚输出呢?这个需要自己配置吗? 可能有些问题在您看来比较白痴,但是我真的对这方面一无所知,之前也没有这方面的基础,还请各位前辈耐心解答,多多指教!万
夏漠简 EE_FPGA学习乐园
小广播
设计资源 培训 开发板 精华推荐

何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2022 EEWORLD.com.cn, Inc. All rights reserved