基于STM32使用嵌套结构体摸索内存对齐


用来描述对象特征的各类信息,通常会被整合成记录,而记录使得信息组织、表示以及存储变得轻松。而记录由字段组成,不同的字段用来表示不同的信息,C中可以通过结构体来组合这些字段(成员)。

一、结构体的声明以及初始化

联合体实际上来说是特殊数据结构的一类,通过关键词struct来定义。定义结构体有几种方法:

struct FUNC1//仅声明一个模板
{
	char*  	name;
	uint32_t		height;
	uint32_t		length;
};

第一种方法,将会定义一个名称为struct FUNC1的结构体模板,它里面包含了一个指向字符的指针,两个整形的变量。模板并不代表已经分配了空间,或许可以说定义了一个叫FUNC1的新类型。如果要声明一个这样的变量,那么需要借助关键词struct FUNC1 variable1来完成。

struct FUNC1//声明一个模板的同时声明一个变量
{
	char*  	name;
	uint32_t		height;
	uint32_t		length;
}variable1;

这种写法,实际上与第一种方法没什么差别,只是在声明模板的过程中同时声明了一个变量,此时这个变量已经分配了相应的空间。

typedef struct FUNC1//声明一个模板并将这个模板的名称重定义
{
	char*  	name;
	uint32_t		height;
	uint32_t		length;
}FUNC2;

第三种写法则是在第一种写法的基础上利用了关键词typedef,在看这段的时候,可以将其分解为两个步骤:
1、声明了一个名称为FUNC1的模板。
2、将类型 struct FUNC1 映射到FUNC2中。
由此一来,声明一个变量除了使用struct FUNC1 variable1以外,还能使用来FUNC2 variable1实现。

typedef struct//直接声明一个模板但名称为FUNC2
{
	char*  	name;
	uint32_t		height;
	uint32_t		length;
}FUNC2;

这第四中写法,可以和其他几种进行对比,是常用的方法,省去了声明的struct FUNC1的功夫,新的类型名称也不需要前缀struct,变量类型更具隐蔽性。

说完了结构体的声明,那么对应的变量在空间中究竟是怎么样排放的?
在这里插入图片描述

结构体的空间排放由模板中成员的声明顺序决定,从低地址向高地址生长,而在STM32中内存使用的是小端模式,故变量variable1的内存情况如上图所示,一个指针的长度与地址总线长度一致,占用32bit,整形同理。但是这个模板并不是最佳的说明案例,先介绍联合体再进入主题。

结构体变量的初始化可以分为两种,第一种是全成员的赋值初始化,例如variable1变量:

FUNC2 variable1={"xx",20,20};

从C99之后,添加了一个新的初始化方案:标准C的标记化结构初始语法,实际上在Linux内核中这个用法比较常见,例如驱动程序中的file_operation结构体就是使用这个方案。

FUNC2 variable1={
.length = 20,
.height = 20
};

这个方案大幅度的灵活了初始化的操作,例如结构体的有些成员在一开始就有含义,那么就只需要初始化相应的成员。

顺便一提,结构体的成员一般情况下必须是大小确认的,即不允许可变长度的变量在结构体中。若是结构体中的最后一个成员,则允许该变量为可变数组。但是该结构体模板的空间大小并没有包含这个弹性可变数字成员,所以在使用malloc时,需要额外分配所需的空间:

FUNC2*	variable1 = malloc(sizeof(FUNC2)+sizeof(float)*10);

二、结构体成员的内存对齐规则

结构体成员的内存对齐法则在此处不做详细介绍,位字段的内存对齐才是讨论的重点。
简言之就是结构体中成员的取址方式可能受到其他成员的影响:

typedef struct
{
	char  	name;
	uint32_t		height;
	uint16_t  		length;
}Type1;
typedef struct
{
	char  	name;
	uint16_t  		length;
	uint32_t		height;

}Type2;
Type1 variable3;
Type2 variable4;

