单片机 原子性操作_「正点原子NANO STM32开发板资料连载」第四章 STM32F4 基础入门...

1)实验平台:ALIENTEK NANO STM32F411 V1开发板

2)摘自《正点原子STM32F4 开发指南(HAL 库版》关注官方微信号公众号,获取更多资料:正点原子

4ad334a2b5b38396ca92878b866f3a71.png

第四章 STM32F4 开发基础知识入门

这一章,我们将着重 STM32 开发的一些基础知识,让大家对 STM32 开发有一个初步的了

解,为后面 STM32 的学习做一个铺垫,方便后面的学习。这一章的内容大家第一次看的时候

可以只了解一个大概,后面需要用到这方面的知识的时候再回过头来仔细看看。这章我们分 7

个小结,

·4.1 MDK 下 C 语言基础复习

·4.2 STM32F4 系统架构

·4.3 STM32F411 时钟系统

·4.4 端口复用和重映射

·4.5 STM32F4 NVIC 中断管理

·4.6 MDK 中寄存器地址名称映射分析

·4.7 MDK 固件库快速开发技巧

·4.8 手把手教你入门 STM32CubeMX 图像配置工具

4.1 MDK 下 C 语言基础复习

这一节我们主要讲解一下 C 语言基础知识。C 语言知识博大精深,也不是我们三言两语能

讲解清楚,同时我们相信学 STM32 这种级别 MCU 的用户,C 语言基础应该都是没问题的。我们

这里主要是简单的复习一下几个 C 语言基础知识点,引导那些 C 语言基础知识不是很扎实的用

户能够快速开发 STM32 程序。同时希望这些用户能够多去复习一下 C 语言基础知识,C 语言毕

竟是单片机开发中的必备基础知识。对于 C 语言基础比较扎实的用户,这部分知识可以忽略不

看。

4.1.1 位操作

C 语言位操作相信学过 C 语言的人都不陌生了,简而言之,就是对基本类型变量可以在位级

别进行操作。这节的内容很多朋友都应该很熟练了,我这里也就点到为止,不深入探讨。下面

我们先讲解几种位操作符,然后讲解位操作使用技巧。

C 语言支持如下 6 中位操作

203afd9e25d7f2a8b80c7816da870e9b.png

这些与或非,取反,异或,右移,左移这些到底怎么回事,这里我们就不多做详细,相信

大家学 C 语言的时候都学习过了。如果不懂的话,可以百度一下,非常多的知识讲解这些操作

符。下面我们想着重讲解位操作在单片机开发中的一些实用技巧。

1) 不改变其他位的值的状况下,对某几个位进行设值。

这个场景单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作,

然后用|操作符设值。比如我要改变 GPIOA 的状态,可以先对寄存器的值进行&清零操作

GPIOA->CRL&=0XFFFFFF0F; //将第 4-7 位清 0

然后再与需要设置的值进行|或运算

GPIOA->CRL|=0X00000040;

//设置相应位的值,不改变其他位的值

2) 移位操作提高代码的可读性。

移位操作在单片机开发中也非常重要,下面让我们看看固件库的 GPIO 初始化的函数里

面的一行代码

GPIOx->BSRR = (((uint32_t)0x01) << pinpos);

这个操作就是将 BSRR 寄存器的第 pinpos 位设置为 1,为什么要通过左移而不是直接设

置一个固定的值呢?其实,这是为了提高代码的可读性以及可重用性。这行代码可以

很直观明了的知道,是将第 pinpos 位设置为 1。如果你写成

GPIOx->BSRR =0x0030;

这样的代码就不好看也不好重用了。

类似这样的代码很多:

GPIOA->ODR|=1<<5;

//PA.5 输出高,不改变其他位

这样我们一目了然,5 告诉我们是第 5 位也就是第 6 个端口,1 告诉我们是设置为 1 了。

3) ~取反操作使用技巧

SR 寄存器的每一位都代表一个状态,某个时刻我们希望去设置某一位的值为 0,同时

其他位都保留为 1,简单的作法是直接给寄存器设置一个值:

TIMx->SR=0xFFF7;

这样的作法设置第 3 位为 0,但是这样的作法同样不好看,并且可读性很差。看看库函数

代码中怎样使用的:

TIMx->SR = (uint16_t)~TIM_FLAG;

而 TIM_FLAG 是通过宏定义定义的值:

#define TIM_FLAG_Update

((uint16_t)0x0001)

#define TIM_FLAG_CC1

((uint16_t)0x0002)

看这个应该很容易明白,可以直接从宏定义中看出 TIM_FLAG_Update 就是设置的第 0 位了,

可读性非常强。

4.1.2 define 宏定义

define 是 C 语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供

方便。常见的格式:

#define 标识符 字符串

“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。例如:

#define SYSCLK_FREQ_72MHz 72000000

定义标识符 SYSCLK_FREQ_72MHz 的值为 72000000。

至于 define 宏定义的其他一些知识,比如宏定义带参数这里我们就不多讲解。

4.1.3 ifdef 条件编译

单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而

当条件不满足时则编译另一组语句。条件编译命令最常见的形式为:

#ifdef 标识符

程序段 1

#else

程序段 2

#endif

它的作用是:当标识符已经被定义过(一般是用#define 命令定义),则对程序段 1 进行编

译,否则编译程序段 2。 其中#else 部分也可以没有,即:

#ifdef

程序段 1

#endif

这个条件编译在 MDK 里面是用得很多的,在 stm32f10x.h 这个头文件中经常会看到这样的

语句:

#ifdef STM32F10X_HD

大容量芯片需要的一些变量定义

#end

而 STM32F10X_HD 则是我们通过#define 来定义的。条件编译也是 c 语言的基础知识,这里

也就点到为止吧。

4.1.4 extern 变量申明

C 语言中 extern 可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编

译器遇到此变量和函数时在其他模块中寻找其定义。这里面要注意,对于 extern 申明变量可以多

次,但定义只有一次。在我们的代码中你会看到看到这样的语句:

extern u16 USART_RX_STA;

这个语句是申明 USART_RX_STA 变量在其他文件中已经定义了,在这里要使用到。所以,你

肯定可以找到在某个地方有变量定义的语句:

u16 USART_RX_STA;

下面通过一个例子说明一下使用方法。

在 Main.c 定义的全局变量 id,id 的初始化都是在 Main.c 里面进行的。

Main.c 文件

u8 id;//定义只允许一次

main()

{

id=1;

printf("d%",id);//id=1

test();

printf("d%",id);//id=2

}

但是我们希望在 test.c 的 changeId(void)函数中使用变量 id,这个时候我们就需要在

test.c 里面去申明变量 id是外部定义的了,因为如果不申明,变量 id的作用域是到不了 test.c

文件中。看下面 test.c 中的代码:

extern u8 id;//申明变量 id 是在外部定义的,申明可以在很多个文件中进行

void test(void){

id=2;

}

在 test.c 中申明变量 id 在外部定义,然后在 test.c 中就可以使用变量 id 了。

对于 extern 申明函数在外部定义的应用,这里我们就不多讲解了。

4.1.5 typedef 类型别名

typedef 用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。

typedef 在 MDK 用得最多的就是定义结构体的类型别名和枚举类型了。

struct _GPIO

{

__IO uint32_t CRL;

__IO uint32_t CRH;

};

定义了一个结构体 GPIO,这样我们定义变量的方式为:

struct _GPIO GPIOA;//定义结构体变量 GPIOA

但是这样很繁琐,MDK 中有很多这样的结构体变量需要定义。这里我们可以为结体定义一

个别名 GPIO_TypeDef,这样我们就可以在其他地方通过别名 GPIO_TypeDef 来定义结构体变量

了。方法如下:

typedef struct

{

__IO uint32_t CRL;

__IO uint32_t CRH;

} GPIO_TypeDef;

Typedef 为结构体定义一个别名 GPIO_TypeDef,这样我们可以通过 GPIO_TypeDef 来定义结

构体变量:

GPIO_TypeDef _GPIOA,_GPIOB;

这里的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了。 这样是不是方便很多?

4.1.6 结构体

经常很多用户提到,他们对结构体使用不是很熟悉,但是 MDK 中太多地方使用结构体以及

结构体指针,这让他们一下子摸不着头脑,学习 STM32 的积极性大大降低,其实结构体并不是

那么复杂,这里我们稍微提一下结构体的一些知识,还有一些知识我们会在下一节的“寄存器

地址名称映射分析”中讲到一些。

声明结构体类型:

Struct 结构体名{

成员列表;

}变量名列表;

例如:

Struct G_TYPE {

uint32_t Pin;

uint32_t Mode;

uint32_t Speed;

}GPIOA,GPIOB;

在结构体申明的时候可以定义变量,也可以申明之后定义,方法是:

Struct 结构体名字

结构体变量列表 ;

例如:struct G_TYPE GPIOA,GPIOB;

结构体成员变量的引用方法是:

结构体变量名字.成员名

比如要引用 GPIOA 的成员 Mode,方法是:GPIOA. Mode;

结构体指针变量定义也是一样的,跟其他变量没有啥区别。

例如:struct G_TYPE *GPIOC;//定义结构体指针变量 GPIOC;

结构体指针成员变量引用方法是通过“->”符号实现,比如要访问 GPIOC 结构体指针指向的结

构体的成员变量 Speed,方法是:

GPIOC-> Speed;

上面讲解了结构体和结构体指针的一些知识,其他的什么初始化这里就不多讲解了。讲到这里,

有人会问,结构体到底有什么作用呢?为什么要使用结构体呢?下面我们将简单的通过一个实

例回答一下这个问题。

在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如 IO 口。它的初始化状态

是由几个属性来决定的,比如模式,速度等。对于这种情况,在我们没有学习结构体的时候,

我们一般的方法是:

void HAL_GPIO_Init (uint32_t Pin, uint32_t Mode, uint32_t Speed);

这种方式是有效的同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函数里

面再传入一个参数,那么势必我们需要修改这个函数的定义,重新加入上下拉 Pull 这个入口参

数。于是我们的定义被修改为:

void HAL_GPIO_Init (uint32_t Pin, uint32_t Mode, uint32_t Speed,uint32_t Pull);

但是如果我们这个函数的入口参数是随着开发不断的增多,那么是不是我们就要不断的修改函

数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢?

这样如果我们使用到结构体就能解决这个问题了。我们可以在不改变入口参数的情况下,

只需要改变结构体的成员变量,就可以达到上面改变入口参数的目的。

结构体就是将多个变量组合为一个有机的整体。上面的函数中 Pin, Mode,

Speed 和 Pull 这些参数,他们对于 GPIO 而言,是一个有机整体,都是来设置 IO 口参数的,所

以我们可以将他们通过定义一个结构体来组合在一个。MDK 中是这样定义的:

typedef struct

{

uint32_t Pin;

uint32_t Mode;

uint32_t Pull;

uint32_t Speed;

uint32_t Alternate;

}GPIO_InitTypeDef;

于是,我们在初始化 GPIO 口的时候入口参数就可以是 GPIO_InitTypeDef 类型的变量或者指针

变量了,MDK 中是这样做的:

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);

这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需

要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义

就可以达到增加变量的目的。

理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多,

如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可

以提高你的代码的可读性。

使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作

用就远远不止这个了,同时,MDK 中用结构体来定义外设也不仅仅只是这个作用,这里我们只

是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲

解结构体的一些其他知识。

4.2 STM32F4 系统架构

