【FreeRTOS】【应用篇】任务创建

news2024/9/24 11:21:30

前言

从本篇开始,将不再太过于关心 FreeRTOS 的内核细节,把重心转移到对 FreeRTOS 的应用上来。
本篇代码大部分参考野火的 FreeRTOS 教程。

一、静态任务和动态任务创建的区别

1. 概念解析

在 FreeRTOS 中,我们可以选择两个不同的函数进行任务的创建:

  • xTaskCreateStatic() 静态任务创建函数
  • xTaskCreate() 动态任务创建函数

静态和动态的区别在于

  • 静态创建任务:

    • 静态创建任务是在编译时分配任务所需的内存空间。
    • 创建任务时需要定义并初始化一个StaticTask_t类型的变量,该变量用于存储任务的相关信息。
    • 静态创建任务的内存空间在任务整个运行期间都被任务所占用,直到任务被删除。
    • 静态创建任务通常在应用程序的启动阶段进行,任务的数量是固定的,无法在运行时动态调整。
  • 动态创建任务:

    • 动态创建任务是在运行时通过动态内存分配函数(例如pvPortMalloc())来分配任务所需的内存空间。
    • 创建任务时,不需要定义和初始化额外的变量,任务的相关信息直接存储在动态分配的内存中。
    • 动态创建任务的内存空间可以在任务完成后释放,可以在运行时动态地创建和删除任务。
    • 动态创建任务通过调用函数xTaskCreate()来实现,任务的数量可以在运行时根据需求进行动态调整。

2. 动态创建任务的应用

也就是说,我们可以在一个任务运行的时候创建一个新任务!如下:

要在一个任务运行的时候创建新任务,可以在任务函数中调用xTaskCreate()函数。这样,当任务运行到这个调用语句时,会创建一个新的任务,并将其添加到任务列表中。新任务会在调度器启动后开始执行。

  • 可能的代码如下:
void Task1(void *pvParameters)
{
    // 任务1的代码
    
    // 创建新任务
    BaseType_t status = xTaskCreate(Task2, "Task2", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
    if (status != pdPASS) {
        // 创建任务失败的处理
    }
    
    // 任务1的其余代码
}

void Task2(void *pvParameters)
{
    // 任务2的代码
    
    // 任务2的其余代码
}

int main(void)
{
    // FreeRTOS初始化和任务创建

    xTaskCreate(Task1, "Task1", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
    
    // 启动调度器
    vTaskStartScheduler();
    
    // 不应该跳转到这里
    
    while (1) {}
    return 0;
}

在上面的示例中,当任务1运行到创建新任务的代码时,会调用xTaskCreate()函数创建任务2。任务2会被添加到任务列表中,并在调度器启动后开始执行。

3. 动态创建任务的好处

看到这里,可能你有个疑惑:即使可以这么做,我们也要先写好任务2的上下文,那么还不如在编程时直接创建两个任务而不是等到运行的时候再创建任务2,在运行时创建任务2有什么意义吗?

实际上,对于初学者来说,我们的程序不够复杂,可能很难体会到动态创建任务的好处。在复杂的系统中,运行时动态创建任务的一个主要优点是可以动态地根据系统的需求来创建任务。这样可以在运行时根据实际情况来决定是否创建任务,以及创建多少个任务。这种灵活性可以提高系统的可扩展性和适应性。

① 运行时创建任务的应用场景和意义:

  1. 动态任务管理:在某些应用中,任务的数量和类型可能会根据用户的交互或特定条件的触发而动态改变。通过在运行时创建任务,可以根据需要来创建或删除任务,以适应不同的系统状态和用户行为。
  2. 资源管理和优化:有时系统中的任务可能共享某些资源,例如共享内存区域或外设。在某些情况下,只有在需要时才会分配这些资源,以避免资源浪费。通过在运行时创建任务,可以根据资源的可用性和需求来创建任务,以有效地管理系统资源和优化性能。
  3. 动态优先级调整:有些任务可能有不同的优先级,而优先级可能会随着系统状态的变化而改变。通过在运行时创建任务,可以根据系统的需求和优先级策略来动态地调整任务的优先级,以满足系统的实时性要求。

② 动态创建任务的实际应用

考虑以下这些具体的场景,动态创建任务可能优于静态创建任务:

  1. 事件驱动系统:在某些系统中,任务的创建可能是由特定的事件触发的。例如,当用户按下某个按钮或触发某个传感器时,可以根据不同的事件动态创建任务来响应用户操作或处理传感器数据。
  2. 多协议通信系统:在某些通信系统中,需要支持多种通信协议,例如TCP/IP、UDP等。根据网络条件和通信需求的变化,可以动态创建任务以处理不同的协议,以实现系统的灵活性和适应性。
  3. 并行处理系统:在需要处理大量计算密集型任务的系统中,可以根据系统资源和负载情况动态创建任务。通过根据系统负载动态调整任务数量,可以最大程度地利用多核处理器或分布式计算系统的性能。
  4. 系统调试和故障排查:在调试和故障排查过程中,可能需要动态添加一些特定的监控任务或记录任务,以收集系统信息并帮助分析问题。通过在运行时动态创建这些任务,可以根据需要收集所需的信息,而不需要一直运行这些任务,从而减少对系统性能的影响。

二、任务函数的结构

  • 任务必须是一个无限循环的函数,一般用 while(1) 或者 for(;😉 作为循环体,在循环体里面编写任务正文。这是因为如果任务在循环之外返回(通过 LR 返回),那么当 LR 指向了非法的内存时,可能会发生 HardFault_Handler,导致系统出错。为了避免这种情况,FreeRTOS 中的任务返回值会指向一个任务退出函数 prvTaskExitError(),而该函数是一个死循环。
  • 任务的延时需要使用 vTaskDelay() 这种阻塞延时,而不能使用循环的忙等延时。这是因为在阻塞延时时,任务会被挂起,从而让出 CPU 控制权,让其他任务得以执行,提高 CPU 利用率;而如果使用循环延时,对于非最高优先级的任务来说,他们将不断循环直到其执行任务的时间片结束,但是如果是最高优先级的任务使用了循环延时,就会一直循环执行最高优先级任务,其他优先级较低的任务就得不到执行,这种情况是要避免的。所以 FreeRTOS 中任务延时尽量使用阻塞延时。
    以下面这个 LED任务为例:
/**********************************************************************
  * @ 函数名  : LED_Task
  * @ 功能说明: LED_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void LED1_Task(void* parameter)
{	
    while (1)
    {
        LED1_ON;
        vTaskDelay(500);   /* 延时500个tick */
        printf("LED1_Task Running,LED1_ON\r\n");
        
        LED1_OFF;     
        vTaskDelay(500);   /* 延时500个tick */		 		
        printf("LED1_Task Running,LED1_OFF\r\n");
    }
}

三、任务创建时内存分配的相关问题

1. STM32 的存储空间详解

  • 因为在我们的程序中 FreeRTOS 的内存分配是建立在 STM32 上的,所以首先我们需要了解 STM32 的存储空间。
    推荐一篇好文,详细地对 STM32 的存储空间作了介绍:
    STM32深入系列01——内存简述(Flash和SRAM)

2. FreeRTOS 的内存管理实现

  • 而在 FreeRTOS 中,提供了5种内存管理实现。分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c。后面笔者会写一篇有关 FreeRTOS 的内存管理的文章。
    可以先参考这篇好文:
    【freeRTOS内存管理策略详解】

3. 什么是内存对齐

  • 内存对齐是指将数据存储在内存中时按照规定的边界对齐方式进行存储。对齐方式是按照特定字节的倍数对数据进行对齐,通常是按照 1、2、4、8 等字节进行对齐。对齐的基本单位是字节,也可以是其他数据类型的大小。

  • 内存对齐的目的是为了优化内存访问,提高处理器的访问速度和效率。特定的硬件架构和处理器通常对对齐的内存访问更高效,而对非对齐的内存访问可能会引起额外的开销或性能损失。

  • 例如,假设要存储一个 int 类型的数据(通常是 4 字节)在内存中,如果数据按照 4 字节对齐存储,即保证存储地址是 4 的倍数,那么处理器就可以以更高效的方式访问这个数据,提高读取和写入的速度。如果数据非对齐存储,处理器可能需要进行额外的内存访问操作,导致性能下降。

  • 在 FreeRTOS 中,我们使用 8 字节对齐,要求数据按照8字节的倍数进行存储,这样可以保证数据存储在连续的内存块中,提高内存访问的效率。

  • 额外开销:当申请13字节内存,按照最接近的8字节进行对齐。这意味着实际上会分配16字节的内存空间来满足对齐要求。

代码:

定义对齐的字节数:

#define portBYTE_ALIGNMENT			8

根据定义的字节数确定对齐使用的掩码:
这里的掩码是 0x0007,也就是二进制 0000 0000 0000 0111

#if portBYTE_ALIGNMENT == 32
	#define portBYTE_ALIGNMENT_MASK ( 0x001f )
#endif

#if portBYTE_ALIGNMENT == 16
	#define portBYTE_ALIGNMENT_MASK ( 0x000f )
#endif

#if portBYTE_ALIGNMENT == 8
	#define portBYTE_ALIGNMENT_MASK ( 0x0007 )
#endif

#if portBYTE_ALIGNMENT == 4
	#define portBYTE_ALIGNMENT_MASK	( 0x0003 )
#endif

#if portBYTE_ALIGNMENT == 2
	#define portBYTE_ALIGNMENT_MASK	( 0x0001 )
#endif

#if portBYTE_ALIGNMENT == 1
	#define portBYTE_ALIGNMENT_MASK	( 0x0000 )
#endif

计算栈顶指针:
(栈指针 + 栈深 - 1)

pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );

对齐栈顶指针:
这里的掩码是 0x0007,也就是二进制 0000 0000 0000 0111,取反就是低三位都为 0(1111 1111 1111 1000),再与 pxTopOfStack 与就可以将pxTopOfStack 低三位清 0,也就变成了 8 的倍数(原来的内存地址是 **** **** **** ****,现在的内存地址是 **** **** **** * ✖ 8字节),这就是内存对齐的原理。

pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );

最后,检查内存对齐是否正确:
原理:掩码除了低三位都为 0,而如果栈顶指针正确对齐,其低三位都为 0,所以两者与起来应该结果为 0。
这里使用断言检测结构是否为 0。

/* Check the alignment of the calculated top of stack is correct. */
		configASSERT( ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack & ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) == 0UL ) );

4. 动态创建任务时,任务栈究竟多大

  • 由于静态任务创建的任务栈是我们自行分配的,很容易清楚其任务栈大小。但是对于动态创建任务,由于任务栈的空间分配是内核帮我们分配的,而且封装较深,再加上野火的教程中对任务栈大小的描述出现了错误,导致笔者花了很多时间去理解,下面进行说明!

① 野火教程中出现的错误

在野火的教程书中,代码上任务栈大小是 512(查看源码可知这个参数指的应该是字数,也就是 512字 = 512 × 4 = 2048 字节 ),下面的解释却是 128 × 4 = 512字节。前后矛盾,让人迷惑:
在这里插入图片描述

② 对野火教程的更正

实际上,动态创建任务这个函数的任务栈深度这个参数应该指的是字

③ 为什么栈深度指的是字的数目?

xTaskCreate() 函数中,使用了 pvPortMalloc() 进行空间分配:
( size_t ) usStackDepth ) * sizeof( StackType_t ) 就是我们传入的栈深度 × StackType_t 的字节数,而 StackType_t 实际上就是 unsigned int,也就是 4 个字节。

#if( 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 ) /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
	{
	TCB_t *pxNewTCB;
	BaseType_t xReturn;

		/* If the stack grows down then allocate the stack then the TCB so the stack
		does not grow into the TCB.  Likewise if the stack grows up then allocate
		the TCB then the stack. */
		#if( portSTACK_GROWTH > 0 )
		{
			/* Allocate space for the TCB.  Where the memory comes from depends on
			the implementation of the port malloc function and whether or not static
			allocation is being used. */
			pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );

			if( pxNewTCB != NULL )
			{
				/* Allocate space for the stack used by the task being created.
				The base of the stack memory stored in the TCB so the task can
				be deleted later if required. */
				pxNewTCB->pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */

				if( pxNewTCB->pxStack == NULL )
				{
					/* Could not allocate the stack.  Delete the allocated TCB. */
					vPortFree( pxNewTCB );
					pxNewTCB = NULL;
				}
			}
		}
		#else /* portSTACK_GROWTH */
		{
		StackType_t *pxStack;

			/* Allocate space for the stack used by the task being created. */
			pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */

			if( pxStack != NULL )
			{
				/* Allocate space for the TCB. */
				pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); /*lint !e961 MISRA exception as the casts are only redundant for some paths. */

				if( pxNewTCB != NULL )
				{
					/* Store the stack location in the TCB. */
					pxNewTCB->pxStack = pxStack;
				}
				else
				{
					/* The stack cannot be used as the TCB was not created.  Free
					it again. */
					vPortFree( pxStack );
				}
			}
			else
			{
				pxNewTCB = NULL;
			}
		}
		#endif /* portSTACK_GROWTH */

		if( pxNewTCB != NULL )
		{
			#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
			{
				/* Tasks can be created statically or dynamically, so note this
				task was created dynamically in case it is later deleted. */
				pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
			}
			#endif /* configSUPPORT_STATIC_ALLOCATION */

			prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
			prvAddNewTaskToReadyList( pxNewTCB );
			xReturn = pdPASS;
		}
		else
		{
			xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
		}

		return xReturn;
	}

#endif /* configSUPPORT_DYNAMIC_ALLOCATION */

而pvPortMalloc() 函数的结构如下:
在这个函数中,真正分配内存的是 pvReturn 变量。在函数的开头,pvReturn 被初始化为 NULL ,表示当前还没有成功分配内存。

在函数执行的过程中,会经过一系列的条件判断和操作,最终可能会将 pxBlock 结构体中的内存空间分配给 pvReturn

  1. 首先,会调用 vTaskSuspendAll() 函数来暂停任务调度,以确保在进行内存分配操作时不会被其他任务中断。

  2. 然后,会进行一些初始化操作,检查是否需要对堆进行初始化。

  3. 接下来,会根据用户请求的内存大小 xWantedSize 进行一系列判断和计算。其中会判断请求的大小是否合法,是否需要对内存进行对齐操作等。

  4. 然后,会遍历空闲内存块链表,找到合适大小的内存块。如果找到了合适的内存块,会将其分配给 pvReturn 变量,并进行相应的调整和更新。

  5. 最后,会更新剩余可用内存的大小,并根据需求设定一些标记和指针,表示该内存块已被分配出去。

  6. 在函数结尾,会调用 traceMALLOC() 函数进行一些跟踪记录操作,并恢复任务调度,继续执行其他任务。

  7. 最终,函数会根据分配结果返回相应的指针给用户。

因此,这个函数中真正分配内存的是 pvReturn 变量,它会被赋值为成功分配的内存空间的指针。

这个函数使用一种被称为内存块的东西来记录空闲内存块并把空闲内存块分配给任务栈。空闲内存块的结构体如下:

/* Define the linked list structure.  This is used to link free blocks in order
of their memory address. */
typedef struct A_BLOCK_LINK
{
	struct A_BLOCK_LINK *pxNextFreeBlock;	/*<< The next free block in the list. */
	size_t xBlockSize;						/*<< The size of the free block. */
} BlockLink_t;

这个结构体存储了指向下一个内存块的指针以及当前内存块的大小,而内存块大小是以字节为单位的。
值得一提的是,每一块空闲的内存中可供利用的部分是这块内存减去该空闲内存块结构体的大小。

