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
- APB1时钟线上,PCLK1时钟信号是
- 进入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
- AHB 的HCLK时钟信号
1.3 系统时钟的时钟源
1.3.1 时钟源
- STM32一共有
4个时钟源
,时钟源就是产生时钟信号(高低电平脉冲)的源头- 有2个高速晶振:
HSI
:高速时钟 内部高速时钟源,是一个由电阻和电容构成 RC振荡器,可以产生8M HzHSE
:芯片外部的高速时钟,这个是通常接外部石英晶振
,4~16M 之间,一般选择8M Hz
- 还有2个低速时钟:
LSI
:芯片内部 ,40K Hz,会受温度影响LSE
:外部低速时钟,32.768K Hz
- 有2个高速晶振:
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 只有
TIM1
、TIM8
这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、BOOT1GPIO引脚
:以P开头的 PA0~PA15…PG0-PG15
2.1 GPIO 框图
该图从最右端看起,最右端就是代表 STM32 芯片引出的 GPIO 引脚,其余左边部件都位于芯片内部。
保护二极管
:- IO 引脚上下两边两个二极管用于防止引脚外部过高、过低的电压输入,当引脚电压高于 VDD 时,上方的二极
管导通,当引脚电压低于 VSS 时,下方的二极管导通,防止不正常电压引入芯片导致芯片烧毁 。
- IO 引脚上下两边两个二极管用于防止引脚外部过高、过低的电压输入,当引脚电压高于 VDD 时,上方的二极
上拉、下拉电阻
:- 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 型号的芯片有
GPIOA
、GPIOB
、GPIOC
… 至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;
每个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代码中进行以下 步骤:
- 启用GPIO时钟:使用
RCC_APB2PeriphClockCmd
函数启用特定GPIO管脚的时钟信号
以使其变得可用。 - 配置GPIO模式:使用
GPIO_InitTypeDef
结构体配置GPIO管脚的输入/输出模式
、引脚速度
和引脚上下拉控制
。 - 读取或控制GPIO状态:使用
GPIO_ReadInputDataBit
和GPIO_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; //高低电平输出切换速度,10MGPIO_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);}
}
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;
在配置中断的时候我们一般只用 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;
}
优先级分组真值表:
优先级分组 | 主优先级 | 子优先级 | 描述 |
---|---|---|---|
NVIC_PriorityGroup_0 | 0 | 0-15 | 主-0bit,子-4bit |
NVIC_PriorityGroup_1 | 0-1 | 0-7 | 主-1bit,子-3bit |
NVIC_PriorityGroup_2 | 0-3 | 03 | 主-2bit,子-2bit |
NVIC_PriorityGroup_3 | 0-7 | 0-1 | 主-3bit,子-1bit |
NVIC_PriorityGroup_4 | 0-15 | 0 | 主-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;
③ 编写中断服务函数
- 在启动文件
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是RXRCC_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 functionsGPIO_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);}
}
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;
-
EXTI_Line
:EXTI 中断/事件线选择,可选 EXTI0 至 EXTI19,可参考上表 选择。
-
EXTI_Mode
:EXTI 模式选择,可选为产生中断(EXTI_Mode_Interrupt)或者产生事件(EXTI_Mode_Event)。
-
EXTI_Trigger
:EXTI 边沿触发事件,可选上升沿触发(EXTI_Trigger_Rising)、下降 沿 触 发 ( EXTI_Trigger_Falling) 或 者 上 升 沿 和 下 降 沿 都 触 发( EXTI_Trigger_Rising_Falling)。
-
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是RXRCC_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 functionsGPIO_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;//外部中断,外部中断源3NVIC_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;//外部中断线3EXTI_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){}
}