[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; 

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

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

相关推荐
楼台的春风6 分钟前
【MCU驱动开发概述】
c语言·驱动开发·单片机·嵌入式硬件·mcu·自动驾驶·嵌入式
kongba00713 小时前
Cursor提示词模板,开发GD32,C语言开发GD32 ARM单片机编程规范提示词 大厂风格代码规范
c语言·arm开发·单片机
亿道电子Emdoor1 天前
【ARM】MDK如何生成指定大小的bin文件,并指定空区域的填充数据
arm开发·stm32·单片机
楼台的春风1 天前
【详细讲解在STM32的UART通信中使用DMA机制】
stm32·单片机·嵌入式硬件·mcu·物联网·嵌入式·信息与通信
电子科技圈1 天前
在低功耗MCU上实现人工智能和机器学习
人工智能·经验分享·科技·嵌入式硬件·mcu·物联网·机器学习
蓑衣客VS索尼克1 天前
什么是逻辑分析仪?
arm开发·人工智能·fpga开发
苏三福2 天前
rk3588/3576板端编译程序无法运行视频推理
arm开发·音视频
年轮不改2 天前
ARM Linux平台下 OpenCV Camera 实验
linux·arm开发·qt
初学者52132 天前
SRS编译arm版本出错 ERROR: opus not found using pkg-config
arm开发
艾格北峰2 天前
STM32 物联网智能家居 (六) OLED显示设备
arm开发·stm32·单片机·嵌入式硬件·物联网·智能家居