来自官网的简介
这里我们学习的开发板芯片具体型号是STM32F407ZGT6,采用工作频率为168 MHz的Cortex™-M4内核,性能较强。
本篇包含的内容:

  1. 固件库简介
  2. 开发环境的简介
  3. 开发板的基础知识

一、固件库的介绍:

前言: 在51单片机中,我们经常是直接操作 寄存器:

P0=0x11;  //通过16进制数赋值0,1直接设置寄存器每一位开启关闭

在STM32中,面对大量的寄存器,很难全部记住并通过直接赋值来操作,开发效率太低且维护起来很麻烦,于是可以通过函数的方式将对寄存器的操作封装起来,我们大多数时候只需要使用函数调用接口(API):

/**库函数控制电平的翻转*void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)*{*   GPIO->BSRRL = GPIO_Pin;  //实际上还是控制寄存器,可以自行查询源码*}*/
//我们控制BSRRL寄存器实现电平控制只需要如下操作:
GPIO_SetBits(GPIOF,GPIO_Pin_10);   //LED1对应引脚GPIOF.10,拉高电平表示灯灭


CMSIS标准:
上面我们得知,固件库的存在实际上在一定程度上使得硬件与软件相分离,如果对于不同的硬件我们都使用同一个标准(比如,规范统一的命名,相同的结构层次)就能够实现较好的软件可移植性或者实现较好的兼容性
arm公司为实现不同芯片公司生产的Cortex-M4芯片软件上的兼容,提出了CMSIS标准(Cortex Microcontroller Software Interface Standard)。可以简单理解为为了实现接口的规范统一。

以后我们还会知道其他如HAL、LL库等的使用,基本概念原理是一样的,此处不做讲解。

STM32官方固件库讲解:
下载连接(提取1234)
在一个工程中,固件库之间的相互关系如下图:

1、Application.c:也就是main.c,程序的主逻辑文件。
2、stm32f4xx.h:片上外设访问头文件,里面包含大量结构体和宏定义(寄存器定义声明及内存访问操作的封装)。
3、system_stm32f4xx.h:包含SystemInit()函数的声明,用于系统启动时调用来设置整个系统和总线时钟
4、stm32f4xx_it.c:编写中断服务函数,当然也可以将其编写在其他文件中比如各个硬件的驱动文件里面。
5、stm32f4xx_conf.h:外设驱动配置文件,可以根据需要include相关的外设头文件。
6、启动文件(startup_stm32f40_41xxx.s):堆栈的初始化、中断向量表以及中断函数定义。

(加粗部分为我们经常需要打交道的地方)

二、开发环境的简介

前言: 我们使用MDK5(提取1234)进行软件的开发:
软件的组成结构如下:

一般来说,第一次打开软件表观认识最多的应该是IDE Editer也就是代码编辑界面,其次如果首次使用某款芯片,会提示安装相关支持库,会使用到包安装器,对于软件其他部分可以在使用中慢慢感受(只需要明白MDK5不是简单的代码编辑器而已,是一个软件集合)。

1、工程模板的建立:
工程模板的建立是为了代码组织的清晰以及软件维护的方便。
我们先看下ST官方库给出的工程模板包含的文件

  - Template/system_stm32f4xx.c   STM32F4xx 系统时钟配置文件- Template/stm32f4xx_conf.h     固件库配置文件- Template/stm32f4xx_it.c       中断处理文件- Template/stm32f4xx_it.h       中断处理头文件- Template/main.c               Main program- Template/main.h               Main program header file

我们观察下对应工程模板在MDK5上的头文件路径设置:

第一步就是要搞明白,一个工程包含使用了哪些文件,我们对这些文件分类管理,创建出属于自己的工程模板。
通过对比正点原子、st官方模板、cubeMx创建的模板,可以知道,完整的工程需要:

1、core_cm4.h //内核功能的定义,比如NVIC相关寄存器的结构体和Systick配置;
2、core_cm4_simd.h   //包含与编译器相关的处理
3、core_cmFunc.h //内核核心功能接口头文件
4、core_cmInstr.h    //包含一些内核核心专用指令5、startup_stm32f40_41xxx.s    //devices vector table for MDK-ARM toolchain
6、stm32f4xx.h   //contains all the peripheral register's definitions, bits definitions //and memory mapping for STM32F4xx devices
7、system_stm32f4xx.c    //contains the system clock configuration for STM32F4xx devices,//关键函数SystemInit(),系统启动文件startup_stm32f40_41xxx.s里面调用
8、stm32f4xx_conf.h  //里面包含了我们需要使用的标准库,对于需要使用的库需要在其中加以说明include,       //stm32f4xx.h里面可以看到对其include9、stm32f4xx_it.c    //provides template for all exceptions handler and // peripherals interrupt service routine.
10、main.c       //主程序
11、led.c    //对于关键使用硬件的驱动配置文件

对此我们对上面的文件分类存放管理:

我们创建这样一个目录结构后,导入相关文件,使用MDK5以该文件夹创建工程,然后将相关头文件路径进行设置好,一个工程模板就完成了(自己最好找到一个工程模板,将上述文件找到,并理清各个文件间的调用关系)。

2、程序的下载调试:
程序的下载方式:USB、串口(通常使用USB转成串口)、JTAG、SWD等。

  • 使用串口下载时,首先需要电脑安装相关的驱动(CH340驱动),然后使用flymcu选择相关hex文件下载到开发板中。
  • 使用JLINK下载:串口下载较为缓慢(每次都要全片擦除)且不能使用调试工具实时跟踪调试,使用JLINK、ULINK、STLINK等可以快速下载并实现程序跟踪调试。使用JLINK下载调试时需要注意设置为SW调试模式(JTAG模式会占用较多I/O口使得部分外设无法使用),相关驱动的安装不做详述。

程序的调试:
(注意:开发板上B0与B1要设置到GND以便代码下载后自动运行)
按下debug键进入仿真调试模式(Load键会直接下载执行而不进入调试模式)

三、基础知识入门

1、关于STM32系统总线架构:
首先需要认识什么是总线以及总线的分类(联系计算机基础中学到的DB、CB、AB总线)。
可以简单的认为总线就是信息的传递通道,连接各种外设和芯片,详细的介绍可以看这里
一般说来,有系统总线AHB和外设总线APB,在STM32F4中,总线矩阵实现8条主控总线和7条被控总线之间的互连并完成相互间访问的仲裁(可以当成交警,指挥疏导交通运输)。不同速率要求的外设会挂载在不同的总线上组成相互之间的通信网络。

2、时钟系统:
学习之前首先想一下为什么要时钟(看数电)?STM32时钟为什么那么多?时钟树是什么?
首先考虑到不同外设需要的时钟频率以及时钟精度并不一致,我们对于不同的外设使用不同的时钟可以有效降低能耗并提高系统稳定性。
时钟树就是对时钟源进行一系列的倍频和分频得到不同的频率时钟信号(如图一棵树,由根部向上分出枝干)。
可以参考这篇博文学习(CLICK)

2.1 系统时钟SYSCLK的初始化:
系统时钟的配置是在SystemInit()函数中完成的(system_stm32f4xx.c文件),配置完寄存器后该函数会调用SetSysClock函数。
SystemInit()调用是在启动文件(startup_stm32f40_41xxx.s文件)中设置的:系统启动后,进入main之前会调用SystemInit()对系统时钟进行初始化,比如对外部晶振的时钟进行分频倍频得到不同的时钟源初始值。

2.2 时钟的使能和配置:
系统初始化完成后可能会需要修改某些时钟的默认配置或者使能外设时钟以便使用相关外设,这便需要使用到 stm32f4xx_rcc.h 文件。
对于时钟的很多操作都是定义在 stm32f4xx_rcc.h 文件里,该文件的函数大致分类如下:

  • 时钟使能函数(外设使能与时钟源使能)
  • 外设复位函数(与上面的使能相对应,使用类似)
  • 时钟源选择和分频因子配置函数

下面对这几类函数做简单讲解:
+++ 首先我们从功耗角度考虑假定:任何时钟,你不设置它,默认为关闭。
+++ 然后明确基本知识:

  • 我们有5条总线AHB1、AHB2、AHB3、APB1、APB2,下面分别挂载了不同的外设(具有不同的时钟)。
  • 我们有5大类时钟源(形象点:5个时钟树相互交叉在一起)


做个不严谨的比喻:我们可以认为时钟源就是一个大的水源,外设时钟是一个个小水库,有的水库可以有多个水源的水补充(比如系统时钟),水库周围的人(外设)要用水,就要打开水库(使能外设时钟),想喝多少(分频因子设置函数),喝哪里的水(时钟源选择函数)都可以选择。
我们不可能一直开着水闸(浪费,所以设置一堆使能函数);
不同的人不同用途对水的要求不一样(所以可以选择水库的水的来源,给你一堆时钟源配置函数)。
现在你在回头看看这三个函数类别,是不是有点理解了?
挨着顺序举个例:
我们使用串口1,首先就要打开时钟:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);  //串口1挂载在APB2总线,函数使用要对应其挂载的总线。

然后再对串口1做相关的初始化参数配置就可以使用串口1了(不做展开)。
时钟复位函数用法一致,不做举例。
对于时钟源的开关,我们也有相关函数可以控制(5大类,6个函数):

void        RCC_HSICmd(FunctionalState NewState);
void        RCC_LSICmd(FunctionalState NewState);
void        RCC_PLLCmd(FunctionalState NewState);
void        RCC_PLLI2SCmd(FunctionalState NewState);
void        RCC_PLLSAICmd(FunctionalState NewState);
void        RCC_RTCCLKCmd(FunctionalState NewState);

时钟源的选择(配置):

void        RCC_SYSCLKConfig(uint32_t RCC_SYSCLKSource);  //配置系统时钟的来源,可以查看相关函数定义了解细节

有的传入参数是分频,有的是时钟源,这样Config函数根据参数一般分为时钟源设置和分频设置或者兼具,具体可以查看函数定义,一般函数定义会有对参数有效性判断的函数,进入这个函数就可以清晰明白函数的真正用途(结合时钟树可以加深理解)。

void RCC_PCLK1Config(uint32_t RCC_HCLK)
{uint32_t tmpreg = 0;/* Check the parameters */assert_param(IS_RCC_PCLK(RCC_HCLK));   //查看这里的IS_RCC_PCL函数定义,如下......  // 省略部分内容
}#define RCC_HCLK_Div1                    ((uint32_t)0x00000000)
#define RCC_HCLK_Div2                    ((uint32_t)0x00001000)
#define RCC_HCLK_Div4                    ((uint32_t)0x00001400)
#define RCC_HCLK_Div8                    ((uint32_t)0x00001800)
#define RCC_HCLK_Div16                   ((uint32_t)0x00001C00)
#define IS_RCC_PCLK(PCLK) (((PCLK) == RCC_HCLK_Div1) || ((PCLK) == RCC_HCLK_Div2) || \((PCLK) == RCC_HCLK_Div4) || ((PCLK) == RCC_HCLK_Div8) || \((PCLK) == RCC_HCLK_Div16))
//看名字就可以知道这里是提高分频设置的

3、I/O引脚复用与映射:
一个芯片会有很多引脚,一块板子上面有很多外设,外设是需要与芯片引脚相连实现特定功能的,而我们又需要GPIO的存在(像我们点个灯啊,简单输入输出高低电平啥的),如果各自都单独使用,引脚会存在浪费情况(毕竟不是所有外设同时使用),所以一个引脚我们设置成可以复用为GPIO和多个内置外设的功能引脚。
具体一个引脚作为什么功能用,可以通过寄存器控制参数(这里就讲讲相关设置的函数就行了,具体可以自己查阅相关寄存器功能列表和函数实现)。
举个使用GPIOA.9以及GPIOA.10作为串口1的复用例子(串口需要收发就要两个引脚):

/*1、如同前面所说,使用一个外设就要首先使能时钟,这里我们使能GPIO时钟以及串口的外设时钟*/
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); /*2、初始化GPIOA的Pin9和Pin10引脚,配置为复用功能*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;  //这里配置为复用功能,其他还有输入输出等选项
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化GPIOA的两个引脚/*3、使用GPIO_PinAFConfig配置复用功能为串口1,具体是什么功能要看手册或者查源码(编辑器自动查看)*/
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);  //配置Pin9为TX端
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);  //配置Pin10为RX端

这样我们就完成了GPIO的复用环节了,可能大家注意到我们对GPIOA做了初始化(步骤2),但没有对串口初始化,因为这里只是讲解初始化GPIOA作为复用功能,对于串口的使用(如何配置,后续再做讲解)。

4、NVIC中断优先级管理:
4.1 一些简单的补充:
中断: 计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。STM32将中断分为内核中断和外部中断,可以就简单区别为内核中断是最高有限级的,不能打断(比如复位,非屏蔽中断,硬件错误等),而外部中断是我们主要操作对象,如外设发出的中断请求(IQR)我们可以设置优先级,可以直接屏蔽等。
优先级: 分为抢占优先级和响应优先级。顾名思义,抢占的意思就是可以强制打断正在执行的中断响应(第一时间得到处理),而响应优先即就是简单的谁级别高谁就优先得到响应(排队排的快),但是不能打断正在执行的中断(别人已经再接收服务,自己来晚了你就不能让别人下来)。
中断向量表: 一个中断向量占据4字节空间,是指中断服务程序入口地址的偏移量与段基值。中断向量表占据一个特定存储空间,它的作用就是按照中断类型号从小到大的顺序存储对应的中断向量。在中断响应过程中,CPU通过从接口电路获取的中断类型号(中断向量号)计算对应中断向量在表中的位置,并从中断向量表中获取中断向量,将程序流程转向中断服务程序的入口地址。

4.2 言归正传:
STM32F40xx/STM32F41xx总共有92个中断,包含10个内核中断和82个可屏蔽中断,具有16级可编程的中断优先级。
Nested Vectored Interrupt Controller (NVIC)相关的寄存器定义在core_cm4.h文件内,可以看到里面很多位是保留的,M4内核提供的中断数量以及相关功能在STM32F40xx/STM32F41xx中只使用到一部分。我们对于中断的控制就是通过对下面这些寄存器操作实现的。

/** \brief  Structure type to access the Nested Vectored Interrupt Controller (NVIC).*/
typedef struct
{__IO uint32_t ISER[8];                 /*!< Offset: 0x000 (R/W)  Interrupt Set Enable Register           */uint32_t RESERVED0[24];__IO uint32_t ICER[8];                 /*!< Offset: 0x080 (R/W)  Interrupt Clear Enable Register         */uint32_t RSERVED1[24];__IO uint32_t ISPR[8];                 /*!< Offset: 0x100 (R/W)  Interrupt Set Pending Register          */uint32_t RESERVED2[24];__IO uint32_t ICPR[8];                 /*!< Offset: 0x180 (R/W)  Interrupt Clear Pending Register        */uint32_t RESERVED3[24];__IO uint32_t IABR[8];                 /*!< Offset: 0x200 (R/W)  Interrupt Active bit Register           */uint32_t RESERVED4[56];__IO uint8_t  IP[240];                 /*!< Offset: 0x300 (R/W)  Interrupt Priority Register (8Bit wide) */uint32_t RESERVED5[644];__O  uint32_t STIR;                    /*!< Offset: 0xE00 ( /W)  Software Trigger Interrupt Register     */
}  NVIC_Type;

