FreeRTOS学习记录

前言:参考野火出的《FreeROTS内核实现与应用开发实战——基于STM32》

1. 移植系统

首先,在野火的stm32教学例程中,选择了一个固件库点亮LED灯的裸机工程模板,以他为基准添加FreeRTOS内核代码移植系统。根据教程推荐,在网站下载V9.0版本源码。

1. 文件夹内容概述

1. FreeRTOS

FreeRTOS 包含 Demo 例程和内核源码(比较重要,我们就需要提取该目录下的大部分文件)。FreeRTOS 文件夹下的 Source 文件夹里面包含的是 FreeRTOS 内核的源代码,我们移植 FreeRTOS 的时候就需要这部分源代码; FreeRTOS 文件夹下的Demo 文件夹里面包含了 FreeRTOS 官方为各个单片机移植好的工程代码,这部分 Demo 非常有参考价值。我们把 FreeRTOS 到 STM32 的时候,FreeRTOSConfig.h 这个头文件就是从这里拷贝过来的。
在这里插入图片描述

1. Source

include中包含的是FreeRTOS中通用的头文件,加上零散的.c文件,这两部分适用于各种编译器和处理器,是通用的。而需要移植的是portble文件夹中的文件。
在这里插入图片描述

1. portble

portble文件夹中包含许多与编译器相关的文件夹,这里使用keil做为环境,打开会发现和RVDS一样,所以只需要这个文件夹里的内容即可。另外,还有MemMang文件夹中存放的是与内存管理相关的内容。
在这里插入图片描述

1. RVDS

RVDS文件夹中包含了与各种处理器相关的文件夹,包括STM32的M0,M3,M4等各种系列。把FreeRTOS当成一个软件,单片机当成硬件,要想在上面运行则需要有相互关联的接口文件,这通常由汇编和C语言联合编写。这些接口文件是跟硬件密切相关的,不同的硬件接口文件也不同,而编写这些接口文件的过程我们就叫做移植,这一步通常由FreeRTOS和mcu原厂的人来负责,编写好的接口文件就放在RVDS这个文件夹下。
在这里插入图片描述
如图可见,cortex-m0(3.4.7)等内核的单片机的接口文件已经写好了,如果我们要用这些内核的单片机来运行FreeRTOS,那只能叫做使用官方的移植。以ARM_CM3文件夹为例,里面包含port.c和portmacro.h两个文件。前者是由FreeRTOS官方的技术人员为Cortex-M3内核的处理器写的接口文件,里面核心的上下文切换代码是由汇编语言编写;后者则是前者对应的头文件,主要是一些数据类型和宏定义。
在这里插入图片描述

2. MemMang

存放与内存管理相关的文件,总共有5个heap文件,在移植时必须选择一个使用。

2. 向裸机工程添加源码

1. 首先在裸机工程根目录下新建一个文件夹,命名为“FreeRTOS”,并在其中新建两个文件夹“src”和“port”,src文件夹用于保存FreeRTOS中的核心源文件(.c文件),port用于保存内存管理以及处理器架构相关代码(接口文件)(位于Portable文件夹中)。

2. 将Source文件夹中所有.c文件复制到我们新建的src文件夹中。

3. 将portable文件夹中的MemMang和RVDS两个文件夹复制到新建的port文件夹中。

4. 将Source文件夹下的include文件夹复制到我们自建的FreeRTOS文件夹下。

至此,工程根目录下自建的FreeRTOS文件夹已经包含了FreeRTOS实时操作系统的核心代码。另外,我们需要将Demo文件夹中的CORTEX_STM32F103_Keil中的FreeRTOSConfig.h文件复制到工程中的user(用户自己编写的代码文件)文件夹中。余下的工作就是在keil中打开工程文件,新建相关文件夹,添加相应文件,并包含文件路径。

3. 修改FreeRTOSConfig.h

这里使用的FreeRTOSConfig.h是野火修改过的,非源码中的源文件。这个文件对FreeRTOS所需功能的宏均做了定义,在这个文件中可以配置相关功能使能。具体宏定义的意义可以参照教程,这里暂时使用默认定义。但要包含两个头文件并添加两个宏定义。
在这里插入图片描述
在这里插入图片描述

3. 修改 stm32f10x_it.c

FreeRTOS 帮我们实现了 SysTick 的启动的配置:在 port.c 文件中已经实现 vPortSetupTimerInterrupt()函数,并且FreeRTOS 通用的 SysTick 中断服务函数也实现了:在 port.c 文件中已经实现 xPortSysTickHandler()函数,所以移植的时候只需要我们在 stm32f10x_it.c 文件中实现我们对应(STM32)平台上的 SysTick_Handler()函数即可。同时,PendSV_Handler()与 SVC_Handler()这两个很重要的函数都帮我们实现了, 在 port.c 文件中已经实现 xPortPendSVHandler()与 vPortSVCHandler()
函 数 , 防 止 我 们 自 己 实 现 不 了 , (PendSV 中断服务函数是真正实现任务切换的地方)(vPortSVCHandler()函数开始真正启动第一个任务)那 么 在 stm32f10x_it.c 中 就 需 要 我 们 注 释 掉PendSV_Handler()与 SVC_Handler()这两个函数了。
在这里插入图片描述

2. 创建任务

1. 静态创建

