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