实战篇 | 基于freeRTOS的多任务事件传输demo(附代码)

之前分享了很多关于freeRTOS的知识,那么我们怎么在实战中去写代码呢?本篇文章重在对基于freeRTOS的架构代码的解析。整个功能如下图:

为什么要用freeRTOS

在实际项目中,如果程序等待一个超时事件,传统的无RTOS情况下,就只能在原地等待而不能执行其它任务,如果使用RTOS,则可以很方便的将当前任务阻塞在该事件下,然后自动去执行别的任务,这样可以高效的利用CPU了。

一般使用情况

我们在开发的时候,我总是在main函数看到以下的代码,这让我感觉不是很爽


    
    
  1. int main()
  2. {
  3.   xTaskCreate( vTask1,  "Task 1"1000, NULL,  1, NULL );
  4.   xTaskCreate( vTask2,  "Task 2"1000, NULL,  1, NULL );
  5.   xTaskCreate( vTask3,  "Task 3"1000, NULL,  2, NULL );
  6.   vTaskStartScheduler();
  7.   while( 1);
  8. }

然后在每个task中,一般代码会这样写


    
    
  1. void vTask1( void *pvParameters )
  2. {
  3.   volatile unsigned long ul;
  4.    for( ;; )
  5.   {
  6.     xQueueSend( USART1_MSGQ,  "task 1 !\n",portMAX_DELAY);
  7.      for( ul =  0; ul < mainDELAY_LOOP_COUNT; ul++ );
  8.   }
  9. }

而任务之间的通信也是比较繁琐,总体来说,代码不易维护,增减一个任务的话要改的东西太多了。为此我特意设计一个框架,可以很方便的增减任务,同时任务之间通过事件队列来通信。

demo

任务创建函数的封装

我们首先定义两个任务,把所有任务信息封装在taskRecord里,并且申明如下:


    
    
  1. #define TASK_NUM  2
  2. //所有任务的信息
  3. static TaskRecord taskRecord[TASK_NUM];

那么TaskRecord怎么安排呢,我们把所有的任务信息都放在结构体里。其中包括任务ID,任务任务函数taskFucn,任务名字,栈的大小stackDep,还有优先级prio,任务句柄taskHandle,任务队列queue


    
    
  1. typedef  struct
  2. {
  3.  int16_t Id;
  4.  TaskFunction_t taskFucn;
  5.   const char *  name;
  6.  configSTACK_DEPTH_TYPE stackDep;
  7.  void *  parameters;
  8.  UBaseType_t prio;
  9.  TaskHandle_t taskHandle;
  10.  QueueHandle_t  queue;
  11. } TaskRecord;

把任务中的一些参数封装起来,放在结构体TaskInitPara,其中包括了任务函数taskFucn,任务名字,栈的大小stackDep,还有优先级prio


    
    
  1. typedef  struct
  2. {
  3.   TaskFunction_t taskFucn;
  4.    const char *  name;
  5.    const configSTACK_DEPTH_TYPE stackDep;
  6.   UBaseType_t prio;
  7. } TaskInitPara;

我们做好了这些之后,就需要把结构体中的参数放到创建任务函数中,那么这个函数createTasks代码如下:


    
    
  1. void createTasks(TaskRecord* taskRecord,  const TaskInitPara* taskIniPara,  int num){
  2.   int i;
  3.   for(i= 0;i<num;i++){
  4.   taskRecord[i].Id = i;
  5.   taskRecord[i].taskFucn = taskIniPara[i].taskFucn;
  6.   taskRecord[i].name = taskIniPara[i].name;
  7.   taskRecord[i].stackDep = taskIniPara[i].stackDep;
  8.   taskRecord[i].parameters = &taskRecord[i];
  9.   taskRecord[i].prio = taskIniPara[i].prio;
  10.   
  11.   xTaskCreate( taskRecord[i].taskFucn,
  12.     taskRecord[i].name,
  13.     taskRecord[i].stackDep,
  14.     taskRecord[i].parameters,
  15.     taskRecord[i].prio,
  16.     &taskRecord[i].taskHandle );
  17.   taskRecord[i].queue = xQueueCreate(  100, sizeof( Event ) );
  18.  }
  19. }

其中num为任务数量,先把任务信息放到初始化的taskRecord中,再把其中的信息创建任务。那么任务创建函数就做好了。

main函数

接着,在我们的main函数中,就不需要那么繁琐的一个一个的创建任务了,按照这个封装的main函数如下:


    
    
  1. int main( void )
  2. {
  3.  createTasks(taskRecord,taskInitPara,TASK_NUM);
  4.   /* Start the tasks and timer running. */
  5.  vTaskStartScheduler();
  6. }

