A0100 开源软件搭建STM32开发环境介绍 - matianfu/arabesque GitHub Wiki
在Windows平台上为STM32开发搭建基于GNU工具链的Eclipse开发环境,覆盖源代码编辑、编译、调试、和代码管理等功能。
- 基于健壮和功能丰富的Eclipse/CDT编辑器,提供卓越的代码编辑,索引(Indexing),重构(Refactoring)等功能;
- 使用Eclipse连接GDB Client,搭配OpenOCD/GDB Server实现调试;支持breakpoint, watchpoint,backtrace等功能;
- 使用Eclipse Marketplace提供的EmbSysRegView Plugin实现调试时i/o寄存器读写,这也是Keil等商业IDE相对于开源工具链最大的优势,EmbSysRegView可以提供同样的功能;
- OpenOCD支持telnet接口和Tcl脚本命令,可以为产线工具制作和自动化测试提供便利;
- Eclipse内置EGit插件,提供Git功能,与项目浏览界面紧密集成,可以与Github配合使用,方便代码的版本管理;界面简单易用,无需第三方Git工具;
- Eclipse内置Mylyn插件,提供任务管理功能;
- GNU工具链支持GCC扩展特性,尤其是广泛使用的GCOV覆盖测试和GPERF性能测试;
本教程在Windows XP 32bit上搭建;在使用中未曾遇到问题;但最新的Java Runtime并不提供对XP系统的支持,所以建议用户尽量使用Windows 7或者以上版本的操作系统;如果使用Windows XP,建议使用32bit系统,有开发者说最新版的JRE在64bit Windows上有很多问题。
本教程所涉及软件均有跨平台版本,如果使用Ubuntu Linux搭建也是非常可行的,而且因为Linux下的build工具是内置的,所以事实上会比Windows平台更为简单;在Ubuntu平台上的Debugger/GDB Server程序,除OpenOCD之外还有STLINK可用,后者的包依赖更少,使用更为简单。
本教程所使用的硬件调试器为ST-Link V2。
-
Java,最新官方版本即可。
-
GNU Toolchain For ARM,由ARM官方出品,提供代码编译和调试工具链。
-
Cygwin(推荐)或者独立发行的GNU Make for Windows + GNU CoreUtils for Windows。如果你不打算使用Cygwin的其他功能,例如可以本机编译C/C++程序,可以使用独立发行的GNU工具包。Cygwin或GNU Make/CoreUtils提供Build工具。
-
Eclipse CDT,版本Luna SR1。
-
EmbSysRegView Eclipse Plugin,提供Debug时浏览和修改I/O寄存器功能。
-
OpenOCD,提供GDB Server和烧录接口;支持ST-Link V2, JLink等流行调试工具。
-
ST-Link V2驱动,由ST官方提供。
-
STM32Firmwware 1.3.0,STM32的平台软件和驱动代码,由ST官方提供。
-
STM32CubeMX,ST官方提供的代码生成工具,可用于生成基础框架代码,推荐使用。
-
TrueStudio For ARM Lite,Atollic提供的ARM CPU集成开发环境(IDE),Lite版本为免费版,但有32K代码限制。STM32CubeMX生成的代码包含TrueStudio支持,TrueStudio内部使用GNU Toolchain,安装该软件可以用于对比Toolchain的编译和链接选项,同时TrueStudio提供的Linker文件可以直接使用。
程序的生成过程包括这样一些步骤:
-
开发者编写源代码,包括C语言的.c/.h文件,和汇编语言的.S文件;在实际的STM32项目中,用到汇编文件的地方一般只有程序启动文件(start up),ST的Firmware中提供了这个文件,一般情况下无需修改直接使用即可;
-
预处理(宏定义)和编译源码,包括.c文件和.S文件,生成目标文件,通常为.o结尾;
-
将目标文件和库文件Link在一起,生成可执行文件,通常为elf格式,文件名后缀可由用户在工具中指定,通常为.elf;
-
从Elf文件生成可烧录的.hex和/或.bin文件;
可执行文件可以有多种格式,最常用的是Elf格式;Elf格式内不仅包含机器码,也包含说明性信息和Debug信息,例如调试时需要的符号表(Symbol,函数或变量在源文件里的名称)和源文件代码行映射信息;Elf格式有很多中,Keil中使用.axf后缀名,是ARM Executable Format的缩写,是Elf格式的一种。
对于有操作系统的程序(例如Linux应用程序),Elf文件作为可执行程序是足够的,因为操作系统可以在载入程序的时候,剥离机器码之外的信息,把机器码放置在指定的(虚拟)内存地址空间并开始运行程序;但是对于没有操作系统的MCU(STM32)而言,Elf文件是不能直接烧录到目标板上运行的,因为MCU无法识别除机器码之外的信息,而且程序的代码和变量的地址也不会正确。
能够烧录或下载到目标MCU内运行的文件必须剥离机器码之外的信息,且下载的代码和数据的(相对)地址正确;工具链(Toolchain)中会提供二进制文件工具(objcopy)将Elf格式文件的信息剥离,生成目标芯片的可执行程序,一般为bin格式;和bin格式对等的一种格式是hex格式,与bin格式相比hex格式更为紧凑,可以添加校验信息;与bin文件不同它和烧录后的内存地址镜像不完全一致,但是大多数烧录器都接受hex格式。
对于调试程序,例如GDB,是可以接受Elf格式的,他们会自动在载入(load)的时候将Elf格式中的非代码和数据信息剥离,象操作系统一样把代码和数据放在目标MCU的指定Flash或内存地址上,然后运行程序。
编译过程有很多选项;需要提供给编译器如下信息:
- 源文件路径
- 头文件路径
- 库文件路径
- 库文件的头文件路径
- CPU的构架和指令集
- 是否使用硬件浮点单元
- 优化程度和方式
- 警告信息
- 是否包含调试信息
- C语言标准
- 预处理器参数
- 其他编译选项,例如编译地址无关代码(影响结果寻址指令)
其中大部分信息都是必须的;详细的编译选项文档参加工具链内文档(而不是通用文档)。
对于小规模的程序,编译器通常可以一次完成编译链接过程;但是稍微有些规模的项目,通常采用两步编译的过程,先编译生成目标文件,通常每个.c文件对应一个.o文件,再链接成为可执行程序。
链接程序(Linker)需要做的工作是解析所有目标文件中的外部引用(例如调用其他模块中的函数或者访问全局变量),并根据指定的地址分配表,把代码和数据放到指定的内存地址;地址分配表包括代码段地址、数据段地址、栈空间和堆空间的起始和结束位置、中断向量表等等;地址分配的方式在单片机上是与硬件有紧密关联的,即使是同一个Family的芯片其地址空间也可能不同,而且通常开发者在开发阶段会根据需要做出调整,例如调整堆空间的大小,程序是在Flash上运行还是在调试阶段放在内存中运行等等,所以需要向Linker提供这些信息;包含这些信息的文件,称为Linker script,文件后缀通常为.ld或者.lds。
和编译器一样,Linker也接受参数;例如选择C语言的标准库,是否清除未使用的代码(虽然在程序内未使用这些代码,但是你可能需要在调试的时候直接通过调试器调用这些代码),是否生成最终的变量或函数的内存地址映射文件(Map),等等。与编译器参数一样,请参阅工具链提供的文档。
在开发过程中反复手工执行上述操作是不可行的;开发者们借助Build工具来自动化上述过程,这也为修改编译或链接参数提供方便、减少错误。
传统的C/Unix系统开发者们使用Make工具。Make的内在逻辑可以理解为定义了很多依赖性(Dependency),它的基本单元可以理解为:
目标A依赖于B/C/D,使用B/C/D生成A的做法是bla bla bla...
如果需要生成A,则必须首先有B/C/D,如果B/C/D已经存在,Make就可以执行生成目标A的做法(实际上是一段脚本程序)来生成A;如果B/C/D不存在,则根据B/C/D的依赖性和生成规则先生成B/C/D。
这样做的原因是,对于规模很大的项目,在开发者做了局部修改之后,Make可以判断出那些部分需要重新编译,而不是从头编译所有文件,这可以给开发者节省很多时间。(Make的判断依据是文件的最后修改时间)。
实际上,对于MCU项目而言,因为涉及到的文件数量非常少,很少外部链接第三方库文件,所以书写Make文件是非常清楚和简单可行的,而且编译非常快速;但是对于不熟悉Make文件的开发者来说,学习Make文件的书写有较高的学习曲线。
Eclipse支持使用Make文件作为Build方式的项目;它同时提供另外两种Build方式,一种称为Generate Makefile,即Eclipse CDT自动生成Make文件,用户可以读但是不能修改,每次编译这些文件都会重新生成;另一种称为CDT Internal Builder,它仍然使用Make,但是Makefile对用户不可见。
我们的配置过程中选择最后一种对用户最友好的方式;它对外部命令的需求也最低,仅需要PC上具有Make命令和rm命令(用于删除所有生成的目标文件和可执行文件,即Clean)。
Make命令可以通过安装Cygwin/GNU Make或GNU Make for Windows获得,rm命令可以通过安装Cygwin/GNU CoreUtils或GNU CoreUtils for Windows获得,采用Cygwin的方式得到的Make和CoreUtils(Unix常用命令)版本新很多,但实际使用没分别。
推荐安装Cygwin的主要好处是:
(1)你可以很容易用Eclipse开发命令行程序,有时一些简单的数据结构和算法代码调试(例如字串处理),在PC上单独写个本机程序调试或测试逻辑,更快捷方便;
(2)你获得很多Unix常用命令和Cygwin Shell,比DOS Shell好用很多,如果你熟悉Unix命令的话;
(3)Cygwin里有git包,如果你喜欢git的命令行界面的话;
调试是一个相当复杂的过程。
如果在操作系统上,调试程序(例如GDB,GNU Debugger)需要操作系统内核支持;由于程序运行在用户态和虚拟内存空间上,操作系统内核可以停止(调度)程序的运行,观察程序的内存,包括代码和数据,观察寄存器状态等等,这些信息可以传递给调试程序(Debugger),调试程序可以把代码反汇编,可以根据程序的调试信息(符号表),反向定位源码文件和当前执行代码行位置,可以把内存和寄存器信息提供给调试者观察,可以把程序的栈内存数据解析成与源码对应和用户易读的格式(函数的局部/自动变量),可以把数据结构根据源文件定义翻译成易读的格式,等等。
在MCU上,如果没有操作系统,完成这件事情就需要一个硬件单元来帮助,称为On Chip Debugger;ARM芯片上集成了硬件调试单元,可以读取和修改CPU寄存器,内存地址和I/O寄存器,可以让CPU执行和停止,可以设置硬件地址访问断点——如果是代码地址,对应breakpoint功能,如果是数据地址,对应watchpoint功能,等等。
芯片上的On Chip Debugger与外部通讯的界面,对ARM而言,就是JTAG/SWD接口,JTAG/SWD定义了通讯的电气界面和传输协议;但是和调试程序(GDB)之间,他们还需要一个硬件来连接,例如ST-Link V2,JLink等等;这种硬件可以称为硬件调试器(Debug Probe),他们与电脑之间的传输通常通过USB界面,数据协议一般为私有协议。
因为习惯,很多MCU开发者也把硬件调试器称为仿真器(Simulator),但是这种称谓是不严格的;仿真器一般是硬件仿真目标CPU指令,因为目标硬件CPU可能没有芯片内部的Debug单元,可能无法实现例如单步执行等功能,所以才做了一个(通常是)性能更强的硬件,模拟目标硬件的时序和逻辑执行指令,这种才是严格意义上的仿真器。
在PC上,还需要一个软件程序来配合Debug Probe使用;它的功能是把来自调试程序(例如GDB)的通用调试指令(例如GDB命令)翻译成Debug Probe的(私有)指令协议,将Debug Probe返回的结果翻译成调试程序可以理解的格式;这个软件程序没有统一的称谓,你可以称之为Debug Probe Software。通常生产Debug Probe硬件的厂商提供配套的软件,用品牌来命名,例如JLink。
在我们搭建的工具链里,这个Debug Probe Software是OpenOCD,是一个成熟的开源软件,广泛支持多种平台的多种Debug Probe,包括JLink,ST-Link等等。
OpenOCD实现的是一个GDB Server接口,提供给调试程序(GDB使用);可以理解为把前面所说的操作系统上的调试程序拆分成了Server和Client两个部分;这样做的好处有两个,其一,它可以基于网络实现远程调试(remote debugging);其二,对MCU而言,这样的设计剥离了工具链开发者和MCU硬件调试工具开发者的工作,使得前者更通用,后者更容易实现,例如理解指令和反汇编的工作可以放在Client一端;大多数Debug Probe Software都采用这样的方式工作。
在调试工具链中,调试程序的Client端(GDB)包含在GNU工具链中,是工具链的一部分;一次完整的通讯,指令的动作顺序如下;
例如用户通过Eclipse界面按钮向GDB(arm-none-eabi-gdb)发出调试指令halt(暂停执行)时:
-
Eclipse ->
-
arm-none-eabi-gdb --(tcp/ip)-->
-
GDB Server/OpenOCD --(libusb库)-->
-
st-link v2 driver(USB驱动)--(USB)-->
-
st-link v2(Debug Probe)--(JTAG/SWD)->
-
ARM芯片上的On Chip Debugger单元
如果是读取内存地址的指令,返回结果就是沿着这个路径反向走回来的。
不是所有开发工具链上的组件都叫做Toolchain。Toolchain的严格意义包含如下内容:
-
编译器,包括C和汇编
-
二进制工具(binutils, http://en.wikipedia.org/wiki/GNU_Binutils ),包括链接器等等
-
调试器
-
C标准库,通常提供选择,用户可通过Linker选项指定链接的目标库
通常我们说GCC指的是C/C++编译器,实际上GCC经过很多个版本的进化之后,已经发展成一个庞大的编译工具群,不仅仅支持C/C++语言,还支持包括Java(是的,Java也可以编译成机器码执行)在内的很多其他语言支持,GCC官方的名称解释是GNU Compiler Collection。
具体到GCC命令程序(arm-none-eabi-gcc),它所作的工作也不仅仅是编译,实际上它是一个前端(Frontend)程序,它内部调用后端程序可以把预处理(Preprocessing),编译(Compile),汇编(Assemble),和链接(Link)一次完成。
- 关于预处理,在GNU工具链文档中看到cpp一词,它通常不是指C++,而是指预处理器,C Preprocessor。
交叉编译(Cross-Compile)的意思是,编译器运行的硬件平台(Host)的CPU指令与它生成的CPU指令的硬件平台(Target)不同;我们使用的GNU Toolchain For ARM,Host平台是x86,Target平台是ARM,它是交叉编译工具链。
如果在Eclipse里直接把Keil等IDE里的源文件复制过来,设置好编译和链接指令开始Build,你可能会遇到类似这样的错误:
Undefined reference to '_sbrk'
Undefinef reference to '_write'
你可能会像我第一次遇到类似的错误的时候一样吃惊;原来工具链里提供的C语言标准库(libc.a)里竟然没有提供这些函数功能。
实际上,这些对程序(可能是)必要的函数的缺失,不是因为MCU用的C语言库被剪裁,而是因为C语言标准的定义就是这样的。
这些函数被称为syscall或者libc stub;它们的实现在不同的操作系统或者无操作系统的裸芯片(Bare Metal)上,可能是完全不同的,统一的实现可能会严重影响效率,或者,在另外一些场合可能完全不可能;它们被留给系统开发者自己来实现。
这个问题反应了C语言和Unix系统的渊源与紧密的孪生关系。造成这种Linker错误信息的原因是:如果在Unix系统上,这些函数都是标准的系统API(遵循Unix系统API的POSIX标准);而在STM32上,这些都是没有的。
比如_sbrk,它是修改程序的进程地址空间中的堆内存边界的系统函数;如果你的程序使用了malloc,就会通过libc调用到_sbrk;
比如_write,如果你的程序中调用了printf函数,同时又没有自己“截获”和重新定义putc的话,就会调用到_write,向文件写入字符;
在自己的项目中,如果使用了这些libc提供的C语言标准库函数,就必须提供他们依赖的syscall实现;如何实现这些_syscall完全取决于开发者自己的策略,其中的一些可以完全不实现(通过返回错误,例如_open, _read, _write, _close);另外一些需要实现的,例如_sbrk,可以找到其他IDE里的实现作为参考,网上也有很多代码可供参考。
幸运的是,STM32在Firmware的一些工程项目里提供了官方的例子代码(syscalls.c),可以直接复制到项目中使用。
另外,在测试时,可能处于某种目的给syscall一些特殊的实现,一个后面会提到的例子是:开启gcc的coverage test功能时,获取内存中的coverage统计数据的办法,就是在gdb里直接调用__gcov_flush()库函数,而这个函数就会依次调用_open, _write, _close等syscall来实现将coverage统计数据写入文件的功能;如果是在操作系统上,程序自己(而不是GDB)就会在当前目录产生一个数据文件;当然在STM32上没有操作系统也没有文件系统,但我们可以通过提供这几个函数功能的特殊实现(尤其是_write),截取数据写入内存特殊地址,最终用gdb的内存dump功能把数据获取到本地。
下一篇我们介绍整个软件的下载和安装过程,然后用STM32CubeMX生成项目初始代码,把这些文件导入Eclipse项目,配置编译参数,完成编译;配置Debug Configuration,实现调试;其中最复杂的部分在编译器参数配置上,另外Eclipse有些细节需要特殊配置。
Keep Tuned.