中断管理函数主要是在misc.c文件里面:
中断优先级分组函数(这里的分组是设置IP[240]里面我们使用的82个可屏蔽中断的抢占和响应优先级各占8 bit中的几位):

void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
{/* Check the parameters */assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup));/* Set the PRIGROUP[10:8] bits according to NVIC_PriorityGroup value */SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup;
}

设置好优先级分组后,就需要设置抢占优先级和响应优先级对应各自具体优先级大小(区别前面的分组和这里具体设置优先级的关系):
这就要使用中断初始化函数

void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
typedef struct
{uint8_t NVIC_IRQChannel;                    /*!< Specifies the IRQ channel to be enabled or disabled.This parameter can be an enumerator of @ref IRQn_Type enumeration (For the complete STM32 Devices IRQ Channelslist, please refer to stm32f4xx.h file) */uint8_t NVIC_IRQChannelPreemptionPriority;  /*!< Specifies the pre-emption priority for the IRQ channelspecified in NVIC_IRQChannel. This parameter can be a valuebetween 0 and 15 as described in the table @ref MISC_NVIC_Priority_TableA lower priority value indicates a higher priority */uint8_t NVIC_IRQChannelSubPriority;         /*!< Specifies the subpriority level for the IRQ channel specifiedin NVIC_IRQChannel. This parameter can be a valuebetween 0 and 15 as described in the table @ref MISC_NVIC_Priority_TableA lower priority value indicates a higher priority */FunctionalState NVIC_IRQChannelCmd;         /*!< Specifies whether the IRQ channel defined in NVIC_IRQChannelwill be enabled or disabled. This parameter can be set either to ENABLE or DISABLE */
} NVIC_InitTypeDef;

举个具体例子:

NVIC_InitTypeDef   NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;//外部中断0
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;//抢占优先级0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;//子优先级2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能外部中断通道
NVIC_Init(&NVIC_InitStructure);//配置

注:
这里我们只是讲解了中断控制相关文件里面的中断优先级设置,还有一些不常用的函数可以自行查看misc.c文件,这里不做展开。
需要明白的是:前面的是对所有中断都必须的通用的操作,而具体使用某一个外设中断,还需要设置对应的外设中断触发方式,清除中断标准,配置响应函数等等需要去查看对应的外设库文件。

四、总结:

在STM32库函数开发中,可以看到,对于开发板各个功能的实现还是类似51单片机一样对寄存器的操作。只不过,为了开发的方便快速,代码组织的清晰性,对各个寄存器使用结构体 组织在一起,对寄存器的操作变成了使用各种封装好的函数来对结构体赋值实现对寄存器的各个位的设置。
平时可以多去看看固件库的源码,看看各个函数、结构体是怎么分类组织在各个文件中,各个文件在一个工程项目中的相互关系是怎么样的,可以找一个工程在脑海里思考下这个工程执行的时候各个文件函数是怎么样的执行顺序、调用关系。