Type1类型的变量,空间存放有稍许不同,规则规定第一个成员name无需任何的偏移。而第二个成员height的放置则和第一个成员的长度有关,height是一个32位(4个字节)的变量,它的地址必须满足是4的倍数,而第一个成员只占了一个字节,那么后面的3个字节都会被编译器填充(padding),所以height偏移了3个字节。第三个成员length长度为2个字节,当前的地址满足被2整除的要求,所以不用偏移。

照这么算起来,这个模板的总空间长度应该是10个字节,运行代码sizeof(Type1) 得到12个字节。这是由于编译器为了cpu能够保证所有的成员只通过一次地址访问就可以获得,所以整个结构体都默认以最大地址长度为单位,所以在尾部也填充了2个字节
在这里插入图片描述
需要留意的是系统填充的空间由于是匿名状态,所以访问不到,但是可以通过强制类型转换进行访问以及修改。

三、联合体以及嵌套结构体的用法

联合体是借助关键词union实现的,所谓联合体实际上就是共用一块内存,而联合体所占用的内存根据成员中占用的最大内存来确认。在声明和初始化上,联合体也支持结构体相关的操作。

typedef union
{
	uint8_t data_8[5];
	uint16_t data_16[2];
	uint32_t data_32;
}FUNC2;

FUNC2 variable2;

在这里插入图片描述
此时若对变量variable2使用函数sizeof可以得到它的长度为5。联合体在数据协议管理上有着重要的用途,通常在面对一些定制且复杂的协议,根据数据去一个个解析是相当麻烦的,但若是配合联合体嵌套则简化很多。例如:在一个字节中隐藏多个状态位:
在这里插入图片描述

若每次接收到这一帧数据时都通过位计算则过于繁琐,那么试一下嵌套以及位字段

typedef union
{
	uint8_t data;
	struct
	{
		uint8_t ERR_BIT : 1;
		uint8_t STATUS_BIT : 3;	
		uint8_t IT_BIT : 1;
		uint8_t OVER_BIT : 1;
		uint8_t BUSY_BIT : 1;
		uint8_t ENABLE_BIT : 1;
	};
}STATUS;

这个嵌套结构体涉及到了两个概念,一个是位段的概念,另一个是小端模式的概念。

位段

在通常情况下,计算机处理的最小单元是字节,一个字节是8bit。但是有时候某些开关量的状态并不需要占用一个字节的空间,为了提高空间效率,便产生了位段的概念,位段可以指定一个变量占用的bit。
位段声明的方式为:
类型 成员名称: 宽度;
类型是用来解释该成员的方式,宽度则是这个成员占有的位数。实际上,在声明的过程中,成员名称是可选的,若是该成员匿名了,则在结构体中无法引用它,一般该方式用来填充空间(可以设成0宽度,编译器将默认从下一个可寻址内存地址来读取成员)。

例如上述STATUS联合体的匿名成员则指定了位,若将一个字节数据赋值给联合体中的data就可以等价于:

ERR_BIT 	= (uint8_t)(data & 0x01);
STATUS_BIT 	= (uint8_t)(data & (0x07<<1));
IT_BIT 		= (uint8_t)(data & (0x01<<4));
OVER_BIT 	= (uint8_t)(data & (0x01<<5));
BUSY_BIT 	= (uint8_t)(data & (0x01<<6));
ENABLE_BIT 	= (uint8_t)(data & (0x01<<7));

位段的操作可以放到任意的结构体中,包括联合体,但是表现出来的形式则不会一样

typedef union
{
	uint8_t data;
	uint8_t ERR_BIT : 1;
	uint8_t STATUS_BIT : 3;	
	uint8_t IT_BIT : 1;
	uint8_t OVER_BIT : 1;
	uint8_t BUSY_BIT : 1;
	uint8_t ENABLE_BIT : 1;
}STATUS_union;

若将一个字节数据赋值给联合体中的data就可以等价于:

ERR_BIT 	= (uint8_t)(data & 0x01);
STATUS_BIT 	= (uint8_t)(data & (0x07));
IT_BIT 		= (uint8_t)(data & (0x01));
OVER_BIT 	= (uint8_t)(data & (0x01));
BUSY_BIT 	= (uint8_t)(data & (0x01));
ENABLE_BIT 	= (uint8_t)(data & (0x01));