在硬件初始化(硬件初始化函数,有一个硬件加一个硬件)的基础上,对于创建任务我们需要有一个明确的思路。首先,我这个任务要干什么(定义任务函数)。在 FreeRTOS 系统中,每一个任务都是独立的,他们的运行环境都单独的保存在他们
的栈空间当中。那么在定义好任务函数之后,我们还要为任务定义一个栈,目前我们使用的是静态内存,所以任务栈是一个独立的全局变量。定义好任务函数和任务栈之后,我们还需要为任务定义一个任务控制块,通常我们称这个任务控制块为任务的身份证。在 C 代码上,任务控制块就是一个结构体,里面有非常多的成员,这些成员共同描述了任务的全部信息。至此,有了一个任务的三要素(任务主体函数、任务栈、任务控制块),需要用静态任务创建函数(xTaskCreateStatic())将三者相互联系在一起。
整个创建任务的思路就是这样,但具体写代码时还有许多需要增添的地方。首先我们要创建任务句柄,且在实际代码中,我们是通过一个创建任务任务(AppTaskCreate(void))来创建其他任务。然后定义任务堆栈,当我们使用了静态创建任务的时候,需 要 实 现两 个 函数 :vApplicationGetIdleTaskMemory()vApplicationGetTimerTaskMemory(),这两个函数是用
户设定的空闲(Idle)任务与定时器(Timer)任务的堆栈大小,必须由用户自己分配。
具体main.c代码如下;


	
#include "FreeRTOS.h"
#include "task.h"
#include "bsp_led.h"
#include "bsp_usart.h"

//创建任务句柄
 /* 创建任务句柄 */
 static TaskHandle_t AppTaskCreate_Handle;
 /* LED 任务句柄 */
 static TaskHandle_t LED_Task_Handle;

static StackType_t Idle_Task_Stack[configMINIMAL_STACK_SIZE];
static StackType_t Timer_Task_Stack[configTIMER_TASK_STACK_DEPTH];
static StaticTask_t Idle_Task_TCB;
static StaticTask_t Timer_Task_TCB;
 /* AppTaskCreate 任务任务堆栈 */
 static StackType_t AppTaskCreate_Stack[128];

 /* LED 任务堆栈 */
 static StackType_t LED_Task_Stack[128];
/* AppTaskCreate 任务控制块 */
 static StaticTask_t AppTaskCreate_TCB;
 /* AppTaskCreate 任务控制块 */
 static StaticTask_t LED_Task_TCB;


static void BSP_Init(void);
static void LED_Task(void);
static void AppTaskCreate(void);/* 用于创建任务 */
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
                                    StackType_t **ppxIdleTaskStackBuffer,
                                    uint32_t *pulIdleTaskStackSize);
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
                                       StackType_t **ppxTimerTaskStackBuffer,
                                       uint32_t *pulTimerTaskStackSize);


/**
  * @brief  主函数
  * @param  无  
  * @retval 无
  */
int main(void)
{	
    BSP_Init();
    printf("这是一个[野火]-STM32 全系列开发板-FreeRTOS-静态创建任务!\r\n");
   /* 创建 AppTaskCreate 任务 */
   AppTaskCreate_Handle = xTaskCreateStatic((TaskFunction_t )AppTaskCreate,
                                             (const char* )"AppTaskCreate",//任务名称
                                             (uint32_t )128, //任务堆栈大小
                                             (void* )NULL,//传递给任务函数的参数
                                             (UBaseType_t )3, //任务优先级
                                             (StackType_t* )AppTaskCreate_Stack,
                                             (StaticTask_t* )&AppTaskCreate_TCB);

   if (NULL != AppTaskCreate_Handle) /* 创建成功 */
   vTaskStartScheduler(); /* 启动任务,开启调度 */

   while (1); /* 正常不会执行到这里 */
}



//专门用于存放外设初始化函数
static void BSP_Init(void)
{
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
  
  LED_GPIO_Config();
  
  //LED1_ON;
  
  USART_Config();
  
  //while(1);
  
  
}

static void LED_Task()
{
  while(1)
  {
    LED1_ON;
    vTaskDelay(500);
    
    LED1_OFF;
    vTaskDelay(500);
  }
}

void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
                                    StackType_t **ppxIdleTaskStackBuffer,
                                    uint32_t *pulIdleTaskStackSize)
{
  *ppxIdleTaskStackBuffer=Idle_Task_Stack;
  *ppxIdleTaskTCBBuffer=&Idle_Task_TCB;
  *pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}

void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
                                       StackType_t **ppxTimerTaskStackBuffer,
                                       uint32_t *pulTimerTaskStackSize)
 {
   *ppxTimerTaskTCBBuffer=&Timer_Task_TCB;/* 任务控制块内存 */
   *ppxTimerTaskStackBuffer=Timer_Task_Stack;/* 任务堆栈内存 */
   *pulTimerTaskStackSize=configTIMER_TASK_STACK_DEPTH;/* 任务堆栈大小 */
 }

 static void AppTaskCreate(void)
 {
   taskENTER_CRITICAL(); //进入临界区
    LED_Task_Handle = xTaskCreateStatic((TaskFunction_t )LED_Task, //任务函数
                                         (const char*)"LED_Task",//任务名称
                                         (uint32_t)128, //任务堆栈大小
                                         (void* )NULL, //传递给任务函数的参数
                                        (UBaseType_t)4, //任务优先级
                                         (StackType_t*)LED_Task_Stack,//任务堆栈
                                         (StaticTask_t*)&LED_Task_TCB);//任务控制块
    
    if (NULL != LED_Task_Handle) /* 创建成功 */
     printf("LED_Task 任务创建成功!\n");
    else
      printf("LED_Task 任务创建失败!\n");
    vTaskDelete(AppTaskCreate_Handle); //删除 AppTaskCreate 任务
    taskEXIT_CRITICAL(); //退出临界区
 
 }
 
