奋斗版 stm32开发板的 v5工作原理_ALIENTEK 阿波罗 STM32F767 开发板资料连载第三章 软件入门...

1)实验平台:alientek 阿波罗 STM32F767 开发板2)摘自《STM32F7 开发指南(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子

55b745f29b4c9f04e4a02e7ee9775f77.png

第三章 软件入门

本章将向大家介绍 MDK5 软件和 STM32CubeF7,通过本章的学习,我们最终将建立一个

基于 HAL 库的的 MDK5 工程,同时本章还将向大家介绍 MDK5 软件的一些使用技巧,希望大

家在本章之后,能够对 MDK5 这个软件有个比较全面的了解。

本章分为如下个小结:

3.1,MDK5 简介与安装

3.2,STM32CubeF7 简介

3.3,新建基于 HAL 库的工程模板和工程结构讲解

3.4,程序下载与调试

3.5,MDK5 使用技巧;

3.1 MDK5 简介与安装

MDK 源自德国的 KEIL 公司,是 RealView MDK 的简称。在全球 MDK 被超过 10 万的嵌

入式开发工程师使用。目前最新版本为:MDK5.21,该版本使用 uVision5 IDE 集成开发环境,

是目前针对 ARM 处理器,尤其是 Cortex M 内核处理器的最佳开发工具。

MDK5 向后兼容 MDK4 和 MDK3 等,以前的项目同样可以在 MDK5 上进行开发(但是头文

件方面得全部自己添加), MDK5 同时加强了针对 Cortex-M 微控制器开发的支持,并且对传统

的开发模式和界面进行升级,MDK5 由两个部分组成:MDK Core 和 Software Packs。其中,

Software Packs 可以独立于工具链进行新芯片支持和中间库的升级。如图 3.1.1 所示:

02f00325d2411cefde1b487b0c8db62d.png

图 3.1.1 MDK5 组成

从上图可以看出,MDK Core 又分成四个部分:uVision IDE with Editor(编辑器),ARM

C/C++ Compiler(编译器),Pack Installer(包安装器),uVision Debugger with Trace(调试跟踪

器)。uVision IDE 从 MDK4.7 版本开始就加入了代码提示功能和语法动态检测等实用功能,相

对于以往的 IDE 改进很大。

Software Packs(包安装器)又分为:Device(芯片支持),CMSIS(ARM Cortex 微控制器

软件接口标准)和 Mdidleware(中间库)三个小部分,通过包安装器,我们可以安装最新的组

件,从而支持新的器件、提供新的设备驱动库以及最新例程等,加速产品开发进度。

MDK5 安装包可以在:http://www.keil.com/demo/eval/arm.htm 下载到。而器件支持、设备

驱动、CMSIS 等组件,则可以点击 MDK5 的 Build Toolbar 的最后一个图标调出 Pack Installer,

来进行各种组件的安装。也可以在 http://www.keil.com/dd2/pack 这个地址下载,然后进行安装。

具体安装步骤请参考光盘教程“/1,ALIENTEK 阿波罗 STM32F7 开发板入门资料/MDK5.21

安装手册.pdf”即可。

在 MDK5 安装完成后,要让 MDK5 支持 STM32F7 的开发,还要安装 STM32F7 的器件支

持包:
Keil.STM32F7xx_DFP.2.7.0.pack(STM32F7 系列的器件包)。这个包以及 MDK5.21 安装

软件,我们都已经在开发板光盘提供了,跟安装软件在同一级目录。

3.2 STM32CubeF7 简介

STM32Cube 是 ST 提供的一套性能强大的免费开发工具和嵌入式软件模块,能够让开发人

员在 STM32 平台上快速、轻松地开发应用。它包含两个关键部分:

1、 图形配置工具 STM32CubeMX。允许用户通过图形化向导来生成 C 语言工程。

2、 嵌入式软件包(STM32Cube 库)。包含完整的 HAL 库(STM32 硬件抽象层 API),配套

的中间件(包括 RTOS,USB,TCP/IP 和图形),以及一系列完整的例程。

嵌入式软件包完全兼容 STM32CubeMX。对于图形配置工具 STM32CubeMX 入门使用,由于需

要 STM32F7 基础才能入门使用,所以我们安排在后面 4.8 小节给大家讲解。本小节,我们主要

讲解 STM32Cube 的嵌入式软件包部分。在讲解之前,首先我们来看看库函数和寄存器开发的

关系。

3.2.1 库开发与寄存器开发的关系

很多用户都是从学 51 单片机开发转而想进一步学习 STM32 开发,他们习惯了 51 单片机

的寄存器开发方式,突然一个 STM32 固件库摆在面前会一头雾水,不知道从何下手。下面我

们将通过一个简单的例子来告诉 STM32 固件库到底是什么,和寄存器开发有什么关系?其实

一句话就可以概括:固件库就是函数的集合,固件库函数的作用是向下负责与寄存器直接打交

道,向上提供用户函数调用的接口(API)。

在 51 的开发中我们常常的作法是直接操作寄存器,比如要控制某些 IO 口的状态,我们直

接操作寄存器:

P0=0x11;

而在 STM32 的开发中,我们同样可以操作寄存器:

GPIOF->BSRR=0x00000001; //这里是针对 STM32F7 系列

这种方法当然可以,但是这种方法的劣势是你需要去掌握每个寄存器的用法,你才能正确使用

STM32,而对于 STM32 这种级别的 MCU,数百个寄存器记起来又是谈何容易。于是 ST(意法

半导体)推出了官方固件库,固件库将这些寄存器底层操作都封装起来,提供一整套接口(API)

供开发者调用,大多数场合下,你不需要去知道操作的是哪个寄存器,你只需要知道调用哪些

函数即可。

比如上面的控制 BSRRL 寄存器实现电平控制,官方 HAL 库封装了一个函数:

void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState

PinState)

{

if(PinState != GPIO_PIN_RESET)

{

GPIOx->BSRR = GPIO_Pin;

}

else

{

GPIOx->BSRR = (uint32_t)GPIO_Pin << 16;

}

}

这个时候你不需要再直接去操作BSRRL寄存器了,你只需要知道怎么使用HAL_GPIO_WritePin

这个函数就可以了。在你对外设的工作原理有一定的了解之后,你再去看固件库函数,基本上

函数名字能告诉你这个函数的功能是什么,该怎么使用,这样是不是开发会方便很多?

任何处理器,不管它有多么的高级,归根结底都是要对处理器的寄存器进行操作。但是固

件库不是万能的,您如果想要把 STM32 学透,光读 STM32 固件库是远远不够的。你还是要了

解一下 STM32 的原理,了解 STM32 各个外设的运行机制。只有了解了这些原理,你在进行固

件库开发过程中才可能得心应手游刃有余。只有了解了原理,你才能做到“知其然知其所以然”,

所以大家在学习库函数的同时,别忘了要了解一下寄存器大致配置过程。

3.2.2 STM32CubeF7 固件包介绍

STM32Cube 目前几乎支持 STM32 全系列,本手册我们讲解的是 STM32F7 的使用,所以

我们主要讲解 STM32CubeF7 相关知识。如果大家使用的是其他系列的 STM32 芯片,请到 ST

官网下载对应的 STM32Cube 包即可。完整的 STM32CubeF7 包在我们开发板配套光盘有提供,

目录为:8,STM32 参考资料1,STM32CubeF7 固件包

接下来我们看看 STM32CubeF7 包目录结构,如下图 3.2.2.1 所示:

f6b7ec5f28eb3d6cca57bcb3a20bf1d6.png

图 3.2.2.1 STM32CubeF7 包目录结构

对于 Documentation 文件夹,里面是一个 STM32CubeF7 的英文说明文档,这里我们就不做过多

解释。接下来我们通过几个表格依次来介绍一下 STM32CubeF7 中几个关键的文件夹。

1)Drivers 文件夹。Drivers 文件夹包含 BSP,CMSIS 和 STM32F7xx_HAL_Driver 三个子文件

夹。三个子文件夹具体说明请参考下表 3.2.2.2:

460b25102ffda30a3404384957c77459.png

表 3.2.2.2 Drivers 文件夹介绍

2)Middlewares 文件夹。

该文件夹下面有 ST 和 Third_Party 2 个子文件夹。ST 文件夹下面存放的是 STM32 相关的

一些文件,包括 STemWin 和 USB 库等。Third_Party 文件夹是第三方中间件,这些中间价都是

非常成熟的开源解决方案。具体说明请见下表 3.3.2.3:

d260168ab56e659beff3edee6324f698.png

c7abf2a7c86d05d80509099f468539da.png

表 3.2.2.3 Middlewares 文件夹介绍

3)Projects 文件夹。

该文件夹存放的是一些可以直接编译的实例工程。每个文件夹对应一个 ST 官方的 Demo

板。比如我们要查看 STM32F767 相关工程,所以我们直接打开子文件夹 STM32F767ZI-Nucleo

即可。里面有很多实例,我们都可以用来参考。这里大家注意,每个工程下面都有一个

MDK-ARM 子文件夹,该子文件夹内部会有名称为 Project.uvprojx 的工程文件,我们只需要点

击它就可以在 MDK 中打开工程。例如我们打开ProjectsSTM32F767ZI-Nucleo 文件夹,内容如

下图 3.2.2.4:

17bc99c918c0ab68126fc687fc169e54.png