这是因为这两个数据类型的本质区别,结构体的成员内存地址是向上长的,而联合体的成员内存地址是固定的。

小端模式

与小端模式对应的是大端模式,他们代表两种相反的数据存放规律。以往最常用的是大端模式,即一组数据的高字节部分放在地址的最前面(起始地址),而低字节则放在地址的最后(最终地址),例如数据0xabcd,高字节是0xab,低字节是0xcd,若是大端模式:
在这里插入图片描述

大端模式这样的排序方式比较符合人类的直觉。若是小段模式:
在这里插入图片描述

而stm32是小端模式,那么数据的低位将会被放在起始地址,根据结构体的定义,成员的空间排放顺序与声明顺序一致,也是从小到大。

三、位段数据跨字节单位导致的内存对齐现象

上述介绍的只是寻常的结构体、联合体的运用,如若出现某些有效的状态位需要跨越字节单位会怎样?例如:
在这里插入图片描述
测试代码(环境: vscode 64位,stm32 32位 ,ubuntu 64位):

typedef union 
{
	uint16_t 	code;
	
	struct
	{
		uint8_t 		ENABLE			    :	1;
		uint8_t 	    Channel				:	8;
		uint8_t 	    STATUS				:	7;
    };	

}Type2;//热电偶配置结构体
Type2 v3;
void main(char argc,char* argv[])
{
    v3.code = 0xa5da;
     printf("v3.code = %x \n v3.ENABLE = %x \n v3.Chunnel = %x \n v3.STATUS = %x \n size of v3  = %d \n ",v3.code,v3.ENABLE,v3.Channel,v3.STATUS,sizeof(Type2));
}

输出:
v3.code = a5da
v3.ENABLE = 0
v3.Channel = a5
v3.STATUS = 0
size of v3 = 4
按照原本的想法:
code = 1010 0101 1101 1010 b,ENABLE = 0 ;Channel=0xed;STATUS=0x52; 造成差异的原因是在于第二个成员Channel占用了8位,但是当前字节只剩下7位了(第一个成员占了一位),由于当前空间不合适,所以编译器将会让这个成员从下一个字节开始取值。所以出现了运行结果的情况。为了进一步验证,对STATUS成员进行赋值并重新打印

void main(char argc,char* argv[])
{
    v3.code = 0xa5da;
    v3.STATUS = 0x75;
     printf("v3.code = %x \n v3.ENABLE = %x \n v3.Chunnel = %x \n v3.STATUS = %x \n size of v3  = %d \n ",v3.code,v3.ENABLE,v3.Channel,v3.STATUS,sizeof(Type2));
}

输出:
v3.code = a5da
v3.ENABLE = 0
v3.Channel = a5
v3.STATUS = 75
size of v3 = 4
可以看到STATUS成员被成功赋值,但是联合体中code并没有体现出来,这是为什么?

实际上,从Type2类型的空间长度中可以窥见,联合体中的匿名体的空间排布应该是
在这里插入图片描述

从匿名体的角度去看,它只需要填充1位,因为他的成员中空间长度最大也只是一个字节,所以只需要保证内存长度被1整除即可。但是从联合体的角度去看,他的成员code是2个字节的空间长度,所以他不得不填充2个字节。而之所以打印code时没有表现出STATUS的改变,是因为code的限定词是2个字节的短整型。下面放开限定,直接输出v3的数值。读者有兴趣也可以直接在联合体中添加一个32位的成员,再打印验证。

void main(char argc,char* argv[])
{
    v3.code = 0xa5da;
    v3.STATUS = 0x75;
     printf("v3.code = %x \n v3.ENABLE = %x \n v3.Chunnel = %x \n v3.STATUS = %x \n size of v3  = %d \n ",v3,v3.ENABLE,v3.Channel,v3.STATUS,sizeof(Type2));
}