/*********************************************END OF FILE**********************/

2. 动态创建

在创建单任务—SRAM 静态内存的例程中,任务控制块和任务栈的内存空间都是从内部的 SRAM 里面分配的,具体分配到哪个地址由编译器决定。现在我们开始使用动态内存,即堆,其实堆也是内存,也属于 SRAM。 FreeRTOS 做法是在SRAM 里面定义一个大数组,也就是堆内存,供 FreeRTOS 的动态内存分配函数使用,在第一次使用的时候,系统会将定义的堆内存进行初始化,这些代码在 FreeRTOS 提供的内存管理方案中实现。
在进行动态创建时,同样要先定义任务函数。而任务栈在任务创建的时候创建,不用跟使用静态内存那样要预先定义好一个全局的静态的栈空间,动态内存就是按需分配内存,随用随取。同时,任务控制块也是在任务创建(xTaskCreate())的时候分配内存空间创建,任务创建函数会返回一个指针,用于指向任务控制块,所以要预先为任务栈定义一个任务控制块指针,也是我们常说的任务句柄。值得注意的是,在动态创建中,当任务创建好后,是处于任务就绪(Ready) ,在就绪态的任务可以参与操作系统的调度。但是此时任务仅仅是创建了,还未开启任务调度器,也没创建空闲任务与定时器任务(如果使能了 configUSE_TIMERS 这个宏定义),那这两个任务就是在启动任务调度器中实现,每个操作系统,任务调度器只启动一次,之后就不会再次执行了, FreeRTOS 中启动任务调度器的函数是 vTaskStartScheduler(),并且启动任务调度器的时候就不会返回,从此任务管理都由FreeRTOS管理,此时才是真正进入实时操作系统中的第一步。


/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
/* 开发板硬件bsp头文件 */
#include "bsp_led.h"
#include "bsp_usart.h"


 /* 创建任务句柄 */
static TaskHandle_t AppTaskCreate_Handle = NULL;
/* LED任务句柄 */
static TaskHandle_t LED_Task_Handle = NULL;

static void AppTaskCreate(void);/* 用于创建任务 */

static void LED_Task(void* pvParameters);/* LED_Task任务实现 */

static void BSP_Init(void);/* 用于初始化板载相关资源 */

int main(void)
{	
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */

  /* 开发板硬件初始化 */
  BSP_Init();
  printf("这是一个[野火]-STM32全系列开发板-FreeRTOS-动态创建任务!\r\n");
   /* 创建AppTaskCreate任务 */
  xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                        (const char*    )"AppTaskCreate",/* 任务名字 */
                        (uint16_t       )512,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )1, /* 任务的优先级 */
                        (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
  /* 启动任务调度 */           
  if(pdPASS == xReturn)
    vTaskStartScheduler();   /* 启动任务,开启调度 */
  else
    return -1;  
  
  while(1);   /* 正常不会执行到这里 */    
}

static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  taskENTER_CRITICAL();           //进入临界区
  
  /* 创建LED_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
                        (const char*    )"LED_Task",/* 任务名字 */
                        (uint16_t       )512,   /* 任务栈大小 */
                        (void*          )NULL,	/* 任务入口函数参数 */
                        (UBaseType_t    )2,	    /* 任务的优先级 */
                        (TaskHandle_t*  )&LED_Task_Handle);/* 任务控制块指针 */
  if(pdPASS == xReturn)
    printf("创建LED_Task任务成功!\r\n");
  
  vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
  
  taskEXIT_CRITICAL();            //退出临界区
}

static void LED_Task(void* parameter)
{	
    while (1)
    {
        LED1_ON;
        vTaskDelay(500);   /* 延时500个tick */
        printf("LED_Task Running,LED1_ON\r\n");
        
        LED1_OFF;     
        vTaskDelay(500);   /* 延时500个tick */		 		
        printf("LED_Task Running,LED1_OFF\r\n");
    }
}

static void BSP_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();

	/* 串口初始化	*/
	USART_Config();
  
}


3. 任务管理

1. 概述

