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

2022-03-24 09:07:50 浏览数 (2)

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

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

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

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

码云仓库:

代码语言:javascript复制
git clone https://gitee.com/morixinguan/personal-open-source-project.git

部分代码如下:

代码语言:javascript复制
/***************按键处理任务*************/
#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);
}

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

代码语言:javascript复制
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为例:

代码语言:javascript复制
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步所定义的结构体以后,我们就可以创建一个配置表了,这个配置表就包含了所有的任务以及初始化这些任务的所需的参数,例如:

代码语言:javascript复制
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里就可以了。例如,我们可以使用以下循环初始化任务:

代码语言:javascript复制
#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一起使用。

0 人点赞