欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

内存对齐,导致char型数组指针转化为float型指针出现的问题

程序员文章站 2022-07-15 10:08:20
...

         前阵子在做一个无线调参软件时,需要把一个float型的变量从上位机(qt平台编写)发给下位机(stm32),然后我采取的做法是将储存float变量的四个字节里面的数据通过串口以此发给下位机,然后下位机用一个char[4]数组进行接收,然后将char型数组的指针类型强行转化为float型,并将其读出,这样子就可以得到原来的float类型,stm32运行时发现了一个奇怪的问题,如下:

uint32_t Remote_time = 0;
char Current_Mode = 3;

unsigned char a[4]={0xA2,0xA1,0xB2,0xB1};
float b;

unsigned char a1[4]={0xA2,0xA1,0xB2,0xB1};
float b1;

int main(void)
{
	systemInit();
	Remote_time = micros();
	
	while(1)
	{
		b = *((float *)a);
		b1 = *((float *)a1);
	}
}

可以发现在stm32运行时b = *((float *)a);是没有任何问题的,而且类型也可以正确读取,而一执行到b1 = *((float *)a1);时,程序便进入void HardFault_Handler(void)这个while(1);死循环函数,刚开始觉得一头雾水,因为b = *((float *)a);和b1 = *((float *)a1);是操作完全相同类型的变量,而且操作内容完全一样,只是变量定义的顺序不一样而已,到时想法是难道变量定义的顺序会影响程序的运行,看着这么简单的四个变量定义又觉得不太可能,于是就打开DEVC++编译器,复制了这一段代码,编译运行,发现DEVC++中两句代码都完全可以运行通过,程序运行非常正常,这让我把怀疑的对象放到了stm32身上。通过memory窗口查看stm32内存可以得到下图:
内存对齐,导致char型数组指针转化为float型指针出现的问题

         可以看到char Current_Mode = 3;这个变量的地址是0x20000018,而unsigned char a[4]={0xA2,0xA1,0xB2,0xB1};的地址是紧接在0x20000018后面也就是0x20000019(地址为奇数),继续往后看可以看到float型应该是4个字节,可以从内存来看却占用了7个字节,然后才开始unsigned char a1[4]={0xA2,0xA1,0xB2,0xB1};变量的内存0x20000024(4的整数倍),于是我猜想到了stm32内存空间里面变量的储存规则应该是一个字节的变量可以存在任意地址空间,二个字节一定要在2的倍数的空间,四个字节一定要4的倍数的空间,通过定义其他一些变量验证了这个想法,后来查资料得到这其实是内存的对齐问题(可以祥看博客《对ARM处理器的内存对齐问题(译) http://blog.csdn.net/xcysuccess3/article/details/8308274)。

回到一执行到b1 = *((float *)a1);时,程序便进入void HardFault_Handler(void)这个while(1);死循环函数,而执行这句b = *((float *)a);就不会出错,这个问题。这个问题就慢慢变得清晰,因为变量char Current_Mode为char占用一个字节,然后接下来定义的是char数组,每个成员是一个字节,于是它直接放到了Current_Mode后面,造成了unsigned char a[4]这个数组的首地址为奇数,而当把这个数组的首地址强行转化为float的地址时,就会出现内存不对齐的问题(float的地址一定要是4的倍数),于是导致arm架构下的stm32运行出错,于是程序便强行跳转到void HardFault_Handler(void)这个while(1);死循环函数;而定义在后面的unsigned char a1[4]={0xA2,0xA1,0xB2,0xB1};float b1;这两个变量因为前面出现了定义了float b;四个字节的变量,于是内存上面便自动将前面多余的空间空开(这里是3个字节没有使用),然后知道是4的整数倍地址是才储存float,这样子后面的unsigned char a1[4];的首地址是出现在4的整数倍的地址上的,于是将他强制转化为float类型时就没有出错,综上可以得到,在stm32中将一个char类型的数组内存强制按float的读取规则去读取的时候一定要保证这个读取的地址是一个4整数倍的地址空间,如果是按double类型的方式去读取(将地址类型强制转化为double的类型地址),就要保证一定是8倍数的地址空间。

回到刚才的问题,还有一个问题没有解决就是为什么在devc++这个编译环境中运行这段代码就没有出现类似的问题,而能够正常的运行呢?经过查资料看到《 C语言字节对齐及设置编译对齐方式方法这篇博客,访问地址:http://blog.csdn.net/edonlii/article/details/12748903,其中提到X86架构下的就算数据没有对齐也不会导致数据出错,只是稍微增加的读取的次数,降低了效率,而有些系统变是对数据的对齐要求很高,stm32这种arm架构应该是属于后者博客对ARM处理器的内存对齐问题(译) http://blog.csdn.net/xcysuccess3/article/details/8308274)可以在x86环境下运行下面代码,查看变量地址来验证。

#include <stdio.h> 

unsigned char a[4]={0xA2,0xA1,0xB2,0xB1};
float b;

char aa = 1;

unsigned char a1[4]={0xA2,0xA1,0xB2,0xB1};
float b1;
short s=3;
long l=2;

int main()
{
	b=*((float *)a);
	b1 =*((float *)a1);
	printf("&a = 0x%x\n",a);
	printf("&b = 0x%x\n",&b);
	printf("&aa = 0x%x\n",&aa);
	printf("&a1 = 0x%x\n",a1);
	printf("&b1 = 0x%x\n",&b1);
	printf("&s = 0x%x\n",&s);
	printf("&l = 0x%x\n",&l);
	
}
到这里一开始提到的所有问题都得以解决了,但是又出现了另一个很重要的问题。就是在像stm32这种对数据对齐要求很严格的系统下需要进行文中所说的数据转换要怎么做呢,因为我们很难保证要转化的地址就是我们目标类型所需要的整数倍。后来采用联合体的方式union,代码如下:
typedef union 
{
	char c[4];
	float f;
}u_weizeng;

char Current_Mode = 3;
u_weizeng ec;
float b = 0.0;

int main(void)
{
	systemInit();
	Remote_time = micros();
	
	while(1)
	{
		ec.c[0] = 0xA2;
		ec.c[1] = 0xA1;
		ec.c[2] = 0xB2;
		ec.c[3] = 0xB1;
		b = ec.f;

	}
}
内存对齐,导致char型数组指针转化为float型指针出现的问题

可以看出union 定义的变量的内存没有紧跟在char型后面,而是自动对齐到了4的整倍数,我猜想这应该是因为union 里面有float型的变量,就直接按最大的对齐了,为了验证在union添加成员longlong类型,可以看到union的内存会自动对齐到8的整倍数。因此用联合体的方式进行类型转化的时候就不会出现这种问题了。到此,所有问题都可以解决了。

第一次在csdn上发博客,记录了自己的一次差错,希望自己可以坚持记录,以后可以多跟各位朋友交流,本人是一个新手,各位前辈有对这篇博客觉得有不妥的或者有错误的地方,希望可以不吝赐教,多多交流。