图 3.2.2.4 Templates 工程中 MDK-ARM 文件夹内容整版,我们光盘目录(压缩包): “8,STM32 参考资料1

4) Utilities 文件夹。

该文件夹下面是一些其他组件,在项目中使用得不多。有兴趣的同学可以学习一下,这里

我们不做过多介绍。

3.3 新建基于 HAL 库的工程模板和工程结构讲解

在前面的章节我们介绍了 STM32F7xx 官方 HAL 库包的一些知识,这些我们将着重讲解建

立基于 HAL 库的工程模板的详细步骤。实际上,我们可以使用 ST 官方的 STM32CubeMX 图

形工具生成一个工程模板,这里之所以我们还要手把手教大家新建一个模板,是为了让大家对

工程新建和运行过程有一个深入的理解,这样在日后的开发中遇到任何问题都可以得心应手的

解决,STM32CubeMX 工具的使用我们在后面的 4.8 小节会详细讲解。在新建模板之前之前,

首先我们要准备如下资料:

1) HAL 库开发包:STM32Cube_FW_F7_V1.4.0 这是 ST 官网下载的 STM32CubeF7 包完整版,我们光盘目录(压缩包):

“8,STM32 参考资料1,STM32CubeF7 固件包”。

我们官方论坛开源电子网帖子 http://www.openedv.com/thread-80566-1-1.html 中也有下载。

2) MDK5.21 开发环境(我们的板子的开发环境目前是使用这个版本)。这在我们光盘

的软件目录下面有安装包:软件资料软件MDK5.21。

3.3.1 新建基于 HAL 库工程模板

在新建之前,首先我们要说明一下,这一小节我们新建的工程放在光盘目录,路径为:“4,

程序源码标准例程-库函数版本实验 0-1 Template 工程模板-新建工程章节使用” 下面,大家

在学习新建工程过程中间遇到一些问题,可以直接打开这个模板,然后对比学习。

1) 在建立工程之前,我们建议用户在电脑的某个目录下面建立一个文件夹,后面所建立的工

程都可以放在这个文件夹下面,这里我们建立一个文件夹为 Template。这是工程的根目录文件

夹。然后为了方便我们存放工程需要的一些其他文件,这里我们还新建下面 4 个子文件夹:

CORE ,HALLIB,OBJ 和 USER。至于这些文件夹名字,实际上是可以任取的,我们这样取

名只是为了方便识别。对于这些文件夹用来存放什么文件,我们后面的步骤会一一提到。新建

好的目录结构如下图 3.3.1.1.

8f3a2dc0ecf9b02266bde3a1bfbe378b.png

图 3.3.1.1 新建文件夹

2) 接下来,打开 MDK,点击菜单 Project –>New Uvision Project ,然后将目录定位到刚才建

立的文件夹 Template 之下的 USER 子目录,工程取名为 Template 之后点击保存,工程文件就都

保存到 USER 文件夹下面。 操作过程如下图 3.3.1.2 和 3.3.1.3 所示:

a38f3c880fda32b7e4e2d1fa352c6065.png

图 3.3.1.2 新建工程

a9550f9b33b4733074b1376dae54ff14.png

图 3.3.1.3 定义工程名称

接下来会出现一个选择 Device 的界面,就是选择我们的芯片型号,大家根据自己使用的芯片型

号依次选择即可。如果阿波罗 STM32F 开发板使用的是 STM32F767IGT 芯片,那么依次选择

STMicroelectronicsSTM32F7 SeriesSTM32F767STM32F767IGSTM23F767IGTx(如果使

用的是其他系列的芯片,选择相应的型号就可以了,例如我们的探索者 STM32 开发板是

STM32F407ZG。特别注意:一定要安装对应的器件 pack 才会显示这些内容)。

a7c02ebfbfdc8b2d3e9a0b331d7e8e92.png

图 3.3.1.4 选择芯片型号

点击 OK,MDK 会弹出 Manage Run-Time Environment 对话框,如图 3.3.1.5 所示:

b1b34d93ad4a6156fc1f00f1890c410a.png

图 3.3.1.5 Manage Run-Time Environment 界面

这是 MDK5 新增的一个功能,在这个界面,我们可以添加自己需要的组件,从而方便构建

开发环境,不过这里我们不做介绍。所以在图 3.3.1.5 所示界面,我们直接点击 Cancel,即可,

得到如图 3.3.1.6 所示界面:

99cc0ec3cbef2f71203b4396097c8055.png

图 3.3.1.6 工程初步建立

3) 现在我们看看 USER 目录下面内容,如下图 3.3.1.7:

abce9cf14c40330dbaa5b2e9175726d7.png

图 3.3.1.7 工程 USER 目录文件

这里我们说明一下, Template.uvprojx 是工程文件,非常关键,不能轻易删除,MDK5.21

生成的工程文件是以.uvprojx 为后缀。DebugConfig,Listings 和 Objects 三个文件夹是 MDK 自

动生成的文件夹。其中 DebugConfig 文件夹用于存储一些调试配置文件,Listings 和 Objects 文

件夹用来存储 MDK 编译过程的一些中间文件。这里,我们把 Listings 和 Objects 文件夹删除,

我们会在下一步骤中新建一个 OBJ 文件夹,用来存放编译中间文件。当然,我们不删除这两个

文件夹也没有关系,只是我们不用它而已。

4) 接下来我们将从官方 STM32CubeF7 包里面复制一些我们新建工程需要的关键文件到我们

的工程目录中。首先,我们要将 STM32CubeF7 包里的源码文件复制到我们的工程目录文件夹下面。打开官方 STM32CubeF7 包,定位到我们之前准备好的 HAL 库包的目录:

STM32Cube_FW_F7_V1.4.0DriversSTM32F7xx_HAL_Driver 下面,将目录下面的 Src,Inc 文件

夹复制到我们刚才建立的 HALLIB 文件夹下面。Src 存放的是固件库的.c 文件,Inc 存放的是对

应的.h 文件,您不妨打开这两个文件目录过目一下里面的文件,每个外设对应一个.c 文件和一

个.h 头文件。操作完成后工程 HALLIB 目录内容如下图 3.3.1.8。

2e243b391b2269c9a4107ed9bb9215a4.png

图 3.3.1.8 官方库源码文件夹

5) 接下来,我们要将 STM32CubeF7 包里面相关的启动文件以及一些关键头文件复制到我们的

工程目录 CORE 之下。打开 STM32CubeF7 包,定位到目录

STM32Cube_FW_F7_V1.4.0DriversCMSISDeviceSTSTM32F7xxSourceTemplatesarm 下面,

将 文 件 startup_stm32f767xx.s 复 制 到 CORE 目 录 下 面 。 然 后 定 位 到 目 录

STM32Cube_FW_F7_V1.4.0DriversCMSISInclude,将里面的五个头文件:cmsis_armcc.h,

core_cm7.h,core_cmFunc.h,core_cmInstr.h ,core_cmSimd.h 同样复制到 CORE 目录下面。

现在看看我们的 CORE 文件夹下面的文件,如下图 3.3.1.9:

0d54127e93d2a730ec56e5d5b634c1ce.png

图 3.3.1.9 CORE 文件夹文件

6) 接下来我们要复制工程模板需要的一些其他头文件和源文件到我们工程。首先定位到目录:

STM32Cube_FW_F7_V1.4.0DriversCMSISDeviceSTSTM32F7xxInclude 将里面的 3 个文件

stm32f7xx.h,system_stm32f7xx.h 和 stm32f767xx.h 复制到 USER 目录之下。这三个头文件是

STM32F7 工程非常关键的头文件,前面我们介绍 STM32CubeF7 包的时候已经给大家介绍过。

然后进入目录STM32Cube_FW_F7_V1.4.0ProjectsSTM32F767ZI-NucleoTemplates 目录下,这

个目录下面有好几个文件夹,如下图 3.3.1.10,我们需要从 Src 和 Inc 文件夹下面复制我们需要

的文件到 USER 目录。

c15734b0308799c9530a4a647829be7d.png

图 3.3.1.10 固件库包 Template 目录下面文件一览

首先我们打开Inc目录,将目录下面的3个头文件stm32f7xx_it.h,stm32f7xx_hal_conf.h 和main.h

全部复制到USER 目录下面。然后我们打开 Src 目录,将下面的四个源文件 system_stm32f7xx.c,

stm32f7xx_it.c, stm32f7xx_hal_msp.c 和 main.c 同样全部复制到 USER 目录下面。相关文件复制

到 USER 目录之后 USER 目录文件如下图 3.3.1.11:

d36f3a191144d26f2430d6fe98dbfb3a.png

图 3.3.1.11 USER 目录文件浏览

7) 前面 6 个步骤,我们将需要的文件复制到了我们的工程目录下面了。接下来,我们还需要

复制 ALIENTEK 编写的 SYSTEM 文件夹内容到工程目录中。首先,我们需要解释一下,这个

SYSTEM 文件夹内容是 ALIENTEK 为开发板用户编写的一套非常实用的函数库,比如系统时

钟初始化,串口打印,延时函数等,这些函数被很多工程师运用到自己的工程项目中。当然,

大家也可以根据自己需求决定是否需要 SYSTEM 文件夹,对于 STM32F767 的工程模板,如果

没有加入 SYSTEM 文件夹,那么大家需要自己定义系统时钟初始化。SYSTEM 文件夹对于库

