当前位置:   article > 正文

02-STM32基础---时钟、GPIO、中断

02-STM32基础---时钟、GPIO、中断

02-STM32基础—时钟、GPIO、中断

此处部分概念为简介,后面会根据具体 外设的使用 和 功能的实现 补充

1. RCC-时钟部分

RCC(reset clock control):复位和时钟控制器。

1.1 性能线框图

STM32F103xC, STM32F103xD and STM32F103xE performance line block diagram。

在这里插入图片描述

1.2 时钟树

在这里插入图片描述

CPU上电的时候,默认情况下:

  • 内部8M RC振荡器(HSI),时钟供给 SYSCLK系统时钟 8M,SYSCLK时钟信号经过AHB预分频器,分频系数默认是1,
    • AHB时钟线上 HCLK高速时钟频率只有8M,
    • AHB时钟线分叉出2条时钟线,也分别对应2个预分频器,APB1预分频和APB2预 分频器里值默认都是1,所以:
      • APB1时钟线上,PCLK1时钟信号是 8M
      • APB2时钟线上,PCLK2时钟信号是 8M
  • 进入main函数之前,库(startup_stm32f10x_hd.s 启动文件 )会帮我们配置时钟,外部的高速时钟可以取值为 4~16M,一般选8M晶振,8M 经过PLL锁相环倍频器,可以让倍频系数值为9,这里出来PLLCLK锁相环时钟为最高72MHz,单刀三击开关选择PLLCLK作为SYSCLK系统时钟,系统时钟信号也是 72M Hz,AHB 和APB2预分频器分频里值是1,APB1预分频器值是2,最终:
    • AHB 的HCLK时钟信号 72M
    • APB1上 PCLK1是 36M
    • APB2上PCLK2是 72M

1.3 系统时钟的时钟源

1.3.1 时钟源
  • STM32一共有 4个时钟源 ,时钟源就是产生时钟信号(高低电平脉冲)的源头
    • 有2个高速晶振:
      • HSI:高速时钟 内部高速时钟源,是一个由电阻和电容构成 RC振荡器,可以产生8M Hz
      • HSE:芯片外部的高速时钟,这个是通常接 外部石英晶振,4~16M 之间,一般选择8M Hz
    • 还有2个低速时钟:
      • LSI:芯片内部 ,40K Hz,会受温度影响
      • LSE:外部低速时钟,32.768K Hz
1.3.2 芯片内3条时钟线
  • AHB时钟线 最高运行稳定可靠频率是72M Hz,时钟信号名字 HCLK 高速时钟

  • APB2时钟线 最高运行稳定可靠频率是72M Hz,时钟信号名字 PCLK2

  • APB1时钟线 最高运行稳定可靠频率是36M Hz,时钟信号名字 PCLK1

  • GPIOA GPIOB … GPIOG 所有的GPIO外设都挂在 APB2时钟线上

  • 8个定时器 TIM1、TIM2 … TIM8 只有TIM1TIM8这2个高级定时器挂在APB2时钟线,其余都在APB1是时钟线

  • 5个串口 USART1、USART2 … UART5 只有USART1 挂在APB2时钟线,其余在APB1 …

1.4 其他时钟

1.4.1 USB 时钟

USB 时钟是由 PLLCLK 经过 USB 预分频器得到,分频因子可以是:[1,1.5],具体的由时钟配置寄存器 CFGR 的位 22:USBPRE 配置。USB 的时钟最高是 48M,根据分频因子反推 过 来 算 , PLLCLK 只 能 是 48M 或 者 是 72M 。 一 般 我 们 设 置 PLLCLK=72M ,USBCLK=48M。USB 对时钟要求比较高,所以 PLLCLK 只能是由 HSE 倍频得到,不能使用 HSI 倍频。

1.4.2 Cortex 系统时钟

Cortex 系统时钟由 HCLK 8 分频得到,等于 9M,Cortex 系统时钟用来驱动内核的系统定时器 SysTick,SysTick 一般用于操作系统的时钟节拍,也可以用做普通的定时。

1.4.3 ADC 时钟

ADC 时钟由 PCLK2 经过 ADC 预分频器得到,分频因子可以是[2,4,6,8],具体的由时钟配置寄存器 CFGR 的位 15-14:ADCPRE[1:0]决定。很奇怪的是怎么没有 1 分频。ADC 时钟最高只能是 14M,如果采样周期设置成最短的 1.5 个周期的话,ADC 的转换时间可以达到最短的 1us。如果真要达到最短的转换时间 1us 的话,那 ADC 的时钟就得是 14M,反推PCLK2 的时钟只能是:28M、56M、84M、112M,鉴于 PCLK2 最高是 72M,所以只能取28M 和 56M。

1.4.4 RTC 时钟、独立看门狗时钟

RTC 时钟可由 HSE/128 分频得到,也可由低速外部时钟信号 LSE 提供,频率为32.768KHZ,也可由低速内部时钟信号 HSI 提供,具体选用哪个时钟由备份域控制寄存器BDCR的位9-8:RTCSEL[1:0]配置。独立看门狗的时钟由 LSI提供,且只能是由 LSI提供,LSI 是低速的内部时钟信号,频率为 30~60KHZ 直接不等,一般取 40KHZ。

1.4.5 MCO 时钟输出

MCO 是 microcontroller clock output 的缩写,是微控制器时钟输出引脚,在 STM32 F1系列中 由 PA8 复用所得,主要作用是可以对外提供时钟,相当于一个有源晶振。MCO 的时钟来源可以是:PLLCLK/2、HSI、HSE、SYSCLK,具体选哪个由时钟配置寄存器CFGR 的位 26-24:MCO[2:0]决定。除了对外提供时钟这个作用之外,我们还可以通过示波器监控 MCO 引脚的时钟输出来验证我们的系统时钟配置是否正确。

2. GPIO

2.1 概述

  • 概念:GPIO(General Purpose Input/Output):stm32 的 通用输入输出 端口,STM32 芯片的 GPIO 引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能;

  • 作用:负责采集外部器件的信息或者控制外部器件工作,即输入输出;

  • 特性:

    • 快速翻转,每次翻转最快只需要2个时钟周期
    • 每个IO都可以用作中断
    • 支持8种工作模式
  • 电气特性:

    • STM32工作电压范围:2V <= VDD <= 3.6V;
    • GPIO识别电压范围:
      • COMS端口(3.3V):-0.3 <= VIL <= 1.164V 逻辑0,1.833V <= VIL <= 3.6V逻辑1;
      • TTL(3.3V或5V):STM32芯片手册里面 I/O电平一栏 用FT标识的是TTL电平,没有标识的就是CMOS端口;
    • GPIO输出电流:单个IO口,最大25mA,想输出更大的电流: 接二极管、MOS 管、继电器等方案
  • 引脚类型:

    • 电源引脚:一般以V字母开头的可以认为是电源引脚;
    • 晶振引脚
      • 外部低速晶振:PC14-OSC32_IN(32.768KHz)、PC15-OSC32_OUT
      • 外部高速晶振:OSC_IN、OSC_OUT
    • 复位引脚:NRST
    • 下载引脚:串口等
    • BOOT引脚:BOOT0、BOOT1
    • GPIO引脚:以P开头的 PA0~PA15…PG0-PG15

2.1 GPIO 框图

在这里插入图片描述

该图从最右端看起,最右端就是代表 STM32 芯片引出的 GPIO 引脚,其余左边部件都位于芯片内部。

  • 保护二极管
    • IO 引脚上下两边两个二极管用于防止引脚外部过高、过低的电压输入,当引脚电压高于 VDD 时,上方的二极
      管导通,当引脚电压低于 VSS 时,下方的二极管导通,防止不正常电压引入芯片导致芯片烧毁 。
  • 上拉、下拉电阻
    • 30K-50K Ω之间
    • 控制引脚默认状态的电压,开启上拉的时候引脚默认电压为高电平,开启下拉的时候引脚默认电压为低电平
  • TTL 施密特触发器
    • 是一种整形电路,可以将非标准方波,整形成方波
    • 基本原理是:
      • 当输入电压高于正向阈值电压,输出为高;
      • 当输入电压低于负向阈值电压,输出为低;
      • 当输入电压在正负阈值电压之间,输出不变;
      • IO 口信号经过触发器后,模拟信号转化为 0 和 1 的数字信号。
  • P-MOS 管和 N-MOS 管
    • MOS管是压控型元件,通过控制栅源电压(Vgs)来实现导通或者关闭;
    • 信号由 P-MOS 管和 N-MOS 管,依据两个 MOS 管的工作方式,使得 GPIO 具有“推挽输出”和“开漏输出”的模
      式。P-MOS 管高电平导通,低电平关闭,下方的 N-MOS 低电平导通,高电平关闭

2.2 GPIO 端口

  • 想要实现硬件功能,当然是通过 控制 STM32 芯片的 I/O 引脚电平的高低来实现。
  • STM32 芯片的 GPIO 被分成很多组,每组有 16 个引脚,如型号为 STM32F103ZET6 型号的芯片有 GPIOAGPIOBGPIOC … 至 GPIOG 共 7 组 GPIO,芯片一共 144 个引脚,其中 GPIO就占了 112个,所有的 GPIO 引脚都有基本的输入输出功能。
  • 基本的输出 功能是由 STM32 控制引脚输出高、低电平,实现开关控制,如:把 GPIO引脚接入到 LED 灯,那就可以控制 LED 灯的亮灭(就是把 GPIO 的引脚连接到 LED 灯的阴极,LED 灯的阳极接电源,然后通过 STM32 控制该引脚的电平,从而实现控制 LED 灯的亮灭),引脚接入到继电器或三极管,那就可以通过继电器或三极管控制外部大功率电路的通断。
  • 基本的输入 功能是检测外部输入电平,如把 GPIO 引脚连接到按键,通过电平高低区分按键是否被按下。
2.2.1 GPIO 寄存器
  • 要控制GPIO端口,就要涉及相关寄存器,参见《STM32参考手册》GPIO寄存器描述。

    在这里插入图片描述

  • 每个 GPIO端口 有:

    • 两个32位配置寄存器(GPIOx_CRL,GPIOx_CRH),
    • 两个32位数据寄存器(GPIOx_IDR,GPIOx_ODR),
    • 一个32位 置位/复位寄存器(GPIOx_BSRR),
    • 一个16位 复位寄存器(GPIOx_BRR)
    • 一个32位锁定寄存器(GPIOx_LCKR)。
  • 关于寄存器名称如GPIOx-CRL,GPIOx_CRH上的标号x,其取值可以为图中括号内的值(A…E),表示这些寄存器也跟GPIO一样,是分组的。也就是说,对于端口GPIOA和GPIOB,它们都有互不相干的一组寄存器,如控制GPIOA的寄存器名为GPIOA-CRL,GPIOA-CRH等,而控制GPIOB的则是不同的被命名为GPIOB_CRL,GPIOB_CRH等的寄存器。

  • 根据数据手册中列出的每个I/O端口的特定硬件特征,GPIO端口的每个位可以由软件分别配置成多种模式:

    typedef enum 
    { 
        GPIO_Mode_AIN = 0x0,           // 模拟输入 
        GPIO_Mode_IN_FLOATING = 0x04,  // 浮空输入 
        GPIO_Mode_IPD = 0x28,          // 下拉输入 
        GPIO_Mode_IPU = 0x48,          // 上拉输入 
        GPIO_Mode_Out_OD = 0x14,       // 开漏输出 
        GPIO_Mode_Out_PP = 0x10,       // 推挽输出 
        GPIO_Mode_AF_OD = 0x1C,        // 复用开漏输出 
        GPIO_Mode_AF_PP = 0x18         // 复用推挽输出 
    } GPIOMode_TypeDef;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

每个I/O端口位可以自由编程,然而I/O端口寄存器必须按32位字被访问(不允许半字或字节访问)。GPIOx_BSRR和GPIOx_BRR寄存器允许对任何GPIO寄存器的读/更改的独立访问;

这样,在读和更改访问之间产生IRQ时不会发生危险。

2.2.2 GPIO 的八种工作模式
1)、浮空输入

在这里插入图片描述

  • GPIO_Mode_IN_FLOATING,输入用,完全浮空,状态不定;
  • 浮空输入模式下,I/O 端口的 电平信号 直接进入 输入数据寄存器。MCU 直接读取 I/O 口电平,I/O 的电平状态是不 确定的,完全由外部输入决定;
  • 如果在该引脚悬空(在无信号输入)的情况下,读取该端口的电平是不确定的,低功耗。
2)、上拉输入

在这里插入图片描述

  • GPIO_Mode_IPU(In Pull Up),输入用,用内部上拉,默认是高电平
  • IO 内部接上拉电阻,此时如果 IO 口外部没有信号输入或者引脚悬空,IO 口默认为高电平,如果 I/O 口输入低电平,那么引脚就为低电平,MCU 读取到的就是低电平。
  • 应用场景:钳位电平、增强驱动能力、抗干扰。可以用来检测外部信号;例如,按键等;
3)、下拉输入

在这里插入图片描述

  • GPIO_Mode_IPD(In Pull Down),输入用,用内部下拉,默认是低电平
  • IO 内部接下拉电阻,此时如果 IO 口外部没有信号输入或者引脚悬空,IO 口默认为低电平,如果 I/O 口输入高电平,那么引脚就为高电平,MCU 读取到的就是高电平,可以用来检测外部信号;例如,按键等;
4)、模拟输入

在这里插入图片描述

  • GPIO_Mode_AIN(Analog Input),输入至ADC、DAC片上外设
  • 当 GPIO 引脚用于 ADC 采集电压的输入通道时,用作"模拟输入"功能,此时信号不经过施密特触发器,直接进入ADC 模块
5)、推挽输出

在这里插入图片描述

  • GPIO_Mode_Out_PP(out push—pull),驱动能力强,25mA(max),通用输出
  • 在推挽输出模式时,N-MOS 管和 P-MOS 管都工作,如果我们控制 输出为 0,则 P-MOS 管关闭,N-MOS管导通,I/O 端口是低电平,若控制 输出为 1 ,则 P-MOS 管导通, N-MOS 管关闭,I/O 端口就是高电平,外部上拉和下拉的作用是控制在没有输出时 IO 口电平
  • 此时施密特触发器是打开的,即输入可用,通过输入数据寄存器 GPIOx_IDR 可读取 I/O 的实际状态。I/O 口的电平一定是输出的电平,一般应用在输出电平为 0 和 3.3 伏而且需要高速切换开关状态的场合。
6)、开漏输出

在这里插入图片描述

  • GPIO_Mode_Out_OD(out open drain),软件IIC的SDA、SCL等

  • 在开漏输出模式时,只有 N-MOS 管工作(P-MOS管始终不被激活),

    • 如果我们控制输出为 0,低电平,则 N-MOS 管导通,使输出低电平,I/O 端口的电平就是低电平,
    • 若控制输出为 1 时,高电平,则 P-MOS 管和 N-MOS 管都关闭,输出指令就不会起到作用,此时 I/O 端口的电平就不会由输出的高电平决定,而是由 I/O 端口 外部的上拉或者下拉决定,如果没有上拉或者下拉 IO 口就处于悬空状态。
  • 并且此时 施密特触发器是打开 的,即 输入可用,通过输入数据寄存器 GPIOx_IDR 可读取 I/O 的实际状态。I/O 口的电平不一定是输出的电平。一般应用在 I2C、SMBUS 通讯等需要"线与"功能的总线电路中。

7)、复用推挽输出

在这里插入图片描述

  • GPIO_AF_PP(alternate function open push—pull),片上外设输出功能(SPI的SCK、MISO、MOSI引脚等)
  • 输出数据寄存器 GPIOx_ODR 无效,输出的高低电平的来源于其它外设;
  • 施密特触发器打开,输入可用,通过输入数据寄存器可获取 I/O 实际状态,除了输出信号的来源改变,其他与推挽输出功能相同。应用于片内外设功能(I2C 的 SCL,SDA)等。
8)、复用开漏输出

在这里插入图片描述

  • GPIO_AF_OD(alternate function open drain),片上外设输出功能(硬件IIC的SDA、SCL引脚等)

  • 输出数据寄存器 GPIOx_ODR 无效,输出的高低电平的来源于片上外设;

  • 施密特触发器打开,输入可用,通过输入数据寄存器可获取 I/O 实际状态,除了输出信号的来源改变 其他与开漏输出功能相同。

2.3 GPIO 使用步骤

为了使用GPIO,需要在STM32F10x代码中进行以下 步骤

  1. 启用GPIO时钟:使用 RCC_APB2PeriphClockCmd 函数 启用特定GPIO管脚的时钟信号 以使其变得可用。
  2. 配置GPIO模式:使用 GPIO_InitTypeDef 结构体配置GPIO管脚的 输入/输出模式引脚速度引脚上下拉控制
  3. 读取或控制GPIO状态:使用 GPIO_ReadInputDataBitGPIO_SetBits 等函数读取或控制GPIO的引脚状态。
2.3.1 示例:小灯闪烁
#include "stm32f10x.h"

//延时函数
void  delay(unsigned int i)
{
    while(i--)
    {
        int j=10000;
        while(j--);
    }
}

// 经过原理图分析,PE5接D1 输出低电平小灯(LED1 发光二极管)亮,高电平不亮
int main()
{//RCC reset clock contrl
	//第一步,开相关片上外设时钟,这里GPIO,所有的GPIO外设都在APB2时钟线上
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,ENABLE);
	
	//第二步,相关GPIO初始化
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;	//引脚号
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz; //高低电平输出切换速度,10M
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //GPIO工作模式。一共有8种,其中4种输入和4种输出模式,配置成通用推挽输出
	GPIO_Init(GPIOE, &GPIO_InitStructure);
	
	while(1)
	{
		GPIO_ResetBits(GPIOE, GPIO_Pin_5); // 让PE5输出低电平
		
		delay(800);
			
		GPIO_SetBits(GPIOE, GPIO_Pin_5); // 让PE5输出高电平灭
		
		delay(800);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

3. 中断

3.1 概述

① 中断的概念
  • CPU执行程序时,由于发生了 某种 随机的事件(外部或内部),
  • 引起 CPU暂时停止正在运行的程序转去执行一段 特殊的服务程序(中断服务子程序或中断处理程序),以 处理该事件
  • 事件处理完 后 又 返回被中断的程序继续执行,这一过程称为中断。

引发中断的称为中断源。

② 中断的意义和作用
  • 1、实时控制:在确定的时间内,对相应的事件做出响应;
  • 2、故障处理:检测到故障,需要第一时间处理
  • 3、数据传输:不确定数据何时会来,如:串口接收数据

中断的意义:高效处理紧急程序,不会一直占用CPU资源。

③ STM32F103中断

STM32 中断非常强大,每个外设都可以产生中断,此处,异常就是中断,中断就是异常。

在这里插入图片描述

F103 在 内核 水平上搭载了一个异常响应系统, 支持为数众多的系统异常和外部中断。其中系统异常有 8 个(如果把 Reset 和 HardFault 也算上的话就是 10 个),外部中断有 60个。除了个别异常的优先级被定死外,其它异常的优先级都是可编程的。有关具体的系统异常和外部中断可在标准库文件 stm32f10x.h 这个头文件查询到,在 IRQn_Type 这个结构体里面包含了 F103 系列全部的异常声明

3.2 NVIC简介

NVIC(Nested Vector Interrupt Controller)是 嵌套向量中断控制器控制着整个芯片中断相关的功能

  • 它跟内核紧密耦合,是内核里面的一个外设;

  • NVIC支持:256个中断(16内核+240外部),支持256个优先级,允许裁剪,(STM32:内核10个+外部60个 中断,16个优先级);

  • 各个芯片厂商在设计芯片的时候会对 Cortex-M3 内核里面的 NVIC 进行裁剪,把不需要的部分去掉,所以说 STM32 的 NVIC 是 Cortex-M3 的 NVIC 的一个子集。

3.2.1 NVIC 寄存器简介

在固件库中,NVIC的结构体定义可谓是颇有远虑,给每个寄存器都预留了很多位,恐怕为的是日后扩展功能。不过 STM32F103 可用不了这么多,只是用了部分而已,具体使用了多少可参考《 Cortex-M3 内核编程手册》-4.3.11:NVIC 寄存器映射。

NVIC 结构体定义,来自固件库头文件:core_cm3.h

typedef struct { 
    __IO uint32_t ISER[8];       // 中断使能寄存器 
    uint32_t RESERVED0[24]; 
    __IO uint32_t ICER[8];       // 中断清除寄存器 
    uint32_t RSERVED1[24]; 
    __IO uint32_t ISPR[8];       // 中断使能悬起寄存器 
    uint32_t RESERVED2[24]; 
    __IO uint32_t ICPR[8];       // 中断清除悬起寄存器 
    uint32_t RESERVED3[24]; 
    __IO uint32_t IABR[8];       // 中断有效位寄存器 
    uint32_t RESERVED4[56]; 
    __IO uint8_t  IP[240];       // 中断优先级寄存器(8Bit wide) 
    uint32_t RESERVED5[644]; 
    __O  uint32_t STIR;          // 软件触发中断寄存器 
}  NVIC_Type;  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在配置中断的时候我们一般只用 ISER、ICER 和 IP 这三个寄存器,ISER 用来使能中断,ICER 用来失能中断,IP 用来设置中断优先级。

3.2.2 NVIC 中断配置固件库

固件库函数:

NVIC 库函数描述
void NVIC_EnableIRQ(IRQn_Type IRQn)使能中断
void NVIC_DisableIRQ(IRQn_Type IRQn)失能中断
void NVIC_SetPendingIRQ(IRQn_Type IRQn)设置中断悬起位
void NVIC_ClearPendingIRQ(IRQn_Type IRQn)清除中断悬起位
uint32_t NVIC_GetPendingIRQ(IRQn_Type IRQn)获取悬起中断编号
void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)设置中断优先级
uint32_t NVIC_GetPriority(IRQn_Type IRQn)获取中断优先级
void NVIC_SystemReset(void)系统复位

