FreeRTOS调度器启动过程分析

news2024/11/16 12:34:55

目录

引出思考

vTaskStartScheduler()启动任务调度器

xPortStartScheduler()函数

FreeRTOS启动第一个任务

vPortSVCHandler()函数

总结


引出思考

首先想象一下如何启动第一个任务?

假设我们要启动的第一个任务是任务A,那么就需要将任务A的寄存器值恢复到CPU寄存器

任务A的寄存器值,在一开始创建任务的时候就保存在任务A的栈中了,这个在创建任务的细节博文中我已经分析过FreeRTOS任务创建及细节-CSDN博客

注意:

1、中断产生时,硬件将xPSR,PC(R15)、LR(R14)、R12、R3-R0保存和恢复,而R4-R11需要手动保存和恢复

2、进入中断后硬件会强制使用MSP指针,此时LR(R14)的值将会自动被更新为特殊的EXC_RETURN

使用FreeRTOS,一个最基本的程序框架如下所示:

int main(void)
{  
    // 必要的初始化工作;
    // 创建任务1;
    // 创建任务2;
    // ...
    vTaskStartScheduler();  /*启动调度器*/
    while(1);   
}

任务创建完成后,静态变量指针pxCurrentTCB,指向优先级最高的就绪任务,但此时任务不能运行,因为接下来还有最为关键的一步:启动FreeRTOS调度器。

调度器是FreeRTOS操作系统的核心,主要负责任务切换,即找出最高优先级的就绪任务,并使之获得CPU运行权。调度器并非自动运行的,需要人为启动它。

vTaskStartScheduler()启动任务调度器

API函数vTaskStartScheduler()用于启动调度器,它会创建一个空闲任务,初始化一些静态变量,最主要的,它会初始化系统节拍定时器并设置好相应的中断,然后启动第一个任务。这里我们分析启动调度器的过程,和之前一样,启动调度器也涉及到硬件架构的一些知识(比如系统节拍定时器初始化),因此本文这里以CortexM3架构为例。

启动调度器的API函数vTaskStartScheduler()的源码这里我直接精简一下屏蔽掉条件编译的部分:

void vTaskStartScheduler( void )
{
BaseType_t xReturn;
StaticTask_t *pxIdleTaskTCBBuffer= NULL;
StackType_t *pxIdleTaskStackBuffer= NULL;
uint16_t usIdleTaskStackSize =tskIDLE_STACK_SIZE;
 
    /*如果使用静态内存分配任务堆栈和任务TCB,则需要为空闲任务预先定义好任务内存和任务TCB空间*/
    #if(configSUPPORT_STATIC_ALLOCATION == 1 )
    {
       vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &usIdleTaskStackSize);
    }
    #endif /*configSUPPORT_STATIC_ALLOCATION */
 
    /* 创建空闲任务,使用最低优先级*/
    xReturn =xTaskGenericCreate( prvIdleTask, "IDLE",usIdleTaskStackSize, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT), &xIdleTaskHandle,pxIdleTaskStackBuffer,pxIdleTaskTCBBuffer, NULL );
 
    if( xReturn == pdPASS )
    {
        /* 先关闭中断,确保节拍定时器中断不会在调用xPortStartScheduler()时或之前发生.当第一个任务启动时,会重新启动中断*/
       portDISABLE_INTERRUPTS();
       
        /* 初始化静态变量 */
       xNextTaskUnblockTime = portMAX_DELAY;
       xSchedulerRunning = pdTRUE;
        xTickCount = ( TickType_t ) 0U;
 
        /* 如果宏configGENERATE_RUN_TIME_STATS被定义,表示使用运行时间统计功能,则下面这个宏必须被定义,用于初始化一个基础定时器/计数器.*/
       portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
 
        /* 设置系统节拍定时器,这与硬件特性相关,因此被放在了移植层.*/
        if(xPortStartScheduler() != pdFALSE )
        {
            /* 如果调度器正确运行,则不会执行到这里,函数也不会返回*/
        }
        else
        {
            /* 仅当任务调用API函数xTaskEndScheduler()后,会执行到这里.*/
        }
    }
    else
    {
        /* 执行到这里表示内核没有启动,可能因为堆栈空间不够 */
       configASSERT( xReturn );
    }
 
    /* 预防编译器警告*/
    ( void ) xIdleTaskHandle;
}

