在编写RTOS代码时,如何设计一个简单、优雅、可拓展的任务初始化结构?

简介: 在编写RTOS代码时,如何设计一个简单、优雅、可拓展的任务初始化结构?

随着写代码功力的提升,个人对于代码的整洁、优雅、可维护、易拓展等就有了一定的要求,虽然自己曾经就属于那种全局变量满天飞,想到哪里写到哪里的嵌入式软件工程师;但是这一切在现在来说必须要结束了!要想做一个好的项目,我们时刻都要去想它的框架如何设计,如何去兼容未来的拓展,以便我们构建一个优雅、整洁、易维护、易拓展的程序,少出问题,少加班,拿高薪;因此,我们必须在代码的设计上利用编程语言的特性来下一些功夫。

640.png

在之前,我就经常发现很多工程师在写RTOS代码的时候存在如下问题:


  • 随意定义任务的位置,随意初始化任务代码。
  • 由于任务函数初始化参数过多,当同时创建多个任务时,任务初始化函数写得非常长,非常难看。


例如我之前写的这个RT-Thread的项目:


码云仓库:

git clone https://gitee.com/morixinguan/personal-open-source-project.git

部分代码如下:

/***************按键处理任务*************/
#define KEY_TASK_PRIORITY      3
#define KEY_TASK_SIZE         2000
static rt_thread_t key_task_thread = RT_NULL;
static void Start_Key_Task(void *parameter);
/***************按键处理任务*************/
/***************传感器任务处理*************/
#define SENSOR_PRIORITY           4
#define SENSOR_TASK_SIZE            2048
rt_sem_t sensor_data_sem = RT_NULL;
static rt_thread_t sensor_task_thread = RT_NULL;
/*状态栏更新线程入口函数 */
static void StartSensor_Task(void *parameter);
/***************传感器任务处理*************/
/***************控制任务处理*************/
#define CONTROL_PRIORITY           5
#define CONTROL_TASK_SIZE           2048
static rt_thread_t control_task_thread = RT_NULL;
/*控制任务更新线程入口函数 */
static void StartControl_Task(void *parameter);
/***************控制任务处理*************/
......省略.....
/*启动其它任务*/
void start_other_rt_thread(void)
{
    /*1、创建按键线程*/
    key_task_thread = rt_thread_create("key_th",
                                       Start_Key_Task, RT_NULL,
                                       KEY_TASK_SIZE,
                                       KEY_TASK_PRIORITY, TASK_TIMESLICE);
    /* 如果获得线程控制块,启动这个线程 */
    if (key_task_thread != RT_NULL)
        rt_thread_startup(key_task_thread);
    /*2、创建控制线程*/
    control_task_thread = rt_thread_create("con_th",
                                           StartControl_Task, RT_NULL,
                                           CONTROL_TASK_SIZE,
                                           CONTROL_PRIORITY, TASK_TIMESLICE);
    /* 如果获得线程控制块,启动这个线程 */
    if (control_task_thread != RT_NULL)
        rt_thread_startup(control_task_thread);
    Menu_Init();
    //关指示灯
    HAL_GPIO_WritePin(BOARD_LED_GPIO_Port, BOARD_LED_Pin, GPIO_PIN_RESET);
}

其实这个看起来还算舒服一点,至少它的位置是比较统一的,而且任务并不算很多;但是如果任务更多,这个代码看起来就会很长,比如我找来的下面这个代码,具体就不说是哪位小伙伴写的了:

static  void  AppTaskStart (void *p_arg)
{
    OS_ERR       err;
 CPU_SR      cpu_sr = 0;
 uint8_t test[10];
   (void)p_arg;
    BSP_Init(); 
    CPU_Init();                  
 delay_init(168);
 uart_init(9600);   
 TxDMAConfig();
 RxDMAConfig((uint32_t)g_usart1RxBuf0,(uint32_t)g_usart1RxBuf1,USART1BUFSIZE);
 USART1_RxCallback = USART1_DMARxCallback;
 __HAL_DMA_ENABLE(&UART1RxDMA_Handler); 
 RTC_Init();
 PRINTER_Init();
 W25QXX_Init();
 LCD_BSP_Init();
 LcdInit();
 ADC_BSP_Init();
 NixieTube_BSPInit();
 MenuSystemInit();
 offplay();
 SRAM_Init();
 CH456IF_Init();
 ch456_test();
 my_mem_init(SRAMEX); /* 初始化外部SRAM */
 Data_Init();         /* 初始化数据存储模块 */
#if OS_CFG_STAT_TASK_EN > 0u
    OSStatTaskCPUUsageInit(&err);
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN
    CPU_IntDisMeasMaxCurReset();
#endif
#if OS_CFG_SCHED_ROUND_ROBIN_EN  //时间片轮度算法  
 OSSchedRoundRobinCfg(DEF_ENABLED,1,&err);  
#endif 
 OS_CRITICAL_ENTER();
 /*mutex create zone:begin*/
 OSMutexCreate((OS_MUTEX* )&TEST_MUTEX,
      (CPU_CHAR* )"TEST_MUTEX",
                  (OS_ERR*  )&err);
 OSMutexCreate((OS_MUTEX* )&FLASH_MUTEX,
      (CPU_CHAR* )"FLASH READ MUTEX",
                  (OS_ERR*  )&err);
 /*mutex create zone:end*/
 /*USER TASK CREATE ZONE:BEGIN*/
 OSTaskCreate(&USBProcessTaskTCB,
     "USB Process Task",
     USBProcessTask,
     0u,
     USB_CFG_PROCESS_TASK_PRIO,
     USBProcessTaskStk,
     USBProcessTaskStk[USB_CFG_PROCESS_TASK_STK_SIZE / 10u],
     USB_CFG_PROCESS_TASK_STK_SIZE,
     0u,   //message amount
     0u,
     0u,
    (OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
    &err);
 OSTaskCreate(&teskTaskTCB,                              /* Create the start task                                */
     "test Process Task",
     testProcessTask,
     0u,
     TEST_CFG_PROCESS_TASK_PRIO,
     TESTProcessTaskStk,
     TESTProcessTaskStk[TEST_CFG_PROCESS_TASK_STK_SIZE / 10u],
     TEST_CFG_PROCESS_TASK_STK_SIZE,
     0u,   //message amount
     0u,
     0u,
    (OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
    &err);
 OS_CRITICAL_EXIT(); 
    while (DEF_TRUE) {   
        udp_flag |= LWIP_SEND_DATA;  
        OSTimeDlyHMSM(0u, 0u, 0u, 100u,
                      OS_OPT_TIME_HMSM_STRICT,
                      &err);
    }
}

难受吗?至少我是觉得很难受的!解决这个问题可以使用一种简单的、可扩展的RTOS初始化设计模式,这个设计模式的原则就是创建一个通用的初始化函数,然后这个函数可以遍历RTOS初始化配置表来初始化所有的任务,让我们来看看如何创建这样的设计模式。

1、创建任务初始化结构

第一步是检查 RTOS 的任务创建函数,并查看初始化任务所需的参数。任务初始化结构只是一个包含初始化任务所需的所有参数的结构。但是不同的RTOS之间可能不同,以freertos为例:

typedef struct
{
    TaskFunction_t const taskptr;           
    const char *   const taskname;                
    const configSTACK_DEPTH_TYPE stackdepth;    
    void * const   parametersptr;                 
    UBaseType_t    taskpriority;                   
    TaskHandle_t * const taskhandle;            
}FreertosTaskParams_t;
2、创建任务配置表

有了第1步所定义的结构体以后,我们就可以创建一个配置表了,这个配置表就包含了所有的任务以及初始化这些任务的所需的参数,例如:

FreertosTaskParams_t Task_Parameters_conf[] = 
{
    {(Function_t)Task_1, "Task_1",TASK_1_STACK_DEPTH, &Telemetry, TASK_1_PRIORITY, NULL}, 
    {(Function_t)Task_2, "Task_2",TASK_2_STACK_DEPTH, NULL      , TASK_2_PRIORITY, NULL}, 
    {(Function_t)Task_3, "Task_3",TASK_3_STACK_DEPTH, &Telemetry, TASK_3_PRIORITY, NULL}, 
    {(Function_t)Task_4, "Task_4",TASK_4_STACK_DEPTH, &Telemetry, TASK_4_PRIORITY, NULL}, 
    {(Function_t)Task_5, "Task_5",TASK_5_STACK_DEPTH, &Telemetry, TASK_5_PRIORITY, NULL}, 
    {(Function_t)Task_6, "Task_6",TASK_6_STACK_DEPTH, &Telemetry, TASK_6_PRIORITY, NULL}, 
};

这个表里有很多参数我们还没有进行宏定义。这些都是我们将在应用程序中定义的用于初始化任务的参数。例如,每个任务的优先级可能都不一样,这里用一个宏,例如TASK_1_PRIORITY来进行表示。

3、创建初始化循环

创建任务配置表以后,初始化任务只用一个for循环就好了,然后将结构体数组里的各个参数分别对应到RTOS创建任务的API里就可以了。例如,我们可以使用以下循环初始化任务:

#define NR(x) (sizeof(x)/sizeof(x[0]))
for(uint8_t count = 0; count < NR(Task_Parameters_conf); count++)
{
    (void)xTaskCreate(Task_Parameters_conf[TaskCount].taskptr,
                      Task_Parameters_conf[TaskCount].taskname,
                      Task_Parameters_conf[TaskCount].stackdepth,
                      Task_Parameters_conf[TaskCount].parametersptr,
                      Task_Parameters_conf[TaskCount].taskpriority, 
                      Task_Parameters_conf[TaskCount].taskhandle);
}

这里要注意的是,我们将(void)放在xTaskCreate前面,其实这样是表示我们在创建任务的时候忽略了xTaskCreate这个函数的的返回值。正常情况下,我们当前希望检查函数的返回值,这样可以增加整个程序的健壮性,但在这种情况下,我们将在初始化期间创建所有任务,并且不会出现任何内存问题。但是,我们可以依靠freerTOS malloc失败的钩子函数来捕获开发过程中的任何动态内存分配问题。或者,我们可以检查返回值,然后创建一个函数,这个函数在出现问题时进行检查和恢复。

4、结论

这种简单的RTOS初始化的设计模式是可扩展的,可重用的,并且能够很容易进行修改。这是嵌入式软件工程师如何利用设计模式的一个很好的例子。这种设计模式可以与任何RTOS一起使用。

往期精彩

C语言表驱动法在定时任务列表中的应用


表驱动+状态机法AD传感器驱动检测框架


嵌入式设计模式:有限状态自动机的C语言实现


回调函数的注册机制为什么会在嵌入式固件开发中应用如此广泛?


一些值得被定义为常用C语言头文件库的漂亮宏定义(值得收藏,以备使用参考)

目录
相关文章
程序三大结构-系统学习一
编程从三大结构考虑问题,这样的思考方式、学习方式也有了更加深刻的认识与理解
|
存储 XML 编译器
【C#基础】C# 程序通用结构
编程语言C# 程序结构的介绍 。
177 0
【C#基础】C# 程序通用结构
什么是调用结构?西门子S7-1200的调用结构如何使用?
今天我们来介绍一下西门子S7-1200的调用结构。在西门子S7-1200中采用调用结构来描述用户程序中块的调用层级,调用结构提供了几个方面的信息,包括所用的块,对其它块的调用,各块之间的关系,每个块的数据要求以及块的状态等
什么是调用结构?西门子S7-1200的调用结构如何使用?
|
数据采集 负载均衡 搜索推荐
会计学包含的两种程序设计思想
会计学包含的两种程序设计思想
会计学包含的两种程序设计思想
|
算法 安全 前端开发
程序常用的设计技巧
程序常用的设计技巧
程序常用的设计技巧
嵌入式程序调用函数的内部过程和机制
嵌入式程序调用函数的内部过程和机制
嵌入式程序调用函数的内部过程和机制
|
程序员
软件基本功:不会代码共用,因为没有设计能力;代码共用都不会,谈什么设计
软件基本功:不会代码共用,因为没有设计能力;代码共用都不会,谈什么设计
111 0
|
程序员 测试技术 数据库
文档驱动式代码设计器——代码是设计出来的!
  代码是敲出来的吗?是批量生成出来的吗?     No no no,代码是设计出来的!     如果说到代码生成器,大家可能会想到三层、动软代码生成器、数据库表等等。其一般的思路是,先有数据库然后根据库里的表自动生成一系列的代码,包括实体类、持久化、业务层(空函数)、页面代码等,还可以生成数据库文档。
1156 0
|
人工智能 运维 数据处理
解密阿里云顶层设计和底层逻辑
几十个问题,万字长文,阿里云新任总裁行癫履新后首次深入讨论阿里云对云计算未来的判断,深度解读未来阿里云生态战略,揭秘阿里技术委员会和阿里中台思想的原生思考。
5474 0