[ARM-2D 专题]5 MDK编译器一个旧版本-Ofast优化bug的问题及解决办法

最近开始大量基于ARM-2D开发应用项目,为了达到最佳性能,我们使用了编译器的许多特殊技能,其中就包含了-Ofast优化,很不幸,一不小心踩坑了。

案发情况如下:

使用的MDK版本5.36,编译器6.16

优化选项配置如下:

不使用mcirolib

代码比较简单,读写一个Norflash数据:

bash 复制代码
uint8_t sFLASH_SendByte(uint8_t byte)
{
	uint32_t i;
	i = 0;
  while ((SPI_I2S_GetFlagStatus(sFLASH_SPI, SPI_I2S_FLAG_TXE) == RESET) && (i++ < 5000)); //set timeout
  SPI_SendData(sFLASH_SPI, byte);

	i = 0;
  while ((SPI_I2S_GetFlagStatus(sFLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET) && (i++ < 5000)); //set timeout
  return (uint8_t)SPI_GetData(sFLASH_SPI);
  }

以前使用microlib库编译,运行良好,所以也没有在意问题,现在使用arm-2d,不使用microlib编译,运行,读数据错误了。一开始莫名奇妙的,采用各种组合验证,得出以下结论:

测试情况:

  1. 优化开-Ofast,使用microlib编译,代码中的i不用volatile修饰,功能正常
  2. 优化开-Ofast,不使用microlib编译,代码中的i不用volatile修饰,功能异常
  3. 优化开-O1,不使用microlib编译,代码中的i不用volatile修饰,功能正常
  4. 优化开-Ofast,不使用microlib编译,代码中的i用volatile修饰,功能正常

初步结论,看起来就是编译器-Ofast,并且不使用microlib库的时候,优化出了问题。代码逻辑上看起来没有任何问题(表面上如此,具体解释在这种情况不同的编译器有不同的说法,编译器说了算)。

追踪了一下反汇编代码:

确实发现了一个优化的问题,如图中所示,R6寄存器没做任何初始化,后面就和R0比较(对应代码i<5000),导致延时时间不够,寄存器数据读取错误。

后来和arm2d的作者讨论了一下,确定了结论:编译器的未定义行为或者说bug

参考:编译器的无副作用代码

为了验证这个问题,安装最新的MDK版本:

还是这一段代码,其他配置一样,编译,链接,下载,测试。运行正常,说明最新的编译器已经修正了这个不是bug的bug。我们看看它生成的代码:

这下子没有问题了,明显看到了对两个变量R6,R4的初始化,运行结果也正确了。

那么对于上面那一段代码,我们要如何来写,才能防止编译器的优化或者不确定行为呢?进而确保我们写出的代码具有健壮性和与编译器的无关性,下面两种改写都有效,共参考:

bash 复制代码
uint8_t sFLASH_SendByte(uint8_t byte)
{
	uint32_t i; 
	i = 0;
  while ((SPI_I2S_GetFlagStatus(sFLASH_SPI, SPI_I2S_FLAG_TXE) == RESET) && (i++ < 5000)) __NOP(); //set timeout
  SPI_SendData(sFLASH_SPI, byte);
	i = 0;
  while ((SPI_I2S_GetFlagStatus(sFLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET) && (i++ < 5000)) _NOP(); //set timeout
  return (uint8_t)SPI_GetData(sFLASH_SPI);
  }

这一种写法,在while后面增加一条_NOP()指令,阻止编译器对看似无用的i++的优化,保证不被未定义行为的优化。

bash 复制代码
	uint8_t sFLASH_SendByte(uint8_t byte)
{
	for(i = 0;i < 1000;i++)
	{
		if ((sFLASH_SPI->SR & SPI_I2S_FLAG_TXE) != (uint16_t)RESET)
			break;
	}
	SPI_SendData(sFLASH_SPI, byte);
	for(i = 0;i < 1000;i++)
	{
		if ((sFLASH_SPI->SR & SPI_I2S_FLAG_RXNE) != (uint16_t)RESET)
			break;
	}
	return (uint8_t)SPI_GetData(sFLASH_SPI);

这一种写法,用循环方式,明确的告诉编译器,i是有用的,也可以达到这个目的。

还有一种方案也可以解决这个问题,就是定义i变量的时候用volatile修正,但是一般不建议采用这种方式。

bash 复制代码
volatile uint32_t i; 

编译器的坑,很难对付,我们理解一下编译器的行为,站在编译器的角度去思考问题,平时写出更优质的代码,才能完全防止这种情况的发生。

文章原创,欢迎转载,请注明出处,未经书面允许,不得用于商业用途

相关推荐
XINVRY-FPGA1 天前
XCZU4EV-1FBVB900E Xilinx FPGA AMD Zynq UltraScale+ MPSoC EV(Embedded Vision)
arm开发·嵌入式硬件·计算机视觉·fpga开发·硬件架构·硬件工程·fpga
猫猫的小茶馆1 天前
【STM32】FreeRTOS 任务的删除(三)
java·linux·stm32·单片机·嵌入式硬件·mcu·51单片机
学不动CV了1 天前
单片机ADC采集机理层面详细分析(二)
c语言·arm开发·stm32·单片机·嵌入式硬件·开源·51单片机
学不动CV了1 天前
51核和ARM核单片机OTA实战解析(二)
c语言·arm开发·stm32·单片机·嵌入式硬件·51单片机
嵌入式小白牙2 天前
ARM-I2C硬实现
arm开发·单片机·嵌入式硬件
老猿讲编程2 天前
提升ARM Cortex-M系统性能的关键技术:TCM技术解析与实战指南
arm开发·arm·c
嵌入式小白牙3 天前
ARM-DMA
arm开发·stm32·单片机
范纹杉想快点毕业3 天前
ZYNQ芯片,SPI驱动开发自学全解析个人笔记【FPGA】【赛灵思
stm32·单片机·嵌入式硬件·mcu·架构·51单片机·proteus
亿道电子Emdoor3 天前
【ARM】ARM架构基础知识
arm开发·架构·arm
猫猫的小茶馆4 天前
【STM32】FreeRTOS 任务的创建(二)
stm32·单片机·嵌入式硬件·mcu·c#·智能硬件