这个API函数首先创建一个空闲任务,空闲任务使用最低优先级0,空闲任务的任务句柄存放在静态变量xIdleTaskHandle中,可以调用API函数xTaskGetIdleTaskHandle()获得空闲任务句柄。

如果任务创建成功,则关闭中断(调度器启动结束时会再次使能中断的),初始化一些静态变量,然后调用函数xPortStartScheduler()来启动系统节拍定时器并启动第一个任务。因为设置系统节拍定时器涉及到硬件特性,因此函数xPortStartScheduler函数由移植层提供,不同的硬件架构,这个函数的代码是不一样的。

函数vTaskStartScheduler()用于启动任务调度器,任务调度器启动后,FreeRTOS便会开始进行任务调度,除非调用函数xTaskEndScheduler()停止调度器,否则不会再返回。

函数vTaskStartScheduler()主要做了六件事情:

  • 创建空闲任务,根据是否支持静态内存管理,使用静态方式或者动态方式创建空闲任务
  • 创建定时器服务任务,创建定时器服务任务需要配置启用软件定时器,创建定时器服务任务,同样是根据是否配置支持静态内存管理,使用静态或者动态方式创建定时器服务任务。
  • 关闭中断,使用portDISABLE_INTERRUPTS()关闭中断,这种方式只关闭受FreeRTOS管理的中断。关闭中断主要是为了防止SysTick中断在任务调度器开启之前或过程中,产生中断。FreeRTOS会在开始运行第一个任务时,重新打开中断。
  • 初始化一些全局变量,并将任务调度器的运行标志设置为已运行
  • 初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计功能的,就无需进行这项硬件定时器的配置。
  • 最后就是调用xPortStartScheduler()

xPortStartScheduler()函数

对于Cortex-M4架构,函数xPortStartScheduler()函数的实现如下:

	/* Pop the core registers. */
	ldmia r0!, {r4-r11, r14}
	msr psp, r0
	isb
	mov r0, #0
	msr	basepri, r0
	bx r14
}
/*-----------------------------------------------------------*/

__asm void prvStartFirstTask( void )
{
	PRESERVE8

	/* Use the NVIC offset register to locate the stack. */
	ldr r0, =0xE000ED08
	ldr r0, [r0]
	ldr r0, [r0]
	/* Set the msp back to the start of the stack. */
	msr msp, r0
	/* Clear the bit that indicates the FPU is in use in case the FPU was used
	before the scheduler was started - which would otherwise result in the
	unnecessary leaving of space in the SVC stack for lazy saving of FPU
	registers. */
	mov r0, #0
	msr control, r0
	/* Globally enable interrupts. */
	cpsie i
	cpsie f
	dsb
	isb
	/* Call SVC to start the first task. */
	svc 0
	nop
	nop
}
/*-----------------------------------------------------------*/

__asm void prvEnableVFP( void )
{
	PRESERVE8

	/* The FPU enable bits are in the CPACR. */
	ldr.w r0, =0xE000ED88
	ldr	r1, [r0]

	/* Enable CP10 and CP11 coprocessors, then save back. */
	orr	r1, r1, #( 0xf << 20 )
	str r1, [r0]
	bx	r14
	nop
}
/*-----------------------------------------------------------*/

/*
 * See header file for description.
 */
