stm32数据对齐、PRESERVE8、freertos堆栈

news2024/9/22 13:29:43

为什么需要数据对齐?

避免数据在内存中跨边界存储,减少读取数据次数,提高效率,本质上是以空间换时间的做法

下图中属于同一水平位置的为同一边界
变量在同一边界里的一次存储周期就可以读取
一旦跨了上下两个边界来存储就需要至少两个存储周期来读取

根据存储器结构,如下图,参考链接:多体并行:高位/低位交叉编址
在这里插入图片描述
CPU数据线有32位一次最多可以从内存读取32数据,这里的一次指一次存储周期
LDR r1 , [pc,#4], 是从pc+#4地址处开始连续读取4个字节的数据到r1寄存器
LDRH r1 , [pc,#4], 是从pc+#4地址处开始连续读取2个字节的数据到r1寄存器
LDRB r1 , [pc,#4] 直接取pc+#4当前那个地址的数据
上述指令都是在一个存储周期里完成的

一般地址线只能确定一个字节所在的地址,而上述指令地址都一样,却不止读到仅仅一个字节的数据,还能读到2个字节或者4个字节的数据,可以看到上述指令除了操作码不一样其它都一样,按照上图的存储结构,指令的地址一样那么体内地址肯定是一样的,体内地址一样就横向选中了同一水平位置的存储,操作码的不同说明控制线输出的信号应该不同,就可以控制一个类似体号的控制器,选中连续的好几个字节的存储
地址线加上控制线就可以实现上述的操作

LDM r1 ,{r0-r1} ; 是将r1指向的内存地址开始连续8个字节的数据存放到r0和r1寄存器中
该指令是在2个存储周期里完成的

详解边界对齐
为什么要内存对齐?字节对齐和边界对齐介绍

对于数据在内存中的位置,需要确定两个因素:大小、起始位置
根据存储器结构,就是通过起始位置和数据大小获取内存中的数据

一、全局变量静态变量,地址从小增大

1、原子类型数据

在这里插入图片描述
按照开始的存储结构的图,short型变量起始位置放在地址1的位置应该也放得下,也能一次性读取,为什么必须得是0,2,4,6.。。。
那是因为放在不好判断地址1和地址3的不同,放在地址3就会跨界,而为了区分地址1和3做出来的电路更为复杂得不偿失,
8字节的也是同理

2、组合类型数据

详细参考链接:
详解边界对齐

// structure C 
typedef struct structc_tag 
{ 
   char        c; 
   double      d; 
   int         s; 
} structc_t; 

在这里插入图片描述
这个结构的大小应该是 sizeof(char) + 7 + sizeof(double) + sizeof(int) = 1 +7 + 8 + 4 = 20

然而,正确答案是24

所以,所有结构的起始存储位置必须是结构中对边界要求最严格的数据类型所要求的位置。

在这里插入图片描述

	struct {
		int a;
		short b;
		int c;
		char d;
	}A;

	struct {
		double a;
		short b;
		int c;
		char d;
	}B;

在这里插入图片描述
上述总结还有另一个表达,组合数据结构的大小是组合结构中最大原子数据大小的整数倍:上述结构B就是20不是8的整数倍,24是8的整数倍

总结:

第一,编译器按照成员列表的顺序给每个成员分配内存.   
第二,当成员需要满足正确的边界对齐时,成员之间用额外字节填充.
第三,结构体的首地址必须满足结构体中边界要求最为严格的数据类型所要求的地址.  (也即首地址为最宽基本类型的整数倍) 
第四,结构体的大小为其最宽基本类型的整数倍.

3、#pragma pack (2)的作用

详细参考链接:为什么要内存对齐?字节对齐和边界对齐介绍。
文章中说的编译器默认4字节对齐是错误的,默认是8字节对齐其它分析都对

这里的指定几字节对齐,意思是数据本身的对齐数大于指定的话就按指定的对齐数来,也就是数据起始地址为指定对齐数的倍数
下面的struct中int a的自身对齐数为4大于指定对齐数2,所以按照2来,它存放的起始地址就为0,2,4,6.。。。这些

在这里插入图片描述

4、结构体中嵌套结构体

#pragma  pack(8)
struct example1 {
    short a;
    long b;
};

struct example2 {
    char c;
    struct example1 e;
    short s;
};

struct example2数据结构的大小是16个字节,这个值是这样计算出来的:
1(char c) + 3(padding) + 8(struct example1 e) + 2(short s) + 2(padding) = 16。

该类型也是按照组合类型数据的规则来计算大小

数据结构内存边界对齐的三条原则

二、局部变量,栈中的变量

不同的数据(或数据结构)按顺序从地址大处向地址低处,同一数据内按顺序地址从小到大
sp要8字节对齐,sp的地址只会是8的倍数,(现象就是每次压栈出栈都是8的倍数个,参考下面的)
栈中的数据与全局数据相同的是大小的计算方法一样,不同的是栈中的数据起始地址不是自身对齐值的倍数而是4的倍数或8的倍数(如果自身对齐值小于4就是4的倍数大于4就是8的倍数)

1、结构体变量加lr寄存器大小小于8字节

typedef struct
{
        char a;
        char b;
} Tchar;

void temp()
{
	
    Tchar a2 = {'!','s'};	
	a2.a = a2.a + a2.b;
	
	printf("the size of struct Tchar  is %d\r\n ",sizeof(a2));
	printf("the address of struct Tchar  is %p\r\n ",&a2);
	printf("the address of struct Tchar a is %p\r\n ",&a2.a);
	printf("the address of struct Tchar b is %p\r\n ",&a2.b);
		
}

int main()
{
	temp();
	
	return 1;
}

在这里插入图片描述

在这里插入图片描述
进入temp()前sp为 0x20000420,执行完Tchar a2 = {‘!’,‘s’};sp为 0x20000418,可以看到汇编指令是执行了
PUSH {r3,lr},执行前是栈指针8字节对齐,执行后也是8字节对齐。
在这里插入图片描述

结构体变量加lr寄存器大小小于8字节,也是要在栈中分配8字节让栈指针8字节对齐

2、结构体变量加lr寄存器大小大于8字节,小于16字节

struct naturalalign
{
char a;
short b;
char c;
};

void temp()
{
	
	struct naturalalign a1 = {'a',511,'5'};			
	a1.b = a1.b + 5;
				
	printf("the size of struct naturalalign is %d\r\n ",sizeof(a1));
				
	printf("the address of struct naturalalign is %p\r\n ",&a1);
	printf("the address of struct naturalalign char a is %p\r\n ",&a1.a);
	printf("the address of struct naturalalign char b is %p\r\n ",&a1.b);
	printf("the address of struct naturalalign char c is %p\r\n ",&a1.c);
		
}

int main()
{			
	temp();
	return 1;
}

在这里插入图片描述

在这里插入图片描述
进入temp()前sp为 0x20000420,执行完struct naturalalign a1 = {‘a’,511,‘5’}; sp为 0x20000410,可以看到汇编指令是执行了
PUSH {r2-r4,lr},执行前是栈指针8字节对齐,执行后也是8字节对齐。
在这里插入图片描述

结构体变量加lr寄存器大小大于8字节小于16字节,也是要在栈中分配16字节让栈指针8字节对齐

3、结构体数组加lr寄存器大小大于8字节,等于16字节

struct naturalalign
{
char a;
short b;
char c;
};

void temp()
{
	
	struct naturalalign a1[2] = {'a',511,'5'};

				
	printf("the size of struct naturalalign is %d\r\n ",sizeof(a1));			
	printf("the address of struct naturalalign is %p\r\n ",&a1);
	printf("the address of struct naturalalign char a is %p\r\n ",&a1[0].a);
	printf("the address of struct naturalalign char b is %p\r\n ",&a1[0].b);
	printf("the address of struct naturalalign char c is %p\r\n ",&a1[0].c);
	printf("the address of struct naturalalign char a is %p\r\n ",&a1[1].a);
	printf("the address of struct naturalalign char b is %p\r\n ",&a1[1].b);
	printf("the address of struct naturalalign char c is %p\r\n ",&a1[1].c);		
}

int main()
{

	temp();
	return 1;
}

在这里插入图片描述
在这里插入图片描述

进入temp()前sp为 0x20000420,执行完struct naturalalign a1[2] = {‘a’,511,‘5’}; sp为 0x20000410,可以看到汇编指令是执行了
PUSH {r1-r3,lr},执行前是栈指针8字节对齐,执行后也是8字节对齐。

在这里插入图片描述

结构体数组加lr寄存器大小等于16字节,也是要在栈中分配16字节让栈指针8字节对齐

4、两个结构体加lr寄存器大小大于8字节,等于16字节

struct naturalalign
{
char a;
short b;
char c;
};

void temp()
{
	
	struct naturalalign a1 = {'a',511,'5'};
	struct naturalalign a2 = {'s',511,'b'};
	printf("the size of struct naturalalign is %d\r\n ",sizeof(a1));

				
	printf("the address of struct naturalalign is %p\r\n ",&a1);
	printf("the address of struct naturalalign char a is %p\r\n ",&a1.a);
	printf("the address of struct naturalalign char b is %p\r\n ",&a1.b);
	printf("the address of struct naturalalign char c is %p\r\n ",&a1.c);
	printf("the address of struct naturalalign char a is %p\r\n ",&a2.a);
	printf("the address of struct naturalalign char b is %p\r\n ",&a2.b);
	printf("the address of struct naturalalign char c is %p\r\n ",&a2.c);
	}


int main()
{

				
				
	temp();
	return 1;
}

在这里插入图片描述
在这里插入图片描述
进入temp()前sp为 0x20000420,执行完struct naturalalign a1 = {‘a’,511,‘5’};
struct naturalalign a2 = {‘s’,511,‘b’};
sp为 0x20000408,可以看到汇编指令是执行了
PUSH {r0-r4,lr} ,执行前是栈指针8字节对齐,执行后也是8字节对齐。
在这里插入图片描述

结构体数组加lr寄存器大小等于16字节,也是要在栈中分配16字节让栈指针8字节对齐

这里做一下对比,如果该两个结构体存放在全局区,是怎么样

struct naturalalign
{
char a;
short b;
char c;
};

struct naturalalign a1 = {'a',511,'5'};
	struct naturalalign a2 = {'s',511,'b'};
	
void temp()
{			
	printf("the size of struct naturalalign is %d\r\n ",sizeof(a1));				
	printf("the address of struct naturalalign is %p\r\n ",&a1);
	printf("the address of struct naturalalign char a is %p\r\n ",&a1.a);
	printf("the address of struct naturalalign char b is %p\r\n ",&a1.b);
	printf("the address of struct naturalalign char c is %p\r\n ",&a1.c);
	printf("the address of struct naturalalign char a is %p\r\n ",&a2.a);
	printf("the address of struct naturalalign char b is %p\r\n ",&a2.b);
	printf("the address of struct naturalalign char c is %p\r\n ",&a2.c);		
}

int main()
{

				
	temp();	
	return 1;
}

在这里插入图片描述
在这里插入图片描述

三、stm32中涉及到数据对齐的地方

0、硬件结构和指令代码

stm32的sp寄存器和pc寄存器的后两位都是为0的,栈指针起码是保持4字节对齐的
由于Cortex-M3架构采用16位和32位指令集,因此其PC指针地址是4字节对齐的,换言之其最低2位一定全是0。指令代码都是4字节对齐的

CM3硬件为了保持8字节对齐,在中断发生时会自动入栈8字节的寄存器数据 退出中断时会自动出栈8字节的寄存器数据

这也是CM3多任务执行的关键

详情可以查看2、task.c中prvInitialiseNewTask()

1、startup_stm32f10xxx.s启动文件

; Amount of memory (in bytes) allocated for Stack
;为Stack分配的内存量(以字节为单位)
; Tailor this value to your application needs
;根据您的应用需求定制此值
; <h> Stack Configuration
;   <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Stack_Size      EQU     0x800      ;2K

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp


ALIGN=3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐(SP mod 8 = 0)。

PRESERVE8
THUMB

PRESERVE8 指定当前文件保持堆栈八字节对齐。
THUMB 表示后面的指令是 THUMB 指令集 ,CM4 采用的是 THUMB -2指令集

这里的PRESERVE8 仅仅是一个声明,声明该文件里的堆栈是8字节对齐的,也即__initial_sp的值是8的倍数的地址,怎么保证__initial_sp的值是8的倍数呢?
首先分配的内存段首地址是8的倍数(ALIGN=3),然后分配的size 2k也是8的倍数,栈顶也就是__initial_sp就是8的倍数

为什么要保持堆栈的8字节对齐?

PRESERVE8的参考资料:
为什么要加 REQUIRE8 and PRESERVE8? 栈的8字节对齐
一般4字节对齐也没什么问题,但是在调用第三方库文件,比如在stm32中调用microlib 里的printf()输出float型变量时就会产生问题
在这里插入图片描述
为什么会造成上述原因?
根据我粗浅的理解:第三方库为了保持兼容性,既能在32位上使用也能在64位上使用,该第三方库采用的8字节对齐的方式,stm32还是采用4字节对齐就会造成数据跨界
对堆栈8字节对齐问题的讨论

STM32 终极字节对齐解析

2、task.c中prvInitialiseNewTask()

#define portBYTE_ALIGNMENT_MASK    ( 0x0007 )

pxTopOfStack = &( pxNewTCB->pxStack[ ulStackDepth - ( uint32_t ) 1 ] );
pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,
                                     TaskFunction_t pxCode,
                                     void * pvParameters )
{
    /* Simulate the stack frame as it would be created by a context switch
     * interrupt. */
    pxTopOfStack--;                                                      /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */
    *pxTopOfStack = portINITIAL_XPSR;                                    /* xPSR */
    pxTopOfStack--;
    *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
    pxTopOfStack--;
    *pxTopOfStack = ( StackType_t ) prvTaskExitError;                    /* LR */

    pxTopOfStack -= 5;                                                   /* R12, R3, R2 and R1. */
    *pxTopOfStack = ( StackType_t ) pvParameters;                        /* R0 */
    pxTopOfStack -= 8;                                                   /* R11, R10, R9, R8, R7, R6, R5 and R4. */

    return pxTopOfStack;
}

创建任务栈时:
1、分配任务栈空间
通过将栈顶指针变量的后3位变为0,将该指针指向的地址8字节对齐
2、手动初始化栈空间,初始化后还是8字节对齐。
后续R11, R10, R9, R8, R7, R6, R5 and R4手动出栈,保持8字节对齐
xPSP, PC, LR,R12以及R3~R0寄存器的值后续会根据psp栈针自动出栈,保持8字节对齐
每个任务最开始运行时的栈顶还是保持着最开始分配的栈顶

具体代码如下:

__asm void prvStartFirstTask( void )
{
	/*1首先是使用了 PRESERVE8,进行 8 字节对齐,这是因为,
	栈在任何时候都是需要 4 字节对齐的,而在调用入口得8字节对齐,
	在进行C编程的时候,编译器会自动完成的对齐的操作,而对于汇编,
	就需要开发者手动进行对齐。*/
	
	/*8字节对齐*/
	PRESERVE8
 
	/* Use the NVIC offset register to locate the stack. */
	
	/*向量表开始寄存器地址*/
	ldr r0, =0xE000ED08
	/* 获取向量表的值*/
	ldr r0, [r0]
	/*获取MSP的初始值(栈底指针)*/
	ldr r0, [r0]
 
	/* Set the msp back to the start of the stack. */
	/*初始化MSP*/
	msr msp, r0
	/* Globally enable interrupts. */
	
	/*使能全局中断*/
	cpsie i
	cpsie f
	dsb
	isb
	/* Call SVC to start the first task. */
	
	/*触发svc中断启动第一个任务*/
	svc 0
	nop
	nop
}
__asm void vPortSVCHandler( void )
{
  /*在进入异常前 会将 把xPSP, PC, LR,R12以及R3~R0寄存器的值压入栈 ,由硬件完成
  因为这个函数是不返回,这个可以不关心。
  */
    extern pxCurrentTCB;
    
    PRESERVE8

	ldr	r3, =pxCurrentTCB	/* 加载pxCurrentTCB的地址到r3 */
	ldr r1, [r3]			     /* 加载pxCurrentTCB到r1 */
	ldr r0, [r1]			 /* 加载pxCurrentTCB指向的值到r0,目前r0的值等于第一个任务堆栈的栈顶
	ldmia r0!, {r4-r11}		/* 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增 */
	msr psp, r0				/* 将r0的值,即任务的栈指针更新到psp 后面异常退出时,根据PSP进行出栈,                        就是前面任务栈初始化值出栈给到寄存器*/
	isb
	mov r0, #0              /* 设置r0的值为0 */
	msr	basepri, r0         /* 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽 */
	orr r14, #0xd           /* 当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
                               使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态 */
    
	bx r14                  /* 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:
                               xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
                               同时PSP的值也将更新,即指向任务栈的栈顶 */
}

当任务切换时,和任务栈初始化入栈到任务启动出栈差不多,也是手动和自动两部分,都能保持8字节对齐
具体入栈出栈的流程如下:
R11, R10, R9, R8, R7, R6, R5 and R4手动出栈,保持8字节对齐
xPSP, PC, LR,R12以及R3~R0寄存器的值后续会根据psp栈针自动出栈,保持8字节对齐
在这里插入图片描述
代码如下:

__asm void xPortPendSVHandler( void )
{
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8

    /* 当进入PendSVC Handler时,上一个任务运行的环境即:
       xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
       这些CPU寄存器的值会自动保存到任务的栈中,剩下的r4~r11需要手动保存 */
    /* 获取任务栈指针到r0 */
	mrs r0, psp
	isb

	ldr	r3, =pxCurrentTCB		/* 加载pxCurrentTCB的地址到r3 */
	ldr	r2, [r3]                /* 加载pxCurrentTCB到r2 */

	stmdb r0!, {r4-r11}			/* 将CPU寄存器r4~r11的值存储到r0指向的地址 */
	str r0, [r2]                /* 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针 */				
   //以上 上下文保存

	stmdb sp!, {r3, r14}        /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,
                                  调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护;
                                  R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护 */
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY    /* 进入临界段 */
	msr basepri, r0
	dsb
	isb
	bl vTaskSwitchContext       /* 调用函数vTaskSwitchContext,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */ 
	mov r0, #0                  /* 退出临界段 */
	msr basepri, r0
	ldmia sp!, {r3, r14}        /* 恢复r3和r14 */

	ldr r1, [r3]
	ldr r0, [r1] 				/* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
	ldmia r0!, {r4-r11}			/* 出栈 */
	msr psp, r0
	isb
	bx r14          /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、
                    使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,
                    然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,
                    当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
	nop
}

四、heap堆内存

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

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

相关文章

干爆源码系列之Step by step lldb/gdb调试多线程

Step by step lldb/gdb调试多线程 0.叙谈1.断点分析2.多线程切换 2.1 并发队列 2.1.1 两次入队 2.2 线程调度 2.2.1 执行build端子MetaPipeline 2.2.1.1 Thread6调度第一个PipelineInitializeTask 2.2.1.2 Thread7调度第二个Pipelin…

TypeScript 数据联合类型的解读

概念&#xff1a; 联合类型&#xff08;Union Types&#xff09;表示取值可以为多种类型中的一种&#xff0c;或者也可以理解将多个类型合并为一个类型对变量进行注解。 语法结构&#xff1a; 联合类型使用 | 分隔每个类型。 let 变量&#xff1a;类型1 | 类型2 | 类型3… 案列…

基于Java校园代购服务订单系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

你的前端技术是否能通过这些高频面试题?

文章目录 1.储存了某个数据到 localStorage&#xff0c;立即进行 localStorage.getItem&#xff0c;能否取到值&#xff1f;2.实现异步的方式有几种3.异步不阻塞4.选择 div 的第二个子元素5.display: none 和 visibility: hidden 的区别6.如果想要让一个元素的宽高包含元素的 b…

【初识C语言】选择语句+循环语句+函数+数组

文章目录 前言1. 选择语句2. 循环语句3. 函数4. 数组 前言 C语言是一门结构化的程序设计语言 顺序结构&#xff1b; 选择结构&#xff1b; 循环结构。 1. 选择语句 生活中处处面临着选择&#xff0c;如果你好好学习&#xff0c;校招时拿一个好offer&#xff0c;走上人生巅峰。…

关于程序员的工作总结

程序员工作总结篇1 从我x月x日进入公司到现在已经过去一年了&#xff0c;从一名刚刚结束实习的学生到一名独立的开发人员&#xff0c;角色改变了&#xff0c;职责也改变了。虽然已经预计了工作之中会有很多困难&#xff0c;可是在实际的项目开发中&#xff0c;自己所遇到远远不…

(超级详细)如何在Mac OS上的VScode中配置OpenGL环境并编译

文章目录 安装环境下载GLAD与GLFW一、下载GLAD二、下载GLFW 项目结构配置测试程序与项目的编译测试可执行文件HelloGL 安装环境 机器&#xff1a;macbook air 芯片&#xff1a; M1芯片&#xff08;arm64&#xff09; macOS&#xff1a;macOS Ventura 13.4 VScode version&#…

Pytorch数据类型Tensor张量操作(操作比较全)

文章目录 Pytorch数据类型Tensor张量操作一.创建张量的方式1.创建无初始化张量2.创建随机张量3.创建初值为指定数值的张量4.从数据创建张量5.生成等差数列张量 二.改变张量形状三.索引四.维度变换1.维度增加unsqueeze2.维度扩展expand3.维度减少squeeze4.维度扩展repeat 五.维度…

SpringCloud Alibaba入门5之使用OpenFegin调用服务

我们继续在上一章的基础上进行开发 SpringCloud Alibaba入门4之nacos注册中心管理_qinxun2008081的博客-CSDN博客 Feign是一种声明式、模板化的HTTP客户端。使用Feign&#xff0c;可以做到声明式调用。Feign是在RestTemplate和Ribbon的基础上进一步封装&#xff0c;使用RestT…

SAP从入门到放弃系列之BOM行项目-虚拟装配-Part4

文章目录 虚拟组件&#xff08;Phantom assemblies&#xff09;&#xff1a;作用&#xff1a;BOM中虚拟件维护的方式&#xff1a; 物料主数据维度BOM组件维度&#xff08;数据优先级最高&#xff09; BOM组件的展开类型&#xff1a;BOM组件的特殊采购类数据测试示例&#xff1…

基于open62541库的OPC UA协议节点信息查询及多节点数值读写案例实践

目录 一、OPC UA协议简介 二、open62541库简介 三、 opcua协议的多点查询、多点读写案例服务端opcua_server 3.1 opcua_server工程目录 3.2 程序源码 3.3 工程组织文件 3.4 编译及启动 四、opcua协议的多点查询、多点读写案例客户端opcua_client 4.1 opcua_client工程目录 4…

医院管理系统源码PACS超声科室源码DICOM影像工作站

一、医学影像系统&#xff08;PACS&#xff09;是一种应用于医院影像科室的系统&#xff0c;主要任务是将日常产生的各种医学影像&#xff08;如核磁、CT、超声、X光机、红外仪、显微仪等设备产生的图像&#xff09;通过各种接口&#xff08;模拟、DICOM、网络&#xff09;以数…

社区活动 | OpenVINO™ DevCon 中国系列工作坊第二期 | 使用 OpenVINO™ 加速生成式 AI...

生成式 AI 领域一直在快速发展&#xff0c;许多潜在应用随之而来&#xff0c;这些应用可以从根本上改变人机交互与协作的未来。这一最新进展的一个例子是 GPT 模型的发布&#xff0c;它具有解决复杂问题的能力&#xff0c;比如通过医学和法律考试这种类似于人类的能力。然而&am…

Android Studio实现推箱子小游戏

项目目录 一、项目概述二、开发环境三、详细设计四、运行演示五、项目总结 一、项目概述 推箱子是一款非常受欢迎的益智游戏&#xff0c;游戏的玩法简单&#xff0c;但是需要玩家具备一定的逻辑思维能力和空间感知能力&#xff0c;因此深受广大玩家的喜爱。在游戏中&#xff0…

正点原子F4HAL库串口中断再解读

七步走&#xff0c;参考usart.c文件 void HAL_UART_MspInit(UART_HandleTypeDef *huart) 这个函数进行了&#xff08;1&#xff09;、&#xff08;2&#xff09;、&#xff08;3&#xff09;、&#xff08;5&#xff09;中的使能中断 void uart_init(u32 bound)函数进行了&…

『手撕 Mybatis 源码』07 - Proxy 对象创建

Proxy 对象创建 问题 sqlSession.getMapper(UserMapper.class) 是如何生成的代理对象&#xff1f; Mapper 代理方式初始化完成后&#xff0c;下一步进行获取代理对象来执行 public class MybatisTest {/*** 问题2&#xff1a;sqlSession.getMapper(UserMapper.class); 是如…

EMC学习笔记(五)传输线模型及反射、串扰

1.概述 在高速数字电路PCB设计中&#xff0c;当布线长度大于20分之一波长或信号延时超过6分之一信号上升沿时&#xff0c;PCB布线可被视为传输线。传输线有两种类型:微带线和带状线。与EMC设计有关的传输线特性包括:特征阻抗、传输延迟、固有电容和固有电感。反射与串扰会影响…

2023年第1季社区Task挑战赛贡献者榜单

基于数字身份凭证的业务逻辑设计&#xff0c;贡献了发放数字身份凭证的参考实现&#xff1b;提供企业碳排放、慈善公益等智能合约库业务场景案例&#xff1b;体验最新发布的WeCross-BCOS3-Stub&#xff0c;跟社区核心开发者碰撞想法并给出自己的见解……这些精彩贡献展现出社区…

<C++项目>高并发内存池

项目介绍&#xff1a; 原型是goole的开源项目tcmalloc(全称:Thread-Caching Malloc),用于替代系统的内存分配相关的函数(malloc, free).知名度非常高。 项目要求知识储备和难度&#xff1a; 会用到C/C、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁等等…

设计模式—“行为变化”

在组件构建过程中,组件行为的变化经常导致组件本身剧烈的变化。“行为变化”模式将组件的行为和组件本身进行解耦,从而支持组件行为的变化,实现两者之间的松耦合。 典型模式有:Command、Visitor 一、Command 动机 在软件构建过程中,"行为请求者"与“行为实现…