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

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

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

相关推荐
mucheni9 小时前
迅为iTOP-RK3576开发板/核心板6TOPS超强算力NPU适用于ARM PC、边缘计算、个人移动互联网设备及其他多媒体产品
arm开发·人工智能·边缘计算
YHPsophie1 天前
MCU通信接口技术解析:UART、SPI与I2C
mcu·通信接口
dadaobusi1 天前
qemu构建arm环境(AI生成)
arm开发
mucheni2 天前
迅为RK3562开发板ARM四核A53核心板多种系统适配全开源
arm开发
阿让啊2 天前
单片机获取真实时间的实现方法
c语言·开发语言·arm开发·stm32·单片机·嵌入式硬件
Tlog嵌入式2 天前
STM32提高篇: 蓝牙通讯
stm32·单片机·嵌入式硬件·mcu·iot
手打猪大屁2 天前
ARM裸机开发——I.MX6U_汇编LED灯驱动
汇编·arm开发
深圳信迈科技DSP+ARM+FPGA2 天前
基于ARM+FPGA+DSP的储能协调控制器解决方案,支持国产化
arm开发·fpga开发·信号处理
承接电子控制相关项目2 天前
STM32F103 单片机(基于 ARM Cortex-M3 内核)的启动过程涉及硬件初始化、固件配置和程序执行流程。
arm开发·stm32·单片机·cortex-m3内核启动
技术领导力2 天前
事关数据安全,ARM被爆不可修复漏洞
arm开发