BaseType_t xPortStartScheduler( void )
{
	/* configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to 0.
	See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */
	configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );

	/* This port can be used on all revisions of the Cortex-M7 core other than
	the r0p1 parts.  r0p1 parts should use the port from the
	/source/portable/GCC/ARM_CM7/r0p1 directory. */
	configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
	configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );

	#if( configASSERT_DEFINED == 1 )
	{
		volatile uint32_t ulOriginalPriority;
		volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
		volatile uint8_t ucMaxPriorityValue;

		/* Determine the maximum priority from which ISR safe FreeRTOS API
		functions can be called.  ISR safe functions are those that end in
		"FromISR".  FreeRTOS maintains separate thread and ISR API functions to
		ensure interrupt entry is as fast and simple as possible.

		Save the interrupt priority value that is about to be clobbered. */
		ulOriginalPriority = *pucFirstUserPriorityRegister;

		/* Determine the number of priority bits available.  First write to all
		possible bits. */
		*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

		/* Read the value back to see how many bits stuck. */
		ucMaxPriorityValue = *pucFirstUserPriorityRegister;

		/* The kernel interrupt priority should be set to the lowest
		priority. */
		configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );

		/* Use the same mask on the maximum system call priority. */
		ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

		/* Calculate the maximum acceptable priority group value for the number
		of bits read back. */
		ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
		while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
		{
			ulMaxPRIGROUPValue--;
			ucMaxPriorityValue <<= ( uint8_t ) 0x01;
		}

		#ifdef __NVIC_PRIO_BITS
		{
			/* Check the CMSIS configuration that defines the number of
			priority bits matches the number of priority bits actually queried
			from the hardware. */
			configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
		}
		#endif

		#ifdef configPRIO_BITS
		{
			/* Check the FreeRTOS configuration that defines the number of
			priority bits matches the number of priority bits actually queried
			from the hardware. */
			configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
		}
		#endif

		/* Shift the priority group value back to its position within the AIRCR
		register. */
		ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
		ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

		/* Restore the clobbered interrupt priority register to its original
		value. */
		*pucFirstUserPriorityRegister = ulOriginalPriority;
	}
	#endif /* conifgASSERT_DEFINED */

	/* Make PendSV and SysTick the lowest priority interrupts. */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

	/* Start the timer that generates the tick ISR.  Interrupts are disabled
	here already. */
	vPortSetupTimerInterrupt();

	/* Initialise the critical nesting count ready for the first task. */
	uxCriticalNesting = 0;

	/* Ensure the VFP is enabled - it should be anyway. */
	prvEnableVFP();

	/* Lazy save always. */
	*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;

	/* Start the first task. */
	prvStartFirstTask();

	/* Should not get here! */
	return 0;
}

从源码上面来看,其中开始的一大段都是冗余代码。因为Cortex-M4的中断优先级有点反直觉:在Cortex-M内核中,优先级的数值越大,表明优先级越低,数值越小代表优先级越高。根据官方统计,在Cortex-M4硬件上使用FreeRTOS,绝大多数问题都是优先级数值设置不对的问题,因此,为了使得FreeRTOS更健壮,FreeRTOS的作者在编写Cortex-M架构的移植层代码时,特意增加了冗余代码。关于详细的Cortex-M架构的中断优先级设置,后续有机会再写一篇博客。

在Cortex-M4架构中,FreeRTOS为了任务启动和任务切换使用了三个重要的异常:SVC、PendSV、SysTick。SVC(系统服务调用)用于任务启动(只在启动第一个任务的时候会调用,以后都不会用到),有些操作系统不允许应用程序直接访问硬件,而是通过提供一些系统服务函数、通过SVC来调用;PendSV(可挂起系统调用)用于完成任务切换,它的最大特性是如果当前有优先级比它高的中断在运行,PendSV会推迟执行,直到高优先级中断执行完毕;SysTick用于产生系统时钟节拍时钟,提供一个时间片,如果多个任务共享同一个优先级,则每次Systick中断,下一个任务将获得一个时间片。关于详细的SVC、PendSV异常描述,推荐《Cortex-M3和M4权威指南》一书的“异常”部分。

 这里将PendSV和SysTick异常优先级设置为最低,这样任务切换不会打断某个中断服务程序,中断服务程序也不会被延迟,这样简化了设计,有利于系统稳定。

      接下来调用函数vPortSetupTimerInterrupt()设置SysTick定时器中断周期并使能定时器运行这个函数比较简单,就是设置SysTick硬件的相应寄存器。

