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

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

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

相关推荐
傻童:CPU16 分钟前
PS(ARM)中的寄存器和地址
arm开发
IAR Systems3 小时前
在IAR工具链中使用overlay命令进行SMP多核工程TCM配置
arm开发·嵌入式·嵌入式开发·iar
Mr..Jackey13 小时前
瑞佑 RUI Builder 图形化 UI 设计工具
arm开发·人工智能·单片机·ui·人机交互·ra8889·lcd控制芯片
IAR Systems1 天前
使用IAR Arm工具链开发和调试Zephyr RTOS
arm开发·嵌入式·iar·zephyr
l'm coming2 天前
[linux]内核启动加载驱动文件的流程
linux·arm开发·驱动开发·嵌入式
Code-keys2 天前
ARM NEON SIMD 编程实战:从音频信号处理到AI算子研发实战
arm开发·音视频·信号处理
有什么事2 天前
云手机多开哪个强?ARM架构:云手机多开的信任基石与性能核心
arm开发·智能手机·架构
头枝2 天前
ARM Cortex-M DWT CYCCNT 必须显式初始化,jlink调试时正常,使用时异常的问题
arm开发·stm32·单片机·问题·cyccnt
咖喱年糕2 天前
KEIL5 编译错误解决:MDK 5.37 及以上版本添加 AC5(ARM Compiler 5)编译器
arm开发·单片机·keil·嵌入式软件·ac5
LeoZY_2 天前
CH347应用 USB转JTAG功能之:probe-rs搭配CH347下载MCU命令全指南
单片机·嵌入式硬件·mcu·开源·github