一开始是用STM32CubeMX+Keil进行STM32的学习和开发,但是keil的界面属实有点丑。。最开始甚至有点把我劝退。抛开颜值不说,这货的代码补全功能有点菜,很多时候都不显示keyword的提示,至少在我的电脑上是这样,极个别时候倒是还反应挺快。
于是便想要用颜值高而且代码补全功能比较强大的vscode来进行STM32的开发。而且无论何种开发都用一个工具也比较方便。看了一些别人的博客,在这里汇总一下,也算做个笔记。
本教程思路为:使用STM32CubeMX生成Makefile工程(STM32CubeMX自动进行初始化配置实在太香了,应该也是目前stm32开发的主流),然后使用vscode对工程进行编写,编译和烧录。
总算抽出时间写完了,累死了...
目录
目录
VSCode搭建STM32开发环境(极简自我搭建&懒人直接使用插件)
方法一:arm工具链+OpenOCD烧录
1.大致思路
2.准备工具
2.1安装工具
2.2补充说明
3.使用STM32CubeMX创建工程
4.VSCode内部配置
4.1添加并配置c_cpp_properties.json
4.2添加settings.json
4.3配置tasks.json
4.4配置launch.json
4.5重定向printf
4.6引入其他库
方法二:直接使用插件ioT Link
1.准备工具
2.创建工程
3.其他事项(一些坑)
使用STM32CubeMX生成工程,生成时选项选择生成Makefile,通过make调用arm工具链编译,然后通过OpenOCD烧录。
通过写c_cpp_properties.json可以利用VSCode牛逼的IntelliSense,可以很爽的写代码。通过task.json方便的完成编译和烧录功能,通过写launch.json以正确进入调试。
此外,也可以正常生成MDK或其他工程,然后vscode只写c_cpp_properties.json,这样的目的是用vscode写代码,用MDK编译和烧录,防止玄学问题发生。
比如我目前没有解决的一个问题是H7的dma问题。看了其他人的博客,是因为dma使用了不该它使用的内存地址,但是用MDK的话官方生成的工程已经帮你把地址规定好了,是可以正常使用的,用arm工具链编译的话我还不清楚在哪里修改这个地址,仅在代码里定向这个地址的话又不好使....有小伙伴解决了的话欢迎评论区或私信告诉我一下;)
(1)VSCode
(2)STM32CubeMX
(3)MinGW-w64
(4)arm-none-eabi-gcc
(5)OpenOCD
(6)Git(可选)
安装不细说了,记得把MinGW-w64,OpenOCD,arm-none-eabi-gcc,Git添加到环境变量。
桌面右键点击此电脑->属性->高级系统设置->环境变量->Path->编辑->新建->浏览,选择对应的路径(到bin文件夹),最后应用->确定。
参考图:
(3)MinGW-w64:Version版本号,选最新即可;Architecture架构跟操作系统有关,64位系统选择x86_64,32位系统选择i686;Threads线程标准windows开发选win32,其他选posix;Exception异常处理模型,x86_64可选为seh和sjlj,i686为dwarf和sjlj,一般选seh和dwarf就可;Build revision构建版本号,选择最大就可。详细可看这篇帖子
(6)Git:原本以为不能用powershell。。。虽然自己也觉得这不可能,但是一开始确实遇到了问题,不过现在姑且已经解决了。用git-bash的话方便一些,powershell的话有些地方要改一下,而且不如用gitbash快,主要体现在执行task时会卡一卡。关于这个地方其实我也不是很懂,不知道有没有其他方法解决,有的话欢迎评论区或私信提醒。:)
前面选芯片,配置外设时钟等等正常配就好,要生成工程前点击Project Manager,在Toolchain/IDE处选择Makefile。
然后直接GENERATE CODE。
首先在VSCode中打开刚才的工程文件夹。
直接按F5,选择C++(GDB/LLDB)->gcc.exe生成活动调试文件,然后会自动生成.vscode文件夹,当然啦,会报错,点中止就可以了。
(1)在.vscode文件夹中添加c_cpp_properties.json文件,配置了这个,VSCode才能找到includePath,写代码的时候才不会一堆红波浪线。它的基本格式是这样的:
{
"configurations": [
{
"name": "Win32",
"browse": {
"path": [
],
"limitSymbolsToIncludedHeaders": true
},
"includePath": [
],
"defines": [
],
"compilerPath": " ",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "gcc-x64"
}
],
"version": 4
}
把鼠标放在对应的参数上可以看到vscode对其的解释,比如把鼠标放在path上:
首先我们看"defines",这个是告诉VSCode你预先定义了哪些宏,怎么知道这里应该填什么呢?打开目录下的Makefile文件便可以看到:
忽略前面的-D,我们便可以知道预先定义的宏是"USE_HAL_DRIVER"和"STM32F407XX"。(一般来说一定是这两个,其中第二个就是你的芯片)
再看"includePath",这个是告诉VSCode去哪些目录里找头文件,怎么知道这里应该填什么呢?同样,打开目录下的Makefile寻找:
忽略前面的-I,将后面的路径填入即可。(或者可以直接偷懒填"${workspaceFolder}/**",其中workspaceFolder表示当前目录,**表示该目录下递归遍历所有文件)
至于"path",似乎不是很重要,和上面的"includePath"填一样的或者填"${workspaceFolder}/"即可。
最后是"compilerPath",显然是填编译器路径,把MinGW-w64下的gcc的路径填进去就可以了,例如我的是这样:"D:\\Programming\\mingw64\\bin\\gcc.exe"。
至此,至少VSCode智能提示和代码补全功能就工作正常了,可以正常的写代码。虽然还不能编译烧录和调试,不过VSCode编辑+Keil编译烧录调试也是不错的选择(毕竟Keil的编辑功能实在是不好用,界面也丑)。
最后大致是这个样子:
{
"configurations": [
{
"name": "Win32",
"browse": {
"path": [
"${workspaceFolder}/"
],
"limitSymbolsToIncludedHeaders": true
},
"includePath": [
"${workspaceFolder}/**"
],
"defines": [
"USE_HAL_DRIVER",
"STM32F407xx"
],
"compilerPath": "D:\\Programming\\mingw64\\bin\\gcc.exe",//编译器路径
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "gcc-x64"
}
],
"version": 4
}
在.vscode文件夹中添加settings.json文件,添加:
{
"terminal.integrated.shell.windows": "D:\\Programming\\Git\\bin\\bash.exe"
}
这样便指定了该文件夹下默认shell是bash,出现提示的时候选择允许改变即可。
vscode更新后已经不推荐使用这种方法
目前虽然会警告,但仍然可以使用,而且比新方法优先级更高,也就是说目前继续使用此方法不会有任何问题,暂时
新方法如下:
点击左下角小齿轮进入设置,在上方搜索栏搜索terminal default,然后点在settings.json中编辑
总之只要能把全局的settings.json打开就行。
到这个位置:
注意"terminal.integrated.defaultProfile.windows"和"terminal.integrated.profiles.windows"可能不在同一行。
后者是终端配置,包含名称,路径(path),参数(args),图标(icon)。路径也可以不用path用source,这样它会自己寻找路径,比如我图里的PowerShell。
此外,名称里不能有空格,别被第二个Command Prompt骗了。如果有空格的话,这个配置不会被识别,在terminal.integrated.defaultProfile.windows填上也会报警告。
如果打算用Bash作为默认终端的话就像我图里一样把对应的路径写上就行了。
前者是默认的终端是哪个配置,值就是终端配置的名称,比如我这里是PowerShell。
然后,只要ctrl+shift+P然后输入terminal,选择默认配置文件,然后选择对应的就可以了。
就算配置文件里没有写任何配置,Command Prompt和PowerShell也是会存在的,可选的,但如果配置里名称有空格,是不可选的。至于我图里那个带空格的Command Prompt为啥会在我也不是很懂。= =
这样选择终端配置文件,是全局的默认终端。仅在对应文件夹下的默认终端,可以通过文件夹下添加settings.json来实现,只需添加"terminal.integrated.profiles.windows"项即可,如:
"terminal.integrated.defaultProfile.windows": "Git-Bash"
tasks.json配置的是任务。对于使用PowerShell和Git-Bash,从这个地方就有些不同了。
自动生成的大致是这样:
{
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: gcc.exe 生成活动文件",
"command": "D:\\Programming\\mingw64\\bin\\gcc.exe",
"args": [
"-g",
"${file}",
"-o",
"${fileDirname}\\${fileBasenameNoExtension}.exe"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "调试器生成的任务。"
}
],
"version": "2.0.0"
}
"tasks"里是包含的任务。
"label"顾名思义标签,可以理解为任务的名称。
"type"是表明任务是作为一个进程运行还是在shell中作为命令运行。
"command"即命令。
"args"是命令的参数,即arguments。
"options"是其他命令选项,可以删掉不管。
"problemMatcher"和"group"是问题匹配程序和分组用的,也不用管,删掉就行。
"detail"顾名思义是细节描述。
对于编译工作,我们使用的是make工具,MinGW-w64自带的名叫mingw32-make.exe,ctrl+`呼出终端输入"mingw32-make"回车执行,便可以编译。
但是
使用gitbash是可以的,而使用PowerShell直接调用会报错:
使用PowerShell需要我们自己先在目录中建一个build文件夹才行。但是用bash的话,它会自动建一个,这和shell自己本身有关吧。
当然每次都输指令还是很麻烦,因此将这个操作编入tasks即可。
那么"type"填"shell","command"填"mingw32-make","args"可以填"-j"提高编译速度,至于"label"和"detail"随意填就好。
如果使用PowerShell的话,就像我刚才说的,需要预先新建build文件夹,所以要加上一个"dependsOn"指定其依赖的其他任务。不过要用powershell的条件判断语句判断是否已经存在build文件夹,否则已经存在build文件夹的情况下mkdir会返回错误,而若每次都在新建build前先将build删除,那么每次都需要重新编译所有文件,效率是很低的。
此外,还可以添加一个Clean任务,用来清除编译文件。
Makefile里的clean操作是这样定义的,其实就是删除build文件夹及其子文件:
对于gitbash可以直接使用"mingw32-make clean",但是对于PowerShell是没有-fR这个参数的,总之用不了,会报和前面类似的错误。所以不用这个clean,在"commands"直接填"rm -r build"即可。
那么对于烧录工作,我们使用的是OpenOCD工具。ctrl+`呼出终端输入"openocd -h"查看帮助可以看到,用选项-f来指定配置文件,用选项-c来运行指令。
那么思路就是:-f来指定调试器类型和芯片型号等,-c进行烧录。
在目录"...\OpenOCD\share\openocd\scripts"下可以看到有很多写好的配置文件:
可以在interface中指定调试器,target中选择芯片型号。比如我的是stm32f407discovery的板子且板载stlink-v2,那么就可以写"-f interface/stlink-v2.cfg -f target/stm32f4x.cfg"。如果是官方板也可以直接在board里找。比如"-f board/stm32f4discovery.cfg".
而-c的参数既然是烧录操作的指令那么就是:"program build/_NAME_.bin verify reset exit 0x08000000"。其中_NAME_是你的工程的名字。
那么总结一下tasks.json可以这样写:
{
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"label": "Create BUILD-DIR",
"command": "if (!(Test-Path build)) {mkdir build}",
"detail": "Create new folder \"build\""
},
{
"type":"shell",
"label": "Build",
"command": "mingw32-make",
"dependsOn": "Create BUILD-DIR",
"args": [
"-j"
],
"detail": "Build with mingw32-make.exe"
},
{
"type":"shell",
"label": "Clean",
"command": "rm",
"args": [
"-r",
"build"
],
"detail": "Build with mingw32-make.exe"
},
{
"type": "shell",
"label": "Burn",
"command": "openocd",
"args": [
"-f",
"interface/stlink-v2.cfg",
"-f",
"target/stm32f4x.cfg",//target
"-c",
"program build/_NAME_.bin verify reset exit 0x08000000"
],
"detail": "Burn with OpenOCD"
},
{
"type": "shell",
"label": "OpenOCD",
"command": "openocd",
"args": [
"-f",
"interface/stlink-v2.cfg",
"-f",
"target/stm32f4x.cfg"//target
],
"detail": "start OpenOCD and wait for gdb"
}
]
}
每个工程的工程名肯定不同,所以_NAME_肯定每次都需要修改。可以复制好工程名之后ctrl+f搜索_NAME_进行全部替换,后文的launch.json同理。工程名可以到Makefile的这个地方直接复制:
由于调试的时候需要用OpenOCD连接gdb,因此此处多加一个仅用OpenOCD连接设备,不烧录的task。
按下F1选择运行任务,就可以了,label和detail的具体效果如下:
可以看到label的内容可以算是标题或者名称,而detail显示为label下的灰字描述。
默认的launch.json大概是这样子:
我们要修改的是"program"项,指定可执行文件的路径,如图所示,改到build下,对应的elf文件,"miDebuggerPath"项,指定调试程序的路径,如图所示,改到arm-none-eabi-gdb.exe,以及"setupCommands",指定执行的命令。里面的"description"是描述信息,填不填都行,"text"是执行的命令,"ignoreFailures"是否忽略错误。
调试的整个过程是,因为是调试stm32,要通过调试器,因此首先启动OpenOCD任务,OpenOCD会开放3333端口供gdb连接,然后F5启动调试,为gdb选择文件,然后连接到3333端口,reset和halt之后,烧录文件,就可以开始调试了,最后launch.json大概长这样:
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "调试",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}\\build\\_NAME_.elf",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "D:\\Programming\\arm-none-eabi\\bin\\arm-none-eabi-gdb.exe",
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": false,
},
{
"description": "select .elf to gdb",
"text": "file D:/MyDocuments/Programming/STM32/Projects/F407DISC/_NAME_/build/_NAME_.elf",
"ignoreFailures": false,
},
{
"description": "connect gdb server",
"text": "target remote localhost:3333",
"ignoreFailures": false,
},
{
"description": "Reset MCU",
"text": "monitor reset",
"ignoreFailures": false,
},
{
"description": "Halt",
"text": "monitor Halt",
"ignoreFailures": false,
},
{
"description": "Burn",
"text": "load",
"ignoreFailures": false,
},
],
"preLaunchTask": "Build"
}
]
}
"preLaunchTask"是调试前执行的任务,因此不用预先调用Build任务。
在HAL库的Projects下有一些板子例程,有的板子的Examples里的UART里会有UART_Printf例程,这个就是官方的重定向printf的示例。keil的例程如下:
可以看到,官方定义了PUTCHAR_PROTOTYPE为输出一个字符的原型,后面再对输出一个字符的原型进行改动,改成对应的串口输出。由于keil的AC5编译器对输出一个字符原型的名字不一样,所以使用了条件编译#ifdef。
但是依葫芦画瓢这样改是不行的。为什么呢?参考了这篇文章后,知道了原来是因为底层实现也不一样,gcc还需要重写_write函数。
初步总结:
#ifdef __GNUC__
/* With GCC, small printf (option LD Linker->Libraries->Small printf
set to 'Yes') calls __io_putchar() */
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */
PUTCHAR_PROTOTYPE
{
/* Place your implementation of fputc here */
/* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */
HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
__attribute__((weak)) int _write(int file, char *ptr, int len)
{
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
{
__io_putchar(*ptr++);
}
return len;
}
如果存在中途也要用keil编译的可能,就把重写_write的部分也加上条件编译。
此外,由于keil新版编译器AC6基于clang,因此,宏定义里定义了__GNU__,所以如果使用AC6的话,仅靠__GNU__就无法区分了。需要同时判断是否定义了__GNU__和__clang__。
再加上对scanf的重定向,最后是:
#if defined (__GNUC__) && !defined (__clang__)
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#define GETCHAR_PROTOTYPE int __io_getchar(void)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#define GETCHAR_PROTOTYPE int fgetc(FILE *f)
#endif
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
GETCHAR_PROTOTYPE
{
uint8_t ch;
HAL_UART_Receive(&huart2, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
#if defined (__GNUC__) && !defined (__clang__)
__attribute__((weak)) int _write(int file, char *ptr, int len)
{
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
__io_putchar(*ptr++);
return len;
}
__attribute__((weak)) int _read(int file, char *ptr, int len)
{
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
*ptr++ = __io_getchar();
return len;
}
#endif
此外,需要在Makefile里加上-u _printf_float和-u _scanf_float否则无法输入输出浮点数:
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
找到这行,加上这俩就行了。
用make进行编译时,make是根据Makefile里的内容寻找宏,头文件和哪些文件需要编译。而这个Makefile是STM32CubeMX根据选择的外设等自动生成的,因此,Makefile里显然只会包含HAL库或HAL的部分(取决于用户自己在生成工程时的选择),如果要引入自己写的驱动,就需要对Makefile进行改动。
添加.c文件,在这个地方:
直接在后面按照格式添加.c文件在工程下的相对路径即可。
添加头文件,在这个地方:
同理,写上头文件在工程下的相对路径就行,别忘了前面的-I。
(1)VSCode
(2)STM32CubeMX
(3)vscode内下载插件ioT Link,如图:
安装好之后应该可以看到左下角有这样一栏:
(1)首先用STM32CubeMX创建工程,还是一样的,具体配置不叙述,注意生成时选择Makefile。
(2)打开vscode,点击左下角ioT LINK插件的home:后,来到如图所示的界面:
(3)点击“导入GCC工程”并选择刚才创建的工程的路径:
工程目录选择刚才生成的工程目录,Makefile会自动选择不用管,硬件平台选择自己的板子就好(没有的话就随便选一个,问题不大)。
(4)再次点击home,然后点击IoT Link设置,选择“调试器”。
(i)如果选择JLink的话,根据板子选择就好,此处不多赘述
(ii)如果选择OpenOCD的话要注意OpenOCD参数的设置,格式为:-f ./interface/{调试器}.cfg -f ./target/{目标器件}.cfg。例如这里我用的是stm32f103,stlink,参数配置如下:
这里-f参数是指定配置文件,openocd的目录中有很多写好的配置文件,其中interface文件夹对应调试器,target文件夹对应目标器件。
(注意这里OpenOCD路径自动填上了${system_default},因为IoT Link这个插件自带gcc,make,openocd这些工具,可以看到其他工具的路径也被自动添上 了${system_default},当然这里也可以选择方法一中自己下载的工具的路径。比如我就因为强迫症把插件自带的这些工具都删了,转而用自己下载的,这样也方便日后管理和更新)
(如果要和我一样使用自己下载的工具路径的话,可以移步最后的“其他事项”部分,这里还是有一些坑的)
(5)点击左下角的按钮进行编译和烧录:
Build即编译,Rebuild即重新编译,Download即烧录,Serial是内置的串口助手,Home就是更改IoT Link的设置。
按F5可以进行调试:
(1)方法二中使用自己下载的工具路径时,注意编译器路径要选择arm-none-eabi的路径且只选择到bin文件夹,不要包含arm-none-eabi-gcc.exe;调试器中的OpenOCD路径要选饿到openocd.exe,而且使用插件的浏览功能只能选择到文件夹,不能选择到openocd.exe这个程序,需要自己手打;工具链中的Make工具路径和GCC工具路径和OpenOCD路径同理都要选择到具体程序。