STM32F4 的系统架构比 51 单片机就要强大很多了。STM32F4 系统架构的知识可以在

《STM32F411xC/E 参考手册》第二章有讲解,这里我们也把这一部分知识抽取出来讲解,是为

了大家在学习 STM32F4 之前对系统架构有一个初步的了解。这里的内容基本也是从中文参考

手册中参考过来的,让大家能通过我们手册也了解到,免除了到处找资料的麻烦吧。如果需要

详细深入的了解 STM32F4 的系统架构,还需要在网上搜索其他资料学习学习。

我们这里所讲的 STM32F4 系统架构主要针对的 STM32F411xC/E 系列芯片。首先我们看看

STM32 的系统架构图:

cbe605da33579e099297e9218f346642.png

图 4.2.1 STM32F411 系统架构图

主系统由 32 位多层 AHB 总线矩阵构成。总线矩阵用于主控总线之间的访问仲裁管理。仲

裁采集循环调度算法。总线矩阵可实现以下部分互联:

六条主控总线是:

Cortex-M4 内核 I 总线, D 总线和 S 总线;

DMA1 存储器总线, DMA2 存储器总线;

DMA2 外设总线;

四条被控总线:

内部 FLASH ICode 总线;

内部 FLASH DCode 总线;

1

AHB1 外设 和 AHB2 外设;

下面我们具体讲解一下图中几个总线的知识:

1 I 总线(S0):此总线用于将 Cortex-M4 内核的指令总线连接到总线矩阵。内核通过此总

线获取指令。此总线访问的对象是包括代码的存储器。

2 D 总线(S1):此总线用于将 Cortex-M4 数据总线和 64KB CCM 数据 RAM 连接到总线矩

阵。内核通过此总线进行立即数加载和调试访问。

3 S 总线(S2):此总线用于将 Cortex-M4 内核的系统总线连接到总线矩阵。此总线用于访

问位于外设或 SRAM 中的数据。

4 DMA 存储器总线(S3,S4):此总线用于将 DMA 存储器总线主接口连接到总线矩阵。

DMA 通过此总线来执行存储器数据的传入和传出。

5 DMA 外设总线:此总线用于将 DMA 外设主总线接口连接到总线矩阵。DMA 通过此

总线访问 AHB 外设或执行存储器之间的数据传输。

对于系统架构的知识,在刚开始学习 STM32 的时候只需要一个大概的了解,大致知道是个

什么情况即可。对于寻址之类的知识,这里就不做深入的讲解,参考手册都有很详细的讲解。

4.3 STM32F4 时钟系统

STM32F4 时钟系统的知识在《STM32F411xC/E 参考手册》第六章复位和时钟控制章节有

非常详细的讲解,网上关于时钟系统的讲解也基本都是参考的这里,这些知识也不是什么原创,

纯粹的是看网友发的帖子和手册来总结的,如有不合理之处望大家谅解。

这里请大家注意,STM32F411 的时钟系统和 STM32F407 的时钟系统有细微的区别,我们这里

是针对 STM32F411 的时钟系统进行讲解。

这部分内容我们分 3 个小节来讲解:

 4.3.1 STM32F411 时钟树概述

 4.3.2 STM32F411 时钟初始化配置

 4.3.3 STM32F411 时钟使能和配置

4.3.1 STM32F411 时钟树概述

众所周知,时钟系统是 CPU 的脉搏,就像人的心跳一样。所以时钟系统的重要性就不言而

喻了。 STM32 的时钟系统比较复杂,不像简单的 51 单片机一个系统时钟就可以解决一切。于

是有人要问,采用一个系统时钟不是很简单吗?为什么 STM32 要有多个时钟源呢? 因为首先

STM32 本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率,比

如看门狗以及 RTC 只需要几十 k 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁干

扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。

首先让我们来看看 STM32F411 的时钟系统图吧:(STM32F411RCT6 手册没有中文版)

c692df3c6e0085b9fab2677b6773850c.png

图 4.3.1 STM32F411 时钟系统图

上图从左往右看,就是整个 STM32F411 的时钟走向。这里,我们挑选出 8 个重要的地方

进行介绍(图 5.2.2.1 中标出的 1~8)。

1 这是进人 PLL 之前的一个时钟分频系数(M),取值范围是:2~63,一般取 8。注

意,这个分频系数,对主 PLL 和 PLLI2S 都有效。

2 这是 STM32F411 的主 PLL,该部分控制 STM32F411 的主频率(PLLCLK)和

USB/SDIO 外设的频率(PLL48CK)。其中,N 是主 PLL voo 的倍频系数,其取值范围是

50~432:P 是系统时钟的主 PLL 分频系数,其取值范围是:2、4、6 和 8(仅限这四个值):

Q 是 USB/SDIO 的主 PLL 分频系数,其取值范围是:2~15:R 没用到。

3 这是 STM32F4 I2S 部分的 PLL,该部分主要用于设置 STM32F411 I2S 内部输入时

钟频率。其中 N 是用于 PLLI2S vco 的倍频系数,其取值范围是:50~432;R 是 I2S 时钟的

分频系数,其取值范围是:2~7;P 和 Q 没用到。

4 这是 PLL 之后的系统主时钟(PLLCLK),STM32F411 的主频最高是 100Mhz,所

以我们一般设置 PLLCLK 为 96Mhz,因为 100Mhz 分频不到 48Mhz(USB 时钟 48Mhz),

所以 M=4,N=96,P=2,通过 SW 选择选择 SYSCLK=PLLCLK 即可得到 96Mhz 的系统运

行频率。

5 这是 PLL 之后的 USB/SDIO 时钟频率,由于 USB 必须是 48Mhz 才可以正常运行,

所以这个频率一般设置为 48Mhz(M=4,N=96,Q=4)。

6 是 I2S 的时钟,通过 I2SSRC 选择内 PLLI2SCLK 还是外部 I2SCKIN 作为时钟。

7 这是 Cortex 系统定时器,也就是 SYSTICK 的时钟。上图清楚的表明 SYSTICK 的

来源是 AHB 分频后再 8 分频(这个 8 分频是可以设置的,即 8 分频,或者不分频,一般

使用 8 分频),我们一般设置 AHB 不分频,则 SYSTICK 的频率为:96Mhz/8=12Mhz。前

面介绍的延时函数,就是基于 SYSTICK 来实现的。

8 这里是 STM32F411 很多外设的时钟来源,即两个总线桥:APB1 和 APB2,其中

APB1 是低速总线(最高 50Mhz),APB2 是高速总线(最高 100Mhz)。另外定时器部分,

如果所在的总线(APB1/APB2)的分频系数为 1,那么就不倍频,如果不为 1(比如 2/4/8/16),

那么就会 2 倍频(Fabpx*2)后,作为定时器时钟输入。

关于时钟的详细介绍,在《STM32F411xC/E 参考手册》第 6.2 节(92~101 页)有详细介

绍。有不明白的地方,可以对照手册仔细研究。

从上图可以看出 STM32F411 的时钟设计的比较复杂,各个时钟基本都是可控的,任何外

设都有对应的时钟控制开关,这样的设计,对降低功耗是非常有用的,不用的外设不开启时钟,

就可以大大降低其功耗。

4.3.2 STM32F411 时钟系统配置

上一小节我们对 STM32F411 时钟树进行了详细讲解,接下来我们来讲解通过 STM32F4 的

HAL 库进行 STM32F411 时钟系统配置步骤。实际上,STM32F4 的时钟系统配置也可以通过图

形化配置工具 STM32CubeMX 来配置生成,这里我们讲解初始化代码,是为了让大家对 STM32

时钟系统有更加清晰的理解。我们将在 4.8 小节讲解图形化配置工具 STM32CubeMX,大家可

以对比参考学习。

前面我们讲解过,在系统启动之后,程序会先执行 HAL 库定义的 SystemInit 函数,进行系

统一些初始化配置。那么我们先来看看 SystemInit 程序:

void SystemInit(void)

{

/* FPU 设置------------------------------------------------------------*/

#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)

SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */

#endif

/* 复位 RCC 时钟配置为默认配置-----------*/

RCC->CR |= (uint32_t)0x00000001;//打开 HSION 位

RCC->CFGR = 0x00000000;//复位 CFGR 寄存器

RCC->CR &= (uint32_t)0xFEF6FFFF;//复位 HSEON, CSSON and PLLON 位

RCC->PLLCFGR = 0x24003010; //复位寄存器 PLLCFGR

RCC->CR &= (uint32_t)0xFFFBFFFF;//复位 HSEBYP 位

RCC->CIR = 0x00000000;//关闭所有中断

#if defined (DATA_IN_ExtSRAM) || defined (DATA_IN_ExtSDRAM)

SystemInit_ExtMemCtl();

#endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */

109

/* 配置中断向量表地址=基地址+偏移地址 ------------------*/

#ifdef VECT_TAB_SRAM

SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;

#else

SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;

#endif

}

从上面代码可以看出,SystemInit 主要做了如下四个方面工作:

1)FPU 设置

2)复位 RCC 时钟配置为默认复位值(默认开始了 HSI)

3)外部存储器配置

4)中断向量表地址配置

HAL 库的 SystemInit 函数并没有像标准库的 SystemInit 函数一样进行时钟的初始化配置。HAL

库的 SystemInit 函数除了打开 HSI 之外,没有任何时钟相关配置,所以使用 HAL 库我们必须编

写自己的时钟配置函数。首先我们打开工程模板看看我们在工程 SYSTEM 分组下面定义的 sys.c

文件中的时钟初始化函数 Stm32_Clock_Init 的内容:

//时钟设置函数

//Fvco=Fs*(plln/pllm);

//Fsys=Fvco/pllp=Fs*(plln/(pllm*pllp));

//Fusb=Fvco/pllq=Fs*(plln/(pllm*pllq));

//Fvco:VCO 频率

//Fsys:系统时钟频率

//Fusb:USB,SDIO 的时钟频率

//Fs:PLL 输入时钟频率,可以是 HSI,HSE 等.

//plln:主 PLL 倍频系数(PLL 倍频),取值范围:50~432.

//pllm:主 PLL 和音频 PLL 分频系数(PLL 之前的分频),取值范围:2~63.

//pllp:系统时钟的主 PLL 分频系数(PLL 之后的分频),取值范围:2,4,6,8.(仅限这 4 个值!)

//pllq:USB/SDIO 的主 PLL 分频系数(PLL 之后的分频),取值范围:2~15.

//外部晶振为 8M 的时候,推荐值:plln=96,pllm=4,pllp=2,pllq=4.

//得到:Fvco=8*(96/4)=192Mhz

//

Fsys=192/2=96Mhz

//

Fusb=192/4=48Mhz

//返回值:0,成功;1,失败。

void Stm32_Clock_Init(u32 plln,u32 pllm,u32 pllp,u32 pllq)

{

HAL_StatusTypeDef ret = HAL_OK;

RCC_OscInitTypeDef RCC_OscInitStructure;

RCC_ClkInitTypeDef RCC_ClkInitStructure;

__HAL_RCC_PWR_CLK_ENABLE(); //使能 PWR 时钟

//下面这个设置用来设置调压器输出电压级别,以便在器件未以最大频率工作

//时使性能与功耗实现平衡,此功能只有 STM32F42xx 和 STM32F43xx 器件有,

__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_

VOLTAGE_SCALE1);//设置调压器输出电压级别 1

RCC_OscInitStructure.OscillatorType = RCC_OSCILLATORTYPE_HSE; //时钟源为 HSE

RCC_OscInitStructure.HSEState = RCC_HSE_ON;//打开 HSE

RCC_OscInitStructure.PLL.PLLState = RCC_PLL_ON;//打开 PLL

RCC_OscInitStructure.PLL.PLLSource = RCC_PLLSOURCE_HSE;

//PLL 时钟源选择 HSE

RCC_OscInitStructure.PLL.PLLM = pllm;

//主 PLL 和音频 PLL 分频系数(PLL 之前的分频),取值范围:2~63.

RCC_OscInitStructure.PLL.PLLN = plln;

//主 PLL 倍频系数(PLL 倍频),取值范围:50~432.

RCC_OscInitStructure.PLL.PLLP = pllp;

//系统时钟的主 PLL 分频系数(PLL 之后的分频),取值范围:2,4,6,8.(仅限这 4 个值!)

RCC_OscInitStructure.PLL.PLLQ = pllq;

//USB/SDIO 的主 PLL 分频系数(PLL 之后的分频),取值范围:2~15.

ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化

if(ret!=HAL_OK) while(1);

RCC_ClkInitStructure.ClockType = RCC_CLOCKTYPE_HCLK|

RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;

RCC_ClkInitStructure.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;

//设置系统时钟时钟源为 PLL

RCC_ClkInitStructure.AHBCLKDivider = RCC_SYSCLK_DIV1;//AHB 分频系数为 1

RCC_ClkInitStructure.APB1CLKDivider = RCC_HCLK_DIV2;//APB1 分频系数为 2

RCC_ClkInitStructure.APB2CLKDivider = RCC_HCLK_DIV1;//APB2 分频系数为 2

ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure, FLASH_LATENCY_3);

//同时设置 FLASH 延时周期为 3WS,也就是 4 个 CPU 周期。

if(ret!=HAL_OK) while(1);

//初始化 HAL Systick 时钟

HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);

HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);

HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);

}

从函数注释可知,函数 Stm32_Clock_Init 的作用是进行时钟系统配置,除了配置 PLL 相关

参数确定 SYSCLK 值之外,还配置了 AHB,APB1 和 APB2 的分频系数,也就是确定了 HCLK,

PCLK1 和 PCLK2 的时钟值。我们首先来看看使用 HAL 库配置 STM32F429 时钟系统的一般步骤:

1) 使能 PWR 时钟:调用函数__HAL_RCC_PWR_CLK_ENABLE()。

2) 设置调压器输出电压级别:调用函数__HAL_PWR_VOLTAGESCALING_CONFIG()。

3) 选择是否开启 Over-Driver 功能:调用函数 HAL_PWREx_EnableOverDrive()。

4) 配置时钟源相关参数:调用函数 HAL_RCC_OscConfig()。

5) 配置系统时钟源以及 AHB,APB1 和 APB2 的分频系数:调用函数 HAL_RCC_ClockConfig()。

步骤 2 和 3,具有一定的关联性,我们放在后面讲解。对于步骤 1 之所以要使能 PWR 时钟,

是因为后面的步骤设置调节器输出电压级别以及开启 Over-Driver 功能都是电源控制相关配置,

所以必须开启 PWR 时钟。接下来我们先着重讲解步骤 4 和步骤 5 的内容,这也是时钟系统配

置的关键步骤。

对于步骤 4,使用 HAL 来配置时钟源相关参数,我们调用的函数为 HAL_RCC_OscConfig(),

该函数在 HAL 库关键头文件 stm32f4xx_hal_rcc.h 中声明,在文件 stm32f4xx_hal_rcc.c 中定义。

首先我们来看看该函数声明:

__weak HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct);

该函数只有一个入口参数,就是结构体 RCC_OscInitTypeDef 类型指针。接下来我们看看结构体

RCC_OscInitTypeDef 的定义:

typedef struct

{

uint32_t OscillatorType; //需要选择配置的振荡器类型

uint32_t HSEState;

//HSE 状态

uint32_t LSEState;

//LSE 状态

uint32_t HSIState;

//HIS 状态

uint32_t HSICalibrationValue; //HIS 校准值

uint32_t LSIState;

//LSI 状态

RCC_PLLInitTypeDef PLL;

//PLL 配置

}RCC_OscInitTypeDef;

对于这个结构体,前面几个参数主要是用来选择配置的振荡器类型。比如我们要开启 HSE,

那么我们会设置 OscillatorType 的值为 RCC_OSCILLATORTYPE_HSE,然后设置 HSEState 的值

为 RCC_HSE_ON 开启 HSE。对于其他时钟源 HSI,LSI 和 LSE,配置方法类似。这个结构体还

有一个很重要的成员变量是 PLL,它是结构体 RCC_PLLInitTypeDef 类型。它的作用是配置 PLL

相关参数,我们来看看它的定义:

typedef struct

{

uint32_t PLLState;

//PLL 状态

uint32_t PLLSource; //PLL 时钟源

uint32_t PLLM;

//PLL 分频系数 M

uint32_t PLLN;

//PLL 倍频系数 N

uint32_t PLLP;

//PLL 分频系数 P

uint32_t PLLQ;

//PLL 分频系数 Q

}RCC_PLLInitTypeDef;

从 RCC_PLLInitTypeDef;结构体的定义很容易看出该结构体主要用来设置 PLL 时钟源以及

相关分频倍频参数。

这个结构体的定义我们就不做过多讲解,接下来我们看看我们的时钟初始化函数Stm32_Clock_Init 中的配置内容:

RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为 HSE

RCC_OscInitStructure.HSEState=RCC_HSE_ON;

//打开 HSE

RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON;

//打开 PLL

RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE;//PLL 时钟源为 HSE

RCC_OscInitStructure.PLL.PLLM=pllm;