函数版本程序和寄存器版本程序是有所区别的,这里我们新建的是库函数工程模板,所以大家

从光盘程序源码目录之下的库函数版本的任何一个实验中复制过来即可。这里我们打开光盘的

4,程序源码标准例程-库函数版本实验 0-1 Template 工程模板-新建工程章节使用”工程目

录,从里面复制 SYSTEM 文件夹到我们的 Template 工程模板根目录即可。操作过程如下图

3.3.1.12 和图 3.3.1.13 所示:

18333cab922e96b046ef532960d48771.png

图 3.3.1.12 复制实验 0-1 的 SYSTEM 文件夹到工程根目录

74d4b0a889c898fb238e573e4bdcb143.png

图 3.3.1.13 复制 SYSTEM 文件夹之后的 Template 根目录文件夹结构

到这里,工程模板所需要的所有文件都已经复制进去。接下来,我们将在 MDK 中将这些文件

添加到工程。

8) 下面我们将前面复制过来的文件加入我们的工程中。右键点击 Target1,选择 Manage Project

Items,如下图 3.3.1.14 所示:

3eaa05f68f3d4def13d2c0e78fea211c.png

图 3.3.1.14 点击 Management Project Itmes

9) Project Targets 一栏,我们将 Target 名字修改为 Template,然后在 Groups 一栏删掉一个 Source

Group1,建立四个 Groups:USER,SYSTEM,CORE,和 HALLIB。然后点击 OK,可以看到

我们的 Target 名字以及 Groups 情况如下图 3.3.1.15 和图 3.3.1.16 所示:

277a30b8f1e0e1701f2b0533df7e476d.png

图 3.3.1.15 新建 GROUP

4cee4e93ba6df2e66e1435252d6cb194.png

图 3.3.1.16 查看工程 Group 情况

10)

下面我们往 Group 里面添加我们需要的文件。我们按照步骤 9 的方法, 右键点击点

击 Tempate,选择 Manage Project Items.然后选择需要添加文件的 Group,这里第一步我们选择

HALLIB,然后点击右边的 Add Files,定位到我们刚才建立的目录HALLIBSrc 下面,将里面所

有的文件选中(Ctrl+A),然后点击 Add,然后 Close.可以看到 Files 列表下面包含我们添加的文

件,如下图 3.3.1.17。这里需要说明一下,对于我们写代码,如果我们只用到了其中的某个外设,

我们就可以不用添加没有用到的外设的库文件。例如我只用 GPIO,我可以只用添加

stm32f7xx_gpio.c 而其他外设相关的可以不用添加。这里我们全部添加进来是为了后面方便,不

用每次添加,当然这样的坏处是工程太大,编译起来速度慢,用户可以自行选择。

b10b5646ba37eb46efe64064c8c979c9.png

图 3.3.1.17 添加文件到 HALLIB 分组

这 里 有 几 个 template 文 件 不 需 要 引 入 , 例 如 stm32f7xx_hal_msp_template.c ,

stm32f7xx_hal_timebase_rtc_alarm_template.c,stm32f7xx_hal_timebase_rtc_wakeup_template.

c 和 stm32f7xx_hal_timebase_tim_template.c 四个文件不需要引入工程,这些文件我们在工程

中可以参考,但是不需要引入工程,所以我们删除即可。删除某个引入文件方法如下图 3.3.1.18

所示:

0599209f206bdaf885644cbaa7121555.png

图 3.3.1.18 删掉 HALLIB 分组中不需要的源文件

使用同样的方法删除其他几个不需要添加的文件即可。

11)

用上面同样的方法,将 Groups 定位到 CORE,USER 和 SYSTEM 分组之下,添加需要的

文件。CORE 分组下面需要添加的文件为一些头文件以及启动文件 startup_stm32f767xx.s(注意,

默认添加的时候文件类型为.c,添加.h 头文件和 startup_stm32f767xx.s 启动文件的时候,你需

要选择文件类型为 All files 才能看得到这些文件)。USER 分组下面需要添加的文件 USER 目录

下面所有的.c 文件:main.c,stm32f7xx_hal_msp.c,stm32f7xx_it.c 和 system_stm32f7xx.c 四个

文件。 SYSTEM 分组下面需要添加 SYSTEM 文件夹下所有子文件夹内的.c 文件,包括 sys.c,

usart.c 和 delay.c 三个源文件。添加完必要的文件到工程之后,最后点击 OK,回到工程主界面。

操作过程如下图 3.3.1.19~3.3.1.22:

e27820c1171918389f1131d110249203.png

图 3.3.1.19 添加文件到 USER 分组

68a4c44fef710a09e24dfec746a09d36.png

图 3.3.1.20 文件添加到 USER 分组完成

使用同样的方法,选中 CORE 分组,点击 Add Files 按钮,添加需要的文件到 CORE 分组。

02ab3325bd8362674f87a2ce44bd7ff0.png

图 3.3.1.21 添加.h 头文件和启动文件到 CORE 分组

6f579696bee9b2a1559c4e1d4bc95ecd.png

图 3.3.1.22 添加启动文件和头文件到 CORE 分组完成

最后添加文件到 SYSTEM 分组,这里需要注意,SYSTEM 文件夹包含三个子文件夹 sys,delay 和

usart。在添加文件的时候,需要分别定为到三个子文件夹内部,依次添加下面的.c 文件即可。

添加完成后如下图 3.3.1.23 所示:

adebdbdeb3f6f181fe6497c95cea0e01.png

图 3.3.1.23 添加文件到 SYSTEM 分组

添加完所有文件到工程中之后,我们点击 OK 按钮,回到 MDK 工程主界面,如下图 3.3.1.24

所示

e95a6d85f36a6b613fc4ba92e0af15be.png

图 3.3.1.24 工程分组情况

12)

接下来我们要在 MDK 里面设置头文件存放路径。也就是告诉 MDK 到那些目录下面去

寻找包含了的头文件。这一步骤非常重要。如果没有设置头文件路径,那么工程会出现报

错头文件路径找不到。具体操作如下图 3.3.1.25 和 3.3.1.26 所示,5 步之后添加相应的

头文件路径。

5cd05bb53325af0bcf325611015576b4.png

图 3.3.1.25 进入 PATH 配置界面

73256cddc6d401365eb43695be148d7f.png

图 3.3.1.26 添加头文件路径到 PATH

这里大家需要注意,这里添加的路径必须添加到头文件所在目录的最后一级。比如在 SYSTEM

文件夹下面有三个子文件夹下面都有.h 头文件,这些头文件在工程中都需要使用到,所以我们

必须将这三个子目录都包含进来。这里我们需要添加的头文件路径包括:CORE, USER,

SYSTEMdelay ,SYSTEMusart,SYSTEMsys 以及HALLIBInc。这里还需要提醒大家,HAL

库存放头文件子目录是HALLIBInc,不是 HALLIBSrc,其次很多朋友都是这里弄错导致报

很多奇怪的错误。添加完成之后如下图 3.3.1.27 所示。

48f031775901ffb54413ef1c10c8ed79.png

图 3.3.1.27 添加头文件路径

13)

接下来对于 STM32F7 系列的工程,还需要添加全局宏定义标识符,所谓全局宏定义

标识符,就是在工程中任何地方都可见。添加方法是点击魔术棒之后,进入 C/C++选项卡,然

后在 Define 输入框连输入:USE_HAL_DRIVER,STM32F767xx。注意这里是两个标识符

USE_HAL_DRIVER 和 STM32F767xx,他们之间是用逗号隔开的,请大家注意。这个字符串大

家可以直接打开我们光盘的新建好的工程模板,从里面复制。模板存放目录为:4,程序源码

标准例程-库函数版本实验 0-1 Template 工程模板-新建工程章节使用。本步骤操作过程如下图

3.3.1.28 所示:

8b9e9c0fa55e78e8feca252ddd56f55a.png

图 3.3.1.28 添加全局宏定义标识符

14)

接下来我们要编译工程,在编译之前我们首先要选择编译中间文件编译后存放目录。

前面我们讲过,MDK 默认编译后的中间文件存放目录为 USER 目录下面的 Listings 和 Objects

子目录,这里为了和我们 ALIENTEK 工程结构保持一致,我们重新选择存放到目录 OBJ 目录

之下。操作方法是点击魔术棒

,然后选择“Output”选项下面的“Select folder for objects…”

,

然后选择目录为我们上面新建的 OBJ 目录,然后依次点击 OK 即可。操作过程如下图 3.3.1.29

和 3.3.1.30 所示:

35bae05ca6bee83b6147ae46cb8bb987.png

图 3.3.1.29 点击按钮“Select Folder for Objects…”

ab16d838c12da66083f73b0f9fca4eb3.png

图 3.3.1.30 选择 OBJ 目录为中间文件存放目录

选择完 OBJ 目录为编译中间文件存放目录之后,点击 OK 回到 Output 选项卡。这里我们还

要勾上“Create HEX File”选项和 Browse Information 选项。Create HEX File 选项选

上是要求编译之后生成 HEX 文件。而 Browse Information 选项选上是方便我们查看工程

中的一些函数变量定义等。具体操作方法如下图 3.3.1.31 所示:

51484cb9dd5805e5a19bb9aa0df7fd6b.png

图 3.3.1.31 勾选上 Create HEX file 和 Browse Information 选项

15)