这里小小的总结一下这个xPortStartScheduler()函数的工作🤣:

  • 在启用断言的情况下,函数xPortStartScheduler会检测用户在FreeRTOSConfig.h文件中对中断的相关配置是否有误
  • 配置PendSV和SysTick的中断优先级为最低优先级
  • 调用函数vPortSetupTimerInterrupt()配置SysTick,函数vPortSetupTimerInterrupt()首先会将SysTick当前计数值清空,并根据FreeRTOSConfig.h文件中配置configSYSTICK_CLOCK_HZ(SysTick 时钟源频率)和 configTICK_RATE_HZ(系统时钟节拍频率)计算并设置 SysTick 的重装载值,然后启动 SysTick 计数和中断。
  • 初始化临界区嵌套计数器为0
  • 调用函数prvEnableVFP()使能FPU,Cortex-M3内核没有FPU,M4内核是有FPU的,执行该函数后,FPU被开启
  • 接下来将FPCCR寄存器的[31:30]置1,这样在进出异常时,FPU的相关寄存器就会自动地保存和恢复,同样地M3内核的代码是没有的,M4内核有这部分代码
  • 调用函数prvStartFirstTask()启动第一个任务

FreeRTOS启动第一个任务

这里是这篇文章比较重要的关键点和难点🧐

      再接下来有一个关键的函数是prvStartFirstTask(),这个函数用来启动第一个任务。我们先看一下源码:

__asm void prvStartFirstTask( void )
{
    /*8字节对齐*/
	PRESERVE8

	/* Use the NVIC offset register to locate the stack. */
	ldr r0, =0xE000ED08  /*0xE000ED08 为 VTOR 地址*/
	ldr r0, [r0]         /*获取VTOR的值*/
	ldr r0, [r0]         /*获取MSP的初始值*/

	/* 初始化MSP */
	msr msp, r0
	/* Clear the bit that indicates the FPU is in use in case the FPU was used
	before the scheduler was started - which would otherwise result in the
	unnecessary leaving of space in the SVC stack for lazy saving of FPU
	registers. */
	mov r0, #0
	msr control, r0
	/*使能全局中断 */
	cpsie i
	cpsie f
	dsb
	isb
	/* 调用SVC启动第一个任务 */
	svc 0
	nop
	nop
}

函数prvStartFirstTask()用于初始化启动第一个任务前的环境,主要是设置MSP指针,并使能全局中断,具体的代码如上面所示;

从上面的代码可以看出,函数prvStartFirstTask()是一段汇编代码,分析它的工作:

  • 首先是使用了PRESERVE8,进行8字节对齐,这是因为,栈在任何时候都是需要4字节对齐的,而在调用入口得8字节对齐,在进行C编程的时候,编译器会自动完成对齐操作,而对于汇编,就需要手动进行对齐
  • 接下来就是为了获得MSP指针得初始值,那么这里肯定会引出两个问题🤣 

1. 什么是MSP指针?

程序在运行过程中需要一定得栈空间来保存局部变量等一些信息。当有信息需要保存到栈中时,MCU会自动更新SP指针,使SP指针指向最后一个入栈元素,那么程序就可以根据SP指针来从栈中存取信息。对于ARM的Cortex-M内核提供了两个栈空间,这两个栈空间的堆栈指针分别是MSP(主堆栈指针)和PSP(进程堆栈指针)。在Free RTOS中MSP是给系统栈空间使用的,而PSP是给任务栈使用的,也就是说,FreeRTOS任务的栈空间是通过PSP指向的,而在进入中断服务函数时,则是使用MSP指针。当使用不同的堆栈指针时,SP会等于当前使用的堆栈指针。

2. 为什么是0xE00ED08?

0xE00ED08是VTOR(向量表偏移寄存器)的地址,VTOR中保存了向量表的偏移地址。一般来说向量表其实是从0x00000000开始的,但是在有些情况下,可能需要修改或重定向向量表的首地址,因此ARM Cortex-M提供了VTOR对向量表进行重定向。而向量表是用来保存中断异常的入口函数地址,即栈顶地址的,并且向量表中的第一个字保存的就是栈底的地址,在start_stm32xxxxx.s文件中有如下定义:

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved

以上就是向量表的部分内容,可以看到向量表的第一个元素就是栈指针的初始值,也就是栈底地址。

在了解了这两个问题之后,接下来再来看看代码。首先是获取 VTOR 的地址,接着获取
VTOR 的值,也就是获取向量表的首地址,最后获取向量表中第一个字的数据,也就是栈底指
针了。
  • 在获取了栈顶指针后,将MSP指针重新赋值为栈底指针。这个操作相当于丢弃了程序之前保存在栈中的数据,因为FreeRTOS从开启任务调度器到启动第一个任务都是不会返回的,是一条不归路,因此将栈中的数据丢弃,也不会有影响。
  • 重新赋值MSP后,接下来就重新使能全局中断,因为之前在函数vTaskStartScheduler()中关闭了受FreeRTOS管理的中断。
  • 最后使用SVC指令,并传入系统调用号0,触发SVC中断

vPortSVCHandler()函数

当使能了全局中断,并且手动触发了SVC中断之后,就会进入到SVC的中断服务函数中。SVC的中断服务函数为vPortSVCHandler(),该函数在port.c文件中有定义,具体的代码如下所示:

__asm void vPortSVCHandler( void )
{
    /*8字节对齐*/
	PRESERVE8

	/*获取任务栈地址. */
	ldr	r3, =pxCurrentTCB /*r3指向优先级最高的就绪态任务的任务控制块*/
	ldr r1, [r3]           /*r1为任务控制块地址*/
	ldr r0, [r1]            /*r0为任务控制块的第一个元素(栈顶)*/

	/* 模拟出栈,并设置PSP */ 
	ldmia r0!, {r4-r11, r14}  /*任务栈弹出到CPU寄存器*/
	msr psp, r0               /*设置PSP为任务栈指针*/ 
	isb

    /*使能所有中断*/
	mov r0, #0
	msr	basepri, r0

	bx r14    /* 使用 PSP 指针,并跳转到任务函数 */
}

从上面的代码中看出,函数vPortSVCHandler就是用来跳转到第一个任务函数中去的,该函数具体解析如下:

  1. 首先通过pxCurrentTCB获取优先级最高的就绪任务的任务栈地址,优先级最高的就绪态任务就是系统将要运行的任务。pxCurrentTCB是一个全局变量,用于指向系统中优先级最高的就绪态任务的任务控制块。
  2. 接下来通过任务的栈顶指针,将任务栈中的内容出栈到CPU寄存器中,任务栈中的内容在调用任务创建函数的时候,已经初始化好了,然后再设置PSP指针,那么,这么一来,任务的运行环境就准备好了。
  3. 通过往BASEPRI寄存器中写0,允许中断
  4. 最后通过汇编指令bx r14,使CPU跳转到任务的函数中去执行

                r14寄存器为链接寄存器LR,用于保存函数的返回地址。但是在异常或中断处理函数中,r14为EXC_RETURN,这个值的各个比特位有特殊的含义

因为此时是在 SVC 的中断服务函数中,因此此时的 r14 应为 EXC_RETURN ,将 r14 0xd
作或操作,然后将值写入 r14 ,那么就是将 r14 的值设置为了 0xFFFFFFED 0xFFFFFFED (具
体看是否使用了浮点单元),即返回后进入线程模式,并使用 PSP 。这里要注意的是, SVC 中断
服务函数的前面,将 PSP 指向了任务栈。
说了这么多, FreeRTOS 对于进入中断后 r14 EXC_RETURN 的具体应用就是,通过判断
EXC_RETURN bit4 是否为 0 ,来判断任务是否使用了浮点单元。
最后通过 bx r14 指令,跳转到任务的任务函数中执行,执行此指令, CPU 会自动从 PSP
向的栈中出栈 R0 R1 R2 R3 R12 LR PC xPSR 寄存器,并且如果 EXC_RETURN
bit4 0 (使用了浮点单元),那么 CPU 还会自动恢复浮点寄存器。

总结

最后,对于程序是个状态机和计算机是个状态机的理解更加深刻了,最后套用韦老师的一句话:

栈是一个真正的幕后英雄🤣 

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

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