从系统的角度看,任务时竞争系统资源的最小运行单元。在FreeRTOS中,任务可以使用或等待CPU、使用内存空间等系统资源,并独立于其他任务运行,任何数量的任务可以共享同一个优先级,如果宏configUSE_TIME_SLICING定义为1,处于就绪态的多个相同优先级任务将会以时间片切换的方式共享处理器。在任务切入切出时保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责。为了实现这点,每个 FreeRTOS 任务都需要有自己的栈空间。当任务切出时,它的执行环境会被保存在该任务的栈空间中,这样当任务再次运行时,就能从堆栈中正确的恢复上次的运行环境,任务越多,需要的堆栈空间就越大,而一个系统能运行多少个任务,取决于系统的可用的 SRAM。
FreeRTOS 中的任务是抢占式调度机制,高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度。若使能configUSE_PORT_OPTIMISED_TASK_SELECTION ,则最大可用优先级数目为32。查找最高优先级任务的过程决定了调度时
间是否具有实时性。FreeRTOS 内核中采用两种方法寻找最高优先级的任务,第一种是通用的方法,在就绪链表中查找从高优先级往低查找 uxTopPriority,因为在创建任务的时候已经将优先级进行排序,查找到的第一个uxTopPriority 就是我们需要的任务,然后通过 uxTopPriority 获取对应的任务控制块。第二种方法则是特殊方法,利用计算前导零指令 CLZ,直接在
uxTopReadyPriority 这个 32 位的变量中直接得出uxTopPriority,这样子就知道哪一个优先级任务能够运行,这种调度算法比普通方法更快捷,但受限于平台(在 STM32 中我们就使用这种方法)。
FreeRTOS 内核中也允许创建相同优先级的任务。相同优先级的任务采用时间片轮转方式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级就绪任务存在的情况下才有效。
在这里插入图片描述
创建任务→就绪态(Ready):任务创建完成后进入就绪态,表明任务已准备就绪,随时可以运行,只等待调度器进行调度;
就绪态→运行态(Running):发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态;
运行态→就绪态:有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪列表中最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,依然在就绪列表中,等待最高优先级的任务运行完毕继续运行原来的任务(此处可以看做是 CPU 使用权被更高优先级的任务抢占了);
运行态→阻塞态(Blocked):正在运行的任务发生阻塞(挂起、延时、读信号量等待)时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务;
阻塞态→就绪态:阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务换,将该任务将再次转换任务状态,由就绪态变成运行态;
就绪态、阻塞态、运行态→挂起态(Suspended):任务可以通过调用 vTaskSuspend() API 函数都可以将处于任何状态的任务挂起,被挂起的任务得不到CPU 的使用权,也不会参与调度,除非它从挂起态中解除;
挂起态→就绪态: 把 一 个 挂 起 状态 的 任 务 恢复的 唯 一 途 径 就 是
调 用 vTaskResume()vTaskResumeFromISR() API 函数,如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
FreeRTOS 中程序运行的上下文包括:中断服务函数、普通任务和空闲任务。中断服务函数:中断服务程序最好保持精简短小,快进快出,一般在中断服务函数中只做标记事件的发生,然后通知任务,让对应任务去执行相关处理,因为中断服务函数的优先级高于任何优先级的任务,如果中断处理时间过长,将会导致整个系统的任务无法正常运行。所以在设计的时候必须考虑中断的频率、中断的处理时间等重要因素,以便配合对应中断处理任务的工作;普通任务:不能出现死循环,否则使优先级更低的任务无法抢占cpu;空闲任务:当调用vTaskStartScheduler()时, 调度器会自动创建一个空闲任务,它一个非常短小的循环,空闲任务是唯一一个不允许出现阻塞情况的任务,因为 FreeRTOS 需要保证系统永远都有一个可运行的任务。

2. 常用任务函数

任务挂起函数:vTaskSuspend() vTaskSuspendAll()
任务恢复函数:vTaskResume() xTaskResumeFromISR()(与前者不同的是这个函数专门用于在中断服务程序中,使用时需把INCLUDE_vTaskSuspendINCLUDE_vTaskResumeFromISR置一) xTaskResumeAll()
任务删除函数:vTaskDelete()(使用时需将INCLUDE_vTaskDelete定义为1)
任务延时函数:vTaskDelay()(属于相对延时,延时时间是从调用 vTaskDelay()结束后开始计算的, 经过指定的时间后延时结束,其它任务和中断活动, 也会影响到 vTaskDelay()的调用(比如调用前高优先级任务抢占了当前任务)使用时需将INCLUDE_vTaskDelay置1)vTaskDelayUntil()(绝对延时函数,常用于较精确的周期运行任务,使用时需将INCLUDE_vTaskDelayUntil置1)

3. 任务挂起实验

LED 在闪烁,按下开发版的KEY1 按键挂起任务,按下 KEY2 按键恢复任务;


	
#include "FreeRTOS.h"
#include "task.h"
#include "bsp_led.h"
#include "bsp_usart.h"
#include "bsp_key.h"

//创建任务句柄
 /* 创建任务句柄 */
 static TaskHandle_t AppTaskCreate_Handle=NULL;
 /* LED 任务句柄 */
 static TaskHandle_t LED1_Task_Handle=NULL;

 static TaskHandle_t KEY_Task_Handle=NULL;

static void BSP_Init(void);
static void LED1_Task(void);
static void AppTaskCreate(void);/* 用于创建任务 */

static void KEY_Task(void);


int main(void)
{	
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */  
  BSP_Init();
    printf("这是一个[野火]-STM32 全系列开发板-FreeRTOS-动态创建任务!\r\n");
   /* 创建 AppTaskCreate 任务 */
   xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,
                                             (const char* )"AppTaskCreate",//任务名称
                                             (uint16_t )512, //任务堆栈大小
                                             (void* )NULL,//传递给任务函数的参数
                                             (UBaseType_t )1, //任务优先级
                                             (TaskHandle_t* )&AppTaskCreate_Handle);

   if (pdPASS == xReturn) /* 创建成功 */
   vTaskStartScheduler(); /* 启动任务,开启调度 */
   else
     return -1;

   while (1); /* 正常不会执行到这里 */
}

//专门用于存放外设初始化函数
static void BSP_Init(void)
{
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
  
  LED_GPIO_Config();
  
  //LED1_ON;
  
  USART_Config();
  Key_GPIO_Config();
  
  //while(1);
  
  
}

static void LED1_Task()
{
  while(1)
  {
    LED1_ON;
    vTaskDelay(500);
    printf("led1_task running,LED1_ON\r\n");
    
    LED1_OFF;
    vTaskDelay(500);
    printf("led1_task running,LED1_OFF\r\n");
  }
}