RCC_OscInitStructure.PLL.PLLN=plln;

RCC_OscInitStructure.PLL.PLLP=pllp;

RCC_OscInitStructure.PLL.PLLQ=pllq;

ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);

通过该段函数,我们开启了 HSE 时钟源,同时选择 PLL 时钟源为 HSE,然后把

Stm32_Clock_Init 的 4 个入口参数直接设置作为 PLL 的参数 M,N,P 和 Q 的值,这样就达到了设

置 PLL 时钟源相关参数的目的。设置好 PLL 时钟源参数之后,也就是确定了 PLL 的时钟频率,

接下来我们就需要设置系统时钟,以及 AHB,APB1 和 APB2 相关参数,也就是我们前面提到

的步骤 5。

接下来我们来看看步骤 5 中提到的 HAL_RCC_ClockConfig()函数,声明如下:

HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct,

uint32_t FLatency);

该函数有两个入口参数,第一个入口参数 RCC_ClkInitStruct 是结构体 RCC_ClkInitTypeDef

指针类型,用来设置 SYSCLK 时钟源以及 AHB,APB1 和 APB2 的分频系数。第二个入口参数

FLatency 用来设置 FLASH 延迟,这个参数我们放在后面跟步骤 2 和步骤 3 一起讲解。

RCC_ClkInitTypeDef 结构体类型定义非常简单,这里我们就不列出来,我们来看看

Stm32_Clock_Init 函数中的配置内容:

//选中 PLL 作为系统时钟源并且配置 HCLK,PCLK1 和 PCLK2

RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|

RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1

|RCC_CLOCKTYPE_PCLK2);

RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;//系统时钟源 PLL

RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1;//AHB 分频系数为 1

RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV2; //APB1 分频系数为 2

RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV1; //APB2 分频系数为 1

ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_3);

第一个参数 ClockType 配置说明我们要配置的是 SYSCLK,HCLK,PCLK1 和 PCLK2 四个时钟。

第二个参数 SYSCLKSource 配置选择系统时钟源为 PLL。

第三个参数 AHBCLKDivider 配置 AHB 分频系数为 1。

第四个参数 APB1CLKDivider 配置 APB1 分频系数为 2。

第五个参数 APB2CLKDivider 配置 APB2 分频系数为 1。

根据我们在主函数中调用 Stm32_Clock_Init(96,4,2,4)时候设置的入口参数值,我们可以计算出,

PLL 时钟为 PLLCLK=HSE*N/M*P=8MHz*96/(4*2)=96MHz,同时我们选择系统时钟源为 PLL,

所以系统时钟 SYSCLK=96MHz。AHB 分频系数为 1,故其频率为 HCLK=SYSCLK/1=96MHz。

APB1 分频系数为 2,故其频率为 PCLK1=HCLK/2=48MHz。APB2 分频系数为 1,故其频率为

PCLK2=HCLK/1=96/1=96MHz。最后我们总结一下通过调用函数 Stm32_Clock_Init(96,4,2,4)之

后的关键时钟频率值:SYSCLK(系统时钟)

=96MHz

PLL 主时钟

=96MHz

AHB 总线时钟(HCLK=SYSCLK/1)

=96MHz

APB1 总线时钟(PCLK1=HCLK/2)

=48MHz

APB2 总线时钟(PCLK2=HCLK/1)

=96MHz

最后我们来看看步骤 2,步骤 3 以及步骤 5 中函数 HAL_RCC_ClockConfig 第二个入口参数

FLatency 的含义。这里我们不想讲解得太复杂,大家只需要知道调压器输出电压级别 VOS,

Over-Driver 功能开启以及 FLASH 的延迟 Latency 三个参数,在我们芯片电源电压和 HCLK 固

定之后,他们三个参数也是固定的。首先我们来看看调压器输出电压级别 VOS,它是由 PWR

控制寄存器 CR 的位 15:14 来确定的:

63046823cfe47782322b5a84b124b315.png

如果我们要配置HCLK时钟为96Mhz,在AHB的分频系数为1的情况下需要时钟为96Mhz,

那么我们必须配置调试器输出电压级别 VOS 为级别 1,源码如下:

__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

配置好调节器电压级别 VOS 之后,如果需要 HCLK 达到 96Mhz,还需要配置 FLASH 延迟

Latency。对于 STM32F411 系列,FLASH 延迟配置参数值是通过下表来确定的:

11d091a2567784c33be43dfd3ec920a3.png

表 4.3.2.1 STM32F411xC/E 系列等待周期表

从上表可以看出,在电压为 3.3V 的情况下,如果需要 HCLK 为 180MHz,那么等待周期必

须为 5WS,也就是 6 个 CPU 周期。下面我们看看我们在 Stm32_Clock_Init 中调用函数

HAL_RCC_ClockConfig 的时候,第二个入口参数设置值:

ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_3);

从上可以看出,我们设置值为 FLASH_LATENCY_3,也就是 3WS,4 个 CPU 周期,与我们预

期一致。时钟系统配置相关知识就给大家讲解到这里。

4.3.3 STM32F4 时钟使能和配置

上一节我们讲解了时钟系统配置步骤。在配置好时钟系统之后,如果我们要使用某些外设,

例如 GPIO,ADC 等,我们还要使能这些外设时钟。这里大家必须注意,如果在使用外设之前

没有使能外设时钟,这个外设是不可能正常运行的。STM32 的外设时钟使能是在 RCC 相关寄

存器中配置的。因为 RCC 相关寄存器非常多,有兴趣的同学可以直接打开《STM32F411xC/E

参考手册》6.3 小节查看所有 RCC 相关寄存器的配置。接下来我们来讲解通过 STM32F4 的 HAL

库使能外设时钟的方法。

在 STM32F4 的 HAL 库中,外设时钟使能操作都是在 RCC 相关固件库文件头文件

