微库 & 断言 & (Keil)代码优化

  这篇介绍 在Keil平台开发嵌入式遇到的一些东西:MicroLIB、Assert、代码优化。

一、MicroLIB

  大多人一般之所以使用 Use MicroLIB,是因为使能后能够直接调用printf()等函数。

1.1 Use MicroLIB & printf

  printf()之类的库函数,是一些很骚的东西;使用printf、 fopen等库函数库函数调用,会让软件进入半主机模式。但是printf()库函数本身 不需要半主机模式(关掉,当然也能用printf)。

使用C标准库(stdio.h)中的函数,例如printf()之类的函数,会进入半主机模式,
发生软件异常,会导致程序无法运行,以下是解决方法 :

  • 方法 1.使用微库,因为使用微库的话 ,不会使用半主机模式。MDK 勾选 Use MicroLIB这样以后就可以使用 printfsprintf 函数了
  • 方法 2.仍然使用标准库,在主程序添加下面代码 :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //此段代码可以在正点原子例程Uart处找到

    #pragma import(__use_no_semihosting) //确保没有从 C 库链接使用半主机的函数
    _sys_exit(int x) //定义 _sys_exit() 以避免使用半主机模式
    {
    x = x;
    }
    struct __FILE // 标准库需要的支持函数
    {
    int handle;
    };

    FILE __stdout;

  选上Use MicroLIB,例如你用printf()函数的时候,就会从串口1输出字符串,直接默认定向到串口1。


  • 法1可实现串口1数据输出,但要定向到串口2,串口3,microLIB就不合用了;
  • 法2虽然能够映射其他串口,但是如果同时涉及到多串口也是有问题。

  总而言之,实际上 法1 & 法2 都不推荐用于实际项目,顶多就用于快速搭建Demo 或 做测试用。

1.2 半主机 & printf

  半主机模式是这么一种机制:它使得在ARM目标上跑的代码,如果主机电脑运行了调试器,那么该代码可以使用该主机电脑的输入输出设备。这点非常重要,因为开发初期,可能开发者根本不知道该ARM器件上有什么输入输出设备,而半主机机制使得你不用知道ARM器件的外设,利用主机电脑的外设就可以实现输入输出调试。

  所以,如果不用主机电脑的外设就可以实现输入输出调试,而是要利用目标ARM器件的输入输出设备,首先要关掉半主机机制。然后再将输入输出重定向到ARM器件上,如printf和scanf,你需要重写fputc和fgetc函数(原有的输入输出,标准库函数的默认输出设备是显示器)。

1.3 MicroLIB的代码优化

  之所以在代码优化提及到MicroLIB,是因为微库本身就是一个精简库,进而有精简代码的效果,因此它可以用来压缩代码量。

MicroLIB 与 缺省C库 之间的主要差异是:

  1. microlib 不符合 ISO C 库标准。 不支持某些 ISO 特性,并且其他特性具有的功能也较少。
  2. microlib 不符合 IEEE 754 二进制浮点算法标准。
  3. microlib 进行了高度优化以使代码变得很小。

  还有更多细节上的差异就不罗列出来了,直接网上一查一大把。但是,也正是这些零零碎碎的差异,可能就导致你做项目时疯狂翻车,所以一般不建议使用 MicroLIB (还有其他原因等等,尤其是项目刚开发时真不建议使用)。


以下是我对该库的总结:

  • MicroLIB库 虽然能够进行代码大小优化。
    • 但是实际测试的效果真的是杯水车薪,可以忽略不计;还不如自己去优化代码 or 提高优化等级。如果是其他方式都用了后,只能通过MicroLIB库优化代码,建议直接换硬件(真的是优化没多少的)。
    • 由于microlib中进行了优化,以尽量减少代码大小,一些功能将会比ARM编译工具提供了标准C库函数更慢执行;例如,memcpy()。效率换空间,在项目大部分是 空间换效率,这当然是不建议的。
  • MicroLIB库 不支持浮点数运算。
    • ST除了F4xx系列,其他是没有FPU单元,都是采用软件模拟运算。
    • 故在F4xx系列,选 Use MicroLIB,开了FPU就会死机(或其他情况)
  • MicroLIB库 不支持半主机模式,进而支持printf()函数。但是实际用起来不太好用(只能固定映射串口1)

Ps: 一般来讲,最好不要加。它和标准库有很多繁琐区别;如果是会迁移平台、项目代码时,用它对代码的维护性不好。

  例如,当旧的项目工程要换新的硬件平台,迁移的时候发现Bug,但是检查应用层代码和底层驱动代码正常。建议查看一下旧项目工程是否采用了微库,而新项目工程没采用微库;然后查看旧代码是不是调用了printf等之类的C库函数。如果调用,先调用Use MicroLIB 或者 想办法关闭半主机模式(+重定向),分析一下问题的来源。

二、assert

  assert() 不仅仅是个(字面意义上)报错函数!对于在开发过程中的程序员来说,加断言是个好习惯,可以帮助调试。

程序在假设条件下,能够正常良好的运作,那assert()其实就相当于一个 if 语句:

1
2
3
4
5
6
7
8
if(假设成立)
{
程序正常运行;
}
else
{
报错&&终止程序!(避免由程序运行引起更大的错误)
}

  可能有人说,断言的功能可以用if语句对异常情况进行处理来代替。以下列举 断言 的好处:

  • 实现效果最后不会增加代码量
    • if是实的,真正的增加代码量,降低执行效率;
    • 断言是虚的,在Debug的时候可以帮助调试,在Release的时候并不存在。
  • 断言,实际上也是一种文档。断言设定了,函数的入口条件。增加了代码的可读性。
  • 断言用于在开发阶段监测BUG,进行调试。
    • 断言其存在的意义在于检测代码在开发过程中是否出现了问题。
    • 而”if… “,更准确的说是错误处理,是在你的release版本中也实实在在应该有的,处理程序运行过程中产生的错误并进行处理,以提高程序的健壮性。

  如果是看过Stm32的库函数实现方式的话,肯定会看到assert_param(expr) ((void)0)。这也是断言,不过是ST官方自己写的断言函数;而且有个宏定义用来是否失活该断言函数。当你打开一份Stm32的例程,进去库函数就会发现这些assert_param(expr) ((void)0)是失活的。


Ps:MicroLIB 库并不支持assert()函数,两者同时用产生报错。

  microlib是一个比ARM标准C库小的独立库。为了节省大小,arm microlib c库不支持或实现几乎所有与操作系统交互的函数,例如abort()、exit()或assert()。


  如何在Release版本去掉assert?

方法一:常见任何平台处理

  在调试结束后,可以通过在包含#include 的语句之前插入 #define NDEBUG 来禁用assert调用,示例代码如下:

1
2
3
#include <stdio.h>
#define NDEBUG
#include <assert.h>

方法二:在Keil平台上

  在工程参数设置一栏,在“Preprocessor Symbols”的“Define”栏输入“NDEBUG”。等同于在代码中添加宏定义“#define NDEBUG”。实际为上面方法。

方法三:在Keil平台上

  提高代码优化等级至2。在代码优化Level 0时,断言是占用空间可执行的;当代码优化提升为Level 2。这个时候就能够去掉assert()函数处理

三、Keil的代码优化等级

3.1 代码优化等级

  C/C++的优化等级会对程序产生 不定性的影响,至于选择哪种优化等级必须从 现有的程序分析才行!

  • Level 0 (-O0):关闭大部分优化,除了一些简单的转换,生成的代码具有最佳的调试视图。
  • Level 1 (-O1):应用受限优化。
    比如:删除未使用的内联函数和静态函数,删除冗余代码和重新排序指令等。生成的代码经过合理优化,具有良好的调试视图。
  • Level 2 (-O2): 高度优化,目标代码到源代码的映射并不一定对应,因此,不利于调试。
  • Level 3 (-O3):最大级别优化。级别3与时间优化相结合可能生成比级别2更多的代码。

  经实际测试,Level 2Level 3并不能节省很多的空间;相反,Level 3更高几率造成程序运行问题。从 Level 0Level 2,相当于20%时间获取80%成果;从 Level 2Level 3,相当于80%时间获取20%成果。如果对程序没有太过严苛的要求,建议程序整体在Level 2即可。

3.2 优化随之带来的Bug

代码优化产生的Bug情况:

  1. 有更新的变量被优化而没有重新读取值,导致错误
  2. 优化后,代码段被跳过(不执行)
  3. Keil软件自带的软件Bug
  4. 小心一些驱动,尤其是涉及到文件管理,因为该底层驱动极有可能里面用了C库函数实现了某些功能;而C库函数有些一旦提高优化等级就会出问题(例如,SD卡文件系统)。

例子1

  楼主编写一个stm32F10x系列的SPI库函数驱动。程序未优化前(LEVEL 0),MISO能正常接收信息,优化后(Level 2),MISO接收的信息都是错误的。

  IAP平台之前也出现这个问题,现在貌似被修复了;但是Keil平台看起来还有。

例子2

  近日在移植LPC1788的lwip驱动和SD卡(带文件系统)驱动时,遇到单独移植每个驱动都正常,移植到一起就一直出现HardFault_Handler错误。单步调试后发现编译器优化导致部分代码被跳过的情况。

  仔细检查后发现官网例程中的LWIP驱动使用的是最高级(LEVEL3)优化等级,而SD卡驱动使用LEVEL0等级的优化。移植后统一修改为LEVEL3导致初始化SD卡f_open文件失败。

网上查找资料后,处理此类问题有下面几种方法:

  1. 单步调试,找到被优化的代码段,看是否有更新的变量被优化而没有重新读取值,导致错误。若有,加入valotile关键字。
  2. 通过options of file”…”将被优化文件的优化等级调成特定等级。

3.3 小总结

  • 不建议小白直接上Level 2Level 3搭建新工程!
  • 代码优化等级方面,我建议新建项目时,最好采用Level 0 搭建工程。等到项目比较完善的时候,再提升优化等级至Level 2,再根据优化等级出现的问题,进行逐步调试。
  • 建议项目整体基本优化等级为Level 2,不需要升为Level 3
  • 有些底层驱动确实是不好提高代码优化等级(尤其涉及到文件系统)。

  努力提高优化等级并不是厉害!在能力有限的情况下,费时费力;尤其是硬件Flash资源明显不够用时,虽然通过最高优化等级能应用,但是会对后面的升级更新、bug检查造成很大的麻烦:

  • 仿真无法查看,优化等级太高
  • 一旦降低优化等级,硬件编译报错,Flash存储不够
  • 唯一的途径,就是把程序其他代码删除,留下所需的代码进行仿真调试局部(无法调试整体)
  • 建议还是更换有更大Flash的MCU,或者自己优化一下程序代码

Ps: 最极端的代码压缩方法,即采用较高的Level2或Level3进行代码优化,然后再选用MicroLIB对代码量再进行压缩一下(最后一步再勾选微库,方便找出微库造成的问题)。

-------------本文结束感谢您的阅读-------------