本教程的示例代码是笔者参加RoboMaster机甲大师赛为机器人编写的控制器框架,你可以直接克隆仓库,阅读仓库下的Markdown文档获得更好的体验:
所有安装包也可以在此百度网盘链接下获得:
链接:pan.baidu.com/s/1sO_EI4
提取码:6666

更新:强烈推荐通过Msys2安装所需的所有环境,只需要一键即可配置。MinGW、Arm GNU toolchain和OpenOCD也可以通过MSYS2使用pacman包管理器(和apt/yum类似)直接安装,这种方法一步到位,这是更推荐使用的方式,请参看附录5

环境配置

  • 所有需要编辑的配置文件都已经在basic_framework的仓库中提供,如果不会写,照猫画虎。
  • 安装STM32CubeMX,并安装F4支持包和DSP库支持包
  • 安装VSCode,并安装以下插件:
  1. C/C++:提供C/C++的调试和代码高亮支持
  2. Better C++ Syntax:提供更丰富的代码高亮和智能提示
  3. C/C++ Snippets:提供代码块(关键字)补全
  4. Cortex-Debug和Cortex-Debug: Device Support Pack - STM32F4:提供调试支持
  5. IntelliCode和Makfile Tools:提供代码高亮支持
  • 安装MinGW,等待界面如下:

安装好后,打开MinGW后将所有的支持包勾选,然后安装:

安装完以后,将MinGW的bin文件夹添加到环境变量中的path下,按下菜单键搜索编辑系统环境变量打开之后:

图片看不清请打开原图。验证安装:

打开命令行(win+R,cmd,回车),输入gcc -v,如果没有报错,并输出了一堆路径和参数说明安装成功。

  • 配置gcc-arm-none-eabi环境变量,把压缩包解压以后放在某个地方,然后同上,将工具链的bin添加到PATH:
安装路径可能不一样,这里要使用你自己的路径而不是直接抄

验证安装:

打开命令行,输入arm-none-eabi-gcc -v,如果没有报错,并输出了一堆路径和参数说明安装成功。

添加到环境变量PATH的意思是,当一些程序需要某些依赖或者要打开某些程序时,系统会自动前往PATH下寻找对应项。一般需要重启使环境变量生效。
  • 将OpenOCD解压到一个文件夹里,稍后需要在VSCode的插件中设置这个路径。
  • CubeMX生成代码的时候工具链选择makefile

生成的目录结构如下:

Makefile就是我们要使用的构建规则文件。

如果你使用basic_framework,不需要重新生成代码。

VSCode编译和调试配置

VSCode常用快捷键包括:

功能 快捷键
选中当前行 Ctrl+L
删除当前行 Ctrl+Shift+K
重命名变量 F2
跳转到定义 Ctrl+点击
在打开的文件页中切换 Ctrl+Tab
在当前文件查找 Ctrl+F
在整个项目文件夹中查找 Ctrl+Shift+F
查找所有引用 Alt+Shift+F12
返回上一动作 Alt+左

更多快捷键可以按ctrl+K再按ctrl+S显示,并且可以修改成你最习惯的方式。此外,使用Snippets可以大幅度提高重复性的代码编写速度,它可以直接帮你补全一个代码块(如for、while、switch);补全和snippet都使用Tab键接受代码提示的提议,通过↑和↓键切换提示。

编译

为了提供完整的代码高亮支持,需要配置Makefile tools插件的make程序路径,ctrl+,打开设置,搜索make path找到设置并填写:

mingw32-make就是下面介绍的make工具(配合makefile替代手动调用gcc)。这里之所以只要输入mingw32-make而不用完整路径,是因为我们将mingw的bin文件夹加入环境变量了,因此系统会在PATH下自动寻找对应项

用VSCode打开创建的项目文件夹,Makefile Tools插件会询问你是否帮助配置intellisense,选择是。

此时就可以享受intellicode带来的各种便利的功能了。我们的项目使用Makefile进行编译,在之前的编译介绍中,以GCC编译器为例,如果需要编译一个文件,要输入如下命令:

gcc your_source_code_name.c -o output

然而,你面对的是一个拥有几百个.c和.h文件以及大量的链接库,如果要将所有文件都输入进去,那将是一件苦恼的事。Makefile在gcc命令上提供了一层抽象,通过编写makefile来指定参与编译的文件和编译选项,再使用make命令进行编译,它会自动将makefile的内容“翻译”为gcc命令。这样,编译大型项目就不是一件困难的事了。更多关于makefile的指令介绍,参见附录3

实际上,在使用keil MDK开发的时候,它调用的仍然是底层的arm cc工具链中的编译器和链接器,在配置“魔术棒”添加项目文件以及包含目录的时候,实际做的使其和makefile差不多。keil使用的参数可以在魔棒的C/C++选项卡下看到。

对于一个已经拥有makefile的项目,打开一个终端,输入:

mingw32-make -j24 # -j参数表示参与编译的线程数,一般使用-j12
注意,多线程编译的时候输出的报错信息有时候可能会被打乱(多个线程同时往一个terminal写入程序运行的信息),要是看不清报错,请使用mingw32-make,不要进行多线程编译。
我对make的编译命令进行了静默处理,只输出error和warning以及最后的生成文件信息。如果想要解除静默(就是下面所说的“你可以看到大致如下的输出”),需要修改Makefile。本仓库下的makefile中已经用注释标明。

就会开始编译了。你可以看到大致如下的输出:

arm-none-eabi-gcc -c -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -
DUSE_HAL_DRIVER -DSTM32F407xx -DARM_MATH_CM4 -DARM_MATH_MATRIX_CHECK -DARM_MATH_ROUNDING -
IHAL_N_Middlewares/Inc -IHAL_N_Middlewares/Drivers/STM32F4xx_HAL_Driver/Inc -
IHAL_N_Middlewares/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy -
IHAL_N_Middlewares/Drivers/CMSIS/Device/ST/STM32F4xx/Include -IHAL_N_Middlewares/Drivers/CMSIS/Include -
IHAL_N_Middlewares/Drivers/CMSIS/DSP/Include -
IHAL_N_Middlewares/Middlewares/ST/STM32_USB_Device_Library/Core/Inc -
IHAL_N_Middlewares/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc -
IHAL_N_Middlewares/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS -
IHAL_N_Middlewares/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F -
IHAL_N_Middlewares/Middlewares/Third_Party/FreeRTOS/Source/include -
IHAL_N_Middlewares/Middlewares/Third_Party/FreeRTOS/Source/include -
IHAL_N_Middlewares/Middlewares/Third_Party/SEGGER/RTT -IHAL_N_Middlewares/Middlewares/Third_Party/SEGGER/Config -
IHAL_N_Middlewares/Middlewares/ST/ARM/DSP/Inc -Iapplication -Ibsp -Imodules/algorithm -
Imodules/imu -Imodules/led_light -Imodules/master_machine -Imodules/motor -Imodules/referee -
Imodules/remote -Imodules/super_cap  -Og -Wall -fdata-sections -ffunction-sections -g -gdwarf-2 -MMD -MP -
MF"build/stm32f4xx_hal_pwr_ex.d" -Wa,-a,-ad,-alms=build/stm32f4xx_hal_pwr_ex.lst 
HAL_N_Middlewares/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr_ex.c -o build/stm32f4xx_hal_pwr_ex.o

仔细看你会发现,make命令根据makefile的内容,调用arm-none-eabi-gcc编译器,传入了一堆的参数以及编译选项然后运行。

最后输出的结果如下:

text    data     bss     dec     hex filename
  31100     484   35916   67500   107ac build/basic_framework.elf
arm-none-eabi-objcopy -O ihex build/basic_framework.elf build/basic_framework.hex
arm-none-eabi-objcopy -O binary -S build/basic_framework.elf build/basic_framework.bin

由于使用了多线程编译,比KEIL的蜗牛单线程要快了不少。以上内容代表了生成的可执行文件的大小以及格式和内容。.elf文件就是我们需要传递给调试器的东西,在使用VSCode调试部分会介绍。

当然了,你可能觉得每次编译都要在命令行里输入参数,太麻烦了。我们可以编写一个task.json,这是VSCode的一个任务配置,内容大致如下:

# makefile是CubeMX自动生成的,我们需要自己添加新编写的源文件路径和头文件文件夹,也可以额外加入自己需要的参数满足需求
######################################
# target
######################################
TARGET = basic_framework # 编译生成的目标文件名,如本项目会生成basic_framework.elf/bin/hex三个
# 注意,makefile会自动生成一个叫@的变量,其值等于TARGET.
# 在makefile中获取变量的值需要通过$(var_name),即加上括号并在前面使用$
​
######################################
# building variables
######################################
# debug build?
DEBUG = 1 # 是否启用debug编译.程序分为DEBUG版和RELEASE版,后者在编译时不会插入调试符号和调试信息相关支持的内容,使得程序运行速度提高.
# optimization
OPT = -Og # 编译优化等级,-Og表示调试级,常见的级别请看代码块下面的表格.
​
​
​
#######################################
# paths
#######################################
# Build path
BUILD_DIR = build # 编译的中间文件和目标文件存放路径,为了区分项目文件和编译输出,一般构建一个build(构建)文件夹,用于存放上述文件. 这个表达式也在生成了一个BUILD_DIR变量(可以把Makefile当作一种语言)
​
######################################
# source
######################################
# C sources, 参与编译的C源代码全部放置于此.注意如果换行写需要在行尾空格之后加反斜杠,最后一行不要加
# p.s. C语言的宏如果不能一行写完,也要在行尾加反斜杠,表示一行没有结束
C_SOURCES =  \
HAL_N_Middlewares/Src/main.c \
HAL_N_Middlewares/Src/gpio.c \
HAL_N_Middlewares/Src/adc.c \
HAL_N_Middlewares/Src/can.c 
​
# ASM sources 汇编源文件,第一个是stm32的启动文件,包含了bootloader的信息使得程序可以找到main函数的入口,第二个文件是添加对segger rtt viewer的支持.
ASM_SOURCES +=  \
startup_stm32f407xx.s \
HAL_N_Middlewares/Middlewares/Third_Party/SEGGER/RTT/SEGGER_RTT_ASM_ARMv7M.s
​
#######################################
# binaries, 下面是要执行的指令
#######################################
PREFIX = arm-none-eabi-  # 指令之前加的前缀,这里也是申明了一个变量
# The gcc compiler bin path can be either defined in make command via GCC_PATH variable (> make GCC_PATH=xxx)
# either it can be added to the PATH environment variable.
ifdef GCC_PATH # 和C语言的宏类似,如果在Makefile里定义或给make命令传递了GCC_PATH变量会执行以下内容.但实际上我们执行的是else的内容
CC = $(GCC_PATH)/$(PREFIX)gcc 
AS = $(GCC_PATH)/$(PREFIX)gcc -x assembler-with-cpp 
CP = $(GCC_PATH)/$(PREFIX)objcopy 
SZ = $(GCC_PATH)/$(PREFIX)size
else
# 定义了一个cc变量,其保存的内容实际上是gcc编译器的路径.makefile中要获取一个变量的值,需通过$(var).这里makefile会自动在环境变量里寻找gcc路径.CC里保存的内容是arm-none-eabi-gcc,就是我们添加到环境变量的arm gnu工具链的路径下的一个可执行文件.你可以尝试在cmd中输入arm-none-eabi-gcc,会发现这是一个可执行的程序.之前我们在验证安装的时候就运行了arm-none-eabi-gcc -v命令.
CC = $(PREFIX)gcc 
# 定义了一个AS变量,稍后会用于C/ASM混合编译
AS = $(PREFIX)gcc -x assembler-with-cpp 
# 定义变量.objcopy能够将目标文件进行格式转换.我们实际上要生成的目标文件是.elf,objcopy可以将其转化为hex和bin格式,用于其他用途.
CP = $(PREFIX)objcopy 
# size命令可以获取可执行文件的大小和包含内容信息.
SZ = $(PREFIX)size  
endif
HEX = $(CP) -O ihex # 这里用到了上面定义的CP,命令含义为将其转换成hex,i的前缀表示intel格式
BIN = $(CP) -O binary -S # 转化为二进制文件
 