任务间通信

首先我们想想,两个任务之间通信需要知道什么,task1想往task2的发送一些数据,那么需要知道task2的ID吧,需要把数据打包吧,task2需要知道是谁发的,那么task1本身的ID也需要知道吧。

按照这几个明确的东西,我们首先把任务事件ID枚举如下


    
    
  1. typedef enum {
  2.  eventID_1,
  3.  eventID_2,
  4.  eventID_3
  5. }Event_ID;

然后把事件ID,发送者ID,以及要传输的结构或者数据打包封装在结构体Event中,代码如下:


    
    
  1. typedef  struct{
  2.  Event_ID ID;
  3.  int16_t src;  //发送者ID
  4.  void* pData;  //传结构、数据
  5. }Event;

接着,我们需要构造一个事件,把这些信息都放在这个事件中,代码如下:


    
    
  1. void makeEvent(Event* pEvent,int16_t myId,Event_ID evtId,  const void* pData){
  2.  pEvent->ID = evtId;
  3.  pEvent->src = myId;
  4.  pEvent->pData = (void*) pData;
  5. }

现在我们假设task2要往task1发送一系列数据,那么task任务中,我们需要做的事如下,获取task1中队列,看是否为空。


    
    
  1.  QueueHandle_t task1Queue;
  2.  int16_t myId = pMyTaskRecord->Id;
  3.  task1Queue = getTaskQueue(getTaskId( "task1"));

构造事件


    
    
  1.  Event event;
  2.   int* ptemp;  //这里自定义一些数据
  3.  makeEvent(&event,myId,eventID_1,(void*)ptemp);

然后把事件发送出去:

xQueueSendToBack( task1Queue, &event, 0);

    
    

对于task1来说,看队列中是否为空,如果有任务事件来,从队列中获取事件


    
    
  1.  TaskRecord* pMyTaskRecord = (TaskRecord*)pPara;
  2.  QueueHandle_t* evntQueue=pMyTaskRecord->queue;

当队列中确实有事件时,接收事件


    
    
  1. BaseType_t status = xQueueReceive( *evntQueue, &event, portMAX_DELAY );
  2. if( status == pdPASS )
  3. {
  4.   task1HandleEvent(event);
  5. }
  6. else
  7. {
  8.   printf(  "Task1 could not receive from the queue.\r\n" );}

然后我们在task1HandleEvent处理接收到的事件,代码如下:


    
    
  1. void task1HandleEvent(Event event){
  2.  xil_printf(  "Task1 is processing event...\r\n" );
  3.   int* p;
  4.   switch(event.ID){
  5.   case eventID_1:
  6.   p= ( int*) event.pData;
  7.   xil_printf( "ID=%d From: %d data=%d\r\n",event.ID, event.src,p[ 7]);
  8.   free(event.pData);
  9.    break;
  10.   case eventID_2:
  11.    break;
  12.   default:
  13.    break;
  14.  }
  15. }

上面代码表示根据事件ID来判断接收的是哪个事件,再把事件ID,数据等等打印出来。

外部中断通信

如果不是任务间的通信,而是有外部中断触发,需要与某个任务进行信息交互,怎么办?例如有一个以太网任务,当外部网络需要发送一个数据包到这个网络任务的时候,那么就需要进行外部通信了。同样我们这样做,在以太网接收函数中,构造事件


    
    
  1.  Event event;
  2.   int* ptemp;  //这里自定义一些数据
  3.  makeEvent(&event,myId,IntrID_1,(void*)ptemp); //可以再自定一些事件ID如IntrID_1

然后再发送到这个事件到这个任务中,如下

测试

如上,我们构造一个事件,发送一些数据如下


    
    
  1.  Event event;
  2.   int* ptemp = malloc(sizeof( int)* 10);
  3.  memset(ptemp, 0x77,sizeof( int)* 10);
  4.  makeEvent(&event,myId,eventID_1,(void*)ptemp);

我们看到结果如下

task1接到来自任务ID为0,事件1的数据。这里每个任务的等待时间也是可以设置的,设置方法如下:


    
    
  1. /* 设置最大等待时间500ms */
  2. const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 500); 
  3. BaseType_t status = xQueueReceive( *evntQueue, &event, xMaxBlockTime );

如果等待时间为portMAX_DELAY或者0的话,说明某个任务一直处于激活状态,比如task2,当等待时间为portMAX_DELAY时候,则测试结果如下:

所以每个任务设置的时间,优先级,栈大小都是很重要的,具体的就需要在项目中调试了。

最后总结

本篇是属于代码实战篇,对于freeRTOS的具体讲解需要大家自己去领会,我这里是写了一个架构,帮助大家在项目中去更好的搭好架子,当我们有很多任务的时候,任务间又有很多交互通信的时候,就更需要理解这种架构了。

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个简单的基于FreeRTOS的TFTP服务器示例代码,其中包含TFTP任务、网络任务和文件系统任务。请注意,这只是一个基本示例,您可能需要根据实际需求进行修改。 ```c #include "FreeRTOS.h" #include "task.h" #include "queue.h" #include "semphr.h" #include "lwip/sockets.h" #include "lwip/inet.h" #include "lwip/netdb.h" #include <stdio.h> #include <stdbool.h> #define TFTP_PORT 69 #define MAX_PACKET_SIZE 512 static QueueHandle_t tftp_pkt_queue = NULL; static SemaphoreHandle_t tftp_pkt_sem = NULL; static SemaphoreHandle_t tftp_file_sem = NULL; static char tftp_filename[256]; static int tftp_file_size = 0; void tftp_task(void *pvParameters) { int fd; struct sockaddr_in addr; struct sockaddr_storage client_addr; socklen_t client_addr_len; int bytes_received; char recv_buf[MAX_PACKET_SIZE]; int block_num = 1; // 创建 TFTP 数据包队列和信号量 tftp_pkt_queue = xQueueCreate(10, sizeof(char[MAX_PACKET_SIZE])); tftp_pkt_sem = xSemaphoreCreateBinary(); // 创建文件系统信号量 tftp_file_sem = xSemaphoreCreateMutex(); // 创建 TFTP 套接字并绑定端口 fd = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(TFTP_PORT); lwip_bind(fd, (struct sockaddr *)&addr, sizeof(addr)); while (1) { // 接收 TFTP 数据包 client_addr_len = sizeof(client_addr); bytes_received = lwip_recvfrom(fd, recv_buf, MAX_PACKET_SIZE, 0, (struct sockaddr *)&client_addr, &client_addr_len); if (bytes_received >= 4) { // 如果是读请求,则处理请求 if (recv_buf[1] == 1) { // 获取文件名 int i; for (i = 2; i < bytes_received; i++) { if (recv_buf[i] == '\0') { break; } tftp_filename[i - 2] = recv_buf[i]; } tftp_filename[i - 2] = '\0'; // 获取文件大小 tftp_file_size = 100; // 发送第一个数据块 char pkt[MAX_PACKET_SIZE]; pkt[0] = 0; pkt[1] = 3; pkt[2] = (block_num >> 8) & 0xff; pkt[3] = block_num & 0xff; memcpy(pkt + 4, "hello world", 11); lwip_sendto(fd, pkt, 15, 0, (struct sockaddr *)&client_addr, client_addr_len); // 等待前一个数据块被确认 xSemaphoreTake(tftp_pkt_sem, portMAX_DELAY); // 发送下一个数据块 block_num++; pkt[2] = (block_num >> 8) & 0xff; pkt[3] = block_num & 0xff; memcpy(pkt + 4, "hello world", 11); lwip_sendto(fd, pkt, 15, 0, (struct sockaddr *)&client_addr, client_addr_len); // 等待前一个数据块被确认 xSemaphoreTake(tftp_pkt_sem, portMAX_DELAY); } // 如果是确认消息,则释放信号量 else if (recv_buf[1] == 4) { xSemaphoreGive(tftp_pkt_sem); } } } } void network_task(void *pvParameters) { while (1) { // 处理网络数据包 // ... } } void fs_task(void *pvParameters) { while (1) { // 管理文件系统 // ... } } int main(void) { // 创建 TFTP 任务 xTaskCreate(tftp_task, "TFTP", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL); // 创建网络任务 xTaskCreate(network_task, "Network", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL); // 创建文件系统任务 xTaskCreate(fs_task, "FS", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL); // 启动任务调度器 vTaskStartScheduler(); return 0; } ``` 该示例中,TFTP任务负责处理TFTP请求,网络任务负责处理网络数据包,文件系统任务负责管理文件系统。当接收到TFTP读请求时,TFTP任务会将文件名和文件大小保存下来,并发送第一个数据块。然后,它会等待前一个数据块被确认后再发送下一个数据块。当接收到TFTP确认消息时,TFTP任务会释放一个信号量,以便等待发送下一个数据块的代码可以继续执行。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值