任务创建和删除 API 函数
FreeRTOS 最基本的功能就是任务管理,而任务管理最基本的操作就是创建和删除任务,FreeRTOS 的任务创建和删除 API 函数如表所示:
遥控器固件程序基本是使用xTaskCreate()动态创建一个任务,使用vTaskDelete()来删除一个任务。
1、函数 xTaxkCreate()
此函数用来创建一个任务,任务需要 RAM 来保存与任务有关的状态信息(任务控制块),任务也需要一定的 RAM 来作为任务堆栈。如果使用函数 xTaskCreate()来创建任务的话那么这些所需的 RAM 就会自动的从 FreeRTOS 的堆中分配,因此必须提供内存管理文件,默认我们使用heap_4.c 这个内存管理文件,而且宏 configSUPPORT_DYNAMIC_ALLOCATION 必须为 1。新创建的任务默认就是就绪态的,如果当前没有比它更高优先级的任务运行那么此任务就会立即进入运行态开始运行,不管在任务调度器启动前还是启动后,都可以创建任务。函数原型如下:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
参数:
pxTaskCode
: 任务函数。
pcName
: 任务名字,一般用于追踪和调试,任务名字长度不能超过configMAX_TASK_NAME_LEN。
usStackDepth
: 任务堆栈大小,注意实际申请到的堆栈是 usStackDepth 的 4 倍。其中空闲任务的任务堆栈大小为 configMINIMAL_STACK_SIZE。
pvParameters
: 传递给任务函数的参数。
uxPriotiry
: 任务优先级,范围 0~ configMAX_PRIORITIES-1。
pxCreatedTask
: 任务句柄,任务创建成功以后会返回此任务的任务句柄,这个句柄其实就是任务的任务堆栈。此参数就用来保存这个任务句柄。其他 API 函数可能会使用到这个句柄。
返回值:
pdPASS: 任务创建成功。
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY: 任务创建失败,因为堆内存不足!
2、函数vTaskDelete()
删除一个用函数 xTaskCreate()或者 xTaskCreateStatic()创建的任务,被删除了的任务不再存在,也就是说再也不会进入运行态。任务被删除以后就不能再使用此任务的句柄!如果此任务是使用动态方法创建的,也就是使用函数 xTaskCreate()创建的,那么在此任务被删除以后此任务之前申请的堆栈和控制块内存会在空闲任务中被释放掉,因此当调用函数 vTaskDelete()删除任务以后必须给空闲任务一定的运行时间。
此函数原型如下:
vTaskDelete( TaskHandle_t xTaskToDelete )
参数:
xTaskToDelete
: 要删除的任务的任务句柄。
返回值: 无
在固件程序创建任务函数startTask()中,动态创建完任务后,任务删除开始:
vTaskDelete(startTaskHandle); /*删除开始任务*/
任务调度器开启函数分析
xTaskCreate(startTask, "START_TASK", 100, NULL, 2, &startTaskHandle);/*创建起始任务*/
vTaskStartScheduler();/*开启任务调度*/
例程中是在 main()函数中先创建一个开始任务 startTask,后面紧接着调用函数 vTaskStartScheduler()。这个函数的功能就是开启任务调度器的,这个函数在文件 tasks.c
中有定义,缩减后的函数代码如下:
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
xReturn = xTaskCreate( prvIdleTask, (1)
"IDLE", configMINIMAL_STACK_SIZE,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
&xIdleTaskHandle );
#if ( configUSE_TIMERS == 1 ) //使用软件定时器使能
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask(); (2)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */
if( xReturn == pdPASS ) //空闲任务和定时器任务创建成功。
{
portDISABLE_INTERRUPTS(); (3)
#if ( configUSE_NEWLIB_REENTRANT == 1 ) //使能 NEWLIB
{
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif /* configUSE_NEWLIB_REENTRANT */
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE; (4)
xTickCount = ( TickType_t ) 0U;
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); (5)
if( xPortStartScheduler() != pdFALSE ) (6)
{
//如果调度器启动成功的话就不会运行到这里,函数不会有返回值的
}
else
{
//不会运行到这里,除非调用函数 xTaskEndScheduler()。
}
}
else
{
//程序运行到这里只能说明一点,那就是系统内核没有启动成功,导致的原因是在创建
//空闲任务或者定时器任务的时候没有足够的内存。
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
//防止编译器报错,比如宏 INCLUDE_xTaskGetIdleTaskHandle 定义为 0 的话编译器就会提
//示 xIdleTaskHandle 未使用。
( void ) xIdleTaskHandle;
}
(1) 创建空闲任务,如果使用静态内存的话使用函数 xTaskCreateStatic()来创建空闲任务,优先级为 tskIDLE_PRIORITY,宏 tskIDLE_PRIORITY 为 0,也就是说空闲任务的优先级为最低。
(2) 如果使用软件定时器的话还需要通过函数 xTimerCreateTimerTask()来创建定时器服务任务。定时器服务任务的具体创建过程是在函数 xTimerCreateTimerTask()中完成的。
(3) 关闭中断,在 SVC 中断服务函数 vPortSVCHandler()中会打开中断。
(4) 变量 xSchedulerRunning 设置为 pdTRUE,表示调度器开始运行。
(5) 当宏 configGENERATE_RUN_TIME_STATS 为 1 的时候说明使能时间统计功能,此时需要用户实现宏 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS,此宏用来配置一个定时器/计数器。
(6) 调用函数 xPortStartScheduler()来初始化跟调度器启动有关的硬件,比如滴答定时器、FPU 单元和 PendSV 中断等等。
临界区
/*创建任务*/
void startTask(void *param)
{
taskENTER_CRITICAL(); /*进入临界区*/
xTaskCreate(radiolinkTask, "RADIOLINK", 100, NULL, 6, &radiolinkTaskHandle);/*创建无线连接任务*/
xTaskCreate(usblinkTxTask, "USBLINK_TX", 100, NULL, 5, NULL); /*创建usb发送任务*/
xTaskCreate(usblinkRxTask, "USBLINK_RX", 100, NULL, 5, NULL); /*创建usb接收任务*/
xTaskCreate(commanderTask, "COMMANDER", 100, NULL, 4, NULL); /*创建飞控指令发送任务*/
xTaskCreate(keyTask, "BUTTON_SCAN", 100, NULL, 3, NULL); /*创建按键扫描任务*/
xTaskCreate(displayTask, "DISPLAY", 200, NULL, 1, NULL); /*创建显示任务*/
xTaskCreate(configParamTask, "CONFIG_TASK", 100, NULL, 1, NULL);/*创建参数配置任务*/
xTaskCreate(radiolinkDataProcessTask, "DATA_PROCESS", 100, NULL, 6, NULL); /*创建无线通信数据处理任务*/
xTaskCreate(usblinkDataProcessTask, "DATA_PROCESS", 100, NULL, 6, NULL); /*创建USB通信数据处理任务*/
vTaskDelete(startTaskHandle); /*删除开始任务*/
taskEXIT_CRITICAL(); /*退出临界区*/
}
临界段代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段,比如有的外设的初始化需要严格的时序,初始化过程中不能被打断。FreeRTOS 在进入临界段代码的时候需要关闭中断,当处理完临界段代码以后再打开中断。FreeRTOS 系统本身就有很多的临界段代码,这些代码都加了临界段代码保护,在写自己的用户程序的时候有些地方也需要添加临界段代码保护。
FreeRTOS 与临界段代码保护有关的函数有 4 个 : taskENTER_CRITICAL() 、taskEXIT_CRITICAL() 、 taskENTER_CRITICAL_FROM_ISR() 和
taskEXIT_CRITICAL_FROM_ISR(),这四个函数其实是宏定义,在 task.h 文件中有定义。这四个函数的区别是前两个是任务级的临界段代码保护,后两个是中断级的临界段代码保护。
taskENTER_CRITICAL()和 taskEXIT_CRITICAL()是任务级的临界代码保护,一个是进入临
界段,一个是退出临界段,这两个函数是成对使用的,这函数的定义如下:
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
而 portENTER_CRITICAL()和 portEXIT_CRITICAL()也是宏定义,在文件 portmacro.h 中有定义,如下:
#define portENTER_CRITICAL() vPortEnterCritical()
#define portEXIT_CRITICAL() vPortExitCritical()
函数 vPortEnterCritical()和 vPortExitCritical()在文件 port.c 中,函数如下:
void vPortEnterCritical( void )
{
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
if( uxCriticalNesting == 1 )
{
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
void vPortExitCritical( void )
{
configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
}
可以看出在进入函数 vPortEnterCritical()以后会首先关闭中断,然后给变量 uxCriticalNesting加一,uxCriticalNesting 是个全局变量,用来记录临界段嵌套次数的。函数 vPortExitCritical()是退出临界段调用的,函数每次将 uxCriticalNesting 减一,只有当 uxCriticalNesting 为 0 的时候才会调用函数 portENABLE_INTERRUPTS()使能中断。这样保证了在有多个临界段代码的时候不会因为某一个临界段代码的退出而打乱其他临界段的保护,只有所有的临界段代码都退出以后才会使能中断。
atkp.c
aktp.c 主要实现解析四轴通过 radiolink.c 返回的应答包并通过 usblink.c 转发给上位机,上位机通过 usblink.c 发下来的数据包 将 通 过 radiolink 转发给四轴,即的 radiolinkDataProcessTask 和 usblinkDataProcessTask。
float plane_yaw,plane_roll,plane_pitch;
float plane_bat;
u8 rssi;
/*atkp解析*/
static void atkpAnalyze(atkp_t *p)
{
if(p->msgID == UP_STATUS)
{
plane_roll = (s16)(*(p->data+0)<<8)|*(p->data+1);
plane_roll = plane_roll/100;
plane_pitch = (s16)(*(p->data+2)<<8)|*(p->data+3);
plane_pitch = plane_pitch/100;
plane_yaw = (s16)(*(p->data+4)<<8)|*(p->data+5);
plane_yaw = plane_yaw/100;
}
else if(p->msgID == UP_POWER)
{
plane_bat = (s16)(*(p->data+0)<<8)|*(p->data+1);
plane_bat = plane_bat/100;
}
else if(p->msgID == UP_REMOTOR)
{
switch(p->data[0])
{
case ACK_MSG:
miniFlyMsgACKProcess(p);
break;
}
}
else if(p->msgID == UP_RADIO)
{
radioConfig_t radio;
switch(p->data[0])
{
case U_RADIO_RSSI:
rssi = p->data[1];
break;
case U_RADIO_CONFIG:
memcpy(&radio, p->data+1, sizeof(radio));
setMatchRadioConfig(&radio);
break;
}
}
}
/*无线连接数据处理任务*/
void radiolinkDataProcessTask(void *param)
{
atkp_t p;
while(1)
{
radiolinkReceivePacketBlocking(&p); /*接收四轴上传的数据,包括四轴数据和遥控器数据*/
atkpAnalyze(&p);
usblinkSendPacket(&p); /*把接收到的四轴数据发送到上位机*/
vTaskDelay(1);
}
}
/*USB连接数据处理任务*/
void usblinkDataProcessTask(void *param)
{
atkp_t p;
while(1)
{
usblinkReceivePacketBlocking(&p); /*接收上位机发送的数据*/
atkpAnalyze(&p);
radiolinkSendPacket(&p);
}
}
遥控器与四轴 NRF51822 无线通信,无线通信本身自带了 CRC16 校验,传输是可靠的,因此直接可以传输 ATKP 数据包。Usblink 链路通信是串口方式,为了区分一帧数据和保证传输可靠性,所以需要加上帧头和校验。
当遥控器与四轴 NRF51822 通信成功后,遥控器会不断发送数据包给 NRF51822,NRF51822 接收并解析,然后再转发给 STM32F11,STM32F411 接收到后会立即向 NRF51822返回一条数据包,一应一答模式。NRF51822 与 STM32F411 通信使用的串口方式,数据格式跟 usblink 链路数据格式一样。如果遥控器连接了上位机,遥控器会将接收到的应答数据包先解析,解析完成后转发给上位机。同样,上位机发下来的数据遥控器也会先解析再转发给四轴。Radiolink 链路中,遥控器定周期发送控制命令,四轴定周期返回姿态和其他数据。
Usblink 链路中,如果四轴连接了上位机,四轴会定周期返回姿态和其他数据给上位机;如果遥控器连接了上位机,遥控器将四轴返回的数据转发给上位机。上位机同一时刻只能连接一个 Usblink 链路。