stm32f4xx_hal_rcc.h 定义的。大家打开 stm32f4xx_hal_rcc.h 头文件可以看到文件中除了少数几

个函数声明之外大部分都是宏定义标识符。外设时钟使能在 HAL 库中都是通过宏定义标识符

来实现的。首先,我们来看看 GPIOA 的外设时钟使能宏定义标识符:

#define __HAL_RCC_GPIOA_CLK_ENABLE()

do {

__IO uint32_t tmpreg = 0x00;

SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);

tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);

UNUSED(tmpreg);

} while(0U)

这 几 行 代 码 非 常 简 单 , 主 要 是 定 义 了 一 个 宏 定 义 标 识 符

__HAL_RCC_GPIOA_CLK_ENABLE(),它的核心操作是通过下面运行代码实现的。

SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);

这行代码的作用是,设置寄存器 RCC->AHB1ENR 的相关位为 1,至于是哪个位,是由宏

定义 RCC_AHB1ENR_GPIOAEN 的值决定的,而它的值为;

#define RCC_AHB1ENR_GPIOAEN

((uint32_t)0x00000001)

所以,我们很容易理解上面代码的作用是设置寄存器 RCC->AHB1ENR 寄存器的最低位为 1。

我们可以从 STM32F4 的中文参考手册中搜索 AHB1ENR 寄存器定义,最低位的作用是用来使

用 GPIOA 时钟。AHB1ENR 寄存器的位 0 描述如下:

位 0 GPIOAEN:IO 端口 A 时钟使能

由软件置“1”或清“0”

0:禁止 IO 端口 A 时钟。

1:使能 IO 端口 A 时钟。

那 么 我 们 只 需 要 在 我 们 的 用 户 程 序 中 调 用 宏 定 义 标 识 符

__HAL_RCC_GPIOA_CLK_ENABLE()就可以实现 GPIOA 时钟使能。使用方法为:

__HAL_RCC_GPIOA_CLK_ENABLE();//使能 GPIOA 时钟

对于其他外设,同样都是在 stm32f4xx_hal_rcc.h 头文件中定义,大家只需要找到相关宏定

义标识符即可,这里我们列出了几个常用使能外设时钟的宏定义标识符使用方法:

__HAL_RCC_DMA1_CLK_ENABLE();//使能 DMA1 时钟

__HAL_RCC_USART1_CLK_ENABLE();//使能串口 1 时钟

__HAL_RCC_TIM1_CLK_ENABLE();//使能 TIM1 时钟

我们使用外设的时候需要使能外设时钟,如果我们不需要使用某个外设,同样我们可以禁

止某个外设时钟。禁止外设时钟使用方法和使能外设时钟非常类似,同样是头文件中定义的宏

定义标识符。我们同样以 GPIOA 为例,宏定义标识符为:

#define __HAL_RCC_GPIOA_CLK_DISABLE()

(RCC->AHB1ENR &= ~(RCC_AHB1ENR_GPIOAEN))

同样,宏定义标识符__HAL_RCC_GPIOA_CLK_DISABLE()作用是设置 RCC->AHB1ENR

寄存器的最低位为 0,也就是禁止 GPIOA 时钟。具体使用方法我们这里就不做过多讲解,我们

这里同样列出几个常用的禁止外设时钟的宏定义标识符使用方法:

__HAL_RCC_DMA1_CLK_DISABLE();//禁止 DMA1 时钟

__HAL_RCC_USART1_CLK_DISABLE();/禁止串口 1 时钟

__HAL_RCC_TIM1_CLK_DISABLE();//禁止 TIM1 时钟

关于 STM32F4 的外设时钟使能和禁止方法我们就给大家讲解到这里。

4.4 IO 引脚复用器和映射

STM32F4 有很多的内置外设,这些外设的外部引脚都是与 GPIO 复用的。也就是说,一个 GPIO

如果可以复用为内置外设的功能引脚,那么当这个 GPIO 作为内置外设使用的时候,就叫做复用。

这部分知识在《STM32F411xC/E 参考手册》第七章和芯片数据手册有详细的讲解哪些 GPIO 管脚

是可以复用为哪些内置外设。

对于本小节知识,STM32F411xC/E 参考手册讲解比较详细,我们同样会从中抽取重要的知识

点罗列出来。同时,我们会以串口使用为例给大家讲解具体的引脚复用的配置。

STM32F4 系列微控制器 IO 引脚通过一个复用器连接到内置外设或模块。该复用器一次只允

许一个外设的复用功能(AF)连接到对应的 IO 口。这样可以确保共用同一个 IO 引脚的外设之

间不会发生冲突。

每个 IO 引脚都有一个复用器,该复用器采用 16 路复用功能输入(AF0 到 AF15),可通过

GPIOx_AFRL(针对引脚 0-7)和 GPIOx_AFRH(针对引脚 8-15)寄存器对这些输入进行配置,每四

位控制一路复用:

1)完成复位后,所有 IO 都会连接到系统的复用功能 0(AF0)。

2)外设的复用功能映射到 AF1 到 AF13。

3)Cortex-M4 EVENTOUT 映射到 AF15。

复用器示意图如下图 4.4.1:

f906f239d5e343bb7276df6e173f5a65.png

图 4.4.1 AFRL 和 APRH 复用功能映射关系简图

接下来,我们简单说明一下这个这个图要如何看,举个例子,NANO STM32F411 开发板的

原理图 PA9 的原理图如图 4.4.2 所示:

图 4.4.2 NANO STM32F411 开发板 PA9 原理图

如上图所示,PC11 可以作为 TIM1_CH2/USART1_TX/SDIO_D2 等复用功能输出。这么多

复用功能,如果这些外设之间互相干扰。但是 STM32F4,由于有复用器功能,可以让 PA9 在