STM32F4学习笔记(基础介绍篇)相关推荐

  1. 经典再现,看到就是赚到。尚硅谷雷神 - SpringBoot 2.x 学习笔记 - 基础入门篇

    SpringBoot 2.x 时代 – 基础入门篇 视频学习地址:https://www.bilibili.com/video/BV1Et411Y7tQ?p=112&spm_id_from=p ...

  2. 设计模式学习笔记-基础知识篇

    1. 设计模式的重要性 1.1 设计模式解决的是在软件过程中如何来实现具体的软件功能.实现同一个功能的方法有很多,哪个设计容易扩展,容易复用,松耦合,可维护?设计模式指导我们找到最优方案. 1.2 设 ...

  3. Hadoop学习笔记—15.HBase框架学习(基础知识篇)

    Hadoop学习笔记-15.HBase框架学习(基础知识篇) HBase是Apache Hadoop的数据库,能够对大型数据提供随机.实时的读写访问.HBase的目标是存储并处理大型的数据.HBase ...

  4. MySQL学习笔记-基础篇1

    MySQL 学习笔记–基础篇1 目录 MySQL 学习笔记--基础篇1 1. 数据库概述与MySQL安装 1.1 数据库概述 1.1.1 为什么要使用数据库 1.2 数据库与数据库管理系统 1.2.1 ...

  5. Redis学习笔记①基础篇_Redis快速入门

    若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 资料链接:https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA( ...

  6. MySQL学习笔记-基础篇2

    MySQL学习笔记-基础篇2 目录 MySQL学习笔记-基础篇2 8.子查询 8.1 需求分析与问题解决 8.1.1 实际问题 8.1.2 子查询的基本使用 8.1.3 子查询的分类 8.2 单行子查 ...

  7. UE4 Material 101学习笔记——01-07 介绍/PBR基础/UV扭曲/数据类型/翻页动画/材质混合/性能优化

    UE4 Material 101学习笔记--01-07 介绍/PBR基础/UV扭曲/数据类型/翻页动画/材质混合/性能优化 Lec 01 什么是着色器 What Is A Shader? 1.1 介绍 ...

  8. python3多线程编程_Python 3多线程编程学习笔记-基础篇

    本文是学习<Python核心编程>的学习笔记,介绍了Python中的全局解释器锁和常用的两个线程模块:thread, threading,并对比他们的优缺点和给出简单的列子. 全局解释器锁 ...

  9. Redis学习笔记(实战篇)(自用)

    Redis学习笔记(实战篇)(自用) 本文根据黑马程序员的课程资料与百度搜索的资料共同整理所得,仅用于学习使用,如有侵权,请联系删除 文章目录 Redis学习笔记(实战篇)(自用) 1.基于Sessi ...

  10. 树莓派4B学习笔记——IO通信篇(UART)

    文章目录 UART简介 树莓派使用UART与串口屏通信 串口屏简介 硬件连接 配置串口接口 树莓派打开UART接口 树莓派安装串口调试助手 编程实现 wiringSerial.h Serial简介 C ...

最新文章

  1. 祝天下所有的老师教师节快乐
  2. 一天一点linux(9):ubuntu下如何搭建LAMP开发环境?
  3. linux系统在硬盘上安装程序,在硬盘中安装Linux操作系统最简单的方法
  4. BaseAction
  5. Android中实现不同文字颜色和图文混排的Span总结
  6. 采用CreateThread()创建多线程程序
  7. Linux: Nginx proxy_pass域名解析引发的故障
  8. Sorting It All Out (易错题+拓扑排序+有向图(判环+判有序)优先级)
  9. java调优方法,jvm监控工具
  10. hadoop重启后 9000端口不在
  11. Linux内核访问外设I/O--动态映射(ioremap)和静态映射(map_desc) (转载)
  12. 微助教课件怎么下载_【微助教课件下载】[微助教]自测一下,您的课堂有好的开始和结束么?...
  13. 从16位到32位再到64位,为何16年过去,依然没有128位系统出现?
  14. java 存储输入_java将用户输入信息保存至txt文件
  15. Docker教程:使用docker配置python开发环境
  16. 为什么年龄大了近视还增加_都是做近视手术,为什么价格区别这么大?
  17. Delphi 10.3 Rio实现FMX应用APP增加Android“应用程序快捷方式”
  18. 社交app的变现方式有哪些?
  19. 5G通讯的认知与见解
  20. CCTV-TIME特别关注:深圳首届弘扬关公文化促进两岸统一忠义论坛

热门文章

  1. ruby 生成哈希值_Ruby哈希值和可变的默认值
  2. Robi改造计划-开篇
  3. js方法padStart()和padEnd()使用示例
  4. 经纬度坐标转换成px_墨卡托坐标与经纬度转换
  5. linux pt 下载软件,centos下pt下载软件rtorrent使用
  6. 如何做一个受上司喜爱的下属
  7. uni-app基础(二)
  8. STP——RSTP快生成树协议讲解
  9. 门户网站java源码vue_vuetify-master
  10. python中os.environ的用法