至此,我们可以清楚看到,动态创建任务时传入的任务栈深度参数的单位是字,转换为字节时应该乘以 4。

四、任务创建、启动流程

以启动两个 LED 以不同频率闪烁为例:

1. 任务句柄创建

我们在 AppTaskCreate 任务中创建其他的任务:

/**************************** 任务句柄 ********************************/
/* 
 * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
 * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
 * 这个句柄可以为NULL。
 */
 /* 创建任务句柄 */
static TaskHandle_t AppTaskCreate_Handle = NULL;
/* LED1任务句柄 */
static TaskHandle_t LED1_Task_Handle = NULL;
/* LED2任务句柄 */
static TaskHandle_t LED2_Task_Handle = NULL;

2. 任务主体编写

/***********************************************************************
  * @ 函数名  : AppTaskCreate
  * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
  * @ 参数    : 无  
  * @ 返回值  : 无
  **********************************************************************/
static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  taskENTER_CRITICAL();           //进入临界区
  
  /* 创建LED_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )LED1_Task, /* 任务入口函数 */
                        (const char*    )"LED1_Task",/* 任务名字 */
                        (uint16_t       )512,   /* 任务栈大小 */
                        (void*          )NULL,	/* 任务入口函数参数 */
                        (UBaseType_t    )2,	    /* 任务的优先级 */
                        (TaskHandle_t*  )&LED1_Task_Handle);/* 任务控制块指针 */
  if(pdPASS == xReturn)
    printf("创建LED1_Task任务成功!\r\n");
  
	/* 创建LED_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )LED2_Task, /* 任务入口函数 */
                        (const char*    )"LED2_Task",/* 任务名字 */
                        (uint16_t       )512,   /* 任务栈大小 */
                        (void*          )NULL,	/* 任务入口函数参数 */
                        (UBaseType_t    )3,	    /* 任务的优先级 */
                        (TaskHandle_t*  )&LED2_Task_Handle);/* 任务控制块指针 */
  if(pdPASS == xReturn)
    printf("创建LED2_Task任务成功!\r\n");
  
  vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
  
  taskEXIT_CRITICAL();            //退出临界区
}



/**********************************************************************
  * @ 函数名  : LED_Task
  * @ 功能说明: LED_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void LED1_Task(void* parameter)
{	
    while (1)
    {
        LED1_ON;
        vTaskDelay(500);   /* 延时500个tick */
        printf("LED1_Task Running,LED1_ON\r\n");
        
        LED1_OFF;     
        vTaskDelay(500);   /* 延时500个tick */		 		
        printf("LED1_Task Running,LED1_OFF\r\n");
    }
}

/**********************************************************************
  * @ 函数名  : LED_Task
  * @ 功能说明: LED_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void LED2_Task(void* parameter)
{	
    while (1)
    {
        LED2_ON;
        vTaskDelay(500);   /* 延时500个tick */
        printf("LED2_Task Running,LED2_ON\r\n");
        
        LED2_OFF;     
        vTaskDelay(500);   /* 延时500个tick */		 		
        printf("LED2_Task Running,LED2_OFF\r\n");
    }
}

3. 硬件初始化函数

  • 中断优先级分组
  • 其他外设初始化
/***********************************************************************
  * @ 函数名  : BSP_Init
  * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
  * @ 参数    :   
  * @ 返回值  : 无
  *********************************************************************/
static void BSP_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();

	/* 串口初始化	*/
	USART_Config();
  
}

4. main 函数主体

  • 硬件初始化
  • AppTaskCreate 任务的创建
  • 任务调度的启动
