IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> 基于STM32使用嵌套结构体摸索内存对齐 -> 正文阅读

[嵌入式]基于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
对于此性质,并没有太好的办法,大佬们可以分享自己的想法。

  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2021-08-24 15:43:22  更:2021-08-24 15:45:28 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/20 22:54:02-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码