1. 引言
在 STM32 的应用中,SPI 算是用的比较多的外设了,也是单片机最常见外设之一。客户说它执行了关闭 SPI 的代码,竟然会导致 Flash 中的 WRPERR 标志置位,致使应用碰到一些问题。这就奇怪了,SPI 和内部 Flash 看起来是风马牛不相及的事情,为什么会发生这种事呢?一起来看看吧。
2. 问题
2.1. 问题起源
客户在使用 STM32L072RBT6 的时候,使用 STM32CubeL0 库,在程序编写时,发现执行关闭 SPI 代码时,会导致 Flash 的写保护错误标志 WRPERR 置位,导致其后面准备写 EEPROM 的时候,就无法对 EEPROM 写入了。
客户使用两个标志 flag1 和 flag2,来观察 WRPERR 标志的变化。代码如图 1 所示。
图1.用户测试代码
在执行这个代码时,前面 flag1 还等于 0,执行到 flag2 那句,就变成 flag2 等于 1 了,同样地取了 WRPERR 标志位的值。所以客户就怀疑执行__HAL_SPI_DISABLE()会把Flash 的 WRPERR 标志置 1 了。
因为在对 EEPROM 编程中,需要先调用位于 stm32l0xx_hal_flash.c 中的FLASH_WaitForLastOperation()函数,此函数中,将会对 Flash 所有错误标志进行检查,如果出现了错误,它则返回 HAL_ERROR,导致后续对 EEPROM 的编程不会被执行。
2.2. 问题重现
使用 NUCLEO-L053R8 来验证客户的这个问题。在\STM32Cube_FW_L0_V1.10.0\Projects\STM32L053R8-Nucleo\Examples\SPI\SPI_FullDuplex_ComPolling 例程中直接进行修改测试。
首先,把客户的测试代码加到例程中 SPI 初始化之后的位置。如图 2 所示。
图2.测试代码 1(位于 SPI 初始化之后)
编译,并在线调试,发现并没有出现客户所描述的问题。如图 3 所示。
图3.测试代码 1 结果(位于 SPI 初始化之后)
可以看到,WRPERR 的值并没有被置 1,flag1 和 flag2 的值也都是 0。那么,为什么客户说他那边会有这个问题呢?
再回头仔细看一下客户的测试代码,发现客户的测试代码中并没有对 SPI 进行初始化,其__HAL_SPI_DISABLE()代码是放在其他外设初始化之后的。
好,那么再来修改一下测试代码,把客户这三句测试代码挪动到 SPI 初始化之前,如图 4 所示。
图4.测试代码 2(位于 SPI 初始化之前)
编译,并在线调试,这时,会惊奇地发现客户所描述的问题来了。其结果如图 5 所示。
图5.测试代码 2 结果(位于 SPI 初始化之前)
可以看到,这时 Flash 的 WRPERR 标志位置 1 了,测试代码中,flag2 的值也跟 flag1不同了。
再做一个实验,将此处的 HAL 库写法,改成直接操作寄存器,来试一下。测试代码变成是图 6 这样的。
图6.测试代码 3(位于 SPI 初始化之前,直接操作寄存器)
编译,在线调试,这次又惊喜地发现,问题又不见了。结果如图 7 所示。
图7.测试代码 3 结果(位于 SPI 初始化之前,直接操作寄存器)
三种操作,为什么只有第二种方式有问题呢?而且为什么错的偏偏是 Flash 的写保护错误标志 WRPERR 呢?接下来可以分析一下它们的反汇编代码,看看到底是哪里出问题了。
2.3. 反汇编分析
对于三种情况,把反汇编拉出来看最清楚其操作过程了。
先分析第一种情况——测试代码位于 SPI 初始化之后。其反汇编如图 8 所示。
图8. 测试代码 1 的反汇编(位于 SPI 初始化之后)
从之前的 Watch 窗口,知道 flag1 的地址为 0x2000000c,flag2 的地址为0x2000000d。
现在对三句 C 语言测试语句的反汇编语句进行解析,如下:
LDR.N R0, ??DataTable0_4 ; 将 Flash_SR 的地址赋值给 R0
LDR R1, [R0] ; 取出 Flash_SR 中的值,赋值给 R1
LSLS R2, R1, #23 ; R1 值左移 23 位,赋值给 R2
LSRS R2, R2, #31 ; R2 值再右移 31 位,赋值给 R2,将 WRPERR 值挪到 Bit0
STRB R2, [R4] ; R2 值写到 R4 指向的数据,此时 R4 的值为
; 0x2000000c,正好是 flag1 的地址,所以此操作将
; WRPERR 值写入 flag1
LDR R1, [R4, #0x4] ; 将地址 0x20000010 的值 0x40003800 赋给 R1
; 0x40003800 为 SPI2_CR1 的地址
LDR R2, [R1] ; 取出 SPI2_CR1 的值,赋值给 R2,R2=0x0000033e
MOVS R3, #64 ; R3 = 0x40,Bit6 对应 SPE
BICS R2, R2, R3 ; 清除 R2 的 Bit6(准备关闭 SPI2)
STR R2, [R1] ; 将 R2 的值写回 SPI2_CR1,关闭 SPI2
LDR R0, [R0] ; 取出 Flash_SR 中的值,赋值给 R0
LSLS R1, R0, #23 ; R0 值左移 23 位,赋值给 R1
LSRS R1, R1, #31 ; R1 值再右移 31 位,赋值给 R1,将 WRPERR 值挪到 Bit0
STRB R1, [R4, #0x1] ; R1 值写到[R4+1](也就是地址 0x2000000d)指向的位置
; 0x2000000d,正好是 flag2 的地址,所以此操作将
; WRPERR 值写入 flag2
可以看到,这段汇编是一点问题都没有的。
接下来,先分析第三种情况——也就是测试代码放在 SPI 初始化之前,但是使用直接操作寄存器的方式。其反汇编如图 9 所示。
图9.测试代码 3 的反汇编(位于 SPI 初始化之前,直接操作寄存器)
从之前的 Watch 窗口,知道 flag1 的地址为 0x2000000c,flag2 的地址为0x2000000d。
现在对三句 C 语言测试语句的反汇编语句进行解析,如下:
LDR.N R0, ??DataTable0_2 ; 将 flag1 的地址赋值给 R0
LDR.N R1, ??DataTable0_3 ; 将 Flash_SR 的地址赋值给 R1
LDR R2, [R1] ; 取出 Flash_SR 中的值,赋值给 R2
LSLS R3, R2, #23 ; R2 值左移 23 位,赋值给 R3
LSRS R3, R3, #31 ; R3 值再右移 31 位,赋值给 R3,将 WRPERR 值挪到 Bit0
STRB R3, [R0] ; R3 值写到 R0 指向的数据,也就是 WRPERR 值写入 flag1
LDR R2, ??DataTable0_4 ; 将 SPI2_CR1 的地址 0x40003800 赋给 R2
LDR R3, [R2] ; 取出 SPI2_CR1 的值,赋值给 R3,R3=0x00000000
MOVS R4, #64 ; R4 = 0x40,Bit6 对应 SPE
BICS R3, R3, R4 ; 清除 R3 的 Bit6(准备关闭 SPI2)
STR R3, [R2] ; 将 R3 的值写回 SPI2_CR1,关闭 SPI2
LDR R1, [R1] ; 取出 Flash_SR 中的值,赋值给 R1
LSLS R2, R1, #23 ; R1 值左移 23 位,赋值给 R2
LSRS R2, R2, #31 ; R2 值再右移 31 位,赋值给 R2,将 WRPERR 值挪到 Bit0
STRB R2, [R0, #0x1] ; R1 值写到[R0+1](也就是地址 0x2000000d)指向的位置
; 0x2000000d,正好是 flag2 的地址,所以此操作将
; WRPERR 值写入 flag2
可以看到,这段汇编也是一点问题都没有的。
最后,再来分析一下有问题的第二种情况——也就是测试代码放在 SPI 初始化之前,但是使用__HAL_SPI_DISABLE()关闭 SPI 的情况。其反汇编如图 10 所示。
图10. 测试代码 2 的反汇编(位于 SPI 初始化之前)
从之前的 Watch 窗口,知道 flag1 的地址为 0x20000008,flag2 的地址为0x20000009。
现在对三句 C 语言测试语句的反汇编语句进行解析,如下:
LDR.N R0, ??DataTable0_2 ; 将 flag1 的地址 0x20000008 赋值给 R0
LDR.N R1, ??DataTable0_3 ; 将 Flash_SR 的地址赋值给 R1
LDR R2, [R1] ; 取出 Flash_SR 中的值,赋值给 R2
LSLS R3, R2, #23 ; R2 值左移 23 位,赋值给 R3
LSRS R3, R3, #31 ; R3 值再右移 31 位,赋值给 R3,将 WRPERR 值挪到 Bit0
STRB R3, [R0] ; R3 值写到 R0 指向的数据,也就是 WRPERR 值写入 flag1
LDR R2, [R0, #4] ; 将地址 0x2000000c 中的值 0x00000000 取出,赋值给 R2
LDR R3, [R2] ; 取出地址 0x00000000 中的值,赋值给 R3,
; R3 值为 0x20000468
MOVS R4, #64 ; R4 = 0x40,Bit6 对应 SPE
BICS R3, R3, R4 ; 清除 R3 的 Bit6
STR R3, [R2] ; 将 R3 的值写回[0x00000000],WRPERR 置位,出错了!
LDR R1, [R1] ; 取出 Flash_SR 中的值,赋值给 R1
LSLS R2, R1, #23 ; R1 值左移 23 位,赋值给 R2
LSRS R2, R2, #31 ; R2 值再右移 31 位,赋值给 R2,将 WRPERR 值挪到 Bit0
STRB R2, [R0, #0x1] ; R1 值写到[R0+1](也就是地址 0x20000009)指向的位置
; 0x20000009,正好是 flag2 的地址,所以此操作将
; WRPERR 值写入 flag2
可以看到,问题出在哪了?问题就出在“STR R3, [R 2]”这个语句上,这个语句在向 0x00000000 这个位置写值,而 0x00000000 此时映射的是 Flash 的地址0x08000000,也就是 Stack Pointer 的位置。如图 11 和图 12 所示。
图11. 0x00000000 地址的数据
图12. 0x08000000 地址的数据
首先,这个位置本来就不应该被修改。
第二,因为没有对 Flash 程序存储器进行解锁,就往里边写值,就会造成写保护错误,导致WRPERR 标志位置位。所以,可以明白为什么 WRPERR 会被置位了。
可是关键的问题在哪儿呢?在执行“LDR R2, [R0, #4]”这条语句时,R2 本来应该是 SPI2_CR1 的地址,但是它竟然是 0x00000000!如图 13 所示。
图13. 0x2000000c 地址的数据
从 Watch 窗口来看一下 SpiHandle 的情况。如图 14 所示。
图14. SpiHandle(未初始化)
从图 14 可以看到,其实刚才的 0x2000000c 地址就是 SpiHandle 结构体的地址,也是SpiHandle.Instance 的地址,而 SpiHandle.Instance 的值为 0。SpiHandle.Instance.CR1的地址为 0x0,导致显示它装载的值是 Stack pointer 的值 0x20000468,这里本应该是SPI2_CR1 的地址和 SPI2_CR1 的值。
也就是因为这里的问题,才会导致了后面的 WRPERR 错误。
2.4. 代码分析
再回到代码这边来看一下,有问题的代码究竟是有什么情况。客户的代码主要就是一句关闭 SPI 的语句“__HAL_SPI_DISABLE(&SpiHandle);”。
这个语句是怎么解析的?它在 stm32l0xx_hal_spi.h 中解析,如图 15 所示。
图15. __HAL_SPI_DISABLE 函数
看到这个函数时,看到了重要的字眼——“Instance” !就明白是什么问题了,因为这个 SpiHandle.Instance 还没有被初始化呢!这也说明了为什么在图 14 中,看到的SpiHandle.Instance 的值为 0x0,而 SpiHandle.Instance.CR2 的值为 0x20000468。关键就在于这个 SpiHandle.Instance 还没有初始化。
所以,把客户的测试代码放在 SPI 初始化代码之后没有问题,就是因为这个SpiHandle.Instance 已经被初始化过了。所以,它不会有问题。
3. 问题解决
本来客户的代码就没有必要这么写,因为 SPI 都没初始化,对它进行关闭并没有什么意义。
如果非要在这里关闭 SPI 的话,那就要先对 SpiHandle.Instance 进行初始化才行。如图 16 所示。
图16. __HAL_SPI_DISABLE 函数
加了 “SpiHandle.Instance=SPIx ;”初始化后,再跑这段代码,就不会出现客户所说的问题了。
现在再来看一下 SpiHandle 的情况。
图17. SpiHandle(SpiHandle.Instance 已初始化)
经过对 SpiHandle.Instance 的初始化,这里就可以看到 SpiHandle.Instance 的值为0x40003800 了,为 SPI2 外设寄存器的基地址,而且可以看到 SpiHandle.Instance.CR1的地址就是SPI2_CR1 的地址 0x40003800,值也是 SPI2_CR1 的值 0x0 了。
4. 小结
在用户代码中,SpiHandle 只是定义了 SPI_HandleTypeDef 结构体,其各种参数并还没有进行实际初始化。在没有初始化的前提下,对其进行操作是不对的,也是危险的,应该在写代码的时候引起重视。
使用 HAL 库的时候,如果要对一个外设进行任何的操作,请务必记得它是被初始化过的。否则,出了问题可能都不一定知道。
参考文献
文档中所用到的工具及版本
IAR EW for Arm 9.20.4
本文档参考ST官方的《【应用笔记】LAT1178+关闭SPI会导致WRPERR错误的问题分析》文档。
参考下载地址:https://download.csdn.net/download/u014319604/88971344