static void KEY_Task()
{
  while(1)
  {
    if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
      printf("挂起 LED 任务! \n");
      vTaskSuspend(LED1_Task_Handle);/* 挂起 LED 任务 */
    }
    if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
      printf("恢复 LED 任务! \n");
      vTaskResume(LED1_Task_Handle);/* 恢复 LED 任务! */
    }
    vTaskDelay(20);/* 延时 20 个 tick */
  }
}

 static void AppTaskCreate(void)
 {
   BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
   taskENTER_CRITICAL(); //进入临界区
    
    
    xReturn = xTaskCreate((TaskFunction_t )LED1_Task, //任务函数
                                         (const char*)"LED1_Task",//任务名称
                                         (uint16_t)512, //任务堆栈大小
                                         (void* )NULL, //传递给任务函数的参数
                                        (UBaseType_t)2, //任务优先级
                                        (TaskHandle_t* )&LED1_Task_Handle);//任务控制块
    
    if (pdPASS == xReturn) /* 创建成功 */
     printf("创建 LED1_Task 任务成功!\r\n");
    
    

    
       xReturn = xTaskCreate((TaskFunction_t )KEY_Task, //任务函数
                                         (const char*)"KEY_Task",//任务名称
                                         (uint16_t)512, //任务堆栈大小
                                         (void* )NULL, //传递给任务函数的参数
                                        (UBaseType_t)3, //任务优先级
                                        (TaskHandle_t* )&KEY_Task_Handle);//任务控制块
    
    if (pdPASS == xReturn) /* 创建成功 */
     printf("创建 KEY_Task 任务成功!\r\n");

    vTaskDelete(AppTaskCreate_Handle); //删除 AppTaskCreate 任务
    taskEXIT_CRITICAL(); //退出临界区
 
 }

4. 消息队列

1. 概述

消息队列(队列)是一种用于任务间通信的数据结构,任务可以从队列里读取消息。若队列为空,读取消息的任务将被阻塞(可以指定阻塞时间)。先进先出原则(FIFO),支持异步读写,读写队列支持超时机制,可以允许不同长度(不超过队列节点最大值)的任意类型消息,一个任务能从任意一个消息队列接收和发送消息,可通过删除函数删除队列。
任务或者中断服务程序都可以给消息队列发送消息, 当发送消息时, 如果队列未满或者允许覆盖入队, FreeRTOS 会将消息拷贝到消息队列队尾,否则,会根据用户指定的阻塞超时时间进行阻塞,在这段时间中,如果队列一直不允许入队,该任务将保持阻塞状态以等待队列允许入队。当其它任务从其等待的队列中读取入了数据(队列未满),该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中还不允许入队,任务也会自动从阻塞态转移为就绪态,此时发送消息的任务或者中断程序会收到一个错误码 errQUEUE_FULL。当发送紧急消息时,会放在队列头。读取类似。
当消息队列被创建时,系统会为控制块分配对应的内存空间,用于保存消息队列的一些信息如消息的存储位置,头指针pcHead、尾指针 pcTail、消息大小 uxItemSize 以及队列长度 uxLength, 以及当前队列消息个数uxMessagesWaiting 等。

2. 消息队列函数

消息队列创建函数:xQueueCreate()用于创建一个新的队列并返回可用于访问这个队列的队列句柄。每创建一个新的队列都需要为其分配 RAM,一部分用于存储队列的状态, 剩下的作为队列消息的存储区域。
队列删除函数:vQueueDelete()
向消息队列发送消息函数:xQueueSend()等同于xQueueSendToBack() xQueueSendToFront()(向队列首发送)
从消息队列读取消息函数:xQueueReceive()(用于从一个队列中接收消息并把消息从队列中删除)xQueuePeek()(不从队列删除消息)

3. 消息队列实验


	
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "bsp_led.h"
#include "bsp_usart.h"
#include "bsp_key.h"

//创建任务句柄
 /* 创建任务句柄 */
 static TaskHandle_t AppTaskCreate_Handle=NULL;
 /* LED 任务句柄 */
 static TaskHandle_t Receive_Task_Handle=NULL;

 static TaskHandle_t Send_Task_Handle=NULL;
 
 QueueHandle_t Test_Queue =NULL;
 
 #define QUEUE_LEN 4 /* 队列的长度,最大可包含多少个消息 */
 #define QUEUE_SIZE 4 /* 队列中每个消息大小(字节) */

static void BSP_Init(void);
static void Receive_Task(void);
static void AppTaskCreate(void);/* 用于创建任务 */

static void Send_Task(void);

int main(void)
{	
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */  
  BSP_Init();
    printf("这是一个[野火]-STM32 全系列开发板-FreeRTOS-动态创建任务!\r\n");
   /* 创建 AppTaskCreate 任务 */
   xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,
                                             (const char* )"AppTaskCreate",//任务名称
                                             (uint16_t )512, //任务堆栈大小
                                             (void* )NULL,//传递给任务函数的参数
                                             (UBaseType_t )1, //任务优先级
                                             (TaskHandle_t* )&AppTaskCreate_Handle);

   if (pdPASS == xReturn) /* 创建成功 */
   vTaskStartScheduler(); /* 启动任务,开启调度 */
   else
     return -1;

   while (1); /* 正常不会执行到这里 */
}



//专门用于存放外设初始化函数
static void BSP_Init(void)
{
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
  
  LED_GPIO_Config();
  
  //LED1_ON;
  
  USART_Config();
  Key_GPIO_Config();
  
  //while(1);
  
  
}

static void Receive_Task()
{
  BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
  uint32_t r_queue; /* 定义一个接收消息的变量 */

  while(1)
  {
    xReturn = xQueueReceive(Test_Queue,&r_queue,portMAX_DELAY);
    if (pdTRUE == xReturn)
    printf("本次接收到的数据是%d\n\n",r_queue);
    else
    printf("数据接收出错,错误代码: 0x%lx\n",xReturn);
  }
}



