一、前言
二、编译器组成与编译流程
2.1 编译流程概述
2.2 Gcc For Arm编译器
2.3 预编译
2.4 编译
2.5 汇编
2.6 链接
2.7 生成HEX镜像
2.8 通过Makefile编译代码
三、调试流程
3.1 Openocd调试工具
3.2 GDB调试器
四、自己搭建IDE
4.1 准备工作
4.2 生成Makefile工程
4.3 安装Vscode及插件
4.4 安装配置GCC For Arm编译器
4.5 通过Msys2获取Make工具
4.6配置OpenOCD
4.7 使用Make编译STM32工程
4.8 使用arm-none-eabi-gdb烧录程序
五、利用Vscode将命令行工具图形化
5.1 让Vscode正确识别工程
5.2 在Vscode下使用Make编译工程
5.3 在Vscode下烧录调试
六、总结
嵌入式开发,必然离不开各自开发平台下的工具链,通常情况下,工具链都被封装为一个完整的IDE,供开发者使用。深度封装的IDE可以让开发者快速上手,避免新手一开始就接触复杂繁琐的底层原理,从而专注于代码本身。实际开发过程中,IDE也确实能胜任开发过程中大部分需求,起到它应用的作用。
在某些特殊场景下,一旦需要自定义开发流程,或对工程进行深入改动。就要求开发者了解IDE的底层原理及代码编译流程。本文是作者探索开发工具链过程中的总结,实际探索过程进行了多个平台和芯片的尝试,在本文中,以流行的STM32平台为例进行记录,文中提及的工具技术,每一个都值得单独开篇详述,限于篇幅,无法在本文中深入研究,以后有机会再探索并单独撰文记录。本文先对编译以及调试流程作了简介,帮助读者先构建基本的知识框架,以便理解后面搭建IDE过程中的每个步骤及其目的,如果读者已对编译及调试流程很熟悉,可以直接跳到搭建IDE部分,参考本文搭建自己的IDE。作者水平有限,如有错漏,烦请指正。
C语言是一门高级语言,其高级语言的描述其实是相对于汇编语言以及机器语言来的。目标代码必须在特定平台上执行才有意义,不同平台架构及指令集的不同注定了在C语言下同样的动作,最终必须转换为目标平台CPU所能识别的特定二进制序列才能正常运行,即:“可执行文件”。从编写完成的C代码到最终的可执行文件所经历的过程,即是被我们泛化的概念:“编译”。而实际上,“编译”仅是转换过程中的一部分。
真实的编译过程,包含了预编译->编译->汇编->链接四个部分,每个部分执行后,均有对应的输出文件,这些中间文件在编译过程中是必需,但最终不被目标平台所使用,因此,通常会在生成可执行文件后被清除。这四个部分所使用的工具,被打包为一个程序集,根据不同的输入参数,执行不同的命令以完成编译过程的各个阶段任务,该程序集,即:“编译器”,实际上,现在的编译器,不仅包含了编译过程本身的工具,还包含着调试以及镜像转换等一系列工具集。针对ARM平台,常见的编译器有ARMCC(Keil)、IAR(IAR)、GCC For ARM(除前两者外的IDE使用)、LLVM(暂未大规模使用)。本文的主角是GCC For ARM,目前主要由ARM在维护及更新,基于GNU开源工具链,主要用于ARM平台的交叉编译。
在此给出Gcc For Arm的官网下载链接:
gcc-arm-none-eabi-10-2020-q4-major-win32.zip
以上编译器压缩包解压后,进入其根目录,将看到以下内容:
上图展示了ARM提供的GCC For Arm工具链中的所有工具,其中标红的是在嵌入式开发过程中常用的几个,下面对几个常用工具逐一讲解,在讲解之前,请对整个编译过程有大概了解:
如上图所示,C代码编译的过程及每个阶段都将产生对应的中间文件,中间文件的格式根据目标平台的不同和编译器的不同会有所区别,但在其所属阶段产生的文件内容,实际上并无本质差异。
有了以上宏观了解后,可以逐个将编译器的工具与编译流程对应:
1.arm-none-eabi-gcc.exe
该文件是编译器的主体,其作用范围涵盖编译流程中的预编译、编译、汇编、链接四个阶段,后文将逐一对以上过程展开讲解。
2.arm-none-eabi-ld.exe
该文件是链接器主体,其作用是将编译器汇编后输出的一系列*.o目标文件合并以及重定位,最终生成目标平台的可执行文件(*.elf)。
3.arm-none-eabi-gdb.exe
该文件是gdb调试服务的可执行文件,其作用是加载由链接器产生的可执行文件(*.elf)至目标芯片的RAM或者Flash,并具有运行、调试、增删断点、读取变量、修改变量等常见调试功能。其作用范围,覆盖嵌入式开发的调试阶段。
4.arm-none-eabi-objcopy.exe
该文件是镜像拷贝程序的主体,由于链接阶段所产生的*.elf目标文件,不止包含了程序的运行地址和数据,还包含了大量信息供调试使用,而程序烧录时,仅需*.flash的地址及对应地址的数据,因此需要将这些信息,从*.elf中提取出来,转换为常见的Flash镜像格式,如:*.hex、*.S19、*.Bin。可以简单理解为:*.elf文件调试时使用,*.hex烧录时使用,关于HEX文件的格式,可以参考作者之前的文章:深入理解工具链-Hex文件详解。
5.arm-none-eabi-objdump.exe
该文件是反汇编程序的主体,某些特殊情况下,C代码层面的逐行调试并不能满足我们的调试需求,需要进一步知道其汇编语言层面的动作。此时,我们可以使用反汇编程序,将目标平台的二进制机器码转换为汇编代码,以便调试过程中确定问题,反汇编后的格式,不同平台稍有区别,通常为*.lst格式,也有*.lss和直接使用*.txt格式的,但其大都为文本文件,均可使用记事本直接打开。
6.arm-none-eabi-size.exe
该文件主要用于打印当前程序的存储占用情况,如:.data段、.bss段、*.text段大小。通过调用该工具,开发者可以准确知道程序占用的硬件存储资源分布情况,通常,.data段、.bss段大小之和即为所占RAM空间大小;.data段、.text段大小之和为所占Flash空间的大小。IDE编译完成后所打印的大小信息,本质上是调用该工具实现。
有别于初学C语言时所有C代码均在一个*.C文件中编写,大型工程为了便于分层与模块解耦,将不同模块的代码封装在不同的*.C文件中。为了让其他模块或上层代码使用本模块中的宏定义、全局变量或函数,常规做法是将模块宏定义以及变量及函数声明放在对应的*.H文件中,*.H文件在被使用的模块中通过#include预编译指令包含。因此,预编译阶段主要的工作,即:将*.H文件的内容,在每一个包含了该*.H文件的*.C文件中按照预编译指令展开。除了#include的展开操作,还有其他的一些预编译指令,但本质上,预编译阶段的内容,都在文本编辑层面,大部分操作,均为复制、粘贴、替换等。经过预编译阶段后,编译器所产生的中间文件格式为*.i。
假设,当前工程下,包含main.c 、main.h、clock.c、clock.h共4个文件,那么,经过预编译阶段,将输出main.i、clock.i这2个文件。
以上过程,由编译器执行预编译命令完成:
执行上述命令后,在源文件目录下,将生成对应的.i文件:
每个*.i的内容 = *.c + *.h
在上述命令中,arm-none-eabi-gcc为编译指令,编译指令后,紧跟指令参数,“-E”表示仅执行预编译动作; “-o”代表执行结果输出到文件 “-o”后紧跟输出文件名。
为了在任意工作目录都能使用该命令,需要提前将该命令所在路径加入环境变量“PATH”中,对于Windows平台,其加入方法如下:
这一步骤,保证了编译器命令在各个工作路径下都能正常执行。
下面是界面示意,后文中用到的工具,若需要加入环境变量“Path”的,其目的和添加方法都一样,后文中将不再重复讲解添加路径操作。
经过预编译阶段后,每一个*.C均已包含了本文件的代码逻辑以及其他文件的函数及变量声明。此时,每个*.i经过编译器继续执行编译动作。编译这一过程,本质上是将C代码这一高级语言转换为汇编代码,和预编译过程一样,其执行后,每个*.i都将生成对应的*.s文件。
编译命令如下所示:
其命令格式与预编译类似,输入参数由“-E”变为了“-S”,该参数表示编译器仅执行编译操作,不进行汇编与链接。执行该命令后,输出目录下将出现*.s的汇编代码文件,其内容与处理器架构直接关联,不同架构处理器具有不同汇编指令,示例生成的main.s内容如下:
经过前面部分的讲解,我们可以合理的猜测,汇编阶段即是将汇编代码转换为目标平台的机器码。和我们猜测的一样,汇编阶段所作的操作,就是为每个*.s文件执行汇编,将其翻译为机器码。但有个需要注意的地方是,此处的*.s文件,不仅包含了每个*.c逐步骤生成的*.s,还包含了该芯片的启动代码:startup.s。启动代码一般是由芯片厂商编写,在执行main函数前执行的初始化代码。其中包括了诸如:堆栈空间分配、初始化中断向量表、RAM初始化、看门狗初始化、时钟初始化、以及跳转main函数等操作,每个处理器在启动代码部分的操作有细微差异,但其目的,都是为了初始化硬件,为main函数运行提供合适的运行环境。其中,堆栈分配自然不必多说,对于嵌入式代码,通常堆空间可以不分配,但栈空间的分配是必须的;RAM初始化主要对全局变量进行重定向及初始化,对于C代码来说,有初始值的全局变量将被分配到.data段,无初始值以及初始值为0的全局变量将被分配到.bss段,因此,初始化代码需要将有初值的全局变量其初值从Flash复制到RAM,将无初值以及初值为0的全局变量赋值为0,其中,初始化.data段这部分即被称为“重定向”。关于启动代码,内容与硬件直接相关,其实可以单独撰文详述,有时间作者将在别的文章中详细讲解。
回到主题:汇编阶段,会为需要汇编的每个*.s文件,都生成对应的*.o文件,不同编译器和目标平台所生成的文件后缀可能不一致,例如:IAR平台下,MP430单片机在这部分扩展名为*.r43,AVR单片机的扩展名为*.r82。但其文件内容,都是相似的,包含了C代码中所有的默认段(*.data、*.bss、*.common、*.text)和用户自定义段。
汇编命令如下所示:
-c参数表示编译器仅执行编译与汇编动作,不进行链接。
以上示例不仅汇编了main.s以及clock.s,还汇编了STM32F407单片机的启动代码,此时,输出的*.o文件不再是文本文件,我们已无法使用记事本打开查看,但每个*.o文件,都包含了代码执行所必须的段以及符号表。所谓符号表,即是C代码中全局变量的变量名,函数名,以及汇编代码中的程序段标号,它们将在链接阶段,被分别合并到最终可执行文件对应的段以及符号表中。这部分内容,读者有兴趣可以参考神作:《深入理解计算机系统》第七章,链接部分。此时我们将这些*.o文件称为:目标文件,它们的运行地址,均以0开始,真实运行地址将在链接阶段被重新指定,因此,它们更完整的名称应该是:可重定向目标文件。
在前面的阶段中,编译器均是对单个文件的处理,其生成的*.o目标文件中,也仅包含本文件的符号,但程序要想运行,一定要明确每个变量和函数的运行地址,而此时,*.o文件中的地址,都是以0开始,显然,直接合并会导致程序的变量和函数地址重叠,这样的程序无法运行,因此,需要链接器来进行最终的统筹工作,为*.o中的每个符号分配合适的地址。
在这个过程中,我们除了可以让源工程的目标文件(*.o)参与之外,还可以让其他可重定向目标文件参与。例如别的*.o文件、*.a、*.dll、*.lib等,它们,都属于可重定向目标文件,其内部的数据,已经符合目标处理器的指令集规则可以直接执行。链接器则需要进一步让他们有序的在存储器中排列,保障运行时程序有序执行,除此之外,部分数据(如代码段.text)我们将其指定链接到NOR-Flash中,即:从Flash中取指令,加载到CPU执行;另一部分(如数据段.data、.bss)将其运行地址链接到RAM中,以便在运行过程中改变数据。试想另一种情况,将全局变量链接到Flash是否可行呢?
显然,逻辑上是可行的,但是,程序运行时,读操作没有问题,写操作一定会异常,因为Flash的写操作,需要Flash驱动的参与,CPU的地址总线读写指令,显然不具备这样的能力。为了让全局变量运行时,能正常读写,我们将其运行地址重定向到RAM中。由于RAM的掉电丢数据特性,在程序main函数运行前,需要将.data段的数据(有初值全局变量的初值)复制到对应的RAM地址中,这部分工作不由链接器完成,而是由启动代码来执行,此时,连接器的重定向和启动代码的初始化RAM动作,有了因果关系。由于链接过程通常在非目标机器上完成,显然链接器只能做到指定运行地址,而无法搬移数据。这整个过程,我们称之为链接,而实际上,链接器还做了重定向工作。
链接是个复杂的过程,如果通过敲命令行进行链接,文件数量和参数少还好,一旦参数变多,那么,工作量将极其庞大,且容易出错。
于是,我们引入了链接脚本的概念,链接脚本中,将链接过程中需要指定的地址、存储分布、各个段的大小及起始地址,加载地址和运行地址,都做了约束。在链接时,我们仅需加载链接脚本文件,即可让链接器根据我们的约束,执行链接操作。关于链接脚本的编写和使用,读者可以根据需要扩展学习,在示例中,仅用于演示编译流程,并未使用链接脚本,以后有机会作者将在其他文章中详述链接脚本的语法格式。
链接命令我们可以使用继续使用arm-none-eabi-gcc指令,也可以使用arm-none-eabi-ld指令,均能生成可执行文件。二者的不同是,gcc指令可以输入*.c文件,直接执行到链接步骤,生成可执行文件,而ld指令,其输入只能是可重定向的目标文件。
链接指令如下所示:
上述两个指令均能生成elf可执行文件。
大多数情况下,都需要生成特定格式的Flash镜像文件(.hex、.s19、.bin),以便进行烧录及BootLoader升级。该需求可以使用arm-none-eabi-objcopy命令完成。
操作指令如下:
通过执行arm-none-eabi-objcopy指令,我们成功生成了hex文件。
文章至此,C代码编译的整个过程,基本介绍完毕,但在此处,还要特别引入MakeFile,尽管有了方便的编译工具帮助我们实现代码编译。源文件和编译参数的输入仍然是一件头疼的事,因此,Makefile的作用,即是将调用编译器编译的过程自动化,通过Makfile文件中约定编译参数、输入和输出,再执行Make命令,解析Makefile文件,帮助我们自动编译和构建可执行文件。
针对前文的示例,我们可以编写简单的Makefile,使用Make指令,解析Makefile,一个命令完成所有编译操作,生成*.elf可执行文件、*.hex文件、*.map文件,反汇编文件。
Makefile的语法不在本文讨论范围内,因此,不要求读者自己写Makefile,仅需会用Makefile完成编译即可。作者将在其他文章中深入探索Makefile语法,并总结记录成文。
本文的示例仅包含四个源文件,为其编写Makefile相对简单,在源文件目录下新建文本文件,去除扩展名,将其重命名为Makefile,输入如下内容,注意第2行和第3行需要以TAB键开头,否则无法将其识别为makefile文件:
Makefile文件需要由make指令调用,make工具很多,其主要作用是解析makefile文件并执行命令,作者电脑里安装了多个make工具,有x86平台的mingw32-make.exe,该工具安装x86平台Gcc编译器后即可用,另外,还有mingw附带的make.exe,无论使用哪一个,将其路径加入环境变量后,均可解析Makefile文件进行编译。
分别使用mingw32-make.exe和make.exe编译结果如下:
实测两个工具均可以成功解析Makefile,并编译生成elf和hex文件,make工具的获取和安装将在后文中讲到。
同链接脚本一样,Makefile的编写是件非常考验技术人员的事,本文中的Makefile仅适合演示简单工程,并不适合大型工程使用,但Makefile的使用方法是一样的。自己动手编写启动代码、Makefile和链接脚本需要对编译器和目标平台足够熟悉,并且需要持续学习,有兴趣的读者可以深入探索。目前常规的开发流程下,其实启动代码、Makefiel和链接脚本并不需要自己编写,芯片厂商一般都会提供,只需要会用即可,某些场景下,需要进行简单的修改,仅此而已,是否需要深入学习,完全取决于兴趣及工程需要。
常规的调试需求包含烧录加载程序、增删断点、查看修改变量等操作,除了Keil和IAR这样的封闭IDE外,大部分基于eclipse的集成开发环境大都支持开源调试工具链,这样能以最快的速度支持自家芯片,并与市面上大部分仿真器兼容。而GDB调试器和OpenOcd是其中的代表。本文的最终目标:自己搭建IDE,同样依赖于这两项强大的调试工具。下面将对GDB调试器和OpenOcd调试服务逐个进行介绍。
Openocd是一套开源的调试工具,目前在很多IDE中流行,跨平台支持着众多目标芯片的仿真和调试,如果说GDB是调试的人机接口,那么Openocd则是GDB调试器与不同芯片之间的接口,其负责将调试指令下发至仿真器驱动直接和目标芯片进行交互,从而实现GDB所希望达到的命令效果。因此OpenOcd充当着GDB调试器和不同目标芯片之间的翻译器。Openocd的代码完全开源,其官网地址为:http://openocd.org/getting-openocd/。
官方并不直接提供其可执行文件,因为OpenOcd支持了太多硬件仿真器,通常不是所有仿真器都需要,可以根据需要自行下载并编译出合适的版本。当前最新版本为0.11,官网虽然不直接给出可执行文件,但在其主页推荐了4个编译维护OpenOcd的网站,它们都有提供可以直接使用的可执行文件程序包。
本文没有使用这四个中的任何一个,而是在VisualGDB的主页找到了支持广泛且自己测试下来更好用的编译版本,这个版本支持的仿真器更多,兼容性更强,在此附上下载地址:openocd-20210625.7z。
下载完成后,同样需要将Openocd的可执行文件所在路径加入环境变量以便使用。添加方法已在前文提及,不在此重复。
Openoce需要加载对应的配置文件才能正常使用,通常需要加载接口配置文件以及目标配置文件,接口配置文件描述了所使用的仿真器类型,如常用的CMSIS-DAP、ST-LINK、JLINK等,在Openocd的interface目录下均能找到对应的配置文件;而所谓目标配置文件描述了需要调试的目标芯片,在Openocd的target目录下,列出了openocd所支持的众多芯片的配置文件。
在开始调试前,需要建立Openocd与硬件的连接,并在后台为GDB调试器开放调试端口,一般在硬件连接成功后,即可打开默认的调试端口:3333。
建立Openocd与目标芯片的连接使用如下命令:
出现上图的提示信息即表示成功打开调试端口,此时,可以进行下一步,使用GDB调试器进行芯片程序的加载,运行与调试。
在前面介绍GCC For Arm时,简单介绍过,对Arm平台调试,通常需要借助arm-none-eabi-gdb工具,该工具提供调试过程中,增删断点,调试目标等一系列操作的交互接口。GDB调试器其全称是:The GNU Project Debugger,从名称可以看出,其来源是大名鼎鼎的GNU项目,GDB一开始用于Linux平台的C代码调试,随着版本的迭代,现在则被Windows平台下的嵌入式开发IDE大量使用。arm-none-eabi-gdb是GDB的Arm平台版本,目前在众多ARM开发环境中被使用,以STM32为例,除Keil和IAR之外,近年来官方大力推广的TrueStudio和CubeIDE,二者都是基于eclipse的集成开发环境,都使用arm-none-eabi-gcc作为编译器,并集成了arm-none-eabi-gdb调试工具。
注:命令行下使用gdb务必先将gdb所在路径加入环境变量
下面对GDB的常用命令进行介绍:
1.启动gdb调试工具并加载elf文件
arm-none-eabi-gdb file_name.elf
2.连接到Openocd的调试端口(默认为:3333)
target remote localhost:3333
3.烧录程序
load
4.main函数处增加断点
b main
5.删除所有断点
delet
6.打印变量值
p 变量名
7.设置变量值
set 变量名 = value
8.继续执行程序
continue
下面附上部分STM32F4通过GDB命令行工具调试的记录:
通过上文的讲解,读者应对整个代码编译及调试流程有了一定了解,清楚了IDE所需的工具都有哪些。最后需要做的是:将这些工具组合起来,让命令行工具链以可视化方式与开发者交互。,最终,能在自己搭建的IDE上实现代码编辑、编译、烧录下载、调试,IDE的搭建就算完成。
在开始搭建IDE前,我们需要先列举一下搭建IDE所需的准备工作,主要是下载IDE所需的各部分替代工具,以及对它们进行配置。大部分内容已在前文中进行详述,并给出工具的下载链接,为了读者阅读方便,所有用到的工具,将在本节再次汇总其获取和配置方法。
下列工具,已链接了超链接,点击即可直接跳转下载。
前面提到,工具链都准备齐全了以后,自己搭建IDE还有三大障碍:启动代码、Makefile、链接脚本。以上三个文件都由开发者手动完成的话,工作量实在不小,实际上,如果都能自己写这三个文件了,其实IDE也没有必要了。
因此,开发STM32不需要自己会写这三个东西,通常芯片官方都会有提供,需要注意的是,对于STM32标准库而言,官方给出的标准库中,其启动代码和链接脚本,分别适配了Keil、IAR、以及GCC,如果使用标准库,提取时需要注意区分。若使用标准库,必须自己编写Makefile,因为Makefile文件的内容与工程文件结构直接关联,官方无法给出固定的Makefile。其实,面对这种情况,也不是无解,还可以通过Cmake工具,编写Cmake文件,自动生成Makefile。关于Cmake这部分内容,作者没有研究,有空研究再开篇单独说。
STM32近年来,大力推广HAL库及其代码配置工具STM32CubeMx,不少人用过,初始化配置自动生成代码确实方便。仔细观察其生成选项,除了可以生成Keil工程、IAR工程、TrueStudio工程、STM32CudeIDE工程,还可以生成Makefile工程。是的,生成的Makefile工程,已经同时包含了我们需要的启动代码、链接脚本、以及Makefile,这就是我们想要的。
CubeMx只用来生成工程,编译过程不需要它,因此,无需将其加入环境变量,下面省略其安装过程直接以图文方式讲解生成步骤,第一次使用需要安装库,可能会稍慢。
1.选择目标芯片,本文以STM32F407开发板为硬件平台,配置编写简单的点灯示例作为演示
2.配置晶振和LED引脚,作者开发板LED引脚为PF9与PF10,将其配置为GPIO_Output,配置PH0及PH1为晶振时钟输入引脚。
3.配置RCC选择时钟源,有外部晶振时,根据需要选择即可。
4.配置Debug调试方式,根据实际情况选择,不能不使能Debug,否则会导致初次烧录完成后无法通过Debug烧录。
5.根据需要配置GPIO引脚别名,笔者此处将其配置为:“LED0”、“LED1”。
6.配置系统时钟,注意选择时钟源、锁相环及时钟频率,填写时钟频率后,CubeMx会根据时钟源自动计算锁相环增益。
7.配置生成Makefile工程,生成后启动文件及链接脚本会自动包含在生成的工程中。
8.其他配置,作者此处勾选了将外设初始化单独放在各自的.C和.H文件中,否则,所有初始化代码都会直接在main.c中定义,实在别扭,此处读者可以根据个人习惯决定。
9.生成工程代码。
生成的工程目录内容如下,可以看到,除了HAL库和初始化配置代码外,启动代码、Makefile、链接脚本都在在里面:
Vscode轻量而强大,原本只是一个文本编辑器,但其丰富强大的插件库,可以适应众多应用场景:各种形式的文本编辑、代码管理、语法高亮、智能补全、文件解析对比等,被众多开发者和普通用户喜爱。在笔者电脑上,VScode已经成为装机必备的万能工具,替代了电脑中原先的多个软件,真正的在朝着All IN ONE的方向发展,这次,被笔者用来作为ARM平台的嵌入式开发IDE,它完美诠释了小而强大:编辑体验碾压Keil和IAR,UI界面碾压Eclipse,加上其他插件,可以做到一个软件搞定大部分开发需求
安装过程此处省略,要想使用Vscode进行嵌入式开发,需要安装两个组件: C/C++插件、Cortex Debug,前者用以分析C代码,语法高亮和智能补全,后者用来配置调试工具以及调试代码。
Vscode插件安装方法很简单,在其扩展商店中直接搜索,安装即可:
安装C/C++插件:
安装Cortex Debug插件:
以上两个插件安装完毕后,Vscode基本可用了。
其他插件,读者可以自己选择性的安装:
Intel HEX format,可解析HEX文件
Hex Editor,可编辑二进制文件
Mapfile Syntax,可解析.map文件
LinkerScript,可解析链接脚本.ld文件
GCC For Arm在前文花了大量篇幅讲述,本文给出的链接下载后是压缩包,因此,解压即可使用,无需安装。
为了让GCC在任意目录下均可用,笔者将GCC For Arm工具链路径加入环境变量,添加方法在2.3节有详细说明,请参照前文。
为验证arm-none-eabi-gcc工具是否正确加入环境变量,可以打开powershell控制台,在控制台输入arm-none-eabi-gcc,若出现如下提示,即表示GCC For Arm已经全局可用。
本节主要目的是获取使用Makfile编译所必须的Make工具,由于历史原因,GCC原本是linux平台下的编译套件,Make工具也集成在其中,因此需要安装x86平台的GCC编译器以获取Make工具,x86平台的GCC,其包名称为:mingw-w64。
GCC下载渠道很多,但笔者这里通过msys2安装,msys2是一个包管理工具,正如其官方介绍:“MSYS2 is a collection of tools and libraries providing you with an easy-to-use environment for building, installing and running native Windows software”。Windows平台下的很多开源工具一开始都在linux下流行,因此,其很多依赖库都是linux移植而来,Msys2就是这样一个工具,可以方便的下载这些依赖,以支持开发者在Windows平台开发和运行原生应用。本文中,我们仅用它安装x86平台GCC以获取Make工具,实际上,开发过程中的很多依赖,都可以用它下载,下面开始安装步骤。
2.打开Msys,运行命令:pacman -Syu,更新数据库。
3.运行命令:pacman -S mingw-w64-x86_64-toolchain,安装GCC工具链,出现提示时,回车全部安装即可,当前的Msys2已添加中科大和清华镜像源,下载安装很快。
安装完成后,进入Msys2安装目录,GCC的bin目录下的mingw32-make.exe即是我们需要的make工具。
4.将mingw32-make.exe所在目录加入环境变量,在控制台输入命令:mingw32-make,出现如下提示即表示加入全局变量成功。
5.提取make工具至GCC For Arm,这一步是可选的,不是必须,如有需要,可将mingw32-make.exe复制到arm-none-eabi-gcc所在目录,更名为make.exe。此时,make也全局可用。
经过以上步骤,生成的工程可以正常编译,并生成烧录文件,而调试功能,除了依赖于arm-none-eabi-gdb,还需要配置OpenOcd以适配仿真器,建立目标芯片,仿真器以及PC的连接。
本文给出的OpenOcd版本为0.11,解压即可使用,为了开发方便,需要将openocd.exe的路径加入环境变量,方法请参照2.3节。
通常调试目标芯片都使用确定的仿真器,因此,为了使用方便,可以直接从Openocd的目录中,将接口配置文件和目标配置文件复制到STM32的工程目录,以便使用:
拷贝完成后,工程目录内容如下所示:
在4.2中生成的工程已经包含了Makefile,至此,编译器和make工具均已就绪,我们可以尝试在命令行下编译STM32工程。
进入到4.2节生成的STM32工程根目录(Makefile所在目录),在该目录下进入控制台,对于WIN10系统,可以按住Shift,然后在根目录下右键打开powershell。
2.执行mingw32-make或make指令,指令后跟-j参数,可启动并行编译,充分利用处理器多核性能,加快编译速度
可以看到,Make编译完成后,控制台打印了程序大小信息,生成了elf文件,以及hex和bin文件以及一堆lst反汇编文件,这些文件以及编译的中间文件,都存放在工程下自动新建的build目录。其中:
GDB调试的前提是OpenOcd调试工具已经正确完成硬件连接,并开启调试端口,因此,调试前需要运行openpcd命令,连接目标芯片,且调试过程中,Openocd服务需要持续运行。
操作步骤如下:
1.连接开发板,上电,确保仿真器正确连接。
2.打开调试端口,在工程目录下打开powershell,由于已经将接口配置文件和目标配置文件复制到工程目录,直接使用目录下的配置文件即可,执行指令:openocd.exe -f .\stlink-v2.cfg -f .\stm32f4x.cfg.执行结果如下,打开3333调试端口成功:
3.开启gdb调试器,加载上一节中编译生成的elf文件,由于Openocd需要持续运行,我们新开一个powershell终端,执行如下指令(elf文件名根据实际情况填写):
arm-none-eabi-gdb.exe .\build\STM32_LED.elf
终端将打印如下内容:
4.连接至3333调试端口
执行target指令即可连接至openocd开启的3333调试端口,成功连接后,gdb提示产生了复位中断,一切正常。
5.烧录elf文件至目标芯片
在GDB工具中输入指令:load,即可将启动gdb时加载的elf文件烧录至目标芯片。烧录成功的打印内容如下,其他调试指令读者可参照3.2节自行尝试:
VScode本质上只是一个文本编辑器,如果要使其自动识别工程下的C语言源文件,需要对C/C++插件进行正确的配置才能获得我们想要的效果:
1.源文件搜索路径
有些工程的源文件并不在工程目录下,可能在别的SDK路径下,此时如果不添加源文件路径,C/C++插件无法找到源文件并正确跳转。(本文示例工程不存在SDK路径,所有用到的源文件均在工程目录下,因此添加工程路径即可)
2.预编译宏定义
许多厂商提供的库都支持自家多款处理器,通常使用不同的预编译宏定义来进行条件编译以适配不同处理器,因此,需要在工程中添加预编译宏定义才能正确编译和高亮。(对于本文示例来说,编译部分由Makefile决定,因此,Makefile中已经添加了预编译宏,编译不会出错。然而,C/C++显然它不会去读取Makefile内容获取工程应加的预编译宏,无法高亮显示正确的代码,因此预编译宏应由我们手动添加)
3.编译器路径
插件要想参与编译,必然要获取编译器的路径,在VScode的C/C++插件中,编译器路径必须完整到编译器的可执行文件。
4.C语言标准
不同的C语言标准下,支持的语法会稍有区别,因此,必须让C/C++插件知道工程的源文件代码以哪一个版本的C语言标准提供识别支持(如果不知道请填C99,大部分人所学的C语言版本普遍为C99,使用GNU工具链则填写GNU99)。
5.高亮感知模式
这部分在该插件下提供了众多可选项,目的是为了更好的适配目标平台进行代码高亮。(本示例中,选用“gnu11”)
以上内容的配置,网上大部分教程通常直接让读者新建c_cpp_properties.json文件配置,该方法适合对Vscode有一定了解的读者,对新手并不友好。其实,Vscode本身提供了图形化的配置界面。
选择图形化配置界面后,将本节提到的几个参数作如下配置:
此处要特别注意预编译宏的添加,具体添加什么内容,可从Makefile中的C_DEFS字段获知。
做完以上更改后,工程目录夹下将会新增.vscode文件夹,目录下新增了c_cpp_properties.json配置文件,打开该文件,可以看到,里面已经有了自定义的ARM配置。
删除并不需要的默认Win32配置后,c_cpp_properties.json中的配置即为以后该工程的默认配置:
上一节我们通过配置C/C++插件,让代码编辑和语法高亮,变量函数跳转这些功能正常使用,体验甚至超过Keil、IAR、Eclipse等主流IDE,本节我们尝试新建build任务,让Vscode可以在图形界面下编译工程。新建build任务笔者暂未找到图形化配置方法,但可以参考Vscode的build任务模板进行配置。
1.创建build任务模板,操作如下图所示:
2.配置build任务和clean任务,最终配置如下
3.尝试执行build命令编译工程
在Vscode界面下按Ctrl+Shfit+B可以唤出任务列表,可以看到task.json中配置的两个任务,一个build,一个clean。
执行build任务后,可以看到,其效果与在命令行下执行make -j或者mingw32-make -j的效果一致,都在build目录下生成了.elf与.hex文件,其本质是编译Makefile中的all伪目标,因此,make -j all才是命令的完整写法,我们使用了简化写法。
4.尝试执行clean命令清除编译输出文件
某些情况下,需要清除所有输出文件,重新进行全新编译而不是增量编译,此时,需要编译Makefile中的clean伪目标,查看Makefile可以知道,其真实操作是在powershell下执行了rm指令,删除中间文件,只不过由make工具来帮助完成解析执行。
执行clean任务后,build目录下的所有文件都将被清除,读者可以自行尝试。
经过上一节的内容,我们已经可以在Vscode下正常编辑编译代码,本节我们将通过Cortex Debug插件,在Vscode下实现正常的烧录,调试。
通过前面的介绍,我们知道,调试需要先开启Opencod调试服务器端口,再使用gdb调试器连接至调试端口,有了这些基础,才能正常调试。而Vscode下的Cortex Debug插件则在GDB调试器的基础上做了深度集成,支持众多调试服务器。如果抽象一点,可以这样理解:Cortex Debug = GDB + GDB Server,其中,GDB Server可以是Openocd GDB Server、J-Link GDB Server、ST-LINK GDB Server、pyOCD GDB Server等的任意一种,本文使用的是Openocd GDB Server。
配置Cortex Debug的步骤如下:
1.新建调试配置模板
2.复制SVD文件至工程目录
由于Cortex Debug支持加载SVD文件,因此,需要自己找到目标芯片的SVD文件,拷贝至工程根目录,供Cortex Debug加载使用。SVD是芯片的资源描述文件,相当于数据化的芯片手册,有了SVD文件的支持,调试时可以查看芯片寄存器和外设的状态。
SVD文件可以在芯片的支持包中找到,具体到本文示例,作者是在Keil的包路径中将所需SVD文件提取出来,其路径为:xx\Keil\Packs\Keil\STM32F4xx_DFP\2.13.0\CMSIS\SVD。其中包含了STM32系列F4系列所有芯片的SVD文件:
本文示例所用芯片为STM32F407,因此将STM32F40x.svd文件拷贝至工程目录下:
3.配置Openocd调试服务器
步骤1后,.vscode文件夹下将新建launch.json文件,我们需要对其默认内容稍作修改:
4.写个简单的流水灯
修改\Core\Src\main.c,在GPIO初始化函数MX_GPIO_Init()下,添加如下两行代码,使LED0与LED1的初始状态相反:
HAL_GPIO_WritePin(GPIOF,LED0_Pin,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOF,LED1_Pin,GPIO_PIN_RESET);
修改\Core\Src\main.c,在While主循环中添加如下两行代码,使LED状态50ms反转一次。
HAL_GPIO_TogglePin(GPIOF,LED0_Pin|LED1_Pin);
HAL_Delay(50);
5.调试运行流水灯
在Vscode界面按下F5,或者点击Debug按钮,后台将执行编译任务,若编译无误,将自动启动Openocd服务,烧录调试,开始运行时会先进入复位中断,点击运行即可正常运行,可以在运行过程中随时打断点,查看寄存器和变量,单步运行。
编辑界面如下,输入时可以智能提示补全:
调试界面如下:
至此,本文的最终目的已经达到了,目标虽然是自己搭建集成开发环境,但实际是为了深入理解工具链、编译流程、调试原理。很多技术细节限于篇幅无法更加深入的探索,而Makefile、启动文件、链接脚本、Openocd、编译器参数、每一个都值得深究,本文算是挖坑文,希望以后有空继续在深入理解工具链的系列文章中补全,再会!
本文用到的所有示例代码及最终工程,已开源,请在本人代码仓库中下载:
https://gitee.com/zhangjiance/vscode_-stm32-f407_-led