输出:
v3.code = 75a5da
v3.ENABLE = 0
v3.Chunnel = a5
v3.STATUS = 75
size of v3 = 4
对于此性质,并没有太好的办法,大佬们可以分享自己的想法。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: STM32L4系列微控制器的Flash写入对齐是指在进行数据写入操作时,需按照特定的对齐规则进行操作。这是由于Flash的写入操作必须按照特定的字节顺序进行,否则可能会导致数据写入错误。 STM32L4系列微控制器的Flash写入对齐要求以字为单位进行,即每次写入的数据长度必须是4字节(32位)的整数倍。如果写入的数据长度不满足该要求,就需要进行数据对齐处理,通常是通过在数据前面添加填充字节来使其长度满足要求。 例如,如果要写入长度为5字节的数据到Flash中,就需要进行对齐处理。在此情况下,可以添加3个填充字节,使总长度变为8字节,然后再进行写入操作。这样就满足了Flash写入对齐的要求。 对Flash进行写入对齐操作的目的是确保数据写入的可靠性和正确性。如果不按照要求进行对齐,可能会导致数据写入错位或错误,进而影响系统的稳定性和功能。 总之,STM32L4系列微控制器的Flash写入对齐要求数据长度必须是4字节的整数倍,如果不满足要求,则需要进行数据对齐处理。这样可以保证数据的正确写入,提高系统的可靠性。 ### 回答2: 在进行STM32 L4系列芯片的Flash写入操作时,对齐是非常重要的一个因素。对齐是指在数据写入Flash存储器时,需要按照特定的规则将数据的存储地址与Flash内存的块边界对齐对齐的目的是提高数据访问效率和保证写入数据的正确性。 在STM32 L4系列芯片中,Flash内存是以块的形式进行管理的,每个块的大小为一个固定的字节数。当进行Flash写入操作时,需要保证待写入数据的存储地址是块大小的倍数,即对齐于块边界。 对齐的好处主要有两点。首先,对齐可以提高数据访问的速度,因为Flash存储器是按块进行操作的,如果数据不对齐,可能会导致读取或写入操作跨越多个块,增加了访问时间。而对齐可以保证数据操作的范围在一个块内,减少了对其他块的访问,提高了效率。 其次,对齐可以确保写入数据的正确性。Flash存储器在进行写入操作时,只能对整个块进行擦除和写入,而不能对部分数据进行操作。如果数据不对齐,可能会导致需要修改的数据与其他数据混杂在同一个块中,从而导致擦除整个块,进而丢失其他数据。而对齐可以保证每个块只包含待写入的数据,避免了数据的损失。 总之,STM32 L4系列芯片的Flash写入对齐非常重要。正确的对齐可以提高数据访问效率和确保数据的正确性。在进行Flash写入操作时,需要根据Flash内存块的大小,保证待写入数据的存储地址是块大小的倍数,以充分利用Flash存储器的性能和功能。 ### 回答3: 在进行STM32 L4系列微控制器的Flash写入时,对齐是一个很重要的概念。 对齐意味着将数据按照特定的边界地址进行整理和存储。在STM32 L4系列中,Flash写入的最小单位是字(Word),每个字大小为4字节(32位)。因此,对齐的概念是基于字节的,要求每次写入的数据长度是4的倍数。 对齐是必要的,因为Flash的写入操作必须按照特定的规则进行。如果数据的长度不是4的倍数,并且没有对齐到起始地址,则会导致无效的写入操作,从而导致数据错误或者系统不稳定。 在进行Flash写入操作时,需要确保待写入数据的长度是4的倍数,并且起始地址是对齐的。一个简单的方法是使用字节对齐的数据类型或者使用填充字节(padding bytes)将数据补齐到4的倍数,并确保起始地址是对齐的。 另外,ST提供了一些库函数来帮助进行Flash写入操作的对齐。例如,通过调用HAL_FLASH_Program函数,可以以字节为单位进行连续写入,该函数会自动进行对齐。 总之,对齐STM32 L4系列Flash写入的一个重要概念,确保数据长度是4的倍数,并且起始地址是对齐的是必要的,以避免写入操作错误和系统不稳定。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值