这篇介绍 在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
这样以后就可以使用printf
,sprintf
函数了 - 方法 2.仍然使用标准库,在主程序添加下面代码 :
1
2
3
4
5
6
7
8
9
10
11
12
13//此段代码可以在正点原子例程Uart处找到
_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库 之间的主要差异是:
- microlib 不符合 ISO C 库标准。 不支持某些 ISO 特性,并且其他特性具有的功能也较少。
- microlib 不符合 IEEE 754 二进制浮点算法标准。
- 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 | if(假设成立) |
可能有人说,断言的功能可以用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 |
方法二:在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 2
升Level 3
并不能节省很多的空间;相反,Level 3
更高几率造成程序运行问题。从 Level 0
升Level 2
,相当于20%时间获取80%成果;从 Level 2
升Level 3
,相当于80%时间获取20%成果。如果对程序没有太过严苛的要求,建议程序整体在Level 2
即可。
3.2 优化随之带来的Bug
代码优化产生的Bug情况:
- 有更新的变量被优化而没有重新读取值,导致错误
- 优化后,代码段被跳过(不执行)
- Keil软件自带的软件Bug
- 小心一些驱动,尤其是涉及到文件管理,因为该底层驱动极有可能里面用了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文件失败。
网上查找资料后,处理此类问题有下面几种方法:
- 单步调试,找到被优化的代码段,看是否有更新的变量被优化而没有重新读取值,导致错误。若有,加入valotile关键字。
- 通过options of file”…”将被优化文件的优化等级调成特定等级。
3.3 小总结
- 不建议小白直接上
Level 2
及Level 3
搭建新工程! - 代码优化等级方面,我建议新建项目时,最好采用
Level 0
搭建工程。等到项目比较完善的时候,再提升优化等级至Level 2
,再根据优化等级出现的问题,进行逐步调试。 - 建议项目整体基本优化等级为
Level 2
,不需要升为Level 3
。 - 有些底层驱动确实是不好提高代码优化等级(尤其涉及到文件系统)。
努力提高优化等级并不是厉害!在能力有限的情况下,费时费力;尤其是硬件Flash资源明显不够用时,虽然通过最高优化等级能应用,但是会对后面的升级更新、bug检查造成很大的麻烦:
- 仿真无法查看,优化等级太高
- 一旦降低优化等级,硬件编译报错,Flash存储不够
- 唯一的途径,就是把程序其他代码删除,留下所需的代码进行仿真调试局部(无法调试整体)
- 建议还是更换有更大Flash的MCU,或者自己优化一下程序代码
Ps: 最极端的代码压缩方法,即采用较高的Level2或Level3进行代码优化,然后再选用MicroLIB对代码量再进行压缩一下(最后一步再勾选微库,方便找出微库造成的问题)。