接下来在编译之前,我们先把 main.c 文件里面的内容替换为如下内容:

#include "sys.h"

#include "delay.h"

#include "usart.h"

void Delay(__IO uint32_t nCount);

void Delay(__IO uint32_t nCount)

{

while(nCount--){}

}

int main(void)

{

GPIO_InitTypeDef GPIO_Initure;

Cache_Enable(); //打开 L1-Cache

HAL_Init();

//初始化 HAL 库

Stm32_Clock_Init(432,25,2,9); //设置时钟,216Mhz


__HAL_RCC_GPIOB_CLK_ENABLE(); //开启 GPIOB 时钟

GPIO_Initure.Pin=GPIO_PIN_0|GPIO_PIN_1; //PB1,0

GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出

GPIO_Initure.Pull=GPIO_PULLUP; //上拉

GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速

HAL_GPIO_Init(GPIOB,&GPIO_Initure);

while(1)

{

HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_SET); //PB1 置 1

HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_SET); //PB0 置 1

Delay(0x7FFFFF);

HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_RESET);

//PB1 置 0

HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_RESET);

//PB0 置 0

Delay(0x7FFFFF);

}

}

上面这段代码,大家如果不方便自己编写,可以直接打开我们光盘库函数源码目录“4,

程序源码标准例程-库函数版本实验 0-1 Template 工程模板-新建工程章节使用 ”找到我们已

经新建好的工程模板 USER 目录下面的 main.c 文件,直接复制过来即可。

16)

这个时候我们对工程进行编译会发现,编译结果有错误,编译结果如下图 3.3.1.32 所

示:

a66b12a0cf4c4cd249d21663e851c563.png

图 3.3.1.32 编译结果

从图中可以看出错误信息为“main.h(44): error: #5: cannot open source input file

"stm32f7xx_nucleo_144.h": No such file or directory”,也就是说错误是在 main.h 头文件的第

44 行中引入了头文件 stm32f7xx_nucleo_144.h,而这个头文件在工程中找不到。这是因为

这个头文件是从 ST 的模板中引入,ST 的模板中对于每个开发板都有一个头文件,并且在

main.h 中引入,所以这里对于我们的平台是无关头文件,我们需要从 main.h 头文件中把对

该头文件的引入代码删掉。删掉后的 main.h 内容如下图所示:

921a7eb89dff36e196d739e8084b228a.png

17)

下面我们点击编译按钮

编译工程,可以看到工程编译通过没有任何错误和警告。

ff006da869a36478fb435708e292681e.png

图 3.3.1.32 编译工程

这里大家可能会遇到编译之后会有一个警告,警告的内容是:“warning: #1-D: last line of

file ends without a newline”。我们只需要在 main.c 函数结尾加一个回车即可解决,这个

是 MDK 自身的 BUG。

17)

到这里,一个基于 HAL 库的工程模板就基本建立完成,同时在工程的 OBJ 目录下面生

成了对应的 hex 文件。大家可以参考后面我们 3.4 小节的内容,将 hex 文件下载到开发板,

会发现两个 led 灯不停的闪烁现象。但是这个工程到这一步实际还没有配置完成,还需要

根据我们开发板的外部高速晶振值大小来修改配置文件 stm32f7xx_hal_conf.h,打开该文件

定位到如下内容:

#if !defined (HSE_VALUE)

#define HSE_VALUE ((uint32_t)8000000U)

#endif

因为阿波罗开发板外部晶振使用的是 25MHz,所以这里我们需要把 HSE_VALUE 值修改为

25000000U。修改后内容如下:

#define HSE_VALUE ((uint32_t)25000000U)

18)

最后还一个地方需要大家修改一下,那就是关于系统初始化之后的中断优先级分组组号的设置。默认情况下调用 HAL 初始化函数 HAL_Init 之后,会设置分组为组 4,这里我们正

点原子所有实验使用的是分组 2,所以我们修改 HAL_Init 函数内部,重新设置分组为组 2 即可。

具体方法是:打开 HALLIB 分组之下的 stm32f7xx_hal.c 文件,搜索函数 HAL_Init,找到函数体,

里面默认有这样一行代码:

HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

我们将入口参数 NVIC_PRIORITYGROUP_4 修改为 NVIC_PRIORITYGROUP_2 即可。关于中

断优先级分组相关知识请参考本手册 4.5 小节即可。

3.3.2 工程模板解读

上一节,我们新建了一个基于 HAL 库的 STM32F7 工程模板,本节,我们将给大家讲解工

程模板中的一些关键文件的作用以及整个工程模板程序运行流程。通过对本节内容的学习,大

家将对 STM32F7 工程有一个比较全面的了解,为后面实验学习打下良好的基础。

3.3.2.1 关键文件介绍

在讲解之前我们需要说明一点,任何一个 MDK 工程,不管它有多复杂,无非就是一些.c

源文件和.h 头文件,还有一些类似.s 的启动文件或者 lib 文件等。在工程中,他们通过各种包含

关系组织在一起,被我们用户代码最终调用或者引用。所以我们必须了解这些文件的作用以及

他们之间的包含关系,从而理解这个工程的运行流程,这样我们才能在项目开发中得心应手。

1) HAL 库关键文件介绍如下表 3.3.2.1 所示:

9daf65cd7fc556e2c79c3c718e178edf.png

表 3.3.2.1 HAL 库文件介绍

2)
stm32f7xx_it.c/stm32f7xx_it.h 文件

这两个文件非常简单,也非常好理解。stm32f7xx_it.h 中主要是一些中断服务函数的申明。

stm32f7xx_it.h 中是这些中断服务函数定义,而这些函数定义除了 Systick 中断服务函数

SysTick_Handler 外基本都是空函数,没有任何控制逻辑。一般情况下,我们可以去掉这两

个文件,然后把中断服务函数写在工程中的任何一个可见文件中。

3) stm32f7xx.h 头文件

头文件 stm32f7xx.h 内容看似非常少,却非常重要,它是所有 stm32f7 系列的顶层头文件。

使用 STM32F7 任何型号的芯片,都需要包含这个头文件。同时,因为 stm32f7 系列芯片型

号非常多,ST 为每种芯片型号定义了一个特有的片上外设访问层头文件,比如 STM32F767

系列,ST 定义了一个头文件 stm32f767xx.h,然后 stm32f7xx.h 顶层头文件会根据工程芯片

型号,来选择包含对应芯片的片上外设访问层头文件。我们可以打开 stm32f7xx.h 头文件可

以看到,里面有如下几行代码:

#if defined(STM32F756xx)

#include "STM32F756xx.h"

#elif defined(stm32f767xx)

#include "stm32f767xx.h"

#else

#error "Please select first the target STM32F7xx device used in your application"

#endif

这几行代码非常好理解,我们以 stm3f767 为例,如果定义了宏定义标识符 STM32F767xx,

那么头文件 stm32f7xx.h 将会包含头文件 stm32f767xx.h。实际上,在我们上一节新建工程

的时候,我们在 C/C++选项卡里面输入的全局宏定义标识符中就包含了标识符 STM32F767xx

(请参考图 3.3.1.28)。所以头文件 stm32f767xx.h 一定会被整个工程所引用。

4) stm32f767xx.h 头文件

根据前面的讲解,stm32f767xx.h 是 stm32f767 系列芯片通用的片上外设访问层头文件,只

要我们进行 STM32F767 开发,就必然要使用到该文件。打开该文件我们可以看到里面主要

是一些结构体和宏定义标识符。这个文件的主要作用是寄存器定义声明以及封装内存操作。

在后面寄存器地址名称映射分析小节我们会给大家详细讲解。

5)
system_stm32f7xx.c/system_stm32f7xx.h 文件

头文件system_stm32f7xx.h和源文件system_stm32f7xx.c主要是声明和定义了系统初始化函

数 SystemInit 以及系统时钟更新函数 SystemCoreClockUpdate。SystemInit 函数的作用是进行

时钟系统的一些初始化操作以及中断向量表偏移地址设置,但它并没有设置具体的时钟值,

这是与标准库的最大区别,在使用标准库的时候,SystemInit 函数会帮我们配置好系统时钟

配置相关的各个寄存器。在启动文件 startup_stm32f767xx.s 中会设置系统复位后,直接调

用 SystemInit 函数进行系统初始化。SystemCoreClockUpdate 函数是在系统时钟配置进行修

改后,调用这个函数来更新全局变量 SystemCoreClock 的值,变量 SystemCoreClock 是一个

全局变量,开放这个变量可以方便我们在用户代码中直接使用这个变量来进行一些时钟运

算。

6) stm32f7xx_hal_msp.c 文件

MSP,全称为 MCU support package,关于怎么理解 MSP,我们后面在讲解程序运行流程的时

候会给大家举例详细讲解,这里大家只需要知道,函数名字中带有 MspInit 的函数,它们

的作用是进行 MCU 级别硬件初始化设置,并且它们通常会被上一层的初始化函数所调用,

这样做的目的是为了把MCU相关的硬件初始化剥夺出来,方便用户代码在不同型号的MCU

上移植。stm32f7xx_hal_msp.c 文件定义了两个函数 HAL_MspInit 和 HAL_MspDeInit。这两个

函数分别被文件 stm32f7xx_hal.c 中的 HAL_Init 和 HAL_DeInit 所调用。HAL_MspInit 函数的