相关文章

腾讯云手动下发指令到设备-用于设备调试

打开腾讯云API Explorer&#xff0c;Publish Msg https://console.cloud.tencent.com/api/explorer?Productiotcloud&Version2021-04-08&ActionPublishMessagehttps://console.cloud.tencent.com/api/explorer?Productiotcloud&Version2021-04-08&ActionPub…

【模电】设置静态工作点的必要性

设置静态工作点的必要性 静态工作点为什么要设置静态工作点 静态工作点 在放大电路中&#xff0c;当有信号输入时&#xff0c;交流量与直流量共存。将输入信号为零、即直流电源单独作用时晶体管的基极电流 I B I\tiny B IB、集电极电流 I C I\tiny C IC、b - e间电压 U B E U\t…

语义分割网络FCN

语义分割是一种像素级的分类&#xff0c;输出是与输入图像大小相同的分割图&#xff0c;输出图像的每个像素对应输入图像每个像素的类别&#xff0c;每一个像素点的灰度值都是代表当前像素点属于该类的概率。 语义分割任务需要解决的是如何把定位和分类这两个问题一起解决&…

佛罗里达大学利用神经网络,解密 GPCR-G 蛋白偶联选择性

内容一览&#xff1a;G 蛋白偶联受体 (GPCRs) 是一种将细胞膜外的刺激&#xff0c;传递到细胞膜内的跨膜蛋白&#xff0c;广泛参与到人体生理活动当中。近日&#xff0c;佛罗里达大学的研究者测定了 GPCRs 和 G 蛋白的结合选择性&#xff0c;并开发了预测二者选择性的算法&…

kubernetes监控GPA安装部署