#######################################
# CFLAGS, 在编译C语言程序的时候给GCC编译器传入的参数
#######################################
# cpu
CPU = -mcpu=cortex-m4 # 目标CPU类型.我们前面介绍过,不同的平台支持的汇编指令不同,一条相同的C语言表达式在翻译成汇编的时候会有不同的实现.比如8051单片机就只有加法器,因此他的乘除法都是通过多次加法和减法实现的,编译器就要完成这一工作.再比如STM32F4系列拥有浮点运算单元(FPU),可以直接在硬件上实现浮点数的加减法.这里指定编译的目标平台是cortex-m4内核的mcu.
​
# fpu 上面说了我们的f407是有FPU的,需要传入特殊的参数.fpv4-sp-d16表示float point,m4内核,single presicion, 16个dword(4字节)运算寄存器.
FPU = -mfpu=fpv4-sp-d16
​
# float-abi 使用软件还是硬件实现浮点运算.也就是我们说的如果没有FPU就只能使用软件实现浮点运算.这里选择hard硬件
FLOAT-ABI = -mfloat-abi=hard
​
# mcu 把上面几个变量合起来弄成一条长的参数
# Thumb是ARM体系结构中的一种16位指令集,这里-mthumb会启用它,感兴趣的同学可以进一步搜索.
MCU = $(CPU) -mthumb $(FPU) $(FLOAT-ABI)
​
# macros for gcc
# AS defines
AS_DEFS = # 汇编的一些宏定义
​
# C defines
C_DEFS =  \  # C语言的宏定义
-DUSE_HAL_DRIVER \ # 使用HAL库.HAL库的许多头文件和源文件里会判断是否定义了这个宏
-DSTM32F407xx \    # HAL库会根据使用的MCU的不同进行条件编译,这是一个很好的封装技术
-DARM_MATH_CM4 \   # 启用ARM MATH运算库,我们在卡尔曼滤波和最小二乘法的时候会用到矩阵运算
-DARM_MATH_MATRIX_CHECK \ # 启用矩阵乘法库
-DARM_MATH_ROUNDING       # 对数学库的输出结果进行取整防止溢出?
​
# AS includes
AS_INCLUDES = # 汇编包含目录.汇编语言也和C一样可以多个文件联合编译,在没有C语言的时候大家都是利用这种方式开发的.在一些运算资源极其受限的情况下也会直接编写汇编.
​
# C includes, C语言的包含目录,将所有参与编译的头文件目录放在这里,注意是目录不需要精确到每一个文件.
# 不想一行写完记得行尾加\,最后一行不要加
C_INCLUDES =  \
-IHAL_N_Middlewares/Inc \
-IHAL_N_Middlewares/Drivers/STM32F4xx_HAL_Driver/Inc
​
​
# compile gcc flags, gcc的编译参数,这些参数自己感兴趣的话去搜索一下.这还将之前定义的一些参数以变量的形式放过来.
ASFLAGS = $(MCU) $(AS_DEFS) $(AS_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections
​
CFLAGS += $(MCU) $(C_DEFS) $(C_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections
​
ifeq ($(DEBUG), 1)
CFLAGS += -g -gdwarf-2
endif
​
​
# Generate dependency information
CFLAGS += -MMD -MP -MF"$(@:%.o=%.d)"
​
​
#######################################
# LDFLAGS,传递给链接器的参数
#######################################
# link script
LDSCRIPT = STM32F407IGHx_FLASH.ld # 需要参与链接的文件.这个文件指明了特定MCU的内存分布情况,使得链接器可以按照此规则进行链接和地址重映射.
​
# libraries,要添加的库,这里我们要使用编译好的math运算库.在CubeMX里面生成的时候可以在第三方库选择DSP运算库,生成makefile时会自动添加进来.
LIBS = -lc -lm -lnosys  \
-larm_cortexM4lf_math
LIBDIR =  \ # 和上一行命令对应,这里引入库的目录,gcc会自动去目录里寻找需要的库文件
-LHAL_N_Middlewares/Drivers/CMSIS/Lib/GCC
LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections
​
# default action: build all
all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin
​
​
#######################################
# build the application
#######################################
# list of objects
# OBJECTS保存了所有.c文件的文件名(不包含后缀),可以理解为一个文件名列表.notdir会判断是否是文件夹
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
vpath %.c $(sort $(dir $(C_SOURCES))) # 对.c文件进行排序,百分号%是通配符,意为所有.c文件vpath是makefile会搜索的文件的路径.如果最终找不到编译中产生的依赖文件所在的路径且不指定搜索路径,makefile会报错没有规则制定目标(no rule to build target)
​
# list of ASM program objects
# 把所有.s文件的文件名加到OBJECTS里面
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s=.o)))
vpath %.s $(sort $(dir $(ASM_SOURCES))) # 对.s文件的文件名也进行排序
​
# 以下是编译命令,命令之前被高亮的@就是静默输出的指令.删除前面的@会将输出显示到命令行.
# 如@$(CC) -c $(CFLAGS) ...... 去掉第一个@即可.
​
# 意味根据makefile,在BUILD_DIR变量指定的路径下将参与编译的所有.c文件编译成.o文件
$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR) 
    @$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@
    # 上面这句话翻译一下实际上是gcc -c -many_param build/xxx -o build
    # 意思是将所有参与编译的文件都列出来,传递一堆编译参数,让他们生成.o文件,并且放在build文件夹下
# 意为根据makefile,将.s文件编译成.o文件,具体和上一条命令差不多
$(BUILD_DIR)/%.o: %.s Makefile | $(BUILD_DIR)
    @$(AS) -c $(CFLAGS) $< -o $@
# 根据前两步生成的目标文件(.o,这些文件的名字保存在OBJECTS变量里),进行链接生成最终的.elf
$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile
    @$(CC) $(OBJECTS) $(LDFLAGS) -o $@
    @$(SZ) $@ # 输出生成的.elf文件的大小和格式信息
​
$(BUILD_DIR)/%.hex: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
    $(HEX) $< $@ # elf转换成hex
    
$(BUILD_DIR)/%.bin: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
    $(BIN) $< $@ # 转换成bin
    
$(BUILD_DIR): # 如果makefile所处的文件目录下没有build文件夹,这里会新建一个build文件夹.
    @mkdir $@   
​
#######################################
# clean up,清除编译信息,可以在命令行中通过rm -r build执行,实际上就是把build文件夹删掉
#######################################
clean:
    rm -r $(BUILD_DIR)
​
  