3.3 优先级

3.3.1 优先级简介
  • 抢占优先级(pre):高抢占优先级可以打断正在执行的低抢占优先级中断
  • 响应优先级(sub,子):当抢占优先级相同时,响应优先级高的先执行,但是不能互相打断
  • 抢占和响应都相同的情况下,自然优先级越高的,先执行
  • 自然优先级:中断向量表的优先级,数值越小,表示优先级越高

在 NVIC 有一个专门的寄存器:中断优先级寄存器 NVIC_IPRx,用来配置外部中断的优先级,IPR 宽度为 8bit,原则上每个外部中断可配置的优先级为 0~255,数值越小,优先级越高。但是绝大多数 CM3芯片都会精简设计,以致实际上支持的优先级数减少,在 F103中,只使用了高 4bit

在这里插入图片描述

3.3.2 优先级分组

优先级的分组由内核外设 SCB 的应用程序中断及复位控制寄存器 AIRCR 的PRIGROUP[10:8]位决定,F103 分为了 5 组,具体如下:主优先级=抢占优先级

在这里插入图片描述

主优先级位:表示抢占优先级的位数

子优先级位:表示响应优先级的位数

特别提示:一个工程中,一般只设置一次中断优先级分组。

判断执行顺序:抢占 > 响应 > 自然优先级

设置优先级分组可调用库函数 NVIC_PriorityGroupConfig() 实现,有关 NVIC 中断相关的库函数都在库文件 misc.c 和 misc.h 中。

中断优先级分组库函数 NVIC_PriorityGroupConfig() :

 /** 
	* 配置中断优先级分组:抢占优先级和子优先级 
	* 形参如下: 
	* @arg NVIC_PriorityGroup_0: 0bit for 抢占优先级 
	*                            4 bits for 子优先级 
	* @arg NVIC_PriorityGroup_1: 1 bit for 抢占优先级 
	*                            3 bits for 子优先级 
	* @arg NVIC_PriorityGroup_2: 2 bit for 抢占优先级 
	*                            2 bits for 子优先级 
	* @arg NVIC_PriorityGroup_3: 3 bit for 抢占优先级 
	*                            1 bits for 子优先级 
	* @arg NVIC_PriorityGroup_4: 4 bit for 抢占优先级 
	*                            0 bits for 子优先级 
	* @注意 如果优先级分组为 0,则抢占优先级就不存在,优先级就全部由子优先级控制 
*/ 
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup) 
{ 
    // 设置优先级分组 
    SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup; 
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

优先级分组真值表:

优先级分组主优先级子优先级描述
NVIC_PriorityGroup_000-15主-0bit,子-4bit
NVIC_PriorityGroup_10-10-7主-1bit,子-3bit
NVIC_PriorityGroup_20-303主-2bit,子-2bit
NVIC_PriorityGroup_30-70-1主-3bit,子-1bit
NVIC_PriorityGroup_40-150主-4bit,子-0bit

3.4 中断编程步骤

使能外设某个中断,这个具体由每个外设的相关中断使能位控制。

  • 比如:串口有发送完成中断,接收完成中断,这两个中断都由串口控制寄存器的相关中断使能位控制。

设置中断优先级分组,初始化 NVIC_InitTypeDef 结构体:

  • 配置中断优先级分组,设置抢占优先级和子优先级,使能中断请求。NVIC_InitTypeDef 结构体在固件库头文件 misc.h 中定义。
typedef struct
{
    uint8_t NVIC_IRQChannel;	//中断源
    uint8_t NVIC_IRQChannelPreemptionPriority://抢占优先级
    uint8_t NVIC_IRQChannelSubPriority://响应优先级
    FunctionalStale NVIC_IRQChannelCmd://中断使能或失能
} NVIC_InitTypeDef;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

编写中断服务函数

  • 在启动文件 startup_stm32f10x_hd.s 中我们预先为每个中断都写了一个中断服务函数(264行往下),只是这些中断函数都是为空,为的只是初始化中断向量表。实际的中断服务函数都需要我们重新编写,为了方便管理我们可以把中断服务函数统一写在 stm32f10x_it.c 这个库文件中。
  • 关于 中断服务函数的函数名必须跟启动文件里面预先设置的一样,如果写错,系统就在中断向量表中找不到中断服务函数的入口,直接跳转到启动文件里面预先写好的空函数,并且在里面无限循环,实现不了中断。

3.5 串口发送接收-示例

  • 接收:接收字符,当串口1每收到1个字节, CPU就会自动取调用对应的中断处理函数,将接收到的字符转小写,并发送;

  • 发送:固定发送字符串。

#include "stm32f10x.h"

void delay(unsigned int i)
{
	while(i--)
	{
		int j = 10000;
		while(j--);
	}
}
void Usart1_SendStr(char *pstr)
{
	while(*pstr != '\0')
	{
		USART_ClearFlag(USART1,USART_FLAG_TC); //清除发送完成标志,为下一个字节的发送做好判断准备工作
		
		USART_SendData(USART1, *pstr);
		pstr++;
		
		while(0 == USART_GetFlagStatus(USART1, USART_FLAG_TC)); //只要没有发送完成, USART_GetFlagStatus函数返回0,CPU就卡在这里
	}
}

void MyUsart_Init()
{
	// 1、开相关片上外设的时钟,GPIOA USART1(串口1)  PA9是TX  PA10是RX
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); 
	
	// 2、相关GPIO口 初始化
	GPIO_InitTypeDef GPIO_InitStructure; 
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; 
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz; 
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出 AF 复用 Alternate functions
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// 3、先关片上外设初始化 USART1
	// 波特率、数据位、停止位、是奇还是偶校验位、是否由硬件流构成、输入输出模式
	// 115200,8个数据位,1个停止位,没有奇偶校验,没有硬件控制流
	USART_InitTypeDef USART_InitStructure; 
	USART_InitStructure.USART_BaudRate = 115200; //反应每秒传输数据的bit数目,单位 bps bit per second 比特每秒
	USART_InitStructure.USART_WordLength = USART_WordLength_8b; 
	USART_InitStructure.USART_StopBits = USART_StopBits_1; 
	USART_InitStructure.USART_Parity = USART_Parity_No; 
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; 
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;  // 增加接收功能
	USART_Init(USART1, &USART_InitStructure); 
	
	// =============如果让  串口1增加接收功能=====begin=====
    // 设置优先级分组
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
    //初始化NVIC结构体
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; 	// 配置串口1为中断源
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;		// 使能中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;	// 抢占优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;			// 子优先级
	NVIC_Init(&NVIC_InitStructure);
	
	//当串口1每收到1个字节, CPU就会自动取调用对应的中断处理函数
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //这个函数类似Qt connect函数把某一个中断事件和对应中断处理函数关联起来
	
	// =============如果让  串口1增加接收功能=====end=====
	
	// USART1使能,即启动串口
	USART_Cmd(USART1,ENABLE);
}

