My Avatar

wtywtykk

wtywtykk的后院

一次STM32 Flash写入异常的分析

2019年12月15日 星期日, 发表于 成都

如果你对本文有任何的建议或者疑问, 可以在 这里给我提 Issues, 谢谢! :)

一个LED引发的血案

在我的一个STM32程序里面,用内部Flash保存配置设置。但是出现了一定几率把自己刷死的情况。

用STLINK读取被刷死的Flash内容,可以看到Flash的前4个字节被写成了0。Flash的前4字节保存的是初始化时候的堆栈地址,写成0之后无法操作堆栈,自然不能启动。

显然问题跟操作Flash的代码有关,相关代码差不多下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool FlashStorage_FlashProgramSafe(uint32_t TypeProgram, uint32_t Address, uint64_t Data)
{
	uint32_t ProgramSize = 8;
	......
	if ((Address >= StorageAddr) && (Address + ProgramSize <= StorageAddr + StorageSize))
	{
		......
	}
	else
	{
		assert(false);
		return false;
	}
}

bool FlashStorage_WriteNewBlockAndEraseOldBlock(uint8_t* NewBlock, uint8_t* NewData, uint32_t DataSize, uint8_t* OldBlock)
{
	uint32_t i = 0;
	uint32_t BlockDataBase = (uint32_t)NewBlock + sizeof(uint32_t);

	assert(DataSize <= STORAGE_RECORD_SIZE - sizeof(uint32_t));
	......
	for (; i < DataSize; i++)
	{
		FlashStorage_FlashProgramSafe(FLASH_TYPEPROGRAM_BYTE, BlockDataBase + i, NewData[i]);
	}
	......
	if (OldBlock)
	{
		FlashStorage_FlashProgramSafe(FLASH_TYPEPROGRAM_WORD, (uint32_t)OldBlock, 0);
	}
	......
}

但是看代码这个问题无法解释,在写这段代码的时候就感觉Flash操作比较危险,所以加了很多的校验,不仅检查了0指针,还查了在不在指定范围内,不应该是地址写错导致的。

不过,看网上别人写的Flash操作,大部分关了中断,而官方手册并没有说这一点。

加上关中断之后,一切居然都正常了,证明确实是有中断在搞鬼。那么下一步要判断的是因为某个中断里面代码导致的问题,还是单纯“进中断”这个操作就会导致问题。

同样思路,往下删减代码,当把Systick里面的东西删掉后,问题消失。这时SysTick里面有这堆东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void HAL_SYSTICK_Callback(void)
{
	LED_Set(true);
	if (InitDone)
	{
		static bool b = false;

		xxxxxx();
		xxxxxxxxxxx();
		b = !b;
		if (b)
		{
			xxxxxxxxxxxxxxxxxxxxxxx();
			xxxxxxxxxxxxxxxxx();
		}
	}
	LED_Set(false);
}

删到最后,发现是LED_Set的问题,这个函数大概是这样的:

1
2
3
4
void LED_Set(bool Val)
{
	GPIO_SetVal(&LEDHandle, !Val);
}

这么简单的一个东西,怎么可能出错,跟进去

是一个用bitband实现的io操作函数

1
2
3
4
5
6
7
8
void GPIO_SetVal(GPIO_Handle* Handle, bool Val)
{
	assert(Handle);
	if (Handle)
	{
		*Handle->ODRAddr = Val ? 1 : 0;
	}
}

调试到这里,发现问题了,Handle->ODRAddr居然是0!

而在HAL库里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
static void FLASH_Program_Word(uint32_t Address, uint32_t Data)
{
  /* Check the parameters */
  assert_param(IS_FLASH_ADDRESS(Address));
  
  /* If the previous operation is completed, proceed to program the new data */
  CLEAR_BIT(FLASH->CR, FLASH_CR_PSIZE);
  FLASH->CR |= FLASH_PSIZE_WORD;
  FLASH->CR |= FLASH_CR_PG;

  *(__IO uint32_t*)Address = Data;
}


HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data)
{
  HAL_StatusTypeDef status = HAL_ERROR;
  
  /* Process Locked */
  __HAL_LOCK(&pFlash);
  
  /* Check the parameters */
  assert_param(IS_FLASH_TYPEPROGRAM(TypeProgram));
  
  /* Wait for last operation to be completed */
  status = FLASH_WaitForLastOperation((uint32_t)FLASH_TIMEOUT_VALUE);
  
  if(status == HAL_OK)
  {
    if(TypeProgram == FLASH_TYPEPROGRAM_BYTE)
    {
      /*Program byte (8-bit) at a specified address.*/
      FLASH_Program_Byte(Address, (uint8_t) Data);
    }
    else if(TypeProgram == FLASH_TYPEPROGRAM_HALFWORD)
    {
      /*Program halfword (16-bit) at a specified address.*/
      FLASH_Program_HalfWord(Address, (uint16_t) Data);
    }
    else if(TypeProgram == FLASH_TYPEPROGRAM_WORD)
    {
      /*Program word (32-bit) at a specified address.*/
      FLASH_Program_Word(Address, (uint32_t) Data);
    }
    else
    {
      /*Program double word (64-bit) at a specified address.*/
      FLASH_Program_DoubleWord(Address, Data);
    }
    
    /* Wait for last operation to be completed */
    status = FLASH_WaitForLastOperation((uint32_t)FLASH_TIMEOUT_VALUE);
    
    /* If the program operation is completed, disable the PG Bit */
    FLASH->CR &= (~FLASH_CR_PG);  
  }
  
  /* Process Unlocked */
  __HAL_UNLOCK(&pFlash);
  
  return status;
}

在FLASH_Program_Word函数中,FLASH->CR |= FLASH_CR_PG;打开了Flash的写入功能,一直到HAL_FLASH_Program里的FLASH->CR &= (~FLASH_CR_PG); 才关闭。

如果在这几句之间,产生了一个SysTick中断,0地址就被刷掉了!

下面这张图大概表示了整个执行过程:

回到Systick函数,问题在于,LED_Set在InitDone外面!也就是说,可能在GPIO没有初始化的时候,去设置了一下LED,向0地址写了个0进去,刷掉了堆栈指针。

但是大多数情况下Flash不是只读的吗?这里我一直以为跟电脑一样,向只读区域写数据会产生一个HardFault之类的错误。但实际并非如此,程序还会继续执行,只有Flash操作时候才会表现出问题。

把LED操作放到if内部之后,问题修复。就算不关中断也可以正常工作了。当然,最后关中断这个还是保留了下来,避免之后出其他的问题,毕竟Flash操作时候本来就不能响应中断,实际性能损失也不大。

PS:这个问题并不是我一人解决的,实际调试的时候是个概率发生的问题,并且对代码也不熟悉,花了两个周时间才搞定的。LED操作也很不起眼,很难被怀疑,甚至一度认为是芯片bug。