static void Send_Task()
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
  uint32_t send_data1 = 1;
  uint32_t send_data2 = 2;
  while(1)
  {
    if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
     /* KEY1 被按下 */
     printf("发送消息 send_data1! \n");
     xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */
                           &send_data1,/* 发送的消息内容 */
                           0 ); /* 等待时间 0 */
     if (pdPASS == xReturn)
     printf("消息 send_data1 发送成功!\n\n");
     }
     if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
     /* KEY2 被按下 */
     printf("发送消息 send_data2! \n");
     xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */
                           &send_data2,/* 发送的消息内容 */
                           0 ); /* 等待时间 0 */
     if (pdPASS == xReturn)
     printf("消息 send_data2 发送成功!\n\n");
     }
     vTaskDelay(20);/* 延时 20 个 tick */
  }
}



 static void AppTaskCreate(void)
 {
   BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
   taskENTER_CRITICAL(); //进入临界区
    
   Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */
                              (UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */
 if (NULL != Test_Queue)
 printf("创建 Test_Queue 消息队列成功!\r\n");
    
    xReturn = xTaskCreate((TaskFunction_t )Receive_Task, //任务函数
                                         (const char*)"Receive_Task",//任务名称
                                         (uint16_t)512, //任务堆栈大小
                                         (void* )NULL, //传递给任务函数的参数
                                        (UBaseType_t)2, //任务优先级
                                        (TaskHandle_t* )&Receive_Task_Handle);//任务控制块
    
    if (pdPASS == xReturn) /* 创建成功 */
     printf("创建 LED1_Task 任务成功!\r\n");
    
    

    
       xReturn = xTaskCreate((TaskFunction_t )Send_Task, //任务函数
                                         (const char*)"Send_Task",//任务名称
                                         (uint16_t)512, //任务堆栈大小
                                         (void* )NULL, //传递给任务函数的参数
                                        (UBaseType_t)3, //任务优先级
                                        (TaskHandle_t* )&Send_Task_Handle);//任务控制块
    
    if (pdPASS == xReturn) /* 创建成功 */
     printf("创建 KEY_Task 任务成功!\r\n");

    vTaskDelete(AppTaskCreate_Handle); //删除 AppTaskCreate 任务
    taskEXIT_CRITICAL(); //退出临界区
 
 }
 

5. 信号量

1. 概述

信号量是一个非负整数,所有获取它的任务都会将该整数减一(获取它当然是为了使用资源),当该整数值为零时,所有试图获取它的任务都将处于阻塞状态。通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。
二值信号量和互斥信号量(以下使用互斥量表示互斥信号量) 非常相似,但是有一些细微差别:互斥量有优先级继承机制, 二值信号量则没有这个机制。这使得二值信号量更偏向应用于同步功能(任务与任务间的同步或任务和中断间同步), 而互斥量更偏向应用于临界资源的访问。
计数信号量:每当某个事件发生时,任务或者中断将释放一个信号量(信号量计数值加 1),当处理被事件时(一般在任务中处理),处理任务会取走该信号量(信号量计数值减 1),信号量的计数值则表示还有多少个事件没被处理。
递归信号量:对于已经获取递归互斥量的任务可以重复获取该递归互斥量, 该任务拥有递归信号量的所有权。 任务成功获取几次递归互斥量, 就要返还几次,在此之前递归互斥量都处于无效状态, 其他任务无法获取, 只有持有递归信号量的任务才能获取与释放。

2. 信号量函数

创建二值信号量:xSemaphoreCreateBinary()(用于创建一个二值信号量, 并返回一个句柄,其根本与前面创建消息队列的函数是一样的)
创建技术信号量:xSemaphoreCreateCounting()
信号量删除函数:vSemaphoreDelete()
信号量释放函数:xSemaphoreGive()(用于任务)xSemaphoreGiveFromISR()(用于中断)
信号量获取函数:xSemaphoreTake()(同上)

3. 二值信号量实验


	
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "bsp_led.h"
#include "bsp_usart.h"
#include "bsp_key.h"

//创建任务句柄
 /* 创建任务句柄 */
 static TaskHandle_t AppTaskCreate_Handle=NULL;
 /* LED 任务句柄 */
 static TaskHandle_t Receive_Task_Handle=NULL;

 static TaskHandle_t Send_Task_Handle=NULL;
 
 SemaphoreHandle_t BinarySem_Handle =NULL;
 
 #define QUEUE_LEN 4 /* 队列的长度,最大可包含多少个消息 */
 #define QUEUE_SIZE 4 /* 队列中每个消息大小(字节) */

static void BSP_Init(void);
static void Receive_Task(void);
static void AppTaskCreate(void);/* 用于创建任务 */

static void Send_Task(void);


int main(void)
{	
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */  
  BSP_Init();
    printf("这是一个[野火]-STM32 全系列开发板-FreeRTOS-动态创建任务!\r\n");
   /* 创建 AppTaskCreate 任务 */
   xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,
                                             (const char* )"AppTaskCreate",//任务名称
                                             (uint16_t )512, //任务堆栈大小
                                             (void* )NULL,//传递给任务函数的参数
                                             (UBaseType_t )1, //任务优先级
                                             (TaskHandle_t* )&AppTaskCreate_Handle);

   if (pdPASS == xReturn) /* 创建成功 */
   vTaskStartScheduler(); /* 启动任务,开启调度 */
   else
     return -1;

   while (1); /* 正常不会执行到这里 */
}


