对于嵌入式开发人员,想必最熟悉MDK5或者IAR的IDE开发了。MDK5的调试和仿真功能是其他IDE所不能比拟的,IAR我没用过,本文章只是兴趣指导,旨在搭建一套VSCode的嵌入式开发环境,了解从编译到调试的过程。当然,项目的合作开发还是MDK5的兼容性好。
用到的工具有:
1)VSCode:微软旗下的开源编辑器,需要搭配插件使用。
2)arm-none-eabi-gcc:开源的交叉编译工具链,PC平台和嵌入式平台交叉编译,将编译到调试的一整套过程链接起来。
3)Make工具:识别并执行Makefile文件。Make工具的安装可以参考这个链接:https://developer.aliyun.com/article/1113601
4)Makefile文件:自动化构建脚本,负责文件的编译顺序和编译规则等等功能(这里不细说,大家可自行探索)。
5)openocd工具:作为PC平台到嵌入式平台的软件接口调试工具,与JLINK/STlink/CMSIS-DAPlink硬件接口对应。
上述的下载方式和环境变量设置自行搜索,网上的教程都很细。这里不作过多介绍,我们进入正题:
本文以STM32F401CCU6单片机作为示例,来进行演示。
前提:需要安装好对应的VSCode插件
一、工程架构
二、VSCode各个脚本文件
task.json,主要作用是使用XX名称,执行XX命令和XX参数。
//任务脚本
{
//ctrl+shift+B
"tasks": [
//编译
{
"type": "shell",
"label": "build",
"command": "make -j 8",
"args": [],
"problemMatcher": [
"$gcc"
],
"group": "build"
},
//清除
{
"type": "shell",
"label": "clean",
"command": "make clean",
"args": [],
"problemMatcher": [
"$gcc"
],
"group": "build"
},
//重编译
{
"type": "shell",
"label": "rebuild",
"command": "make -j 8",
"args": [],
"problemMatcher": [
"$gcc"
],
"group": "build",
"dependsOn": [ //每次执行这个任务,会先执行clean任务,再执行build任务,这便是所谓的依赖。
"clean"
]
},
//下载
{
"type": "shell",
"label": "flash",
"command": "openocd",
"args": [
"-f",
// "interface/stlink.cfg",
"interface/cmsis-dap.cfg",
"-f",
"target/stm32f4x.cfg",
"-c",
"program build/${workspaceRootFolderName}.elf verify reset exit" //将工程根目录名称作为可执行文件名称
], /*command+args相当于主命令+子命令,也就是openocd -f interface/stlink-v2.cfg -f target/stm32f1x.cfg -c "program Project.hex verify reset exit"的效果*/
"problemMatcher": [
"$gcc"
],
"group": "build",
"dependsOn": [ //每次执行这个任务,会先执行clean任务,再执行build任务,这便是所谓的依赖。
"build"
]
},
{
"type": "cppbuild",
"label": "C/C++: arm-none-eabi-gcc.exe 生成活动文件",
"command": "D:/Software/arm_riscv_develop_tools/arm-none-eabi-gcc/10 2021.10/bin/arm-none-eabi-gcc.exe",
"args": [
"-fdiagnostics-color=always",
"-g",
"${file}",
"-o",
"${fileDirname}\\${fileBasenameNoExtension}.exe",
"-mcpu=cortex-m4",
"-mthumb",
"-mfpu=fpv4-sp-d16",
"-mfloat-abi=hard"
],
"options": {
"cwd": "D:/Software/arm_riscv_develop_tools/arm-none-eabi-gcc/10 2021.10/bin"
},
"problemMatcher": [
"$gcc"
],
"group": "build",
"detail": "编译器: D:/Software/arm_riscv_develop_tools/arm-none-eabi-gcc/10 2021.10/bin/arm-none-eabi-gcc.exe"
}
],
"version": "2.0.0"
}
launch.json,主要作用是MCU调试,以及选择什么样的下载调试器。
//调试脚本
{
"configurations": [
{
"name": "Debug with OpenOCD",
"cwd": "${workspaceRoot}",
"executable": "./build/${workspaceRootFolderName}.elf", //将工程根目录名称作为可执行文件名称
"request": "launch",
"type": "cortex-debug",
"servertype": "openocd",
"device": "STM32F401CCU6",
"configFiles": [
// "interface/stlink.cfg",
"interface/cmsis-dap.cfg",
"target/stm32f4x.cfg"
],
"svdFile": "./STM32F401.svd", //选择寄存器文件
"liveWatch": {
"enabled": true,
"samplesPerSecond": 4
},
"searchDir": [],
"runToEntryPoint": "main",
"showDevDebugOutput": "none",
"preLaunchTask": "flash" //每次调试之前会先调用下载任务再调试
}
],
"version": "2.0.0"
}
settings.json,主要作用是设置一些快捷功能,以及配合插件Task Buttons来显示编译、重编译、下载、调试等UI按钮。
//UI和功能设置脚本
{
// 1秒后自动保存
"files.autoSave": "afterDelay",
// 开启 material icons
"workbench.iconTheme": "vscode-icons",
// theme主题设置
"workbench.colorTheme": "Dracula Theme",
// 粘贴的时候格式化
"editor.formatOnPaste": true,
// 保存的时候格式化
"editor.formatOnSave": true,
// 字体样式
// "editor.fontFamily": "'Fira Code', monospace",
// 字体大小
"editor.fontSize": 16,
// 字体宽度
// "editor.fontWeight": "bold",
"security.workspace.trust.enabled": false,
"VsCodeTaskButtons.showCounter": true,
"VsCodeTaskButtons.tasks": [
{
"label": "$(tools) Build",
"task": "build",
"tooltip": "🛠️ build"
},
{
"label": "$(notebook-delete-cell) Clean",
"task": "clean",
"tooltip": "🧹 clean"
},
{
"label": "$(notebook-delete-cell) $(tools) Re-bulid", //"$(notebook-delete-cell) & $(tools)",
"task": "rebuild",
"tooltip": "🛠️ rebuild" // "🧹 & 🛠️ rebuild"
},
{
"label": "$(zap) Flash",
"task": "flash",
"tooltip": "⚡ flash"
}
],
// "terminal.integrated.defaultProfile.windows": "Git Bash",
"C_Cpp.default.cppStandard": "c++17",
"C_Cpp.default.intelliSenseMode": "gcc-arm",
"C_Cpp.default.cStandard": "c99",
"C_Cpp.default.compilerPath": "D:/Software/arm_riscv_develop_tools/arm-none-eabi-gcc/10 2021.10/bin/arm-none-eabi-gcc.exe"
}
c_cpp_properties.json,主要作用是选择对应MCU的宏定义展开,以及MCU的编译选项参数(或者说MCU硬件参数)
{
"configurations": [
{
"name": "STM32F401CCU6",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE",
"USE_STDPERIPH_DRIVER",
"STM32F401xx"
],
"compilerPath": "D:/Software/arm_riscv_develop_tools/arm-none-eabi-gcc/10 2021.10/bin/arm-none-eabi-gcc.exe",
"compilerArgs": [
"-mcpu=cortex-m4",
"-mthumb",
"-mfpu=fpv4-sp-d16",
"-mfloat-abi=hard"
],
"cStandard": "c99",
"cppStandard": "c++17",
"intelliSenseMode": "gcc-arm"
}
],
"version": 4
}
三、MCU链接脚本.ld
该文件的主要功能是划分SRAM和Flash的起始地址和地址空间。和keil这里的设置原理基本一致。
获取方法,
第1种方法:使用STM32CubeMX生成一个简单的STM32F401CCU6工程,对应文件夹下含有。
第2种方法:也可以根据不同的MCU内核自定义修改,这种需要耐心一点。
我这里按照第1种方法直接拷贝STM32CubeMX生成的.ld文件。
四、MCU汇编启动文件.s
该文件的功能是包含了中断向量表信息。需要注意的是,ARMcc/ARMclang与GCC for ARM两者交叉编译工具链下的启动文件是不通用的,这里我们需要选择GCC for ARM工具链对应的汇编启动文件。
获取方法,
第1种方法:同样是使用STM32CubeMX生成一个简单的STM32F401CCU6工程,对应文件夹下含有。
第2种方法:去STM官网==https://www.st.com.cn/content/st_com/zh.html==,按照以下步骤找到STM32F4系列的标准库
我个人是按照第2种方法添加的启动文件。
由于阿美莉卡国的封禁,ST官网目前不给予中国地区用户下载权限,我们可以选择访客的方式登录,然后进行邮箱验证,即可下载,具体自行操作。
在下载的标准库文件中,按照以下路径:en.stsw-stm32065_v1-9-0\STM32F4xx_DSP_StdPeriph_Lib_V1.9.0\Libraries\CMSIS\Device\ST\STM32F4xx\Source\Templates\gcc_ride7,即可寻找到GCC对应的汇编启动文件。如下图所示,
将该启动文件复制到我们的工程目录下,
五、Makefile脚本
##########################################################################################################################
# File automatically-generated by tool: [projectgenerator] version: [4.2.0-B44] date: [Fri Jul 05 19:24:39 CST 2024]
##########################################################################################################################
# ------------------------------------------------
# Generic Makefile (based on gcc)
#
# ChangeLog :
# 2017-02-10 - Several enhancements + project update mode
# 2015-07-22 - first version
# ------------------------------------------------
######################################
# target-------编译生成目标烧写软件的名称
######################################
TARGET = F401CCU6_demo
######################################
# building variables
######################################
# debug build
# /*
# 编译选项:是否debug模式,如果DEBUG=1,则可以后续使用调试软件gdb等工具进行在线调试
# 如果DEBUG=0,则不能支持在线调试,
# 且DEBUG=1,生成的文件比DEBUG=0大,因为里面包含了调试信息。
# */
DEBUG = 1
# optimization
# /*
# 编译选项:优化等级
# -O0:无任何优化,
# -O1:1级优化,
# -O2: 2级优化,
# -Os: 2.5级优化,
# -O3: 最高级优化。
# -Og:优化调试体验。 -Og启用不会干扰调试的优化。 它是标准编辑 - 编译 - 调试周期可以选择的优化级别,提供合理的优化级别,同时保持快速编译和良好的调试体验。
# */
OPT = -Og
#######################################
# paths
#######################################
# Build path
# /*
# 编译路径: 生成的编译文件保存在build文件夹中,
# 这样做的好处是工程框架比较清晰,且清除编译文件比较简单。
# */
BUILD_DIR = build
######################################
# source-------工程所有需要编译的C文件: 指定需要编译的C文件名称相对路径
######################################
# C sources
C_SOURCES = \
$(wildcard *.c ./CMSIS/Source/*.c) \
$(wildcard *.c ./Core/Src/*.c) \
$(wildcard *.c ./STM32F4xx_StdPeriph_Driver/src/*.c) \
$(wildcard *.c ./User/Src/*.c) \
# STM32F4xx_StdPeriph_Driver/src/stm32f4xx_gpio.c
# STM32F4xx_StdPeriph_Driver/src/stm32f4xx_syscfg.c
# STM32F4xx_StdPeriph_Driver/src/stm32f4xx_usart.c
# STM32F4xx_StdPeriph_Driver/src/stm32f4xx_exti.c
# STM32F4xx_StdPeriph_Driver/src/stm32f4xx_pwr.c
# ASM sources
# 工程所有需要编译的汇编文件: 指定需要编译的汇编文件名称相对路径
# ASM_SOURCES = \
# startup_stm32f103xe.s
ASM_SOURCES =
ASM_SOURCES += $(wildcard *.s ./startup_s/*.s)
# ASM sources
ASMM_SOURCES =
#######################################
# binaries
#######################################
# /*
# 工程使用编译的类型: arm-none-eabi-是基于arm芯片开发的编译器,
# none表示无操作系统,eabi表示交叉编译器,即在linux上编译嵌入式arm芯片的代码
# 生成可烧写文件。
# */
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.
# /*
# 编译器路径宏:表示arm-none-eabi-gcc在linux中调用是否需要带路径,
# 一般情况安装好arm-none-eabi-gcc后,系统将安装的可执行程序路径放在了系统的环境变量中,
# 无需要路径即可执行,所以GCC_PATH不用宏定义。
# */
ifdef GCC_PATH#如果定义了GCC_PATH的本地路径,则进行本地索引编译
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 = $(PREFIX)gcc
AS = $(PREFIX)gcc -x assembler-with-cpp
CP = $(PREFIX)objcopy
SZ = $(PREFIX)size
endif
HEX = $(CP) -O ihex
BIN = $(CP) -O binary -S
#######################################
# CFLAGS
#######################################
# cpu
# 编译选型:CPU类型,STM32芯片的内核是cortex-m4,指定该内核对应的寄存器库
CPU = -mcpu=cortex-m4
# fpu
# 编译选型:FPU浮点计算器,STM32支持浮点运算
FPU = -mfpu=fpv4-sp-d16
# float-abi
# 编译选型:浮点计算类型,设定硬件浮点运算,还可选型纯软件浮点计算,或者结合形式
FLOAT-ABI = -mfloat-abi=hard
# mcu
# 编译MCU总选项:M4内核,支持浮点运算,浮点计算采用硬件浮点计算器,指定生成thumb精简指令集,【默认不开启:允许编译器进行ARM指令和Thumb指令的相互调用】
# MCU = $(CPU) -mthumb $(FPU) $(FLOAT-ABI)
MCU = $(CPU) $(FPU) $(FLOAT-ABI) -mthumb -mthumb-interwork
# macros for gcc
# AS defines
# 汇编编译宏定义:该Makefile宏定义将在汇编代码中有效
AS_DEFS =
# C defines
# C文件编译宏定义:该Makefile宏定义将在C代码中有效
C_DEFS = \
-DUSE_STDPERIPH_DRIVER \
-DSTM32F401xx
# AS includes----------汇编头文件路径:编译过程中文件内的头文件搜索路径
AS_INCLUDES =
# C includes--------C文件的头文件路径:编译过程中文件内的头文件搜索路径
C_INCLUDES = \
-ICore/Inc \
-ICMSIS/Include \
-ISTM32F4xx_StdPeriph_Driver/inc \
-IUser/Inc
# compile gcc flags
# 汇编编译选型:s汇编文件编译成Obj文件需要的设置选项
ASFLAGS = $(MCU) $(AS_DEFS) $(AS_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections
# C编译选型:C文件编译成Obj文件需要的设置选项
CFLAGS += $(MCU) $(C_DEFS) $(C_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections
# Debug模式下的C编译选型:仅仅在开启DEBUG模式下有效
ifeq ($(DEBUG), 1)
CFLAGS += -g -gdwarf-2
endif
# Generate dependency information
# /*
# C文件自动依赖关系 :-MMD -MP -MF"$(@:%.o=%.d)"
# 自动生成.d文件,里面保存了对应的源文件C代码中包含的非标准库的头文件路径和名称,
# 生成.d文件的目的是产生C文件生成obj的依赖文件,
# 当关联的头文件发生变化时,触发make重新生成obj文件。
# -MMD等同于-MM -MF,-MM表示依赖的头文件(不包括标准头文件夹,-M则是所有头文件),
# -MF生成依赖文件。
# */
CFLAGS += -MMD -MP -MF"$(@:%.o=%.d)"
#######################################
# LDFLAGS
#######################################
# link script
# /*
# 可执行文件链接脚本: STM32F401CCUx_FLASH.ld
# 文件中详细给出了芯片的RAM和ROM片区分类区间与大小,
# 代码、全局变量、常数、堆栈等的分配区间。
# */
# LDSCRIPT = STM32F103VETx_FLASH.ld
LDSCRIPT = ./link_script/STM32F401CCUx_FLASH.ld
# libraries
LIBS = -lc -lm -lnosys
LIBDIR =
LDFLAGS = $(MCU) -u _printf_float -u _scanf_float -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections
# default action: build all
# Makefile总目标: 这是伪目标,在第一依赖关系位置,输入make指令时必定执行该目标
# 可执行文件elf , hex 和 bin 是arm的常用烧写文件
all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin
# build/LED_Toggle.elf
# build/LED_Toggle.hex
# build/LED_Toggle.bin
#######################################
# build the application
#######################################
# list of objects
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
# /*
# 指定C文件的搜索路径: $(sort $(dir $(C_SOURCES)))
# $(dir $(C_SOURCES)):所有源文件只保留文件路径,
# sort:对所有路径排序 ,‘d g a’ -> 'a d g'
# */
vpath %.c $(sort $(dir $(C_SOURCES)))
# list of ASM program objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s=.o)))
vpath %.s $(sort $(dir $(ASM_SOURCES)))
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASMM_SOURCES:.S=.o)))
vpath %.S $(sort $(dir $(ASMM_SOURCES)))
# /*
# 通配符(%)指定所有c文件编译成OBJ文件:
# $(BUILD_DIR)/%.o: 生成OBJ文件的路径固定不变,在BUILD_DIR文件夹,
# %.c:依赖源文件C文件,地址未指定,Makefile将在本地目录和vpath %c目录下搜索源文件,
# Makefile :Makefile文件自己也是生成obj文件的依赖文件,Makefile文件变化时会重新编译,
# | $(BUILD_DIR): 竖线左边的依赖文件是正常依赖文件,竖线右边的依赖文件是命令提前的依赖文件,即BUILD_DIR会自动执行,编译生成OBJ前生成build文件夹。
# -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)):生成临时中间文件
# -c:仅编译不链接 $<:第一个依赖文件即C文件 $@ 目标文件
# */
$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR)
$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@
# /*
# 通配符(%)指定所有汇编文件编译成OBJ文件:
# $(BUILD_DIR)/%.o: 生成OBJ文件的路径固定不变,在BUILD_DIR文件夹,
# %.c:依赖源文件C文件,地址未指定,Makefile将在本地目录和vpath %c目录下搜索源文件,
# Makefile :Makefile文件自己也是生成obj文件的依赖文件,Makefile文件变化时会重新编译,
# | $(BUILD_DIR): 竖线坐标的依赖文件是正常依赖文件,竖线右边的依赖文件是命令提前的依赖文件,
# -c:仅编译不链接 $<:第一个依赖文件即C文件 $@ 目标文件
# */
$(BUILD_DIR)/%.o: %.s Makefile | $(BUILD_DIR)
$(AS) -c $(CFLAGS) $< -o $@
$(BUILD_DIR)/%.o: %.S Makefile | $(BUILD_DIR)
$(AS) -c $(CFLAGS) $< -o $@
# 生成可执行文件ELF文件:依赖于所有OBJ文件
$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile
$(CC) $(OBJECTS) $(LDFLAGS) -o $@
$(SZ) $@
# 生成HEX文件:依赖于elf文件和build文件夹
$(BUILD_DIR)/%.hex: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
$(HEX) $< $@
# 生成BIN文件:依赖于elf文件和build文件夹
$(BUILD_DIR)/%.bin: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
$(BIN) $< $@
# 生成build文件夹:伪指令
# 通过上面的优先依赖关系生成
$(BUILD_DIR):
mkdir $@
#######################################
# clean up
#######################################
# /* 清除编译结果:将build文件中所有文件和子文件夹删除。*/
clean:
-rm -fR $(BUILD_DIR)
#######################################
# dependencies
#######################################
# 添加所有.d依赖文件
-include $(wildcard $(BUILD_DIR)/*.d)
# *** EOF ***
如果顺利的话,到这里就能进行我们的编译、重编译、下载、调试操作了。
由于篇幅过长,后续我会各出一篇文章讲解Makefile脚本需要修改的地方,各个VSCode脚本需要修改的地方,.svd寄存器文件如何根据不同厂商的MCU修改,.ld链接文件如何根据不同厂商的MCU修改。
--------------------------------------------------------------------欢迎各位+QQ交流:1986317910----------------------------------------------------------------------
如果需要工程文件和细节指导可作交流,不作任何收费用途。