主要作用是进行 MCU 相关的硬件初始化操作。例如我们要初始化某些硬件,我们可以硬

件相关的初始化配置写在 HAL_MspDeinit 函数中。这样的话,在系统启动后调用了 HAL_Init

之 后 , 会 自 动 调 用 硬 件 初 始 化 函 数 。 实 际 上 , 我 们 在 工 程 模 板 中 直 接 删 掉

stm32f7xx_hal_msp.c 文件也不会对程序运行产生任何影响。对于这个文件存在的意义,我

们在后面讲解完程序运行流程之后,大家会有更加清晰的理解。

7) startup_stm32f767xx.s 启动文件

STM32 系列所有芯片工程都会有一个.s 启动文件。对于不同型号的 stm32 芯片启动文件也

是不一样的。我们的开发板是 STM32F767 系列,所以我们需要使用与之对应的启动文件

startup_stm32f767xx.s。启动文件的作用主要是进行堆栈的初始化,中断向量表以及中断函

数定义等。启动文件有一个很重要的作用就是系统复位后引导进入 main 函数。打开启动文

件 startup_stm32f767xx.s,可以看到下面几行代码:

; Reset handler

Reset_Handler PROC

EXPORT Reset_Handler [WEAK]

IMPORT SystemInit

IMPORT __main

LDR R0, =SystemInit

BLX R0

LDR R0, =__main

BX R0

ENDP

Reset_Handler 在我们系统启动的时候会执行,这几行代码的作用是在系统启动之后,首先

调用 SystemInit 函数进行系统初始化,然后引导进入 main 函数执行用户代码。

接下来我们看看 HAL 库工程模板中各个文件之间的包含关系,如下图 3.3.2.2 所示:

f2eb5e72bcb5ba451559b6a453df5dda.png

图 3.3.2.2 HAL 库工程文件包含关系

从上面工程文件包含关系图可以看出,顶层头文件 stm32f7xx.h 直接或间接包含了其他所

有工程必要头文件,所以在我们的用户代码中,我们只需要包含顶层头文件 stm32f7xx.h 即可。

这里我们还需要说明一下,在我们 ALIENTEK 提供的 SYSTEM 文件夹内部的 sys.h 头文件中,

我们默认包含了 stm32f7xx.h 头文件,所以在我们用户代码中,只需要包含 sys.h 头文件即可,

当然也可以直接包含顶层头文件 stm32f7xx.h。关于工程模板中关键文件内容我们就给大家介绍

到这里。

3.3.2.2 HAL 库中__weak 修饰符讲解

在 HAL 库中,很多回调函数前面使用__weak 修饰符,这里我们有必要给大家讲解__weak

修饰符的作用。

weak 顾名思义是“弱”的意思,所以如果函数名称前面加上__weak 修饰符,我们一般称

这个函数为“弱函数”。加上了__weak 修饰符的函数,用户可以在用户文件中重新定义一个同

名函数,最终编译器编译的时候,会选择用户定义的函数,如果用户没有重新定义这个函数,

那么编译器就会执行__weak 声明的函数,并且编译器不会报错。

这里我给大家举个例子来加深大家的理解。比如我们打开工程模板,找到并打开文件

stm32f7xx_hal.c 文件,里面定义了一个函数 HAL_MspInit,定义如下:

__weak void HAL_MspInit(void)

{

}

大家可以看出,HAL_MspInit 函数前面有加修饰符__weak,是一个空函数,没有任何控制逻辑。

同时,在 stm32f7xx_hal.c 文件的前面有定义函数 HAL_Init,并且 HAL_Init 函数中调用了函数

HAL_MspInit。

HAL_StatusTypeDef HAL_Init(void)

{

…//此处省略部分代码

HAL_MspInit();

return HAL_OK;

}

如果我们没有在工程中其他地方重新定义 HAL_MspInit()函数,那么 HAL_Init 初始化函数执行

的时候,会默认执行 stm32f7xx_hal.c 文件中定义的 HAL_MspInit 函数,而这个函数没有任何控

制逻辑。如果用户在工程中重新定义函数 HAL_MspInit,那么调用 HAL_Init 之后,会执行用

户自己定义的 HAL_MspInit 函数而不会执行 stm32f7xx_hal.c 默认定义的函数。也就是说,表面

上我们看到函数 HAL_MspInit 被定义了两次,但是因为有一次定义是弱函数,使用了__weak

修饰符,所以编译器不会报错。

__weak 在回调函数的时候经常用到。这样的好处是,系统默认定义了一个空的回调函数,

保证编译器不会报错。同时,如果用户自己要定义用户回调函数,那么只需要重新定义即可,

不需要考虑函数重复定义的问题,使用非常方便,在 HAL 库中__weak 关键字被广泛使用。

3.3.2.3 Msp 回调函数执行过程解读

大家先打开我们前面新建的工程模板,搜索 MspInit 字符串可以发现,在我们的工程模板

文件中,有 80 多个文件定义或者调用了函数名字中包含 MspInit 字符串的函数,而且函数名字

基本遵循 HAL_PPP_MspInit 格式(PPP 代表任意外设)。那么这些函数是怎么被程序调用,又

是什么作用呢?下面我们以串口为例进行讲解。

大家打开我们的工程模板 SYSTEM 分组下面的 usart.c 文件可以看到,内部我们定义了两

个函数 uart_init 和 HAL_UART_MspInit。我们先来大致看看这两个函数的定义(基于篇幅考虑

我们省略部分非关键代码行):

void uart_init(u32 bound)

{

UART1_Handler.Instance=USART1;

//USART1

UART1_Handler.Init.BaudRate=bound;

//波特率

…//此处省略部分串口 1 参数设置代码

UART1_Handler.Init.Mode=UART_MODE_TX_RX;

//收发模式

HAL_UART_Init(&UART1_Handler); //HAL_UART_Init()会使能 UART1

}

//UART 底层初始化,时钟使能,引脚配置,中断配置

void HAL_UART_MspInit(UART_HandleTypeDef *huart)

{

…//此处省略部分代码

GPIO_Initure.Pin=GPIO_PIN_9;

//PA9

GPIO_Initure.Mode=GPIO_MODE_AF_PP;

//复用推挽输出

HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9

GPIO_Initure.Pin=GPIO_PIN_10;

//PA10

HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA10

…//此处省略部分代码

}

用户函数 uart_init 主要作用是设置串口 1 相关参数,包括波特率,停止位,奇偶校验位等,

并且最终是通过调用 HAL_UART_Init 函数进行参数设置。而函数 HAL_UART_MspInit 则主要

进行串口 GPIO 引脚初始化设置。接下来我们打开 usart_init 函数内部调用的 UART 初始化函数

HAL_UART_Init 可以看到代码如下:

HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart)

{

…//此处省略部分代码

if(huart->State == HAL_UART_STATE_RESET)//如果串口没有进行过初始化

{

huart->Lock = HAL_UNLOCKED;

HAL_UART_MspInit(huart);

}

…//此处省略部分代码

return HAL_OK;

}

在函数 HAL_UART_Init 内部,通过判断逻辑判断如果串口还没有进行初始话,那么会调

用函数 HAL_UART_MspInit 进 行 相 关 初 始 化 设 置 。 同 时 , 我 们 可 以 看 到 , 在 文 件

stm32f7xx_hal_uart.c 内部,有定义一个弱函数 HAL_UART_MspInit,内容如下:

__weak void HAL_UART_MspInit(UART_HandleTypeDef *huart)

{

UNUSED(huart);

}

这里定义的弱函数 HAL_UART_MspInit 是一个空函数,没有任何实际的控制逻辑。根据前面的

讲解可知,__weak 修饰符定义的弱函数如果用户自己重新定义了这个函数,那么会优先执行用

户定义函数。所以,实际上在函数 HAL_UART_Init 内部调用的 HAL_UART_MspInit()函数,最

终执行的是用户在 usart.c 中自定义的 HAL_UART_MspInit()函数。

那 么 整 个 串 口 初 始 化 的 过 程 为 : 用 户 函 数 usart_init HAL_UART_Init

HAL_UART_MspInit。学到这里有同学会问,为什么串口相关初始化不在 HAL_UART_Init 函数

内部一次初始化而还要调用函数 HAL_UART_MspInit()呢?这实际就是 HAL 库的一个优点,它

通过开放一个回调函数 HAL_UART_MspInit(),让用户自己去编写与串口相关的 MCU 级别的

硬件初始化,而与 MCU 无关的串口参数相关的通用配置则放在 HAL_UART_Init。

我们要初始化一个串口,首先要设置和 MCU 无关的东西,例如波特率,奇偶校验,停止

位等,这些参数设置和 MCU 没有任何关系,可以使用 STM32F1,也可以是 STM32F2/F3/F4/F7

上的串口。而一个串口设备它需要一个 MCU 来承载,例如用 STM32F7 来做承载,PA9 做为发

送,PA10 做为接收,MSP 就是要初始化 STM32F7 的 PA9,PA10,配置这两个引脚。所以 HAL

驱动方式的初始化流程就是:HAL_USART_Init()—>HAL_USART_MspInit() ,先初始化与 MCU

无关的串口协议,再初始化与 MCU 相关的串口引脚。在 STM32 的 HAL 驱动中

HAL_PPP_MspInit()作为回调,被 HAL_PPP_Init()函数所调用。当我们需要移植程序到 STM32F1