//专门用于存放外设初始化函数
static void BSP_Init(void)
{
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
  
  LED_GPIO_Config();
  
  //LED1_ON;
  
  USART_Config();
  Key_GPIO_Config();
  
  //while(1);
  
  
}

static void Receive_Task()
{
  BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
  uint32_t r_queue; /* 定义一个接收消息的变量 */

  while(1)
  {
    xReturn = xSemaphoreTake(BinarySem_Handle,portMAX_DELAY);
    if (pdTRUE == xReturn)
    printf("BinarySem_Handle 二值信号量获取成功!\n\n");
    LED1_TOGGLE;
  }
}


static void Send_Task()
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */

  while(1)
  {
    if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
     /* KEY1 被按下 */
     printf("发送消息 send_data1! \n");
     xReturn = xSemaphoreGive( BinarySem_Handle );
     if (xReturn == pdTRUE )
     printf("BinarySem_Handle 二值信号量释放成功!\r\n");
     }
     if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
     /* KEY2 被按下 */
     printf("发送消息 send_data2! \n");
     xReturn = xSemaphoreGive( BinarySem_Handle );
     if (xReturn == pdTRUE )
     printf("BinarySem_Handle 二值信号量释放成功!\r\n");
     }
     vTaskDelay(20);/* 延时 20 个 tick */
  }
}



 static void AppTaskCreate(void)
 {
   BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
   taskENTER_CRITICAL(); //进入临界区
    
   BinarySem_Handle = xSemaphoreCreateBinary();
 if (NULL != BinarySem_Handle)
 printf("创建 Test_Queue 消息队列成功!\r\n");
    
    xReturn = xTaskCreate((TaskFunction_t )Receive_Task, //任务函数
                                         (const char*)"Receive_Task",//任务名称
                                         (uint16_t)512, //任务堆栈大小
                                         (void* )NULL, //传递给任务函数的参数
                                        (UBaseType_t)2, //任务优先级
                                        (TaskHandle_t* )&Receive_Task_Handle);//任务控制块
    
    if (pdPASS == xReturn) /* 创建成功 */
     printf("创建 LED1_Task 任务成功!\r\n");
    
    

    
       xReturn = xTaskCreate((TaskFunction_t )Send_Task, //任务函数
                                         (const char*)"Send_Task",//任务名称
                                         (uint16_t)512, //任务堆栈大小
                                         (void* )NULL, //传递给任务函数的参数
                                        (UBaseType_t)3, //任务优先级
                                        (TaskHandle_t* )&Send_Task_Handle);//任务控制块
    
    if (pdPASS == xReturn) /* 创建成功 */
     printf("创建 KEY_Task 任务成功!\r\n");

    vTaskDelete(AppTaskCreate_Handle); //删除 AppTaskCreate 任务
    taskEXIT_CRITICAL(); //退出临界区
 
 }


6. 互斥里

与二值信号量类似,但多了优先级继承机制,防止优先级翻转。某个临界资源受到一个互斥量保护,如果这个资源正在被一个低优先级任务使用,那么此时的互斥量是闭锁状态,也代表了没有任务能申请到这个互斥量,如果此时一个高优先级任务想要对这个资源进行访问,去申请这个互斥量,那么高优先级任务会因为申请不到互斥量而进入阻塞态,那么系统会将现在持有该互斥量的任务的优先级临时提升到与高优先级任务的优先级相同,这个优先级提升的过程叫做优先级继承。
举个例子,现在有 3 个任务分别为 H 任务(High)、 M 任务(Middle)、 L 任务(Low), 3 个任务的优先级顺序为 H 任务>M 任务>L 任务。正常运行的时候 H 任务可以打断 M 任务与 L 任务, M 任务可以打断 L 任务,假设系统中有一个资源被保护了,此时该资源被 L 任务正在使用中,某一刻, H 任务需要使用该资源,但是 L 任务还没使用完, H任务则因为申请不到资源而进入阻塞态, L 任务继续使用该资源,此时已经出现了“优先级翻转”现象,高优先级任务在等着低优先级的任务执行,如果在 L 任务执行的时候刚好M 任务被唤醒了,由于 M 任务优先级比 L 任务优先级高,那么会打断 L 任务,抢占了
CPU 的使用权,直到 M 任务执行完,再把 CUP 使用权归还给 L 任务, L 任务继续执行,等到执行完毕之后释放该资源, H 任务此时才从阻塞态解除,使用该资源。这个过程,本来是最高优先级的 H 任务,在等待了更低优先级的 L 任务与 M 任务,其阻塞的时间是 M任务运行时间+L 任务运行时间,这只是只有 3 个任务的系统,假如很多个这样子的任务打断最低优先级的任务,那这个系统最高优先级任务岂不是崩溃了,这个现象是绝对不允许出现的,高优先级的任务必须能及时响应。

7. 事件

主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。与信号量不同的是,它可以实现一对多,多对多的同步。即一个任务可以等待多个事件的发生:可以是任意一个事件发生时唤醒任务进行事件处理;也可以是几个事件都发生后才唤醒任务进行事件处理。同样,也可以是多个任务同步多个事件。
与信号量不同的是,事件的发送操作是不可累计的,而信号量的释放动作是可累计的。另外,接受任务可等待多种事件,信号量只能识别单一同步动作。

8. 软件定时器

系统节拍配置为configTICK_RATE_HZ,该宏在 FreeRTOSConfig.h 中有定义,默认是 1000。那么系统的时钟节拍周期就为 1ms(1s 跳动 1000 下,每一下就为 1ms)。软件定时器的所定时数值必须是这个节拍周期的整数倍。
当用户创
建并启动一个软件定时器时, FreeRTOS 会根据当前系统时间及用户设置的定时确定该定时器唤醒时间,并将该定时器控制块挂入软件定时器列表, FreeRTOS 中采用两个定时器列表维护软件定时器, pxCurrentTimerList 与 pxOverflowTimerList 是列表指针, 在初始化的时候分别指向 xActiveTimerList1 与 xActiveTimerList2。
pxCurrentTimerList: 系统新创建并激活的定时器都会以超时时间升序的方式插入到pxCurrentTimerList 列表中。系统在定时器任务中扫描 pxCurrentTimerList 中的第一个定时器,看是否已超时,若已经超时了则调用软件定时器回调函数。否则将定时器任务挂起,因为定时时间是升序插入软件定时器列表的,列表中第一个定时器的定时时间都还没到的话,那后面的定时器定时时间自然没到。

9. 内存管理

在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,它们地址不连续,不能够作为一整块的大内存分配出去,所以一定会在某个时间,系统已经无法分配到合适的内存了,导致系统瘫痪。
heap_1.c 管理方案是 FreeRTOS 提供所有内存管理方案中最简单的一个,它只能申请内存而不能进行内存释放,并且申请内存的时间是一个常量,这样子对于要求安全的嵌入式设备来说是最好的,因为不允许内存释放,就不会产生内存碎片而导致系统崩溃,但是也有缺点,那就是内存利用率不高,某段内存只能用于内存申请的地方,即使该内存只使用一次,也无法让系统回收重新利用。
heap_2.c 方案与 heap_1.c 方案采用的内存管理算法不一样,它采用一种最佳匹配算法(best fit algorithm),比如我们申请 100 字节的内存,而可申请内存中有三块对应大小 200 字节, 500 字节和 1000 字节大小的内存块,按照算法的最佳匹配,这时候系统会把 200 字节大小的内存块进行分割并返回申请内存的起始地址,剩余的内存则插回链表留待下次申请Heap_2.c 方案支持释放申请的内存, 但是它不能把相邻的两个小的内存块合成一个大的内存块, 对于每次申请内存大小都比较固定的,这个方式是没有问题的,而对于每次申请并不是固定内存大小的则会造成内存碎片。
heap_3.c 方案只是简单的封装了标准 C 库中的 malloc()和 free()函数, 并且能满足常用的编译器。 重新封装后的malloc()和 free()函数具有保护功能,采用的封装方式是操作内存前挂起调度器、完成后再恢复调度器。
heap_4.c在2的最佳匹配算法基础上,包含了一种内存碎片合并算法。
heap_5.c在4的基础上,允许内存堆跨越多个非连续的内存区。

10. 中断管理

中断号:每个中断请求信号都会有特定的标志,使得计算机能够判断是哪个设备提出的中断请求,这个标志就是中断号。
中断请求:“紧急事件”需向 CPU 提出申请,要求 CPU 暂停当前执行的任务,转而处理该“紧急事件”,这一申请过程称为中断请求。
中断优先级:为使系统能够及时响应并处理所有中断,系统根据中断时间的重要性和紧迫程度,将中断源分为若干个级别,称作中断优先级。
中断处理程序:当外设产生中断请求后, CPU 暂停当前的任务,转而响应中断申请,即执行中断处理程序。
中断触发:中断源发出并送给 CPU 控制信号,将中断触发器置“1”,表明该中断源产生了中断,要求 CPU 去响应该中断, CPU 暂停当前任务,执行相应的中断处理程序。
中断触发类型:外部中断申请通过一个物理信号发送到NVIC,可以是电平触发或边沿触发。
中断向量:中断服务程序的入口地址。
中断向量表:存储中断向量的存储区,中断向量与中断号对应,中断向量在中断向量表中按照中断号顺序存储。
临界段:代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断。
中断的处理过程是:外界硬件发生了中断后, CPU 到中断处理器读取中断向量,并且查找中断向量表,找到对应的中断服务子程序( ISR)的首地址,然后跳转到对应的 ISR去做相应处理。这部分时间,我称之为:识别中断时间。
在不支持中断嵌套(高优先级中断中断低优先级中断)的实时嵌入式系统中,如果当前系统正在处理一个中断,而此时另一个中断到来了,系统也是不会立即响应的,而只是等处理完当前的中断之后,才会处理后来的中断。此部分时间,称其为:等待中断打开时间。
进入临界段,系统不允许当前状态被中断打断,直到退出临界段,此部分时间为关闭中断时间。
中断延迟=识别中断时间+等待中断打开时间+关闭中断时间。

11. GD32F470移植

前面是用的教程建议的STM32F103来移植的系统,这里在GD32F470上试一次。整体的思路和流程其实是差不多的,主要遇到的问题

  1. FreeRTOSConfig.h的选择问题,在下载的系统文件里所给出的开发板没有咱这一款。经过查阅了解,暂时没有发现这个头文件与硬件有特别大的关联,它主要还是在FreeRTOS系统功能上做裁剪,其中包含的宏定义可以在FreeRTOS.h里找到。我觉得可以就用之前f103的那个头文件,具体要用到系统相关功能后再去添加相关的宏并使能;
  2. GD32给出的驱动代码里有一个Queue和FreeRTOS系统的queue属于是重名了,在编译时混淆了两者,我将其中一个进行了重命名;
  3. 外设初始化函数要仔细看,必要的函数在头文件中进行宏定义。
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值