某个时刻仅连接到需要使用的特定的外设,因此不存在互相干扰的情况。

上图 4.4.1 图中可以看出,当需要使用复用功能的时候,我们配置相应的寄存器

GPIOx_AFRL 或者 GPIOx_AFRH,让对应引脚通过复用器连接到对应的复用功能外设。这里我

们列出 GPIOx_AFRL 寄存器的描述,GPIOx_AFRH 的作用跟 GPIOx_AFRL 类似,只不过

GPIOx_AFRH 控制的是一组 IO 的高八位,GPIOx_AFRL 控制的是一组 IO 口的低八位。

GPIOx_AFRL 控制的是一组 IO 口的低八位。GPIOx_AFRL 寄存器描述如下图 4.4.3 所示:

5a58d14cc31b5178275b700f3b3faf55.png

图 4.4.3 GPIOx_AFRL 寄存器位描述

从表中可以看出,32 位寄存器 GPIOx_AFRL 每四个位控制一个 IO 口,所以每个寄存器控

制 32/4=8 个 IO 口。寄存器对应四位的值配置决定这个 IO 映射到哪个复用功能 AF。

在微控制器完成复位后,所有 IO 口都会连接到系统复用功能 0(AF0)。这里大家需要注意,

对于系统复用功能 AF0,我们将 IO 口连接到 AF0 之后,还要根据所用功能进行配置:

1)JTAG/SWD:在器件复位之后,会将这些功能引脚指定为专用引脚。也就是说,这些引

脚在复位后默认就是 JTAG/SWD 功能。如果我们要做为 GPIO 来使用,就需要对对应

的 IO 口复用器进行配置。

2)RTV_REFIN:此引脚在系统复位之后要使用的话要配置为浮空输入模式。

3)MCO1 和 MCO2:这些引脚在系统复位之后要使用的话要配置为复用功能模式。

对于外设复用功能的配置,除了 ADC 要将 IO 配置为模拟通道之外其他外设功能一律要配

置为复用功能模式,这个配置是在 IO 口对应的 GPIOx_MODER 寄存器中配置的。同时要配置

GPIOx_AFRH 或者 GPIOx_AFRL 寄存器,将 IO 口通过复用器连接到所需要的复用功能对应的

AFx。

不是每个 IO 口都可以复用为任意复用功能外设。到底哪些 IO 可以复用为相关外设呢?这

在芯片对应的数据手册上面会有详细的表格列出来。对于 STM32F411,数据手册里面的 Table

9.Alternae function mapping 表格列出了所有的端口 AF 映射表,因为表格比较大,所以这里只

列出 PORTA 的几个端口为例方便大家理解:

ad98feb37e4b5ec0b673e4c33bc29af4.png
96dd50c6518c5fc96e8b992792f8b0ec.png

表 4.4.4 PORTA 复位端口 AF 映射表

从表 4.4.4 可以看出,PA9 连接 AF7 可以复用为串口 1 的发送引脚 USART1_TX,PA0 连接

可以复用为串口 1 的接收引脚 USART1_RX。

接下来我们以串口 1 位例来讲解 GPIOA.9,GPIOA.10 口为串口 1 复用功能的一般步骤。

①首先我们要使用 IO 复用功能,必须先打开对应的 IO 时钟和复用功能外设时钟,这里我

们使用了 GPIOA 以及 UDART1,所以我们要使能 GPIOA 和 USART1 时钟。方法如下:

__HAL_RCC_GPIOA_CLK_ENABLE();

//使能 GPIOA 时钟

__HAL_RCC_USART1_CLK_ENABLE();

//使能 USART1 时钟

②其次,我们在 GPIOx_MODER 寄存器中将需 IO(对应串口 1 是 PA9,PA10)配置为复

用功能(ADC 设置为模拟通道)。

③再次,我们还需要对 IO 口的其他参数,例如上拉/下拉以及输出速度等进行配置。

④最后,我们需要配置 GPIOx_AFRL 或者 GPIOx_AFRH 寄存器,将 IO 连接到所需的 AFx。

对于 PA9,PA10 复用为 USART1 的发送接收引脚,根据表 4.4.4 可知都需要连接 AF7。

上面三步,在我们 HAL 库中是通过 HAL_GPIO_Init 函数来实现的,参考代码如下:

GPIO_InitTypeDef GPIO_Initure;

GPIO_Initure.Pin=GPIO_PIN_9;

//PA9

GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出

GPIO_Initure.Pull=GPIO_PULLUP;

//上拉

GPIO_Initure.Speed=GPIO_SPEED_FAST;

//高速

GPIO_Initure.Alternate=GPIO_AF7_USART1;//连接 AF7 复用为串口 1 的发送引脚

HAL_GPIO_Init(GPIOA,&GPIO_Initure);

//初始化 PA9

通过上面的配置,PA9 就通过映射器链接到 AF7,也就是复用为串口 1 的发送引脚。这个时

候,PA9 将不再作为普通的 IO 口使用。对于 PA10,配置方法一样,同样也是链接 AF7,修改 Pin

成员变量值为 PIN_10 即可。对于 GPIO 初始化结构体成员变量 Alternate 的取值范围,在 HAL

库中有详细定义,取值范围如下:

#define IS_GPIO_AF(AF) (((AF) == GPIO_AF0_RTC_50Hz)||((AF) == GPIO_AF9_TIM14)

||

((AF) == GPIO_AF0_MCO) || ((AF) == GPIO_AF0_TAMPER) ||

((AF) == GPIO_AF0_SWJ) || ((AF) == GPIO_AF0_TRACE)

||

((AF) == GPIO_AF1_TIM1)|| ((AF) == GPIO_AF1_TIM2)

||

...//此处省略部分代码

STM32F4 的端口复用和映射就给大家讲解到这里,希望大家课余结合相关实验工程和手册

巩固本小节知识。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值