#######################################
# dependencies
#######################################
-include $(wildcard $(BUILD_DIR)/*.d) # 包含所有的依赖文件(d=dependency),这是编译产生的中间文件,当hello.c包含hello.h而后者又包含了其他头文件时,会产生一个hello.d,它包含了hello.h中包括的其他的头文件的信息,提供给hello.c使用.
​
# *** EOF ***

这样,你就可以点击VSCode工具栏上方的Terminal->Run task选择你刚刚配置的任务开始编译了。更方便的方法是使用快捷键:ctrl+shift+B

还没配置任务的时候,需要在Terminal标签页中选择Configure Tasks... 创建一个新的.json文件。
P.S. VSCode中的大部分配置都是通过json文件保存的。当前工作区的配置在项目文件夹中的.vscode下,全局配置在设置中修改。全局配置在当前工作区没有配置的时候会生效,反之被前者覆盖。

如果你编写了新的代码文件...

Makefile的大部分内容在CubeMX初始化的时候就会帮你生成。如果新增了.c的源文件,你需要在C_SOURCES中新增:

换行需要在行尾加反斜杠\

如果新增了头文件,在C_INCLUDES中新增头文件所在的文件夹:

换行需要在行尾加反斜杠\

添加完之后,重新编译即可

和KEIL新增文件的方式很相似,但是更方便。

简单的调试配置

在VSCode中调试不能像Keil一样查看变量动态变化,但是支持以外的所有操作,如查看外设和反汇编代码,设置断点触发方式等。
用于调试的配置参考这篇博客:Cortex-debug 调试器使用介绍,这里包含了一些背景知识的介绍。你也可以直接查看下面的教程。

你需要配置arm gnu工具链的路径(工具链包括编译器、链接器和调试器等),OpenOCD的路径(使得GDB调试器可以找到OpenOCD并调用它,从而连接硬件调试器如j-link等),JlinkGDBServer的路径,以及该工作区(文件夹)的launch.json文件(用于启动vscode的调试任务)。

VSCode ctrl+,进入设置,通过搜索找到cortex-debug插件的设置。

  1. 搜索armToolchainPath,设置你的arm gcc toolchain的bin文件夹。bin是binary的缩写,实际上文件夹内部是一些可执行文件,整个工具链都在这里(注意该文件夹是刚刚解压的arm gcc toolchain的根目录下的bin文件夹,里面有很多以arm-none-eabi为前缀的可执行文件)。此路径必须配置。
  2. 搜索openocdPath,设置你的openocd路径(需要包含到openocd的可执行文件)。使用daplink调试需要配置这个路径。
  3. 搜索JLinkGBDServer,设置JlinkGDBServerlCL.exe的路径(在Jlink安装目录下,CL代表command line命令行版本)。使用jlink调试需要配置这个路径。

注意,windows下路径需要使用两个反斜杠\\代表下一级文件夹。

其他配置需要的文件已经全部在basic_framework中提供,包括openocd.cfg STM32F407.svd .vscode/launch.json

主要需要配置这三个路径,第四个gdbPath可以选配

如果教程中的启动json文件看不懂,请看仓库里的.vscode下的launch.json,照葫芦画瓢。

根目录下已经提供了C板所需的.svd和使用无线调试器时所用的openocd.cfg配置文件。

然后选择run and debug标签页,在选项中选择你配置好的选项,开始调试。或者使用快捷键:F5

我们的仓库中默认提供了两种下载器的支持,dap-link(无线调试器属于这一种)和j-link(包括小的j-link OB和黑色大盒子jlink)。

调试介绍

开始调试后,显示的界面如下:

  1. 变量查看窗口,包括当前调用栈(当前作用域或代码块)内的局部变量、当前文件的静态变量和全局变量。register选项卡可以查看cpu内核的寄存器数值。
  2. 变量watch窗口。右键单击要查看的变量,选择watch加入查看。

还支持直接运行到指针所选处(Run to Cursor)以及直接跳转到指针处执行(Jump to Cursor)。添加行内断点(若一个表达式由多个表达式组成)也是很方便的功能,可以帮助进一步定位bug。

右键点击添加到watch窗口的变量,可以临时修改它们的值。调参的时候非常好用。

VSCode提供的一个最大的便利就是,你可以将鼠标悬停在需要查看的变量上,不需要添加到watch就能观察变量值。如果是指针还可以自动解析,获取解引用后的值。结构体也支持直接展开。

3. 调用栈。表明在进入当前代码块之前调用了哪些函数,称之为栈也是因为调用的顺序从下至上。当前函数结束之后栈指针会减小,控制权会返还给上一级的调用者。通过调用栈可以确认程序是如何(按怎样的顺序)运行到当前位置的。

4.片上外设。这里可以查看外设的控制寄存器状态寄存器的值,如果通过断点无法定位bug,则需要查找数据手册和Cortex M4指南的相关内容,根据寄存器值来判断程序当前的情况。

5.断点。所有添加的断点都会显示于此,注意,不像我们自己的电脑,单片机的DBG外设对断点的数量有限制(资源所限),超过5个断点会导致debug失败,此时将断点减少即可。

6.调试控制台。调试器输出的信息会显示在这里,要查看追踪的变量的信息也会显示在这里。如果调试出现问题,报错信息同样也会在这里显示。要是出现异常,可以复制这里的信息在搜索引擎里查找答案,不过最好的方法是查询gdb和openocd的官方文档。

7.调试控制。

  1. 复位:单片机复位
  2. 继续运行/暂停
  3. 单步跳过,如果这一行有函数调用,不会进入内部
  4. 进入,如果这一行有函数调用,会进入函数内部
  5. 跳出,跳出当前调用栈顶层的函数,即如果在函数内部会直接运行到return
  6. 重启调试器(当然单片机也会复位,一般出现异常的时候使用这个按钮)
  7. 终止调试
如果你希望在编译之后立刻启动调试,不要分两次点击,你可以在launch.json中添加一个prelaunchtask(意为在启动调试之前要运行的任务),将他设置为我们在编译章节介绍的构建任务。我们已经提供了这个选项,取消注释即可使用。
如果你想在VSCode中也使用segger RTT viewer的功能(即bsp_log提供的日志功能),请参阅附录2
如果想直接下载代码不想调试,参阅附录4

附录3:Makefile指令介绍

如果想要进一步学习Makefile,可以参考这个链接:Makefile Tutorial By Example。你会发现,当项目越来越大的时候,makefile也会变得复杂起来,这就有了后继者CMake。cmake可以根据一定的规则,生成makefile,然后再利用make命令调用gcc进行程序的编译。也许以后还会有ccccmake
# makefile是CubeMX自动生成的,我们需要自己添加新编写的源文件路径和头文件文件夹,也可以额外加入自己需要的参数满足需求
######################################
# target
######################################
TARGET = basic_framework # 编译生成的目标文件名,如本项目会生成basic_framework.elf/bin/hex三个
# 注意,makefile会自动生成一个叫@的变量,其值等于TARGET.
# 在makefile中获取变量的值需要通过$(var_name),即加上括号并在前面使用$
​
######################################
# building variables
######################################
# debug build?
DEBUG = 1 # 是否启用debug编译.程序分为DEBUG版和RELEASE版,后者在编译时不会插入调试符号和调试信息相关支持的内容,使得程序运行速度提高.
# optimization
OPT = -Og # 编译优化等级,-Og表示调试级,常见的级别请看代码块下面的表格.
​
​
​
#######################################
# paths
#######################################
# Build path
BUILD_DIR = build # 编译的中间文件和目标文件存放路径,为了区分项目文件和编译输出,一般构建一个build(构建)文件夹,用于存放上述文件. 这个表达式也在生成了一个BUILD_DIR变量(可以把Makefile当作一种语言)
​
######################################
# source
######################################
# C sources, 参与编译的C源代码全部放置于此.注意如果换行写需要在行尾空格之后加反斜杠,最后一行不要加
# p.s. C语言的宏如果不能一行写完,也要在行尾加反斜杠,表示一行没有结束
C_SOURCES =  \
HAL_N_Middlewares/Src/main.c \
HAL_N_Middlewares/Src/gpio.c \
HAL_N_Middlewares/Src/adc.c \
HAL_N_Middlewares/Src/can.c 
​
# ASM sources 汇编源文件,第一个是stm32的启动文件,包含了bootloader的信息使得程序可以找到main函数的入口,第二个文件是添加对segger rtt viewer的支持.
ASM_SOURCES +=  \
startup_stm32f407xx.s \
HAL_N_Middlewares/Middlewares/Third_Party/SEGGER/RTT/SEGGER_RTT_ASM_ARMv7M.s
​
#######################################
# binaries, 下面是要执行的指令
#######################################
PREFIX = arm-none-eabi-  # 指令之前加的前缀,这里也是申明了一个变量
# The gcc compiler bin path can be either defined in make command via GCC_PATH variable (> make GCC_PATH=xxx)
# either it can be added to the PATH environment variable.
ifdef GCC_PATH # 和C语言的宏类似,如果在Makefile里定义或给make命令传递了GCC_PATH变量会执行以下内容.但实际上我们执行的是else的内容
CC = $(GCC_PATH)/$(PREFIX)gcc 
AS = $(GCC_PATH)/$(PREFIX)gcc -x assembler-with-cpp 
CP = $(GCC_PATH)/$(PREFIX)objcopy 
SZ = $(GCC_PATH)/$(PREFIX)size
else
# 定义了一个cc变量,其保存的内容实际上是gcc编译器的路径.makefile中要获取一个变量的值,需通过$(var).这里makefile会自动在环境变量里寻找gcc路径.CC里保存的内容是arm-none-eabi-gcc,就是我们添加到环境变量的arm gnu工具链的路径下的一个可执行文件.你可以尝试在cmd中输入arm-none-eabi-gcc,会发现这是一个可执行的程序.之前我们在验证安装的时候就运行了arm-none-eabi-gcc -v命令.
CC = $(PREFIX)gcc 
# 定义了一个AS变量,稍后会用于C/ASM混合编译
AS = $(PREFIX)gcc -x assembler-with-cpp 
# 定义变量.objcopy能够将目标文件进行格式转换.我们实际上要生成的目标文件是.elf,objcopy可以将其转化为hex和bin格式,用于其他用途.
CP = $(PREFIX)objcopy 
# size命令可以获取可执行文件的大小和包含内容信息.
SZ = $(PREFIX)size  
endif
HEX = $(CP) -O ihex # 这里用到了上面定义的CP,命令含义为将其转换成hex,i的前缀表示intel格式
BIN = $(CP) -O binary -S # 转化为二进制文件
 
#######################################
# CFLAGS, 在编译C语言程序的时候给GCC编译器传入的参数
#######################################
# cpu
CPU = -mcpu=cortex-m4 # 目标CPU类型.我们前面介绍过,不同的平台支持的汇编指令不同,一条相同的C语言表达式在翻译成汇编的时候会有不同的实现.比如8051单片机就只有加法器,因此他的乘除法都是通过多次加法和减法实现的,编译器就要完成这一工作.再比如STM32F4系列拥有浮点运算单元(FPU),可以直接在硬件上实现浮点数的加减法.这里指定编译的目标平台是cortex-m4内核的mcu.
​
# fpu 上面说了我们的f407是有FPU的,需要传入特殊的参数.fpv4-sp-d16表示float point,m4内核,single presicion, 16个dword(4字节)运算寄存器.
FPU = -mfpu=fpv4-sp-d16
​
# float-abi 使用软件还是硬件实现浮点运算.也就是我们说的如果没有FPU就只能使用软件实现浮点运算.这里选择hard硬件
FLOAT-ABI = -mfloat-abi=hard
​
# mcu 把上面几个变量合起来弄成一条长的参数
# Thumb是ARM体系结构中的一种16位指令集,这里-mthumb会启用它,感兴趣的同学可以进一步搜索.
MCU = $(CPU) -mthumb $(FPU) $(FLOAT-ABI)
​
# macros for gcc
# AS defines
AS_DEFS = # 汇编的一些宏定义
​
# C defines
C_DEFS =  \  # C语言的宏定义
-DUSE_HAL_DRIVER \ # 使用HAL库.HAL库的许多头文件和源文件里会判断是否定义了这个宏
-DSTM32F407xx \    # HAL库会根据使用的MCU的不同进行条件编译,这是一个很好的封装技术
-DARM_MATH_CM4 \   # 启用ARM MATH运算库,我们在卡尔曼滤波和最小二乘法的时候会用到矩阵运算
-DARM_MATH_MATRIX_CHECK \ # 启用矩阵乘法库
-DARM_MATH_ROUNDING       # 对数学库的输出结果进行取整防止溢出?
​
# AS includes
AS_INCLUDES = # 汇编包含目录.汇编语言也和C一样可以多个文件联合编译,在没有C语言的时候大家都是利用这种方式开发的.在一些运算资源极其受限的情况下也会直接编写汇编.
​
# C includes, C语言的包含目录,将所有参与编译的头文件目录放在这里,注意是目录不需要精确到每一个文件.
# 不想一行写完记得行尾加\,最后一行不要加
C_INCLUDES =  \
-IHAL_N_Middlewares/Inc \
-IHAL_N_Middlewares/Drivers/STM32F4xx_HAL_Driver/Inc
​
​
# compile gcc flags, gcc的编译参数,这些参数自己感兴趣的话去搜索一下.这还将之前定义的一些参数以变量的形式放过来.
ASFLAGS = $(MCU) $(AS_DEFS) $(AS_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections
​
CFLAGS += $(MCU) $(C_DEFS) $(C_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections
​
ifeq ($(DEBUG), 1)
CFLAGS += -g -gdwarf-2
endif
​
​
# Generate dependency information
CFLAGS += -MMD -MP -MF"$(@:%.o=%.d)"
​
​
#######################################
# LDFLAGS,传递给链接器的参数
#######################################
# link script
LDSCRIPT = STM32F407IGHx_FLASH.ld # 需要参与链接的文件.这个文件指明了特定MCU的内存分布情况,使得链接器可以按照此规则进行链接和地址重映射.
​
# libraries,要添加的库,这里我们要使用编译好的math运算库.在CubeMX里面生成的时候可以在第三方库选择DSP运算库,生成makefile时会自动添加进来.
LIBS = -lc -lm -lnosys  \
-larm_cortexM4lf_math
LIBDIR =  \ # 和上一行命令对应,这里引入库的目录,gcc会自动去目录里寻找需要的库文件
-LHAL_N_Middlewares/Drivers/CMSIS/Lib/GCC
LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections
​
# default action: build all
all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin
​
​
#######################################
# build the application
#######################################
# list of objects
# OBJECTS保存了所有.c文件的文件名(不包含后缀),可以理解为一个文件名列表.notdir会判断是否是文件夹
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
vpath %.c $(sort $(dir $(C_SOURCES))) # 对.c文件进行排序,百分号%是通配符,意为所有.c文件vpath是makefile会搜索的文件的路径.如果最终找不到编译中产生的依赖文件所在的路径且不指定搜索路径,makefile会报错没有规则制定目标(no rule to build target)
​
# list of ASM program objects
# 把所有.s文件的文件名加到OBJECTS里面
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s=.o)))
vpath %.s $(sort $(dir $(ASM_SOURCES))) # 对.s文件的文件名也进行排序
​
# 以下是编译命令,命令之前被高亮的@就是静默输出的指令.删除前面的@会将输出显示到命令行.
# 如@$(CC) -c $(CFLAGS) ...... 去掉第一个@即可.
​
# 意味根据makefile,在BUILD_DIR变量指定的路径下将参与编译的所有.c文件编译成.o文件
$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR) 
    @$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@
    # 上面这句话翻译一下实际上是gcc -c -many_param build/xxx -o build
    # 意思是将所有参与编译的文件都列出来,传递一堆编译参数,让他们生成.o文件,并且放在build文件夹下
# 意为根据makefile,将.s文件编译成.o文件,具体和上一条命令差不多
$(BUILD_DIR)/%.o: %.s Makefile | $(BUILD_DIR)
    @$(AS) -c $(CFLAGS) $< -o $@
# 根据前两步生成的目标文件(.o,这些文件的名字保存在OBJECTS变量里),进行链接生成最终的.elf
$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile
    @$(CC) $(OBJECTS) $(LDFLAGS) -o $@
    @$(SZ) $@ # 输出生成的.elf文件的大小和格式信息
​
$(BUILD_DIR)/%.hex: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
    $(HEX) $< $@ # elf转换成hex
    
$(BUILD_DIR)/%.bin: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
    $(BIN) $< $@ # 转换成bin
    
$(BUILD_DIR): # 如果makefile所处的文件目录下没有build文件夹,这里会新建一个build文件夹.
    @mkdir $@   
​
#######################################
# clean up,清除编译信息,可以在命令行中通过rm -r build执行,实际上就是把build文件夹删掉
#######################################
clean:
    rm -r $(BUILD_DIR)
​
  
#######################################
# dependencies
#######################################
-include $(wildcard $(BUILD_DIR)/*.d) # 包含所有的依赖文件(d=dependency),这是编译产生的中间文件,当hello.c包含hello.h而后者又包含了其他头文件时,会产生一个hello.d,它包含了hello.h中包括的其他的头文件的信息,提供给hello.c使用.
​
# *** EOF ***

编译优化等级

优化级别 说明 备注
-O0 关闭所有优化 代码空间大,执行效率低
-O1 基本优化等级 编译器在不花费太多编译时间基础上,试图生成更快、更小的代码
-O2 O1的升级版,推荐的优化级别 编译器试图提高代码性能,而不会增大体积和占用太多编译时间
-O3 最危险的优化等级 会延长代码编译时间,生成更大体积、更耗内存的二进制文件,大大增加编译失败的几率和不可预知的程序行为,得不偿失
-Og O1基础上,去掉了那些影响调试的优化 如果最终是为了调试程序,可以使用这个参数。不过光有这个参数也是不行的,这个参数只是告诉编译器,编译后的代码不要影响调试,但调试信息的生成还是靠 -g 参数的
-Os O2基础上,进一步优化代码尺寸 去掉了那些会导致最终可执行程序增大的优化,如果想要更小的可执行程序,可选择这个参数。
-Ofast 优化到破坏标准合规性的点(等效于-O3 -ffast-math ) 是在 -O3 的基础上,添加了一些非常规优化,这些优化是通过打破一些国际标准(比如一些数学函数的实现标准)来实现的,所以一般不推荐使用该参数。

附录5:利用MSYS2安装依赖环境

之所以要使用Linux进行C++开发,是因为在开发环境中配置依赖包、依赖应用和库非常的方便。Debian系有apt,Fedora和Redhat系有yum,他们都可以方便地帮助我们下载开发软件必须的一些文件和工具。在windows的宇宙最强IDEVisual Studio中配置头文件和动态链接库可以称得上是最折磨的事。好在,现在Windows下也有可以使用的包管理工具了:MSYS2

安装包已经上传到了网盘的EC/VSCode+Ozone环境配置/msys2-x86_64-20221028.exe下。安装之后,打开MSYS2 MSYS软件,他是一个类shell的界面:

输入以下命令然后一路回车即可:

pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-arm-none-eabi-toolchain mingw-w64-x86_64-ccache  mingw-w64-x86_64-openocd
# 需要注意ctrl+V不是黏贴快捷键,而是Ins+Shift.或者右键点击空白处选择黏贴也可以.

​比如上面这样,会让你选择,直接敲回车即可,等待安装。

刚进来第一次安装可能还会更新一下数据库,也是全部更新就行。

安装好之后,把Msys2下的mingw64的bin加入PATH环境变量:D:\Msys2\mingw64\bin(这是我的路径,注意要选自己的)。

注意,如果选用此安装方式,OpenOCD的可执行文件也会被放在上面这个路径下,记得稍后在VSCode中配置的时候找到这里。相应的scripts放在D:\Msys2\mingw64\share\openocd下。

通过这种方式安装之后,还可以选用ccache加速编译。ccache会根据之前的编译输出建立缓存,使得之后编译时可以直接读取缓存。要开启这个功能,直接在Makefile中搜索PREFIX,将下面一行的内容替代原有内容(即增加ccache在arm-none-eabi-之前)。