//串口1中断接收函数 IT(interrupt)
void USART1_IRQHandler() //函数名不能乱写,一定要这个函数名,从启动文件264行往下找复制出来重写
{//当串口1关联的事件发送,CPU就会自动来调用这个函数
	if(USART_GetITStatus (USART1, USART_IT_RXNE))
	{//当因为 串口1 收到1个字节,CPU就会进入到这里
		u16 RxData;
		RxData = USART_ReceiveData (USART1);//读取刚刚收到的那个字节
		USART_SendData (USART1, RxData+32); //收到大写字母,把对应小写字母发送出去
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	}
}

//通过串口1 发送一个字符发送到计算机
int main()
{
	MyUsart_Init();
	while(1)
	{
		Usart1_SendStr("welcome to 嵌入式\n");
		
		delay(1000);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91

在这里插入图片描述

4. EXTI 外部中断/事件控制器

4.1 EXTI简介

  • EXTI(External interrupt/event controller)外部中断/事件控制器,管理了控制器的 20个中断/事件线

  • 每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测

    • 中断:要进入NVIC,有相应的中断服务函数,需要CPU处理
    • 事件:不进入NVIC,仅用于内部硬件自动控制的,如:TIM、DMA、ADC
  • 特性:

    • 每条EXTI 线都可以单独配置:选择类型(中断/事件)、触发方式(上升沿、下降沿或双边沿触发)、支持软件触发、开启/屏蔽、有挂起状态位

4.2 结构框图

在这里插入图片描述

EXTI 可分为两大部分功能,产生中断、产生事件,这两个功能从硬件上就有所不同。

首先我们来看上图中红色虚线指示的电路流程。它是一个 产生中断的线路,最终信号流入到 NVIC 控制器内

  • 编号 1 是 输入线,EXTI 控制器有 19 个中断/事件输入线,这些输入线可以通过寄存器设置为任意一个 GPIO,也可以是一些外设的事件。输入线一般是存在电平变化的信号。
  • 编号 2 是一个 边沿检测电路,它会根据 上升沿触发选择寄存器 (EXTI_RTSR) 和 下降沿触发选择寄存器(EXTI_FTSR)对应位的设置来控制信号触发。边沿检测电路以输入线作为信号输入端,如果检测到 有边沿跳变就输出有效信号 1 给编号 3 电路,否则输出无效信号0。而 EXTI_RTSR 和 EXTI_FTSR 两个寄存器可以控制器需要检测哪些类型的电平跳变过程,可以是只有上升沿触发、只有下降沿触发或者上升沿和下降沿都触发。
  • 编号 3 电路实际就是一个 或门电路
    • 一个输入来自编号 2 电路,
    • 另外一个 输入来自 软件中断事件寄存器(EXTI_SWIER)。
    • EXTI_SWIER允许我们通过程序控制就可以启动中断/事件线,这在某些地方非常有用。我们知道 或门的作用就是有 1 就为 1,所以这两个输入随便一个有有效信号 1 就可以输出 1 给编号 4 和编号 6 电路。
  • 编号 4 电路是一个 与门电路
    • 一个输入是编号 3 电路,
    • 另外一个输入来自中断屏蔽寄存器(EXTI_IMR)。与门电路要求输入都为 1 才输出 1,导致的结果是:
      • 如果 EXTI_IMR 设置为 0 时,那不管编号 3 电路的输出信号是 1 还是 0,最终编号 4 电路输出的信号都为 0;
      • 如果 EXTI_IMR 设置为 1时,最终编号 4 电路输出的信号才由编号 3 电路的输出信号决定,
      • 这样我们可以简单的控制 EXTI_IMR 来实现是否产生中断的目的。编号 4 电路输出的信号会被保存到 挂起寄存器(EXTI_PR) 内,如果确定编号 4 电路输出为 1 就会把 EXTI_PR 对应位置 1。
  • 编号 5 是将 EXTI_PR 寄存器内容 输出到 NVIC 内,从而实现系统中断事件控制。

接下来我们来看看绿色虚线指示的电路流程。它是一个产生事件的线路,最终输出一个脉冲信号。 产生事件线路是在编号 3 电路之后与中断线路有所不同,之前电路都是共用的。

  • 编号 6 电路是一个与门
    • 一个输入来自编号 3 电路,
    • 另外一个输入来自事件屏蔽寄存器(EXTI_EMR)。
      • 如果 EXTI_EMR 设置为 0 时,那不管编号 3 电路的输出信号是 1 还是 0,最终编号 6 电路输出的信号都为 0;
      • 如果 EXTI_EMR 设置为 1 时,最终编号 6 电路输出的信号才由编号 3 电路的输出信号决定,
      • 这样我们可以简单的控制 EXTI_EMR 来实现是否产生事件的目的。
  • 编号 7 是一个 脉冲发生器电路
    • 当它的输入端,即编号 6 电路的输出端,是一个有效信号 1 时就会产生一个脉冲;
    • 如果输入端是无效信号就不会输出脉冲。
  • 编号 8 是一个 脉冲信号,就是产生事件的线路最终的产物,这个脉冲信号可以给其他外设电路使用,比如 定时器 TIM模拟数字转换器 ADC 等等,这样的脉冲信号 一般用来触发 TIM 或者 ADC 开始转换
  • 产生中断线路目的是把输入信号输入到 NVIC,进一步会运行中断服务函数,实现功能,这样是软件级的。
  • 而产生事件线路目的就是传输一个脉冲信号给其他外设使用,并且是电路级别的信号传输,属于硬件级的。
  • 另外,EXTI 是在 APB2 总线上的,在编程时候需要注意到这点。

4.3 中断/事件线

EXTI 有 20 个中断/事件线,每个 GPIO 都可以被设置为输入线,占用 EXTI0 至 EXTI15,还有另外4根用于特定的外设事件,如下表。

在这里插入图片描述

4根特定外设中断/事件线由外设触发,具体用法参考《STM32F10X-中文参考手册》中对外设的具体说明。

EXTI 0 至 EXTI 15 用于 GPIO,通过编程控制可以实现任意一个 GPIO 作为 EXTI 的输入源。

4.4 EXTI 初始化结构体详解

标准库函数对每个外设都建立了一个初始化结构体,比如 EXTI_InitTypeDef,结构体成员用于设置外设工作参数,并由外设初始化配置函数,比如 EXTI_Init() 调用,这些设定参数将会设置外设相应的寄存器,达到配置外设工作环境的目的。

typedef struct { 
    uint32_t EXTI_Line;                 // 中断/事件线 
    EXTIMode_TypeDef EXTI_Mode;         // EXTI 模式 
    EXTITrigger_TypeDef EXTI_Trigger;   // 触发类型 
    FunctionalState EXTI_LineCmd;       // EXTI 使能 
} EXTI_InitTypeDef; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
    1. EXTI_Line:EXTI 中断/事件线选择,可选 EXTI0 至 EXTI19,可参考上表 选择。
    1. EXTI_Mode:EXTI 模式选择,可选为产生中断(EXTI_Mode_Interrupt)或者产生事件(EXTI_Mode_Event)。
    1. EXTI_Trigger:EXTI 边沿触发事件,可选上升沿触发(EXTI_Trigger_Rising)、下降 沿 触 发 ( EXTI_Trigger_Falling) 或 者 上 升 沿 和 下 降 沿 都 触 发( EXTI_Trigger_Rising_Falling)。
    1. EXTI_LineCmd:控制是否使能 EXTI 线,可选使能 EXTI 线(ENABLE)或禁用(DISABLE)。

4.5 AFIO 简介

AFIO:Alternate Function IO,即复用功能IO,主要用于重映射外部中断映射配置

外部中断配置:AFIO_EXTICR1~4,配置EXTI中断线0~15对应到哪个具体IO口

4.6 按键中断-示例

原理图:

在这里插入图片描述

KEY1 PE3 按钮按下低电平,设置输入线路下降沿为中断请求,即检测到下降沿触发中断,按下直至松手为一次

代码:

#include "stm32f10x.h"

void delay(unsigned int i)
{
	while(i--)
	{
		int j = 10000;
		while(j--);
	}
}
void Usart1_SendStr(char *pstr)
{
	while(*pstr != '\0')
	{
		USART_ClearFlag(USART1,USART_FLAG_TC); //清除发送完成标志,为下一个字节的发送做好判断准备工作
		
		USART_SendData(USART1, *pstr);
		pstr++;
		
		while(0 == USART_GetFlagStatus(USART1, USART_FLAG_TC)); //只要没有发送完成, USART_GetFlagStatus函数返回0,CPU就卡在这里
	}
}

void MyUsart_Init()
{
	// 1、开相关片上外设的时钟,GPIOA USART1(串口1)  PA9是TX  PA10是RX
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); 
	
	// 2、相关GPIO口 初始化
	GPIO_InitTypeDef GPIO_InitStructure; 
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; 
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz; 
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出 AF 复用 Alternate functions
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// 默认所有的管脚是 浮空输入模式
	GPIO_InitTypeDef GPIO_InitStructure1; 
	GPIO_InitStructure1.GPIO_Pin = GPIO_Pin_10; 
	GPIO_InitStructure1.GPIO_Speed = GPIO_Speed_10MHz; 
	GPIO_InitStructure1.GPIO_Mode = GPIO_Mode_AF_PP; 
	GPIO_Init(GPIOA, &GPIO_InitStructure1);
	
	// 3、先关片上外设初始化 USART1
	// 波特率、数据位、停止位、是奇还是偶校验位、是否由硬件流构成、输入输出模式
	// 115200,8个数据位,1个停止位,没有奇偶校验,没有硬件控制流
	USART_InitTypeDef USART_InitStructure; 
	USART_InitStructure.USART_BaudRate = 115200; //反应每秒传输数据的bit数目,单位 bps bit per second 比特每秒
	USART_InitStructure.USART_WordLength = USART_WordLength_8b; 
	USART_InitStructure.USART_StopBits = USART_StopBits_1; 
	USART_InitStructure.USART_Parity = USART_Parity_No; 
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; 
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;  // 增加接收功能
	USART_Init(USART1, &USART_InitStructure); 
	
	// =============如果让  串口1增加接收功能=====begin=====
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; // 串口1中断源
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
	NVIC_Init(&NVIC_InitStructure);
	
	//当串口1每收到1个字节, CPU就会自动取调用对应的中断处理函数
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //这个函数类似Qt connect函数把某一个中断事件和对应中断处理函数关联起来
	
	// =============如果让  串口1增加接收功能=====end=====

	// USART1使能,即启动串口
	USART_Cmd(USART1,ENABLE);
}

//串口1中断接收函数 IT(interrupt)
void USART1_IRQHandler() //函数名不能乱写,一定要这个函数名,从启动文件264行往下找复制出来重写
{//当串口1关联的事件发送,CPU就会自动来调用这个函数
	if(USART_GetITStatus (USART1, USART_IT_RXNE))
	{//当因为 串口1 收到1个字节,CPU就会进入到这里
		// unsigned char Revdata=
		
		//typedef uint16_t u16;
		//typedef unsigned short     int uint16_t;
		u16 RxData;
		RxData = USART_ReceiveData (USART1);//读取刚刚收到的那个字节
		USART_SendData (USART1, RxData+32); //收到大写字母,把对应小写字母发送出去
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	}
}

// 轮询(查询)法
//WKUP PA0 按钮按下高电平
//KEY0 PE4 按钮按下低电平
unsigned char WK_Up_Key0_Poll_Init()
{
	
}

//中断法
// KEY1   PE3   按钮按下低电平
void Key1_Inter_Init()
{
	//第一步开相关片上外设时钟,GPIOE,外部引脚中断,还需要开一个AFIO时钟,AFIO这个片上外设也是在APB2时钟线上
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE | RCC_APB2Periph_AFIO,ENABLE);


	//第二步,相关GPIO口初始化,填参数有3种方法,通过函数注释提示、通过函数体开头部分参数检查、通过实参类型定义找到
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IPU;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_3;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_10MHz;

	GPIO_Init(GPIOE,&GPIO_InitStruct);

	GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource3);//配置外部中断线

	//第三步 中断源NVIC配置
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel=EXTI3_IRQn;//外部中断,外部中断源3
	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority=0;		
	NVIC_Init(&NVIC_InitStructure);

	//第四部相关片上外设初始化
	EXTI_InitTypeDef EXTI_InitStruct;
	EXTI_InitStruct.EXTI_Line=EXTI_Line3;//外部中断线3
	EXTI_InitStruct.EXTI_LineCmd=ENABLE;
	EXTI_InitStruct.EXTI_Mode=EXTI_Mode_Interrupt;//中断模式,不是事件模式
	EXTI_InitStruct.EXTI_Trigger=EXTI_Trigger_Falling;//中断触发事件,下降沿信号		

	EXTI_Init(&EXTI_InitStruct);
}

unsigned int Key1_Val=0;
char Key1_buf[100]={0};
#include "stdio.h"
void EXTI3_IRQHandler()
{
	if(EXTI_GetITStatus(EXTI_Line3))
	{//CPU进入到这里
		delay(3);//一般超过10ms,避开抖动区

		if(0 == GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3))
		{//如果CPU进入到这里,说明过了10ms,PE3检测到任然是按钮按下状态,说是一次真实可靠按下
			Key1_Val++;
			sprintf(Key1_buf,"Key1_PE3 中断按钮被按下 Key1_Val=%d\n",Key1_Val);
			Usart1_SendStr(Key1_buf);
		}	 
	EXTI_ClearITPendingBit(EXTI_Line3);
	}
}

// 通过串口1 发送1个字符到计算机
int main()
{
 
	MyUsart_Init();
	Key1_Inter_Init();
	while(1)
	{
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/652010
推荐阅读
相关标签
  

闽ICP备14008679号