本文在于指导如何对k8s的监控GPA(Grafana&#xff0c;prometheus以及alertmanager)进行安装部署。 1. 介绍 Prometheus 在真正部署Prometheus之前&#xff0c;应了解一下Prometheus的各个组件之间的关系及作用&#xff1a; 1&#xff09;MertricServer&#xff1a;是k8s集群…

朋友圈7大黄金发圈时间

众所周知&#xff0c;朋友圈运营是私域运营必不可少的重要环节。 因为做好朋友圈运营&#xff0c;能够打造形成高质量、高价值的私域流量&#xff0c;加快实现用户成交。 那么如何形成一个吸粉又吸金的人设&#xff0c;做出高质量的朋友圈发圈内容呢&#xff1f; 那么如何确保能…

SSM整合(注解版)

SSM 整合是指将学习的 Spring&#xff0c;SpringMVC&#xff0c;MyBatis 进行整合&#xff0c;来进行项目的开发。 1 项目基本的配置类 1.1 Spring 配置类 这个配置类主要是管理 Service 中的 bean&#xff0c;controller 层的 bean 对象是 SpringMVC 管理的 package cn.ed…

二极管:二极管的基本原理

一、认识导体、绝缘体、半导体 什么是导体&#xff1f; 导体 conductor &#xff0c;是指电阻率很小&#xff0c;且容易传导电流的物质。导体中存在大量可自由移动的带电粒子&#xff0c;也称为载流子。在外电场的作用下&#xff0c;载流子作定向运动&#xff0c;形成电流。 …

安装配置JDK1.8

JDK1.8的下载及配置 1.进入甲骨文官网甲骨文官网往下翻找到java8并且点击windows. 2.下载Java8必须登录账号 3下载完后点击进入安装&#xff0c;直接下一步就可以&#xff0c;记住这个路径。 4.右击我的电脑进入环境配置&#xff0c;新增变量。 CLASSPATH .;%JAVAHOME%\lib;…

3.C程序编译步骤

目录 1 预处理 2 编译 3 汇编 4 链接 5 文件大小情况 依次执行下面4个步骤 预处理 将所有头文件展开&#xff0c;比如stdio.h等&#xff0c;展开就相当于把stdio.h中的所有代码粘贴到你的代码里。将所有的宏文件展开&#xff0c;像stdio.h是官方定义的头文件&#x…

C# - Opencv应用(3) 之矩阵Mat使用[图像截取粘贴、ROI操作、位运算、数学计算]

C# - Opencv应用&#xff08;3&#xff09; 之矩阵Mat使用[图像截取粘贴、ROI操作、位运算、数学计算] 图像读取&#xff0c;大小、截取、位运算图像ROI操作&#xff1a;粘贴赋值、滤波图像数学计算部分结果如下&#xff1a; 1.图像读取&#xff0c;大小、截取、位运算 //图…

计算机辅助药物设计AIDD-小分子-蛋白质|分子生成|蛋白质配体相互作用预测

文章目录 计算机辅助药物设计AIDD【小分子专题】AIDD概述及药物综合数据库学习机器学习辅助药物设计图神经网络辅助药物设计自然语言处理辅助药物设计药物设计与分子生成 计算机辅助药物设计【蛋白质专题】蛋白质数据结构激酶-Kinase相似性学习基于序列的蛋白质属性预测基于结构…

解决xshell连接诶树莓派中文乱码的问题

系统版本 解决办法 在根目录下找到 /etc/profile 修改profile文件,添加以下两行.以便重启之后也能生效: export LANGzh_CN.utf8 export LC_ALLzh_CN.utf8注意: /etc/profile的修改需要root权限才能修改! 在xshell的编码格式改为UTF-8

一次性客户的笔记总结

创建一次性客户&#xff0c;系统会给出一个客户编码&#xff1b; 每次记账的时候&#xff0c;在录入过账码及客户编码后&#xff0c;点击回车&#xff0c;都需要录入这个客户的详细信息&#xff08;比如 客户名称等&#xff09; 一次性客户的信息存储在BSEC表中&#xff0c;这种…

飞致云1panel + 雷池WAF

可能有许多人都有这个需求&#xff1a;为自己的个人站点套上WAF&#xff0c;增加安全性&#xff0c;本文将介绍如何将1panel面板深度结合长亭雷池防火墙&#xff0c;实现为个人站点套上WAF并且自动续签ssl证书。 前提条件&#xff1a; 服务器IP已绑定域名 完整的1panel环境 …

springboot简单集成上传和下载(带页面)

来学习一下文件上传和下载 一、页面开发 整体思路 登录页 主页 二、库表设计 SET FOREIGN_KEY_CHECKS0;-- ---------------------------- -- Table structure for t_files -- ---------------------------- DROP TABLE IF EXISTS t_files; CREATE TABLE t_files (id int(11) N…

【五分钟】熟练使用numpy.cumsum()函数(干货!!!)

引言 numpy.cumsum()函数用于计算输入数组的累积和。当输入是多维数组时&#xff0c;numpy.cumsum()函数可以沿着指定轴计算累积和。 计算一维数组的累计和 代码如下&#xff1a; # 计算一维数组的累计和 tmp_array np.ones((4,), dtypenp.uint8) # [1, 1, 1, 1] print(&…

java--接口概述

1.认识接口 ①java提供了一个关键字interface&#xff0c;用这个关键字我们可以定义出一个特殊的结构&#xff1a;接口。 ②注意&#xff1a;接口不能创建对象&#xff1b;接口是用来被类实现(implements)的&#xff0c;实现接口的类称为实现类。 ③一个类可以实现多个接口(接…

1、Spring基础概念总结

Spring概述&#xff1a; Spring体系结构 IOC的概念和作用 耦合指的是对象之间的依赖关系&#xff0c;耦合越小越好 以jdbc为例 通过反射来注册驱动&#xff0c;那么会造成驱动名称写死在程序当中&#xff0c;这种结果显然是不太合理的通过配置文件的形式可以解决这种耦合问…

微服务--一篇入门kubernets

Kubernetes 1. Kubernetes介绍1.1 应用部署方式演变1.2 kubernetes简介1.3 kubernetes组件1.4 kubernetes概念 2. kubernetes集群环境搭建2.1 前置知识点2.2 kubeadm 部署方式介绍2.3 安装要求2.4 最终目标2.5 准备环境2.6 系统初始化2.6.1 设置系统主机名以及 Host 文件的相互…