平台的时候,我们只需要修改 HAL_PPP_MspInit 函数内容而不需要修改 HAL_PPP_Init 入口参

数内容。

在 STM32 的 HAL 库中,大部分外设都有回调函数 HAL_MspInit,通过对本小节学习,大

家对这些回调函数的作用和调用过程会非常熟悉,这里我们就不做一一列举。

3.3.2.4 程序执行流程图

经过前面的讲解,大家对工程模板以及关键文件有了比较详细的了解。接下来我们看看程

序执行流程如下图 3.3.2.4.1 所示:

f0672a3636fb60242939f6e1659007f3.png

图 3.3.2.4.1 程序执行流程

从该流程图可以非常清晰的理解整个程序执行流程,这里我们略微讲解一下。启动文件

startup_stm32f767xx.s 中 Reset_Handler 部分会引导先执行 SystemInit 函数,然后再进入 main 函

数。在 main 函数内部,一般情况下,我们会把 HAL 初始化函数 HAL_Init 放在最开头部分,然

后再进行时钟初始化设置。这些设置完成之后,接下来便是调用外设初始化函数 HAL_PPP_Init

进行外设参数初始化设置,同时重写回调函数 HAL_PPP_MspInit 进行外设 MCU 相关的参数设

置。最后编写我们的控制逻辑。关于程序执行流程我们就给大家介绍到这里。

3.4 程序下载与调试

上一节,我们学会了如何在 MDK 下创建 STM32F7 工程。本节,我们将向读者介绍 STM32F7

的代码下载以及调试。这里的调试包括了软件仿真和硬件调试(在线调试)。通过本章的学习,

你将了解到:1、STM32F7 程序下载;2、利用 ST-LINK 对 STM32F7 进行下载与在线调试。

这里大家要注意,为了让大家能够更好的学习调试,我们将 3.3 小节新建的工程模板中的

main.c 文件内容进行了简单的修改。修改后的工程模板在光盘目录:4,程序源码2,标准例

程-库函数版本实验 0-2 Template 工程模板-调试章节使用。本小节下载和调试的工程请参考该

工程模板。

3.4.1 STM32F7 程序下载

给 STM32F767 下载代码。由于 STM32F7 暂时没有比较好的串口下载软件,所以,我们一

般通过仿真器下载,接下来,我们将介绍如何使用 ST LINK V2,结合 MDK,来给 STM32F7

下载代码。

ST LINK 支持 JTAG 和 SWD 两种通信接口,同时 STM32F767 也支持 JTAG 和 SWD。所

以,我们有 2 种方式可以用来下载代码,JTAG 模式,占用的 IO 线比较多,而 SWD 模式则占

用的 IO 线很少,只需要两根即可。所以,我们一般选择 SW 模式来给 STM32F767 下载代码。

首先,我们需要安装 ST LINK 的驱动。驱动安装,请大家参考:光盘6,软件资料1,

软件ST LINK 驱动及教程 文件夹里面的《STLINK 调试补充教程.pdf》自行安装。

在安装了 ST LINK 的驱动之后,我们接上 ST LINK,并用灰排线连接 ST LINK 和开发板

JTAG 接口,打开之前 3.2 节新建的工程,点击 ,打开 Options for Target 选项卡,在 Debug 栏

选择仿真工具为 ST-Link Debugger,如图 3.4.1.1 所示:

40a82a47f6bc681566706a37bcf13584.png

图 3.4.1.1 Debug 选项卡设置

上图中我们还勾选了 Run to main(),该选项选中后,只要点击仿真就会直接运行到 main 函

数,如果没选择这个选项,则会先执行 startup_stm32f767xx.s 文件的 Reset_Handler,再跳到 main

函数。

然后我们点击 Settings,设置 ST LINK 的一些参数,如图 3.4.1.2 所示:

139e717d0ac4a65b57231e392d9dabd0.png

图 3.4.1.2 ST LINK 模式设置

图 3.4.1.2 中,我们使用 STLINK 的 SW 模式调试,因为我们 JTAG 需要占用比 SW 模式多

很多的 IO 口,而在开发板上这些 IO 口可能被其他外设用到,可能造成部分外设无法使用。所

以,我们建议大家在下载/调试代码的时候,一定要选择 SW 模式。Max Clock 我们设置为最大:

4Mhz(需要更新固件,否则最大只能到 1.8Mhz),这里,如果你的 USB 数据线比较差,那么

可能会出问题,此时,你可以通过降低这里的速率来试试。

单击 OK,完成此部分设置,接下来我们还需要在 Utilities 选项卡里面设置下载时的目标编

程器,如图 3.4.1.3 所示:

6e7445113490d5a6ced98a4a5c0a8d90.png

图 3.4.1.3 FLASH 编程器选择

图 3.4.1.3 中,我们直接勾选 Use Debug Driver,即和调试一样,选择 ST LINK 来给目标器

件的 FLASH 编程,然后点击 Settings,设置如图 3.4.1.4 所示:

6fb46d17af393f2d2129cd2351a37431.png

图 3.4.1.4 编程设置

这里 MDK5 会根据我们新建工程时选择的目标器件,自动设置 flash 算法。我们使用的是

STM32F767IGT6,FLASH 容量为 1M 字节,所以 Programming Algorithm 里面默认会有 1M 型

号的 STM32F7xx FLASH 算法。MDK 默认选择的是 AXIM 总线访问的 FLASH 算法(起始地址

为:0X0800 0000),为了方便大家使用,我们将 ITCM 总线访问的 FLASH 算法(起始地址为:

0X0020 0000)也添加进来,由 MDK 自动选择下载算法(实际上是根据 Target 选项卡的 on-chip

IROM 地址范围设置来选择的,默认为 0X0800 0000)。

特别提醒:这里的 1M flash 算法,不仅仅针对 1M 容量的 STM32F767,对于小于 1M FLASH

的型号,也是采用这个 flash 算法的。最后,选中 Reset and Run 选项,以实现在编程后自动运

行,其他默认设置即可。设置完成之后,如图 3.4.1.4 所示。

在设置完之后,点击 OK,然后再点击 OK,回到 IDE 界面,编译一下工程。然后点击:

(下载按钮),就可以下载代码到 STM32F767 上面了,如图 3.4.1.5 所示:

c3d91bfd0c1c33196a340b9a5317d8fb.png

图 3.4.1.5 通过仿真器给 STM32F767 下载代码

下载完成后,在 Build Output 窗口,会提示 Programming Down,Application running…,如

图 3.4.1.6 所示:

98c31c3c27db491bfbd3546265061582.png

图 3.4.1.6 下载完成并运行代码

下载完后,会自动运行我们刚刚下载的代码(因为我们勾选了 Reset and run,见图 3.4.1.4),

接下来我们就可以打开串口调试助手,来验证是否收到了 STM32F767 串口发送出来的数据。

我们在开发板的 USB_232 处插入 USB 线,并接上电脑,如果之前没有安装 CH340G 的驱

动(如果已经安装过了驱动,则应该能在设备管理器里面看到 USB 串口,如果不能则要先卸载

之前的驱动,卸载完后重启电脑,再重新安装我们提供的驱动),则需要先安装 CH340G 的驱

动,找到光盘软件资料软件 文件夹下的 CH340 驱动,安装该驱动,如图 3.4.1.7 所示:

347d90c2647765b5ffd0526cfa0dc1b1.png

图 3.4.1.7 CH340 驱动安装

在驱动安装成功之后,拔掉 USB 线,然后重新插入电脑,此时电脑就会自动给其安装驱动

了。在安装完成之后,可以在电脑的设备管理器里面找到 USB 串口(如果找不到,则重启下电

脑),如图 3.4.1.8 所示:

cb5bfa29ff0ca2d6bc06d7c0dbdd22de.png

图 3.4.1.8 USB 串口

在图 3.4.1.8 中可以看到,我们的 USB 串口被识别为 COM3,这里需要注意的是:不同电

脑可能不一样,你的可能是 COM4、COM5 等,但是 USB-SERIAL CH340,这个一定是一样的。

如果没找到 USB 串口,则有可能是你安装有误,或者系统不兼容。

在安装完 USB 串口驱动之后,我们就可以开始验证了(注意,开发板的 B0 必须接 GND,

否则将不会运行用户下载的代码!!),打开串口调试助手(XCOM V2.0,在光盘6,软件

资料软件串口调试助手里面)选择COM3(得根据你的实际情况选择),设置波特率为115200,

会发现从 ALIENTEK 阿波罗 STM32F767 开发板发回来的信息,如图 3.4.1.9 所示:

158f69339c900072a7520786a148e5d1.png

图 3.4.1.9 程序开始运行了

接收到的数据和我们期望的是一样的,证明程序没有问题。至此,说明我们下载代码成功

了,并且从硬件上验证了我们代码的正确性。

3.4.2 STM32F7 在线调试

上一节,我们介绍了如何利用 ST LINK给 STM32下载代码,并在 ALIENTEK阿波罗STM32

开发板上验证了我们程序的正确性。这个代码比较简单,所以不需要硬件调试,我们直接就一

次成功了。可是,如果你的代码工程比较大,难免存在一些 bug,这时,就有必要通过在线调

试来解决问题了。

利用调试工具,比如 JLINK(必须是 JLINK V9 或者以上版本!!)、ULINK、STLINK 等,

