stm32 HAL库SPI操作出现的hardfault分析

STM32 电子创客营 285次浏览 已收录 扫描二维码

stm32F030F4调试nrf24l01模块,但是本来在其他地方运行好好的代码,移植过来却不行了,使用的是hal库,没办法出了问题还是要深入进去看HAL库内部的封装。

实验分为两块儿板子,一块儿作为发送,一块儿作为接收。经过各种交叉验证,可以测试出来发送端没问题,可是接收端一上电就挂掉了。调试之后发现是进入了hardfault错误。

而发送和接收都是公用的底层代码,为什么只有接收端进入hardfault,但是发送端却没有。只能一步步跟踪,最终定位在HAL库的SPI发送函数里面。

如下图所示,箭头所指地方,执行到这里一下就over了。

 

stm32 HAL库SPI操作出现的hardfault分析

看下这条语句,首先想到的进入hardfault的原因:是不是操作pData越界了?检查定义的buffer和要发送的数据长度没有问题,buffer长度为5,发送个数也为5。所以不存在这种情况。

那么还有什么可能?

先来看看我定义的发送数组,也就是pData指针所操作的缓冲区:

stm32 HAL库SPI操作出现的hardfault分析

TX_ADR_WIDTH   RX_ADR_WIDTH  这两个是宏定义都为5,也就是这里我在nrf24l01Class类里面定义两个数组,这两个数组大小都为5。

发送端会发送TX_ADDRESS这个数组,接收端会发送RX_ADDRESS这个数组。奇怪的就是HAL_SPI_Transmit    函数发送TX_ADDRESS不会进入hardfault,发送RX_ADDRESS会进入hardfault,这两个数组也除了名称不一样,长度都一模一样。

是该揭开它真正面纱的时候了:我们再看出现问题的那条语句:

hspi->Instance->DR = *((uint16_t *)pData);

再看我们数组的定义:uint8_t

也就是出现问题这条语句把我们定义的uint8_t   数组转换成了uint16_t 同时进行半字的操作(同时操作两个byte)。这样看确实提高了执行效率,但是却也埋下了隐患。

产生这样的问题,我们就不得不扯得更远一点,arm内核对数据的非对齐数据访问。

Arm对内存的访问支持字(4byte)、半字(2byte)、字节(1byte)的直接访问,但是呢他们是有一定的要求的:

  • 存取时要求地址按字对齐,也就是地址要是4的整数倍,如0x0000、0x0004、0x0008(该地址只是举例,mcu的地址分配请参考具体手册的地址映射图)
  • 存取半字是要求地址按半字对齐,也就是地址是2的倍数,这样假如通过0x0001、0x0003这样非2倍数的地址来读取一个半字就会产生错误
  • 存取字节简单,只要地址不超范围就可以

这么看来是不是有点清晰了,我们出现错误的地方不就是在操作一个半字(uint16占用两个byte也就是半字),那么进入到了hardfault应该就是操作了读取了非半字对齐的地址

好,下面我们再来分析一下TX_ADDRESS 和RX_ADDRESS在存储区到底是怎么存储的。

这两个数组是都放在nrf24L01Class里面的,并且RX_ADDRESS 是在TX_ADDRESS。所有分配的存储空间也是挨着的。

stm32 HAL库SPI操作出现的hardfault分析

如图中所示左侧浅蓝色的5个方格代表TX_ADDRESS在内存中的排列,橙色5个方格代表RX_ADDRESS在内存中的排列。假设TX_ADDRESS的开始地址为0x20000000,那么RX_ADDRESS的开始地址就是0x20000005

0x20000005 不是半字对齐的(不是2的整数倍),这样使用(uint16_t *)半字指针直接读取就产生了hardfault。

这样原因就一清二楚了,再来分析一下TX_ADDRESS为什么不会产生hardfault,现在再去回头看下出错的地方的函数。有个if判断,也就是剩余的数量大于1的时候都是按照半字读取,所以前面四个byte按照半字读取两次并且地址也按半字对齐了,就没问题。而剩的最后一个在else语句里面按照字节来读取了,这样就正好不会产生hardfault。

 

好了,原因知道了,那么我们该如何避免这种情况发生。很简单,按照上面的分析我们只要保证RX_ADDRESS按照半字对齐就没问题。

所以提供三种解决思路:

1、把TX_ADDRESS 和RX_ADDRESS的长度都改成6,自己可以画下内存分配图,看看是不是半字对齐了(以后定义数组尽量定义成偶数的长度,顶多浪费一个byte的内存空间这个完全不用在乎)

修改后的代码如下:

stm32 HAL库SPI操作出现的hardfault分析

2、通过一些预编译对齐指令#pragma pack(2)  进行地址对齐,#pragma pack() 结束对齐,能编译通过但是测试不行

查资料后#pragma pack(2)  并不一定会按照2字节对齐,而是和成员自身大小比较,选择较小的,因为我们定义的是uint8_t,也就是一个成员之占用1byte 小于2,所以还是会按照1byte对齐的。

 

3、在数组定义前添加 __align(2) ,但是测试发现,在c里面可以,在c++类定义里面包含的数组前面加它编译不过去。c++

使用      __attribute((aligned (2)))stm32 HAL库SPI操作出现的hardfault分析

最终测试通过方法1和方法3都可以

 


转载请注明转自电子创客营:stm32 HAL库SPI操作出现的hardfault分析! 了解我们点击这里
喜欢 (3)or分享 (0)
电子创客营
关于作者: