要熟悉一个新的处理器平台,第一件事是写一些裸机程序。这时我们最想要的,是实现一个printf打印函数,以便及时输出各种信息。
除去下层的字节输出驱动不说,printf本身的实现就有够麻烦,如果平时有保存相关的代码还好,不然就很浪费时间。除此之外,还有
一些诸如strlen、strcpy之类的函数,我们不愿意自己写,既麻烦而且效率不高,如果能借助已有的代码或库就好了。newlib就满足了
这点需求。
newlib c库是一个开源的c函数库,包括libc和libm两部分。它支持ANSI C库标准,针对不同处理器架构进行优化,轻量级,适用于嵌入式系统。
在我看来,newlib库有如下好处:
1. 支持printf和优化的字符串操作
2. 支持malloc和free等内存操作
3. 支持函数可重入功能(不过这种支持对内存有压力,总之是感觉弊大于利)
4. 支持libm数学库(不过一般嵌入式用不到浮点数,而且用模拟的开销略大)
5. newlib的函数是分文件实现的,如果用不到,绝不加入链接,一般不会造成目标文件猛增的情况。
newlib虽有诸多好处,但要用起来还真有点坡度。它需要先编译成库再使用,而且要用特定的功能还需要写一些底层支持函数。
我就不献丑了,网上已有大牛专业的教程:Howto: porting newlib a simple guide,
http://www.embecosm.com/appnotes/ean9/ean9-howto-newlib-1.0.html
这篇文章是介绍如何将newlib移植到一个开源软核OpenRISC 1000处理器上去。但我们一般用不到这种高难度动作,只是简单地在已有平台上
进行使用即可,下面就介绍我在stm32平台上使用newlib-1.20.0的经验。
下载newlib-1.20.0.tar.gz,解压缩,生成newlib-1.20.0。再创建newlib-1.20.0-build目录用于生成库,创建newlib-1.20.0-install用于安装(当然装完了随便拷贝)。
$cd newlib-1.20.0-build
$../newlib-1.20.0/configure --target=arm-none-eabi --with-newlib --prefix=`pwd`/../newlib-1.20.0-install
因为这里我用的事arm-none-eabi-gcc工具链,如果是arm-elf-gcc什么的要相应地修改--target选项。
$make all
$make install
从理论上来说,这样做之后,就可以生成newlib库到newlib-1.20.0-install目录中。但在此过程中确实出现了问题。newlib支持arm三个架构armv6m、thumb、thumb2,它试图编译对每个架构生成一个库,可惜部分汇编文件中包含swi指令,thumb和thumb2架构不支持,这就糟了,编译过程中就卡住了。当然,这些汇编文件都是系统启动或者进行系统调用时用的,我们用不到(我也不会用)。要想办法删掉它们,修改相应的Makefile.am。
1. 在newlib-1.20.0/newlib/libc/sys/Makefile.am中:
## Process this file with automake to generate Makefile.in
AUTOMAKE_OPTIONS = cygnus
INCLUDES = $(NEWLIB_CFLAGS) $(CROSS_CFLAGS) $(TARGET_CFLAGS)
AM_CCASFLAGS = $(INCLUDES)
noinst_LIBRARIES = lib.a
if MAY_SUPPLY_SYSCALLS
extra_objs = $(lpfx)libcfunc.o $(lpfx)trap.o $(lpfx)syscalls.o
else
extra_objs =
endif
lib_a_SOURCES = aeabi_atexit.c
#lib_a_LIBADD = $(extra_objs) #不生成syscalls.o
#EXTRA_lib_a_SOURCES = trap.S syscalls.c libcfunc.c #不使用syscalls.c
#lib_a_DEPENDENCIES = $(extra_objs) #依赖也不要,因为都不需要,所以干脆全注释了
lib_a_CCASFLAGS = $(AM_CCASFLAGS)
lib_a_CFLAGS = $(AM_CFLAGS)
if MAY_SUPPLY_SYSCALLS
#all-local: crt0.o #这里的crt0.o也是不需要生成的
all-local:
endif
ACLOCAL_AMFLAGS = -I ../../.. -I ../../../..
CONFIG_STATUS_DEPENDENCIES = $(newlib_basedir)/configure.host
改为makefile之后,要在相应目录下执行autoreconf,重新生成Makefile.in。
2. 刚才修改Makefile.am,使其不生成crt0.o,但实际上上层目录还是要用它的。所以就又改了其上层目录的Makefile.am,让其凭空造一个crt0.o出来。修改文件newlib-1.20.0/newlib/libc/sys/Makefile.am:
$(CRT0): $(sys_dir)/$(CRT0)
rm -f $@
ln $(sys_dir)/$(CRT0) $@ >/dev/null 2>/dev/null \
|| cp $(sys_dir)/$(CRT0) $@ || touch $@ #就是在这里最后加上 || touch $@,就凭空造出一个crt0.o,不至于在编译时出错
改完后同样不要忘了执行autoreconf。
3. 编译时除了编译newlib目录,还编译了libgloss目录,所以还要修改其中的Makefile.in(这回改Makefile.in了)。修改文件newlib-1.20.0/libgloss/arm/Makefile.in:
LINUX_CRT0 = linux-crt0.o
LINUX_BSP = libgloss-linux.a
#LINUX_OBJS = linux-syscalls0.o linux-syscalls1.o #这个有syscalls,去掉
LINUX_SCRIPTS = linux.specs
LINUX_INSTALL = install-linux
REDBOOT_CRT0 = redboot-crt0.o
#REDBOOT_OBJS = redboot-syscalls.o #这个有syscalls,去掉
REDBOOT_SCRIPTS = redboot.ld redboot.specs
REDBOOT_INSTALL = install-redboot
RDPMON_CRT0 = rdpmon-crt0.o
RDPMON_BSP = librdpmon.a
#RDPMON_OBJS = syscalls.o libcfunc.o trap.o _exit.o _kill.o #这个有syscalls,去掉
RDPMON_SCRIPTS = rdpmon.specs
RDPMON_INSTALL = install-rdpmon
RDIMON_CRT0 = rdimon-crt0.o
RDIMON_BSP = librdimon.a
RDIMON_OBJS = $(patsubst %,rdimon-%,$(RDPMON_OBJS))
RDIMON_SCRIPTS = rdimon.specs
RDIMON_INSTALL = install-rdimon
CFLAGS = -g
# Here is all of the eval board stuff
PID_SCRIPTS = pid.specs
PID_INSTALL = install-pid
IQ80310_SCRIPTS = iq80310.specs
IQ80310_INSTALL = install-iq80310
# Host specific makefile fragmen
如此成功之后,可以再newlib-1.20.0-install/arm-none-eabi中看到一个include头文件目录,一个lib目录。我的stm32使用的事lib/thumb2/libc.a。
接下来,还要再补齐一些底层支持函数。这个不需要libgloss生成的,而是要看newlib-1.20.0/newlib/configure.host文件。newlib根据$host不同预设了底层接口的支持方式,我们需要看$syscall_dir的值,以及MISSING_SYSCALL_NAMES、REENTRANT_SYSCALLS_PROVIDED两个宏是否添加进$newlib_cflags。下面是不同设定对应的情况:
-DMISSING_SYSCALL_NAME和-DREENTRANT_SYSCALL_PROVIDED
-DMISSING_SYSCALL_NAME
当使用-DMISSING_SYSCALL_NAME宏进行编译时,在_syslist.h中将原本的_close映射为close(等等),_syslist.h在ligloss/libnosys和libc/reent的c文件中被包含。并且,如果使用了-DMISSING_SYSCALL_NAME,则编译时不包括libc/syscall中的文件。而libc/syscall中文件正是有close等函数的定义。
所以,如果定义了_DMISSING_SYSCALL_NAME,看起来就是bsp提供close等函数的实现,而libc/reent中的_close_r等函数能提供简单的可重入支持。
此时,bsp提供一套close等函数的实现,而libc/syscall不被使用。系统流程虽然经过_close_r,但从意思上是不支持可重入性的。
-DREENTRANT_SYSCALLS_PROVIDED
当使用-DREENTRANT_SYSCALLS_PROVIDED宏定义进行编译时,在libc/reent中的c文件全部没有函数体,即不定义_close_r等函数。所以-DREENTRANT_SYSCALLS_PROVIDED的意思,是指bsp提供了可重入的_close_r等函数版本。上层的库如stdio仍调用_close_r等可重入函数进行操作。同时,libc/syscall中的函数被使用。libc/syscall中定义bsp需提供实现的close等函数,这里close由_close_r实现。bsp不需要再提供close的实现,只需提供_close_r的实现。
此时,bsp提供一套_close_r等函数的实现,libc/syscall被使用。整个系统的可重入性,压在bsp的_close_r等函数的可重入性上。从意思上说是支持可重入性的。
两者同时存在
当两者都存在时,libc/syscall和libc/reent都未起作用。bsp提供了一套close等函数的实现。整个newlib的上层,从调用_close_r转而调用close。系统的可重入性,直接压在bsp的close等函数之上。但从意思上来说不支持可重入性。
两者都不存在
两者都不存在的时候,使用libc/syscall中的函数。这个流程就简单了,先是libc/syscall中的close函数,调用libc/reent中的_close_r函数,然后_close_r函数又调用bsp应该实现的_close函数。
此时bsp提供一套_close等函数的实现,libc/syscall被使用。从意思上来说是不支持可重入性的。
对于arm来说,定义如下:
arm-*-*)
syscall_dir=syscalls
要实现printf,需要实现好_write函数,要实现malloc,需要实现好_sbrk函数,__malloc_lock函数,__malloc_unlock函数。这里就不多说了。
printf的打印缓冲区是从_reent结构中获取的,因为我们没有实现可重入功能,是同一缓冲区,所以在打印过程中任务切换可能导致打印混乱。我希望它在打印时使用栈空间,有两个方法:一是自己实现printf,在其中调用sprintf打印到缓冲区,然后用puts输出;二是完全自己实现printf,也就是用使用newlib之前的printf函数。
另外,在malloc调用时要做好互斥工作,但也不适合用关中断,最好用互斥信号量或普通的信号量。
PS: 在编译newlib过程中,重复好多遍,还要改Makefile.am,还必须编译libgloss。我尝试过在configure中添加选项的方式避免修改,但没有成功。更改地很暴力,如果大家有好的编译使用newlib的方式,还请不吝指教,多谢了。