可以实时跟踪程序,从而找到你程序中的 bug,使你的开发事半功倍。这里我们以 ST 公司自

家的仿真器:ST LINK V2 为例,说说如何在线调试 STM32F767。

通过上一节的学习,我们知道 ST LINK 支持 JTAG 和 SWD 两种通信方式,而且 SWD 方

式具有占用 IO 少的优点(2 个 IO 口),所以,我们一般选择 SWD 方式进行调试。MDK 里面,

对 ST LINK 的相关设置,同上一节完全一样,请参考上一节的相关介绍。

在 MDK 的 IDE 界面,编译一下工程。然后点击:

(开始/停止仿真按钮),开始仿真

(如果开发板的代码没被更新过,则会先更新代码(即下载代码),再仿真。特别注意:开发板

上的 B0 脚要接 GND,否则不会运行我们下载的代码!!),如图 3.4.2.1 所示:

0762b1bcd1b31593aaff93581984b35b.png

图 3.4.2.1 开始仿真

因为我们之前勾选了 Run to main()选项,所以,程序直接就运行到了 main 函数的入口处。

另外,此时 MDK 多出了一个工具条,这就是 Debug 工具条,这个工具条在我们仿真的时候是

非常有用的,下面简单介绍一下 Debug 工具条相关按钮的功能。Debug 工具条部分按钮的功能

如图 3.4.2.2 所示:

e39a1c0eb4ce2e6c6c5b29fe74d1c859.png

图 3.4.2.2 Debug 工具条

复位:其功能等同于硬件上按复位按钮。相当于实现了一次硬复位。按下该按钮之后,代

码会重新从头开始执行。

执行到断点处:该按钮用来快速执行到断点处,有时候你并不需要观看每步是怎么执行的,

而是想快速的执行到程序的某个地方看结果,这个按钮就可以实现这样的功能,前提是你在查

看的地方设置了断点停止运行:此按钮在程序一直执行的时候会变为有效,通过按该按钮,就可以使程序停止

下来,进入到单步调试状态。

执行进去:该按钮用来实现执行到某个函数里面去的功能,在没有函数的情况下,是等同

于执行过去按钮的。

执行过去:在碰到有函数的地方,通过该按钮就可以单步执行过这个函数,而不进入这个

函数单步执行。

执行出去:该按钮是在进入了函数单步调试的时候,有时候你可能不必再执行该函数的剩

余部分了,通过该按钮就直接一步执行完函数余下的部分,并跳出函数,回到函数被调用的位

置。

执行到光标处:该按钮可以迅速的使程序运行到光标处,其实是挺像执行到断点处按钮功

能,但是两者是有区别的,断点可以有多个,但是光标所在处只有一个。

汇编窗口:通过该按钮,就可以查看汇编代码,这对分析程序很有用。

堆栈局部变量窗口:通过该按钮,显示 Call Stack+Locals 窗口,显示当前函数的局部变量

及其值,方便查看。

观察窗口:MDK5 提供 2 个观察窗口(下拉选择),该按钮按下,会弹出一个显示变量的

窗口,输入你所想要观察的变量/表达式,即可查看其值,是很常用的一个调试窗口。

内存查看窗口:MDK5 提供 4 个内存查看窗口(下拉选择),该按钮按下,会弹出一个内

存查看窗口,可以在里面输入你要查看的内存地址,然后观察这一片内存的变化情况。是很常

用的一个调试窗口

串口打印窗口:MDK5 提供 4 个串口打印窗口(下拉选择),该按钮按下,会弹出一个类

似串口调试助手界面的窗口,用来显示从串口打印出来的内容。

逻辑分析窗口:该图标下面有 3 个选项(下拉选择),我们一般用第一个,也就是逻辑分析

窗口(Logic Analyzer),点击即可调出该窗口,通过 SETUP 按钮新建一些 IO 口,就可以观察这

些 IO 口的电平变化情况,以多种形式显示出来,比较直观。

系统查看窗口:该按钮可以提供各种外设寄存器的查看窗口(通过下拉选择),选择对应外

设,即可调出该外设的相关寄存器表,并显示这些寄存器的值,方便查看设置的是否正确。

Debug 工具条上的其他几个按钮用的比较少,我们这里就不介绍了。以上介绍的是比较常

用的,当然也不是每次都用得着这么多,具体看你程序调试的时候有没有必要观看这些东西,

来决定要不要看。

特别注意:串口打印窗口和逻辑分析窗口仅在软件仿真的时候可用,而 MDK5 对

STM32F767 的软件仿真,基本上不支持(故本教程直接没有对软件仿真进行介绍了),所以,

基本上这两个窗口用不着。但是对 STM32F1 的软件仿真,MDK5 是支持的,在 F1 开发的时候,

可以用到。

这样,我们在上面的仿真界面里面调出:堆栈局部变量窗口。如图 3.4.2.3 所示:

91a4b5ff860178ede7c98bea863278ef.png

图 3.4.2.3 堆栈局部变量查看窗口

我们把光标放到 main.c 的第 12 行左侧的灰色区域,然后按下鼠标左键,即可放置一个断

点(红色的实心点,也可以通过鼠标右键弹出菜单来加入),再次单击则取消。

然后点击

,执行到该断点处,如图 3.4.2.4 所示

14b856f4a88ce7e9adb57f45119c4c3c.png

图 3.4.2.4 执行到断点处

现在先不忙着往下执行,点击菜单栏的 PeripheralsSystem ViewerUSARTUSART1。

可以看到,有很多外设可以查看,这里我们查看的是串口 1 的情况。如图 3.4.2.5 所示:

163dc9b41f237a7bf11ff2856eef439a.png

图 3.4.2.5 查看串口 1 相关寄存器

单击 USART1 后会在 IDE 右侧出现一个如图 3.4.2.6(a)所示的界面:

8010e0d0159aac25523f84450f85bd8d.png

4.2.6 串口 1 各寄存器初始化前后对比

图 3.4.2.6(a)是 STM32 的串口 1 的默认设置状态,从中可以看到所有与串口相关的寄存

器全部在这上面表示出来了。我们接着单击一下

,执行完串口初始化函数,得到了如图 3.4.2.6

(b)所示的串口信息。大家可以对比一下这两个图的区别,就知道在 uart_init(115200);这个函

数里面大概执行了哪些操作。

通过图 3.4.2.6(b),我们可以查看串口 1 的各个寄存器设置状态,从而判断我们写的代码

是否有问题,只有这里的设置正确了之后,才有可能在硬件上正确的执行。同样这样的方法也

可以适用于很多其他外设,这个读者慢慢体会吧!这一方法不论是在排错还是在编写代码的时

候,都是非常有用的。

此时,我们先打开串口调试助手(XCOM V2.0,在光盘6,软件资料软件串口调试

助手里面)设置好串口号和波特率,然后我们继续单击

按钮,一步步执行,此时在堆栈局部

变量窗口可以看到 t 的值变化,同时在串口调试助手中,也可看到打印出 t 的值,如图 3.4.2.7

和 4.2.8 所示:

dbaf90ce313935dd1c479d5875ebf6d6.png

图 3.4.2.7 堆栈局部变量窗口查看 t 的值

24f533da9202c9ed9fd74804471247dd.png

图 3.4.2.8 串口调试助手收到的数据

关于 STM32F767 的硬件调试,我们就介绍到这里,这仅仅是一个简单的 demo 演示,在实际使

用中,硬件调试更是大有用处,所以大家一定要好好掌握。

3.5 MDK5 使用技巧

通过前面的学习,我们已经了解了如何在 MDK5 里面建立属于自己的工程。下面,我们将

向大家介绍 MDK5 软件的一些使用技巧,这些技巧在代码编辑和编写方面会非常有用,希望大

家好好掌握,最好实际操作一下,加深印象。

3.5.1 文本美化

文本美化,主要是设置一些关键字、注释、数字等的颜色和字体。前面我们在介绍 MDK5

新建工程的时候看到界面如图 3.2.23 所示,这是 MDK 默认的设置,可以看到其中的关键字和

注释等字体的颜色不是很漂亮,而 MDK 提供了我们自定义字体颜色的功能。我们可以在工具

条上点击

(配置对话框)弹出如图 3.5.1.1 所示界面:

8d2b5f1fe44d4a2e65e4fda1cb056de1.png

图 3.5.1.1 置对话框

在该对话框中,先设置 Encoding 为:Chinese GB2312(Simplified),然后设置 Tab size 为:4。

以更好的支持简体中文(否则,拷贝到其他地方的时候,中文可能是一堆的问号),同时 TAB

间隔设置为 4 个单位。然后,选择:Colors&Fonts 选项卡,在该选项卡内,我们就可以设置自

己的代码的子体和颜色了。由于我们使用的是 C 语言,故在 Window 下面选择:C/C++ Editor Files

在右边就可以看到相应的元素了。如图 3.5.1.2 示:

610b7fda69577d5ce818946b51057199.png

图 3.5.1.2 Colors&Fonts 选项卡

然后点击各个元素修改为你喜欢的颜色(注意双击,且有时候可能需要设置多次才生效,

MDK 的 bug),当然也可以在 Font 栏设置你字体的类型,以及字体的大小等。设置成之后,点

击 OK,就可以在主界面看到你所修改后的结果,例如我修改后的代码显示效果如图 3.5.1.3 示:

57cbd6fe7ebf136bccb6244c608ed79f.png

图 3.5.1.3 设置完后显示效果

这就比开始的效果好看一些了。字体大小,则可以直接按住:ctrl+鼠标滚轮,进行放大或

者缩小,或者也可以在刚刚的配置界面设置字体大小。

细心的读者可能会发现,上面的代码里面有一个 u8,还是黑色的,这是一个用户自定义的

关键字,为什么不显示蓝色(假定刚刚已经设置了用户自定义关键字颜色为蓝色)呢?这就又

要回到我们刚刚的配置对话框了,单这次我们要选择 User Keywords 选项卡,同样选择:C/C++

Editor Files,在右边的 User Keywords 对话框下面输入你自己定义的关键字,如图 3.5.1.4 示:

465e395928b248532bcc593553903ad2.png

图 3.5.1.4 用户自定义关键字

图 3.5.1.4 中我定义了 u8、u16、u32 等 3 个关键字,这样在以后的代码编辑里面只要出现

这三个关键字,肯定就会变成蓝色。点击 OK,再回到主界面,可以看到 u8 变成了蓝色了,如

图 3.5.1.5 示:

717c019a0b6b1cd4839b842c976301b8.png

图 3.5.1.5 设置完后显示效果

其实这个编辑配置对话框里面,还可以对其他很多功能进行设置,比如动态语法检测等,

我们将 3.5.2 节介绍。

3.5.2 语法检测&代码提示

MDK5 支持代码提示与动态语法检测功能,使得 MDK 的编辑器越来越好用了,这里我们

简单说一下如何设置,同样,点击

,打开配置对话框,选择 Text Completion 选项卡,如图

3.5.2.1 所示:

29360d8bda269f085d2165384f3ed9ad.png

图 3.5.2.1 Text Completion 选项卡设置

Strut/Class Members,用于开启结构体/类成员提示功能。

Function Parameters,用于开启函数参数提示功能。

Symbols after xx characters,用于开启代码提示功能,即在输入多少个字符以后,提示匹配

的内容(比如函数名字、结构体名字、变量名字等),这里默认设置 3 个字符以后,就开始提示。

如图 3.5.2.2 所示:

09c983a4202ffbdb00c5d8ae6b2f00bd.png

图 3.5.2.2 代码提示

Dynamic Syntax Checking,则用于开启动态语法检测,比如编写的代码存在语法错误的时

候,会在对应行前面出现 图标,如出现警告,则会出现

图标,将鼠标光标放图标上面,则

会提示产生的错误/警告的原因,如图 3.5.2.3 所示

3f1827c75af712a9a4069de108a07f5a.png

图 3.5.2.3 语法动态检测功能

这几个功能,对我们编写代码很有帮助,可以加快代码编写速度,并且及时发现各种问题。

不过这里要提醒大家,语法动态检测这个功能,有的时候会误报(比如 sys.c 里面,就有误报),

大家可以不用理会,只要能编译通过(0 错误,0 警告),这样的语法误报,一般直接忽略即可。

3.5.3 代码编辑技巧

这里给大家介绍几个我常用的技巧,这些小技巧能给我们的代码编辑带来很大的方便,相

信对你的代码编写一定会有所帮助。

1)TAB 键的妙用

首先要介绍的就是 TAB 键的使用,这个键在很多编译器里面都是用来空位的,每按一下移

空几个位,如果你经常编写程序,对这个键一定再熟悉不过了。MDK 的 TAB 键还可以支持块

操作:也就是可以让一片代码整体右移固定的几个位,也可以通过 SHIFT+TAB 键整体左移固

定的几个位。

假设我们前面的串口 1 中断响应函数如图 3.5.3.1 所示:

e5f2ed927274c8f8854ecba56da363d5.png

图 3.5.3.1 头大的代码

图 3.5.3.1 中这样的代码大家肯定不会喜欢,这还只是短短的 30 来行代码,如果你的代码

有几千行,全部是这个样子,不头大才怪。看到这样的代码我们就可以通过 TAB 键的妙用来快

速修改为比较规范的代码格式。

选中一块然后按 TAB 键,你可以看到整块代码都跟着右移了一定距离,如图 3.5.3.2 所示:

cbaa8e5ab80bfd7b087902f357194ce9.png

图 3.5.3.2 代码整体偏移

接下来我们就是要多选几次,然后多按几次 TAB 键就可以达到迅速使代码规范化的目的,

最终效果如图 3.5.3.3 所示

78191a6026a7be984c5e1cc3b3f811a8.png

图 3.5.3.3 修改后的代码

图 3.5.3.3 中的代码相对于图 3.5.3.1 中的要好看多了,经过这样的整理之后,整个代码一下就变得有条理多了,看起来很舒服。

2) 快速定位函数/变量被定义的地方

上一节,我们介绍了 TAB 键的功能,接下来我们介绍一下如何快速查看一个函数或者变量

所定义的地方。

大家在调试代码或编写代码的时候,一定有想看看某个函数是在那个地方定义的,具体里

面的内容是怎么样的,也可能想看看某个变量或数组是在哪个地方定义的等。尤其在调试代码

或者看别人代码的时候,如果编译器没有快速定位的功能的时候,你只能慢慢的自己找,代码

量比较少还好,如果代码量一大,那就郁闷了,有时候要花很久的时间来找这个函数到底在哪

里。型号 MDK 提供了这样的快速定位的功能。只要你把光标放到这个函数/变量(xxx)的上

面(xxx 为你想要查看的函数或变量的名字),然后右键,弹出如图 3.5.3.4 所示的菜单栏 :

2fd87773f1a3ffae11498d2614430150.png

图 3.5.3.4 快速定位

在图 3.5.3.4 中,我们找到 Go to Definition Of‘STM32_Clock_Init’ 这个地方,然后单击

左键就可以快速跳到 STM32_Clock_Init 函数的定义处(注意要先在 Options for Target 的 Output

选项卡里面勾选 Browse Information 选项,再编译,再定位,否则无法定位!)。如图 3.5.3.5 所

示:

dce04a8fb30bbbe4d6d50371787cb412.png

图 3.5.3.5 定位结果

对于变量,我们也可以按这样的操作快速来定位这个变量被定义的地方,大大缩短了你查

找代码的时间很多时候,我们利用 Go to Definition 看完函数/变量的定义后,又想返回之前的代码继续看,

此时我们可以通过 IDE 上的

按钮(Back to previous position)快速的返回之前的位置,这个

按钮非常好用!

3) 快速注释与快速消注释

接下来,我们介绍一下快速注释与快速消注释的方法。在调试代码的时候,你可能会想注

释某一片的代码,来看看执行的情况,MDK 提供了这样的快速注释/消注释块代码的功能。也

是通过右键实现的。这个操作比较简单,就是先选中你要注释的代码区,然后右键,选择

AdvancedComment Selection 就可以了。

以 Stm32_Clock_Init 函数为例,比如我要注释掉下图中所选中区域的代码,如图 3.5.3.6 所

示:

998d12d960c5386406317c13dad89cb6.png

图 3.5.3.6 选中要注释的区域

我们只要在选中了之后,选择右键,再选择 AdvancedComment Selection 就可以把这段代

码注释掉了。执行这个操作以后的结果如图 3.5.3.7 所示:

99a6d42dbd7c40d3bc71256e3df2bff2.png

图 3.5.3.7 注释完毕

这样就快速的注释掉了一片代码,而在某些时候,我们又希望这段注释的代码能快速的取消注释,MDK 也提供了这个功能。与注释类似,先选中被注释掉的地方,然后通过右键

Advanced,不过这里选择的是 Uncomment Selection。

3.5.4 其他小技巧

除了前面介绍的几个比较常用的技巧,这里还介绍几个其他的小技巧,希望能让你的代码

编写如虎添翼。

第一个是快速打开头文件。在将光标放到要打开的引用头文件上,然后右键选择 Open

Document“XXX”,就可以快速打开这个文件了(XXX 是你要打开的头文件名字)。如图 3.5.4.1

所示:

34a14acc4dbddc9f56fe4d68ae0d788a.png

图 3.5.4.1 快速打开头文件

第二个小技巧是查找替换功能。这个和 WORD 等很多文档操作的替换功能是差不多的,

在 MDK 里面查找替换的快捷键是“CTRL+H”,只要你按下该按钮就会调出如图 3.5.4.2 所示界

面:

4e60f8b8b5f499458b5846e3c2287a31.png

图 3.5.4.2 替换文本

这个替换的功能在有的时候是很有用的,它的用法与其他编辑工具或编译器的差不多,相

信各位都不陌生了,这里就不啰嗦了。

第三个小技巧是跨文件查找功能,先双击你要找的函数/变量名(这里我们还是以系统时钟初始化函数:Stm32_Clock_Init 为例),然后再点击 IDE 上面的,弹出如图 3.5.4.3 所示对话框:

c4f6cfeecf91c0273e84e31f80828c47.png

图 3.5.4.3 跨文件查找

点击 Find All,MDK 就会帮你找出所有含有 Stm32_Clock_Init 字段的文件并列出其所在位

置,如图 3.5.4.4 所示:

27a1121091e844fca9a2118a15f9e555.png

图 3.5.4.4 查找结果

该方法可以很方便的查找各种函数/变量,而且可以限定搜索范围(比如只查找.c 文件和.h

文件等),是非常实用的一个技巧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值