int main(void)
{	
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */

  /* 开发板硬件初始化 */
  BSP_Init();
  printf("这是一个[野火]-STM32全系列开发板-FreeRTOS-动态创建多任务实验!\r\n");
   /* 创建AppTaskCreate任务 */
  xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                        (const char*    )"AppTaskCreate",/* 任务名字 */
                        (uint16_t       )512,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )1, /* 任务的优先级 */
                        (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
  /* 启动任务调度 */           
  if(pdPASS == xReturn)
    vTaskStartScheduler();   /* 启动任务,开启调度 */
  else
    return -1;  
  
  while(1);   /* 正常不会执行到这里 */    
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/908180.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微信小程序,封装身高体重选择器组件

wxml代码&#xff1a; // 微信小程序的插值语法不支持直接使用Math <wxs src"./ruler.wxs" module"math"></wxs> <view class"ruler-container"><scroll-view scroll-left"{{scrollLeft}}" enhanced"{{tru…

探索软件项目管理的本质及概念

什么是软件项目管理&#xff1f; 软件项目管理是指对软件项目从规划、组织、指挥、控制到最终交付的全过程进行有效管理的一种方法。它通过合理的资源分配、有效的沟通和高效的协作&#xff0c;确保软件项目能够按照预定的目标、时间和质量要求完成。在现代信息技术逐渐普及和…

互联网企业内部FAQ的设计指南:帮助企业用户快速解决常见问题!

在互联网企业中&#xff0c;内部FAQ&#xff08;Frequently Asked Questions&#xff09;是帮助企业用户快速解决常见问题的重要工具。一个优秀的内部FAQ不仅可以提高用户满意度&#xff0c;还可以减轻客服团队的负担。本文将介绍互联网企业内部FAQ的设计指南&#xff0c;帮助企…

知识储备--基础算法篇-动态规划

1.前言 第一次接触动态规划&#xff0c;不知道具体什么意思&#xff0c;做了题才发现动态规划就是把大问题变成小问题&#xff0c;并解决了小问题重复计算的方法称为动态规划。比如上楼梯&#xff0c;一次上一阶或二阶&#xff0c;求有多少种算法&#xff0c;就可以拆成最后一…

Flutter 小技巧之 3.13 全新生命周期 AppLifecycleListener

Flutter 3.13 在 Framework 里添加了 AppLifecycleListener 用于监听应用生命周期变化&#xff0c;并响应退出应用的请求等支持&#xff0c;那它有什么特殊之处&#xff1f;和老的相比又有什么不同&#xff1f; 简单说&#xff0c;在 Flutter 3.13 之前&#xff0c;我们一般都…

计网第三章(数据链路层)(五)

目录 一、以太网交换机自学习和转发帧的过程 1.两层交换机和三层交换机 2.以太网交换机的基本原理 3.具体实现过程 一、以太网交换机自学习和转发帧的过程 1.两层交换机和三层交换机 大家可能注意到平常做题时有叫两层交换机&#xff0c;或者三层交换机的。 两层交换机就…

为什么需要websocket?

一、为什么需要websocket&#xff1f; 前端和后端的交互模式最常见的就是前端发数据请求&#xff0c;从后端拿到数据后展示到页面中。如果前端不做操作&#xff0c;后端不能主动向前端推送数据&#xff0c;这也是http协议的缺陷。 因此&#xff0c;一种新的通信协议应运而生---…

使用kabeja库解析DXF格式文件

DXF格式是一种开源的CAD文件格式&#xff0c;如何实现Java代码对齐的解析&#xff0c;在网上找了很久&#xff0c;也没有找到非常成熟的库。很奇怪&#xff0c;开源的格式&#xff0c;正常应该会有很多的库来支持。只找到了一个kabeja库&#xff0c;最新版本还是2008年出的0.4版…

GTK3实现自定义列表

使用gtk,如果想自己定义列表,思路可以将每个列表项作为一个hbox,整个列表是一个vbox。通过对容器动态的添加删除,实现列表操作,同时添加任何自己所需要的控件。 下面的例子是实现一个显示图片、按钮和进度条的列表,并且进行上移下移,具有添加和删除列表项功能但没有演示…

网站巡查与SEO:爱校对如何确保内容的最佳质量?

随着互联网的飞速发展&#xff0c;企业和个人正在寻找优化他们网站内容的最佳方式。在这个数字化时代&#xff0c;网站巡查和SEO已成为维持网站高度相关性和可见性的关键。此时&#xff0c;工具如“爱校对”不仅帮助检查文本的质量&#xff0c;还确保内容对搜索引擎优化&#x…

STM32使用定时器实现微秒(us)级延时

STM32使用定时器实现微秒&#xff08;us&#xff09;级延时 引言前期准备介绍系统时钟定时器时钟 项目项目介绍STM32CubeMX程序 引言 目前开发STM32普遍使用HAL库&#xff0c;但 HAL 库封装的延时函数目前仅支持 ms 级别的延时&#xff0c;日常很多情况下会用到 us 延时&#…

IC封装——从基本概念到TSV

一、IC封装 在之前文章中有大致提过封装&#xff0c;这里展开讲讲 芯片生产流程_沧海一升的博客-CSDN博客每个半导体产品的制造都需要数百个工艺&#xff0c;泛林集团将整个制造过程分为八个步骤&#xff1a;晶圆加工-氧化-光刻-刻蚀-薄膜沉积-互连-测试-封装。_芯片生产流程h…

spring异步框架使用教程

背景 在需求开发过程中&#xff0c;为了提升效率&#xff0c;很容易就会遇到需要使用多线程的场景。这个时候一般都会选择建一个线程池去专门用来进行某一类动作&#xff0c;这种任务到来的时候往往伴随着大量的线程被创建调用。而还有另外一种场景是整个任务的执行耗时比较长…

ElasticSearch 7.4学习记录(DSL语法)

上文和大家一起初次了解了很多ES相关的基础知识&#xff0c;本文的内容将会是实际企业中所需要的吗&#xff0c;也是我们需要熟练应用的内容。 面对ES&#xff0c;我们最多使用的就是查询&#xff0c;当我负责这个业务时&#xff0c;现不需要我去考虑如何创建索引&#xff0c;添…

ubuntu18.04安装keil5(踩坑)看完再享用,别直接上手

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、安装winewine的总结 二、安装Keil5总结 前言 切记看完再享用&#xff0c;别直接上手&#xff0c;不然安装的时候会和我一样踩坑的&#xff08;走了很多弯路…

Unity之用Transform 数组加多个空物体-->简单地控制物体按照指定路线自动行驶

文章目录 **原理解释**&#xff1a;**带注释的代码**&#xff1a;实际运用 当你需要实现物体按照指定路线行驶时&#xff0c;你可以通过以下步骤来实现&#xff1a; 原理解释&#xff1a; 路径点&#xff1a;你需要定义一系列路径点&#xff0c;这些点将构成物体行驶的路线。每…

[软件工具]精灵标注助手目标检测数据集格式转VOC或者yolo

有时候我们拿到一个数据集发现是xml文件格式如下&#xff1a; <?xml version"1.0" ?> <doc><path>C:\Users\Administrator\Desktop\test\000000000074.jpg</path><outputs><object><item><name>dog</name>…

纷享销客连接型CRM助力中国企业全球业务增长

近年来&#xff0c;中国企业出海热度越来越高&#xff0c;中国企业出海之路也越走越宽&#xff0c;全球化步伐明显加速。2023年&#xff0c;中国企业业务出海正进入快车道和分水岭阶段&#xff0c;中国也正在从一个世界工厂变成全球资源的整合者。 0 中国企业出海&#xff0c…

精简体积的OLED 基础驱动库 - OLED_BASIC

打算用一个存储空间不大的Arduino 芯片做点简单的文字和图形显示&#xff0c;屏幕芯片SSD1316&#xff0c;感觉u8g2 占用还是太大&#xff0c;想裁剪别人的现成代码又感觉无从下手&#xff0c;所以就基本上重写了一个OLED 显示库&#xff0c;仓库地址&#xff1a;gitee.com/etb…

SQL助你面大厂(Join家族介绍)

在学习SQL时候&#xff0c;在多表查询的时候你肯定使用过Join&#xff0c;无非就是把两表联合在一起进行多表查询&#xff0c;但是你是真的了解它们的用法么&#xff1f; Join家族一般有left Join、Rigth Join、Inner Join、Left Semi Join、Left Anti Join、Full Join为主 C…