基于ARM GCC bare metal 坏境搭建

codesize.png
ARM GCC本身确实比较方便,自身集成了一些标准的C库,用来做开发非常方便,但是对于Flash比较紧张的MCU来讲,标准库还是有一些大,比如最近用到的一个MCU,flash只有64K,一个含有标准库的简单程序产生的二进制文件就到了将近30K左右,这个是不能忍的,于是就要通过删除标准库来缩减程序大小,怎么说呢,ARM GCC用标准库什么参数也不用加,但是如果想省掉标准库可就麻烦很多了。

去除标准库

网上比较多的去除标准库的方法是在编译的时候加上一个-nostdlib,看起来也比较简单,对于不用C库的程序这个好像足够了,比如我做的一个简单的Flashloader,加上这个参数之后,二进制文件只有300多字节,注意是字节,可见这个还是很有效果的。

然而对于要用标准C函数格式的函数,比如printf,如果不用标准库,自行编写是肯定的,不过如果你没有按照标准的库函数头文件的定义就会出现各种烦人的警告,比如

common/io.h:27:6: warning: conflicting types for built-in function 'puts' [-Wbuiltin-declaration-mismatch]

原来加了-nostdlib只是说不用标准的C库,并不是说不用标准的头文件,于是如果你不想用我的标准的头文件你还要加上一个-nostdinc, -nodefaultlibs, 好了,加上之后不会有冲突了,不过还会碰到一些编译的问题。

va_list的处理

我们知道printf是一个可变参数函数,C语言对于这种函数是通过va_list等一系列宏定义实现的,它们的定义在stdarg.h文件中,如果没有这个文件就会出现如下的错误。

common/printf.c:18:38: error: unknown type name 'va_list'
 printk (PRINTK_INFO *, const char *, va_list);
                                      ^~~~~~~
common/printf.c:180:45: error: unknown type name 'va_list'
 printk (PRINTK_INFO *info, const char *fmt, va_list ap)
                                             ^~~~~~~
common/printf.c: In function 'printf':
common/printf.c:566:5: error: unknown type name 'va_list'
     va_list ap;
     ^~~~~~~
common/printf.c:576:5: warning: implicit declaration of function 'va_start' [-Wimplicit-function-declaration]
     va_start(ap, fmt);
     ^~~~~~~~
common/printf.c:577:14: warning: implicit declaration of function 'printk'; did you mean 'printf'? [-Wimplicit-function-declaration]
     rvalue = printk(&info, fmt, ap);
              ^~~~~~
              printf
common/printf.c:581:5: warning: implicit declaration of function 'va_end' [-Wimplicit-function-declaration]
     va_end(ap);
     ^~~~~~
common/printf.c: In function 'sprintf':
common/printf.c:589:5: error: unknown type name 'va_list'
     va_list ap;
     ^~~~~~~

知道了问题所在就好办了,我们只需要将这个文件从ARM GCC目录中copy到我们的工程目录就可以了,注意不能COPY IAR目录的文件,这个应该是和编译器相关的。

修正变量初始化

我们知道因为MCU RAM内容掉电会丢失内容,所以一般编译器会将需要初始化的变量先编译到Flash段,比如__etext段,在程序初始化的时候再将这些数据copy到RAM中去,对应的RAM地址是从__data_start____data_end__这部分,相应的初始化程序如下:

 65     uint32_t *fr = __etext;
 66     uint32_t *to = __data_start__;
 67     unsigned int len = __data_end__ - __data_start__;
 68     while(len--)
 69         *to++ = *fr++;

不过我在程序中用到的一些全局变量却没能正确初始化,仔细研究发现这个变量的初始值都是0,对比变量地址也发现这些变量并没有放到data段,而是放在bss段,也就是未初始化的全局变量,而原来写的程序对于这些变量是么有初始化的,知道了问题原因就简单了,加上如下的初始化程序就好了:

 70     uint32_t *bss = __bss_start__;
 71     len = __bss_end__ - __bss_start__;
 72     while(len--)
 73         *bss++ = 0;

Printf的大问题

搞定上述问题之后,程序运行还算正常,不过一旦遇到printf就会出现hardfault,不过奇怪的是hardfault里面的printf是可以正常工作的,这个就有点奇怪了,Debug发现正常调用printf的时候程序跳转到了puts函数里面,紧接着又瞎蹦跶到_malloc_r里面去了,但是hardfault里面的printf可以正常跳转到我们自己定义的printf函数里面,这个现象十分奇怪,也困扰了我挺久,后面发现hardfault里面的printf是有多个参数的,正常printf只是打印一个字符串,似乎这就是区别,不过最诡异的是我明明没有定义puts_malloc_r这些函数,不知为何会跳转到这些函数里面,debug的时候还发现有__malloc_max_sbrked_mem这样复杂的函数,PC在里面跳来跳去,烦死了,看来还是编译器没有弄干净,网上查了很久也没找到合适的解决方法,不过偶然发现puts是一个builtin函数,好烦呀,不知道怎么去掉这些builtin函数只好瞎猜,加了个-nobuildin, 提示没有这个参数,可能是-fbuiltin,于是我又猜是-fnobuiltin又提示-fno-builtin, 终于猜出来了,加上去,哇塞,完美!最终二进制文件还缩小到7K,真爽。

总结

这次环境搭建大部分时间还是个编译器参数打交道,最终采用的参数如下:

OPTS = -Os -nostdlib -nodefaultlibs -nostdinc -nostartfiles -fno-builtin

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

发表新评论