sentry_gimbal_hzz/VSCode+Ozone使用方法.md

357 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# VSCode+Ozone开发STM32的方法
<center><b><font face="楷体">neozng1@hnu.edu.cn</font></b></center>
[TOC]
> TODO
>
> 1. 添加一键编译+启用ozone调试/一键编译+下载的脚本,使得整个进一步流程自动化
> 2. 增加更多的知识介绍
## 前言
了解过嵌入式开发的你一定接触过Keil这款20世纪风格UI的IDE伴随很多人度过了学习单片机的岁月。然而由于其缺少代码补全、高亮和静态检查的支持以及为人诟病的一系列逆天的设置、极慢的编译速度特别是在开发HAL库时很多开发者开始转向其他IDE。
IAR、CubeIDE等都是广为使用的“其他”IDE但是他们也有各自的缺点不能让笔者满意。作为IDE界的艺术家JetBrains推出的Clion也在相当程度上完善了对嵌入式开发的支持。不过在体验过多款IDE后还是**VSCode**这款高度定制化的编辑器最让人满意。强大的补全和snippet以及代码高亮、定义跳转甩KEIL十条街。
而Ozone则是SEGGER(做jilnk的)推出的调试应用支持变量实时更新变量曲线可视化SEGGER RTT日志DBG虚拟串口等功能大大扩展了调试的功能。很多人习惯使用串口进行可视化调试如vofa串口调试助手等。然而通过这些方式进行调试都是对内核有**侵入性**的会占有内核资源并且导致定时器的时间错乱。由于DBG有单独连接到FLASH和CPU寄存器的高速总线类似于DMA可以在不影响程序正常运行的情况下以极高的频率直接获取变量值。
下面将从工具链介绍、环境配置以及调试工作流三个方面介绍以VSCode为编辑器Ozone为调试接口的开发环境。
开发的大致流程为:
~~~mermaid
graph LR
CubeMX进行初始化 --> VSCode编写代/进行编译/简单调试 --> Ozone变量可视化调试+log
~~~
***本教程不仅希望教会你如何配置环境,同样会告诉你每一步究竟是在做什么,而不是简单的复制黏贴邯郸学步。***
## 前置知识
1. 计算机速成课:[Crash Course Computer Science](https://www.bilibili.com/video/av21376839/?vd_source=ddae2b7332590050afe28928f52f0bda)
2. 从零到一打造一台计算机:
[编程前你最好了解的基本硬件和计算机基础知识(模拟电路)](https://www.bilibili.com/video/BV1774114798/?spm_id_from=333.788.recommend_more_video.11&vd_source=ddae2b7332590050afe28928f52f0bda)
[编程前你最好了解的基本硬件和计算机基础知识(数字电路)](https://www.bilibili.com/video/BV1Hi4y1t7zY/?spm_id_from=333.788.recommend_more_video.0)
[从0到1设计一台计算机](https://www.bilibili.com/video/BV1wi4y157D3/?spm_id_from=333.788.recommend_more_video.0&vd_source=ddae2b7332590050afe28928f52f0bda)
3. C语言基础[程序设计入门——C语言](https://www.icourse163.org/course/ZJU-199001?from=searchPage&outVendor=zw_mooc_pcssjg_)
***务必学完以上课程再开始本教程的学习。***
## 预备知识
1. 软件安装(队伍NAS和资料硬盘内提供了所有必要的依赖,安装包和插件)
2. C语言从源代码到.bin和.hex等机器代码的编译和链接过程
3. C语言的内存模型
4. C语言标准动态链接库和静态编译的区别一些编译器的常用选项
5. STM32F4系列的DBG外设工作原理
### 编译全过程
C语言代码由固定的词汇关键字按照固定的格式语法组织起来简单直观程序员容易识别和理解但是CPU只能识别二进制形式的指令并且这些指令是和硬件相关的感兴趣的同学可以搜索**指令集**相关内容。这就需要一个工具将C语言代码转换成CPU能够识别的二进制指令对于我们的x86平台windows下的程序就是.exe后缀的文件对于单片机一般来说是.bin或.hex等格式的文件调试文件包括axf和elf
能够完成这个转化过程的工具是一个特殊的软件,叫做**编译器Compiler**。常见的编译器包括开源的GNU GCCwindows下微软开发的visual C++以及apple主导的llvm/clang。编译器能够识别代码中的关键字、表达式以及各种特定的格式并将他们转换成特定的符号也就是**汇编语言**(再次注意汇编语言是平台特定的),这个过程称为**编译Compile**。
对于单个.c文件从C语言开始到单片机可识别的.bin文件一般要经历以下几步
![img](assets\v2-2797ea99d0d38eb9996993bb0ad77ab2_720w.webp)
首先是编译**预处理**Preprocessing这一步会展开宏并删除注释将多余的空格去除。预处理之后会生成.i文件。
然后,开始**编译**Compilation的工作。编译器会将源代码进行语法分析、词法分析、语义分析等根据编译设置进行性能优化然后生成汇编代码.s文件。汇编代码仍然是以助记符的形式记录的文本比如将某个地址的数据加载到CPU寄存器等还需要进一步翻译成二进制代码。
下一步就是进行**汇编**Assemble编译器会根据汇编助记符和机器代码的查找表将所有符号进行替换生成.o .obj等文件。但请注意这些文件并不能直接使用烧录我们在编写代码的时候都会包含一些**库**,因此编译结果应当有多个.o文件。我们还需要一种方法将这些目标文件缝合在一起使得在遇到函数调用的时候程序可以正确地跳转到对应的地方执行。
最后一步就由链接器Linker也称LD完成称为**链接**Linking。比如你编写了一个motor.c文件和.h文件并在main.c中包含了motor.h使用了后者提供的`MotorControl()`函数。那么,链接器会根据编译器生成.obj文件时留下的函数入口地址将main.o里的调用映射到生成的motor.o中。链接完成后就生成了单片机可以识别的可执行文件通过支持的串口或下载器烧录便可以运行。
> 另外,上图可以看到左侧的**静态库**,包括`.lib .a`比如我们在STM32中使用的DSP运算库就是这种文件。他在本质上和.o文件相同只要你在你编写的源文件中包含了这些库的头文件链接器就可以根据映射关系找到头文件中声明的函数在库文件的地址。直接提供库而不是.c文件就可以防止源代码泄露因此一些不开源的程序会提供函数调用的头文件和接口具体实现的库你也可以编写自己的库感兴趣自行搜索
链接之后,实际上还要进行不同代码片段的重组、地址重映射,详细的内容请参看:[C/C++语言编译链接过程](https://zhuanlan.zhihu.com/p/88255667)这篇教程还提供了以GCC为例的代码编译示例。
### C语言内存模型
<img src="assets\image-20221112160213066.png" alt="image-20221112160213066" style="zoom:80%;" />
以上是C语言常见的内存模型即C语言的代码块以及运行时使用的内存包括函数、变量等的组织方式。
> 有些平台的图与此相反,栈在最下面(内存低地址),其他区域都倒置,不影响我们理解
**代码段**即我们编写的代码,也就是前面说的编译和链接之后最终生成的可执行文件占据的空间。一些常量,包括字符串和使用`const`关键字修饰的变量被放在常量存储区。`static`修饰的静态变量(包括函数静态变量和文件静态变量)以及全局变量放在常量区上面一点的全局区(也称静态区)。
然后就是最重要的**堆**和**栈**。在一个代码块内定义的变量会被放在栈区,一旦离开作用域(出了它被定义的`{}`的区域),就会立刻被销毁。在调用函数或进入一个用户自定义的`{}`块都会在栈上开辟一块新的空间空间的大小和内存分配由操作系统或C库自动管理。**一般来说,直接通过变量访问栈内存,速度最快**(对于单片机)。而堆则是存储程序员自行分配的变量的地方,即使用`malloc(),realloc() ,new`等方法获取的空间,都被分配在这里。
> 在CubeMX初始化的时候Project mananger标签页下有一个Linker Setting的选项这里是设置最小堆内存和栈内存的地方。如果你的程序里写了大规模的数组或使用`malloc()`等分配了大量的空间可能出现栈溢出或堆挤占栈空间的情况。需要根据MCU的资源大小设置合适的stack size和heap size。
### C language标准和编译器
不同的C语言标准一般以年份作代号支持的语法特性和关键字不同拥有的功能也不同。一般来说语言标准都是向前兼容的在更新之后仍然会保存前代的基本功能支持legacy support。不过为了程序能够正常运行我们还需要一些硬件或平台支持的组件。比如`malloc()`这个函数在linux平台和windows平台上的具体实现就相去甚远跟单片机更是差了不止一点。前两者一般和对应的操作系统有关后者在裸机上则是直接通过硬件或ST公司提供的硬件抽象层代码实现。
然而不同编译器提供的代码实现也不尽相同比如使用clang和gcc这两种c语言编译器他们对于一些标准库也称C库包括stdiostdlibstring等在内的实现的函数的实现就不太一样。再如`__packed`是arm-cc提供的一个字节不对齐关键字在一些其他编译器中就不支持这种实现。
以前大家常用的KEIL使用的是ARM提供的arm-cc工具链非常蛋疼甚至不支持uint8_t=0b00001111这种二进制定义法而该教程选用的是开源的**Arm GNU Toolchain**。在非目标机且和目标机平台不同的平台上进行开发被成为**跨平台开发**,进行的编译也被成为**交叉编译**(在一个平台上生成另一个平台上的 可执行代码)。
> 工具链包含了编译器链接器以及调试器等开发常用组件。我们使用的Arm GNU toolchain中编译器是`arm-none-eabi-gcc.exe`,链接器是`arm-none-eabi-ld.exe`,调试器则是`arm-none-eabi-gdb.exe`。通过跨平台调试器和j-link/st-link/dap-link我们就可以在自己的电脑上对异构平台即单片机的运行进行调试了。
### Debug外设工作原理
![image-20221112145717063](assets\image-20221112145717063.png)
DBG支持模块红框标注部分也可以看作一个外设通过一条专用的AHB-AP总线和调试接口相连Jtag或swd并且有与**数据**和**外设**总线直接相连的桥接器。它还同时连接了中断嵌套管理器因此同样可以捕获中断并进行debug和ITM、DWT、FPB这些调试支持模块。因此DBG可以直接获取内存或片上外设内的数据而不需要占用CPU的资源并将这些数据通过专用外设总线发送给调试器进而在上位机中读取。
FPB是flash patch breakpoint闪存指令断点的缩写用于提供代码断点插入的支持当CPU的指令寄存器读取到某一条指令时FPB会监测到它的动作并通知TPIU暂停CPU进行现场保护。
DWT是data watch trace数据观察与追踪单元的缩写用于比较debug变量的大小并追踪变量值的变化。当你设定了比较断点规则当某个数据大于/小于某个值时暂停程序或将变量加入watch进行查看DWT就会开始工作。DWT还提供了一个额外的计时器即所有可见的TIM资源之外的另一个硬件计时器因为调试其他硬件定时器的计时由于时钟变化可能定时不准而DWT定时器是始终正常运行的。它用于给自身和其他调试器模块产生的信息打上时间戳。我们的bsp中也封装了dwt计时器你可以使用它来计时。
ITM是instrument trace macrocell指令追踪宏单元的缩写它用于提供非阻塞式的日志发送支持相当于大家常用的串口调试SEGGER RTT就可以利用这个模块向上位机发送日志和信息。
以上三个模块都需要通过TPIUtrace port interface unit和外部调试器j-link等进行连接TPIU会将三个模块发来的数据进行封装并通过DWT记录时间发送给上位机。
## 环境配置
- ***所有需要编辑的配置文件都已经在basic_framework的仓库中提供如果不会写照猫画虎。***
- 安装STM32CubeMX并安装F4支持包和DSP库支持包-
- 安装VSCode并安装C/C++Cortex-DebugCortex-Debug: Device Support Pack - STM32F4Better C++ SyntaxIntelliCodeMakfile ToolsC/C++ Snippets插件
![image-20221112172157533](assets\image-20221112172157533.png)
![image-20221112172208749](assets\image-20221112172208749.png)
![image-20221112172221756](assets\image-20221112172221756.png)
![image-20221112172239386](assets\image-20221112172239386.png)
![image-20221112172254809](assets\image-20221112172254809.png)
- 安装MinGW等待界面如下
![image-20221112172051589](assets\image-20221112172051589.png)
安装好后打开MinGW后将所有的支持包勾选然后安装
![image-20221112172348408](assets\image-20221112172348408.png)
![image-20221112172420037](assets\image-20221112172420037.png)
安装完以后将MinGW的bin文件夹添加到环境变量中的path下按下菜单键搜索**编辑系统环境变量**打开之后:
![image-20221112172716320](assets\image-20221112172716320.png)
图片看不清请打开原图。验证安装:
打开命令行win+Rcmd回车输入`gcc -v`,如果没有报错,并输出了一堆路径和参数说明安装成功。
- 配置gcc-arm-none-eabi环境变量
同上将工具链的bin添加到PATH
![image-20221112172858593](assets\image-20221112172858593.png)
<center>安装路径可能不一样,这里要使用你自己的路径而不是直接抄</center>
验证安装:
打开命令行,输入`arm-none-eabi-gcc -v`,如果没有报错,并输出了一堆路径和参数说明安装成功。
> 添加到环境变量PATH的意思是当一些程序需要某些依赖或者要打开某些程序时系统会自动前往PATH下寻找对应项。**一般需要重启使环境变量生效。**
- **将OpenOCD解压到一个文件夹里**稍后需要在VSCode中设置这个路径。
- CubeMX生成代码的时候工具链选择makefile
![image-20221112173534670](assets\image-20221112173534670.png)
生成的目录结构如下:
![image-20221112174211802](assets\image-20221112174211802.png)
Makefile就是我们要使用的构建规则文件。
## 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`键接受代码提示的提议,通过↑和↓键切换提示。
### 编译
用VSCode打开创建的项目文件夹**Makefile Tools插件会询问你是否帮助配置intellisense选择是。**此时就可以享受intellicode带来的各种便利的功能了。我们的项目使用Makefile进行编译在之前的编译介绍中以GCC为例如果需要编译一个文件要输入如下命令
```shell
gcc your_source_code_name.c -o output
```
然而,你面对的是一个拥有几百个.c和.h文件以及大量的链接库如果要将所有文件都输入进去那将是一件苦恼的事。Makefile在gcc命令上提供了一层抽象通过编写makefile来指定参与编译的文件和编译选项再使用`make`命令进行编译它会自动将makefile的内容“翻译”为gcc命令。这样编译大型项目就不是一件困难的事了。
> 实际上在使用keil MDK开发的时候它调用的仍然是底层的arm cc工具链中的编译器和链接器在配置“魔术棒”添加项目文件以及包含目录的时候实际做的使其和makefile差不多。keil使用的参数可以在魔棒的C/C++选项卡下看到。
对于一个已经拥有makefile的项目打开一个终端输入
```shell
mingw32-make -j24 # -j参数表示参与编译的线程数,一般使用-j12
```
> 注意多线程编译的时候输出的报错信息有时候可能会被打乱多个线程同时往一个terminal写入程序运行的信息要是看不清报错请使用`mingw32-make`,不要进行多线程编译。
![image-20221112191712534](assets\image-20221112191712534.png)
就会开始编译了。你可以看到大致如下的输出:
```shell
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编译器传入了一堆的参数以及编译选项然后运行。
最后输出的结果如下:
```shell
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
```
代表了生成的可执行文件的大小以及格式和内容。.elf文件就是我们需要传递给调试器的东西在[使用VSCode调试](###简单调试)部分会介绍。
当然了,你可能觉得每次编译都要在命令行里输入参数,太麻烦了。我们可以编写一个`task.json`这是VSCode的一个任务配置内容大致如下
```json
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"label": "build task", // 任务标签
"type": "shell", // 任务类型,因为要调用mingw32-make,是在终端(CMD)里运行的,所以是shell任务
"command": "mingw32-make -j24",// 任务命令
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
```
这样你就可以点击VSCode工具栏上方的Terminal->Run task选择你刚刚配置的任务开始编译了。**更方便的方法是使用快捷键:`ctrl+shift+B`。**
![image-20221112192133103](assets\image-20221112192133103.png)
> 还没配置任务的时候需要在Terminal标签页中选择Configure Tasks... 创建一个新的.json文件。
>
> P.S. VSCode中的大部分配置都是通过json文件保存的。当前工作区的配置在项目文件夹中的.vscode下全局配置在设置中修改。全局配置在当前工作区没有配置的时候会生效反之被前者覆盖。
### 如果你编写了新的代码文件...
Makefile的大部分内容在CubeMX初始化的时候就会帮你生成。如果新增了.c的源文件你需要在`C_SOURCES`中新增:
![image-20221112192509718](assets\image-20221112192509718.png)
换行需要在行尾加反斜杠\\
如果新增了头文件,在`C_INCLUDES`中新增头文件所在的文件夹:
![image-20221112192610543](assets\image-20221112192610543.png)
换行需要在行尾加反斜杠\\
**添加完之后,重新编译即可**
> 和KEIL新增文件的方式很相似但是更方便。
### 简单调试
> 在VSCode中调试不能像Keil一样查看变量动态变化但是支持以外的所有操作如查看外设和反汇编代码设置断点触发方式等。
用于调试的配置参考这篇博客:[Cortex-debug 调试器使用介绍](https://blog.csdn.net/qq_40833810/article/details/106713462)
你需要配置**arm gnu工具链的路径****OpenOCD的路径**使得GDB调试器可以找到OpenOCD并调用它从而连接硬件调试器如j-link等该工作区文件夹的**launch.json文件**用于启动vscode的调试任务
如果教程看不懂,请看`.vscode`下的`launch.json`,照葫芦画瓢。
根目录下已经提供了C板所需的.svd和使用无线调试器时所用的openocd.cfg配置文件。
然后选择run and debug标签页在选项中选择你配置好的选项开始调试。**或者使用快捷键:`F5`。**
![image-20221112180103750](assets\image-20221112180103750.png)
---
## Ozone可视化调试和LOG功能
### 配置调试项目
### 常用调试窗口和功能
#### 变量动态查看(可视化)
#### 日志打印
#### 外设查看
#### 调用栈
### 常用快捷键