STM32(GPIO, 时钟, 中断, 定时)

319 阅读1小时+

1. 简介

STM32是ST公司基于ARM Cortex-M内核开发的32位微控制器

这里介绍的是:STM32F103C8T6

image.png

image.png

片上外设:

image.png

系统架构:

image.png

stm32 三种开发方式的区别

  • 寄存器模式:最底层的开发,运行速度最快。实际上也是使用了固件库,但是不是使用固件库的函数,而是使用了固件库的定义,包括宏定义,结构体定义。和51的开发差不多,但因为32的寄存器太多,实际开发手动配置大量寄存器极其耗费时间,同时在没有注释的情况下可读性差,所以较少使用。
  • 标准库模式:基于寄存器进行了函数的封装,而由于函数封装以及内部大量的检查参数有效性的代码,运行速度相对于寄存器模式较慢。封装之后可以根据函数名字就能明白代码作用,容易记忆,使用方便,所以较多人使用。
  • HAL库模式:全称是Hardware Abstraction Layer(抽象印象层),相比于标准库更加深入的封装,有句柄、回调函数等概念(有点类似Windows开发),因此相对于标准库模式有更好的可移植性(可在不同芯片的移植),但代价就是更多的性能损失。

2. 基于标准库创建STM32工程

image.png image.png

内核库文件分析

cor_cm3.h:这个头文件实现了:1、内核结构体寄存器定义 2、内核寄存器内存映射 3、内存寄存器位定义。跟处理器相关的头文件stm32f10x.h 实现的功能一样,一个是针对内核的寄存器,一个是针对内核之外,即处理器的寄器。

misc.h:内核应用函数库头文件,对应stm32f10x_xxx.h

misc.c:内核应用函数库文件,对应 stm32f10x_xxx.c。在 CM3 这个内核里面还有一些功能组 件,如 NVIC、SCB、ITM、MPU、CoreDebug,CM3 带有非常丰富的功能组件,但是芯片 厂商在设计 MCU 的时候有一些并不是非要不可的,是可裁剪的,比如 MPU、ITM 等在 STM32 里面就没有。其中 NVIC 在每一个 CM3 内核的单片机中都会有,但都会被裁剪,只能是 CM3 NVIC 的一个子集。在 NVIC 里面还有一个 SysTick,是一个系统定时器,可以提 供时基,一般为操作系统定时器所用。 misc.h 和 mics.c 这两个文件提供了操作这些组件的函数,并可以在 CM3 内核单片机 直接移植。

处理器外设库文件分析

startup_stm32f10x_md.s:这个是由汇编编写的启动文件,是 STM32 上电启动的第一个程序,启动文件主要实现了:

  1. 初始化堆栈指针SP
  2. 设置 PC 指针=Reset_Handler
  3. 设置向量表的地址,并初始化向量表,向量表里面放的是 STM32 所有中断函数的入口地址
  4. 调用库函数 SystemInit,把系统时钟配置成72M,SystemInit 在库文件stytem_stm32f10x.c中定义
  5. 跳转到标号_main,最终去到 C 的世界。

system_stm32f10x.c:这个文件的作用是里面实现了各种常用的系统时钟设置函数,有72M、56M、48M、36M、24M、8M,我们使用的是把系统时钟设置成72M

stm32f10x.h:这个头文件非常重要,这个头文件实现了:

  1. 处理器外设寄存器的结构体定义
  2. 处理器外设的内存映射
  3. 处理器外设寄存器的位定义(一个寄存器有很多个位,每个位写 1 或 者写 0 的功能都是不一样的,处理器外设寄存器的位定义就是把外设的每个寄存器的每一个位写 1 的 16 进制数定义成一个宏,宏名即用该位的名称表示,就不用自己亲自去算这个值是多少,可以直接到这个头文件里面找)

stm32f10x_conf.h: 文件include了所有的库函数头文件,同时在stm32f10x.h的最后又包含了conf,所以在使用这些库函数时,只需要包含stm32f10x.h这一个头文件,就相当于包含了所有的库函数头文件,这样就可以任意地调用库函数

stm32f10x_xxx.h:外设 xxx 应用函数库头文件,这里面主要定义了实现外设某一功能的结构体,比如通用定时器有很多功能,有定时功能,有输出比较功能,有输入捕捉功能,而通用定时器有非常多的寄存器要实现某一个功能,比如定时功能,我们根本不知道 具体要操作哪些寄存器,这个头文件就为我们打包好了要实现某一个功能的寄存器,是以机构体的形式定义的,比如通用定时器要实现一个定时的功能,只需要初始化 TIM_TimeBaseInitTypeDef 这个结构体里面的成员即可,里面的成员就是定时所需要操作的寄存器。有了这个头文件,我们就知道要实现某个功能需要操作哪些寄存器,然后 再回手册中查看这些寄存器的说明即可。

stm32f10x_xxx.c:外设 xxx 应用函数库,这里面写好了操作 xxx 外设的所有常用的函数,我们使用库编程的时候,使用的最多的就是这里的函数。

#include "stm32f10x.h" 

int main()
{
	// enbable clock
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
	
	// init GPIO_InitStructure
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 ;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	
	// init GPIOC
	GPIO_Init(GPIOC, &GPIO_InitStructure);
	
	// set GPIO_Pin_13
	GPIO_SetBits(GPIOC,GPIO_Pin_13); 
	
	while(1);
}

3. GPIO

GPIO(General Purpose Input Output):通用输入输出

  • 输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等。此外,还可以用GPIO来模拟通信协议,比如I2CC、spi或某个芯片特定协议,都可以用GPIO的输出模式来模拟其中的输出时序部分
  • 输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等。除此之外,模拟通信协议时,接收线上的通信数据,也是靠GPIO的输入来完成的

其实GPIO的本质就是芯片的一个引脚,通常在ARM中所有的I/O都是通用的。不过由于每个开发板上都会设计不同的外围电路,这就造成GPIO的功能可能有所不同,大部分GPIO都是有复用功能的,比如有些GPIO可能是串口的TX或RX,也可能是I2C的SCL或SDA线。

3.1 内部结构

image.png

3.2 引脚分布

image.png

以上特殊引脚是不能通过编程控制的,蓝灰色引脚普通引脚可以控制

image.png

3.3 八种输出模式

通过配置GPIO的端口配置寄存器,上面的位结构的电路就会根据配置进行改变

image.png

浮空、上拉、下拉输入

image.png

上面这两个电阻可以选择位上拉工作、下拉工作或者都不工作,对应的就是上拉输入、下拉输入和浮空输入,然后输入通过施密特触发器进行波形整形后,连接到输入数据寄存器

  1. 浮空输入:I/O的电平状态是不确定的,完全由外部输入决定,通常用于IIC、USART等总线设备上
  2. 上拉输入:I/O端口的电平信号直接进入输入数据寄存器。但是在I/O端口悬空(在无信号输入)的情况下,输入端的电平保持在高电平
  3. 下拉输入:I/O端口的电平信号直接进入输入数据寄存器。但是在I/O端口悬空(在无信号输入)的情况下,输入端的电平保持在低电平

一般来说是默认高电平,所以一般上拉输入用的比较多;若不确定外部模块输出的默认状态或外部信号输出功率非常小,这时就尽量选择浮空输入(浮空输入:没有上拉和下拉电阻去影响外部信号,缺点是当引脚悬空,没有默认的电平了,输入就会受噪声干扰,来回不断地跳变)

模拟输入

image.png

ADC模数转换器的专属配置,其他基本用不到

开漏输出和推挽输出

P·MOS管和N·MOS管: GPIO经过两个二极管的保护后向上流入输入模式,向下流入输出模式,而输出模式的控制是由一个由P·MOS管和N·MOS管组成的单元电路,该电路主要是控制输出的模式,该结构单元电路具有推挽输出开漏输出两种模式。 image.png

当系统配置为推挽输出模式时:

  • P·MOS导通,下方的N·MOS关闭,对外输出高电平,电流向外(推)
  • P·MOS关闭,下方的N·MOS导通,对外输出低电平,电流向内(挽)

当系统配置为开漏输出模式时:

  • P·MOS管完全不工作
  • 控制输出为1(它无法直接输出高电平)时,P·MOS关闭,N·MOS关闭,开漏输出模式下引脚既不输出高电平,也不输出低电平,为高阻态。(需外接一个上拉电阻提供高电平)image.png
  • 控制输出为0,低电平,P·MOS关闭,N·MOS导通,使输出接地image.png

开漏模式的作用:

  1. 提供的电压由上拉电阻决定,不由GPIO决定,可以适应不同电压的芯片
  2. 支持几个GPIO同时控制一个输入
image.png

复用开漏和复用推挽,这两模式和普通的开漏输出和推挽输出差不多,只不过是复用的输出,引脚电平是由片上外设控制的

3.4 GPIO相关寄存器

GPIOx_CRL、GPIOx_CRH

STM32的一组GPIO有16个IO口,比如GPIOA这一组,有GPIOA0~GPIOA15一共16个IO口。每一个IO口需要寄存器的4位用来配置工作模式。一组GPIO就需要16x4=64位的寄存器来存放这一组GPIO的工作模式的配置,但STM32的寄存器都是32位的,所以只能使用2个32位的寄存器来存放了。CRL用来存放低八位的IO口(GPIOx0—GPIOx7)的配置,CRH用来存放高八位的IO口(GPIOx8—GPIOx15)的配置。

这两个寄存器的全称是:端口配置低寄存器(GPIOx_CRL)端口配置高寄存器(GPIOx_CRH)

795cb3b531865d8dd56543149276430.jpg

e95a17f34bdbfea48c4b13d1e10b9eb.jpg

4位中又分为了CNFy和MODEy(y表示这组GPIO的第几个IO口)

比如配置上拉(下拉)输入模式,那么4位寄存器的配置就是CNFy[10]MODEy[00]:1000换成十进制数就是8

GPIOA->CRL&=0XFFFFFFF0;
GPIOA->CRL|=8;

GPIOx_IDR、GPIOx_ODR

端口输入数据寄存器 GPIOx_IDR

读取GPIO端口引脚的信号电平值。该寄存器只有0到15位有效,每一位就是对应的pin值

image.png

端口输出数据寄存器 GPIOx_ODR

可以设置GPIO端口引脚的信号值。前提是该引脚为普通的IO输出引脚。该寄存器只有0到15位有效,每一位就是对应的pin值

image.png

GPIOx_BSRR

端口置位/复位寄存器 GPIOx_BSRR

可以通过写入GPIOx_BSRR寄存器值,可以对GPIOx_ODR的对应位进行置位和复位。既然GPIOx_ODR能控制管脚高低电平,为什么还需要GPIOx_BSRR寄存器?

原因是GPIOx_BSRR去改变管脚状态的时候是原子操作置位/复位,没有被中断打断的风险。也就不需要关闭中断,关闭中断明显会延迟或丢失一事件的捕获,所以控制GPIO的状态最好可以用GPIOx_BSRR。该寄存器的0到15位为置位功能,16到31位为复位功能。

  • 设置GPIOA_BSRR的BS0值为1,相当于输出GPIOA的pin0管脚值为1高电平信号;
  • 设置GPIOA_BSRR的BR0值为1,相当于输出GPIOA的pin0管脚值为0低电平信号。
image.png

3.5 按键控制LED

image.png

LED外设初始化、LED控制

GPIOX初始化:

  1. 初始化GPIOX时钟
  2. GPIOX配置
  3. 初始化GPIOX

led控制

// 因为led正极接在电源正极上,所以led低电平点亮
GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 点亮
GPIO_SetBits(GPIOA, GPIO_Pin_0); // 熄灭

// 或者
GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);

led.c

#include "stm32f10x.h"

/**
  * @brief  Initialize pin0 and pin1 of GPIOA.
  * @param  void.
  * @retval void.
  */
void led_init(void)
{
	// enbable clock
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	// init GPIO_InitStructure
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // Push–pull output
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	
	// init GPIOA
	GPIO_Init(GPIOA, &GPIO_InitStructure);
}

/**
  * @brief  Light the led of the pin0.
  *         portLow-level lighting
  * @param  void.
  * @retval void.
  */
void led0_on(void)
{
	GPIO_ResetBits(GPIOA,GPIO_Pin_0);
}

void led0_off(void)
{
	GPIO_SetBits(GPIOA,GPIO_Pin_0);
}

/**
  * @brief  swap led0 status
  * @param  void.
  * @retval void.
  */
void led0_swap(void)
{
	uint8_t change_bit = !GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_0);
	GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)change_bit);
}

void led1_on(void)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_RESET);
}

void led1_off(void)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_SET);
}

void led1_swap(void)
{
	uint8_t change_bit = !GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_1);
	GPIO_WriteBit(GPIOA, GPIO_Pin_1, (BitAction)change_bit);
}

按键初始化、获取按键值

#include "stm32f10x.h"
#include "Delay.h"

/**
  * @brief  Initialize pin1 and pin11 of GPIOB.
  * @param  void.
  * @retval void.
  */
void key_init(void)
{
	// enbable clock
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	// init GPIO_InitStructure
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // Pull up input
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	
	// init GPIOA
	GPIO_Init(GPIOB, &GPIO_InitStructure);
}

/**
  * @brief  get pin1 value of GPIOB.
  *         Connect power negative, when the button press input low level
  * @param  void.
  * @retval Press down: 1, else 0.
  */
uint8_t key1_getNum(void)
{
	uint8_t key_num = 0;
	if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == Bit_RESET)
	{
		Delay_ms(20);
		// Keys eliminate jitter
		while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == Bit_RESET);
		Delay_ms(20);
		key_num = 1;
	}
	else
	{
		key_num = 0;
	}
	return key_num;
}

uint8_t key2_getNum(void)
{
	uint8_t key_num = 0;
	if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == Bit_RESET)
	{
		Delay_ms(20);
		// Keys eliminate jitter
		while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == Bit_RESET);
		Delay_ms(20);
		key_num = 1;
	}
	else
	{
		key_num = 0;
	}
	return key_num;
}

main.c

#include "stm32f10x.h" 
#include "Delay.h"
#include "LED.h"
#include "KEY.h"

int main()
{
	led_init();
	key_init();
	
	while(1)
	{
		if(key1_getNum() == 1)
		{
			led0_swap();
		}
		if(key2_getNum() == 1)
		{
			led1_swap();
		}
	}
}

4. 时钟

时钟是由电路产生的具有周期性的脉冲信号,相当于单片机的心脏,要想使用单片机的外设必须开启相应的时钟

为什么需要时钟?

STM32时钟系统主要的目的就是给相对独立的外设模块提供时钟,主要也是为了降低整个芯片的功耗,所有外设时钟默认都是关闭状态(disable)当我们使用某个外设就要开启这个外设的时钟(enable),不同外设需要的时钟频率不同,没必要所有外设都用高速时钟造成浪费,而且有些外设也接受不了这么高的频率,这也是为什么STM32有四个时钟源的原因,就是兼容不同速度的外设,STM32的四个时钟源分别为:HSI、HSE、LSI、LSE

  • HSE:High Speed External Clock signal,即高速外部时钟。无源晶振(4-16M),通常使用8M
  • HSI:High Speed Internal Clock signal,即高速内部时钟。大小为8M,当HSE故障时,系统时钟 会自动切换到HSI,直到HSE启动成功
  • LSE: low Speed External Clock signal,即低速外部时钟。LSE晶体是一个32.768kHz的低速外部晶体或陶瓷谐振器。它为实时时钟或者其他定时功能提供一个低功耗且精确的时钟源
  • LSI: low Speed Internal Clock signal,即低速内部时钟。LSI时钟频率大约40kHz(在30kHz和60kHz之间)。可以在停机和待机模式下保持运行,为独立看门狗和自动唤醒单元(RTC)提供时钟

4.1 时钟树

image.png 上面这个图看起来比较复杂,可以先把上面的图抽象成下面两个树进行分析:

image.png

SYSCLK

系统时钟为整个芯片提供了时序信号,最高为72M(ST官方推荐的),AHB总线将来自系统时钟的信号进行分频或不分频通过总线再分给其他外设、系统内核时钟或者APB1、APB2上的时钟;

SYSCLK有三种来源:

  1. HSI 8MHZ不分频,作为系统时钟
  2. HSE 8MHZ不分频,作为系统时钟
  3. PLL(锁相环,对输入信号进行2-16倍频),作为系统时钟(常用、灵活、范围宽)
image.png

尝试将SYSCLK配置为72MHZ的几种方式:

image.png

外设的使能和禁止

使用片上外设之前,需要给外设开启对应时钟

例如开启GPIOC的时钟,因为GPIOC对应APB2总线

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 使能GPIOC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, DISABLE); // 禁止GPIOC时钟

外设的复位

由于片上外设都是时序电路,内部有许多记忆性原件,复位就是把这些记忆的信息清除,让片上外设回到最初的状态

image.png

复位后,上图圈起来的寄存器的内容就会被清空

4.2 RCC与时钟树编程

RCC: Reset and Clock Control

RCC也是一个片上外设,控制芯片复位、对时钟系统进行配置

RCC标准库接口

image.png

// 时钟源
void RCC_HSEConfig(uint32_t RCC_HSE); // HSE开关
void RCC_HSICmd(FunctionalState NewState); // HSI开关
// PLL配置
void RCC_PLLConfig(uint32_t RCC_PLLSource, uint32_t RCC_PLLMul); // 时钟来源、倍频系数
void RCC_PLLCmd(FunctionalState NewState); // PLL开关
// SYSCLK来源
void RCC_SYSCLKConfig(uint32_t RCC_SYSCLKSource); 

void RCC_HCLKConfig(uint32_t RCC_SYSCLK); // 配置HCLK
void RCC_PCLK1Config(uint32_t RCC_HCLK); // 配置PCLK1开关
void RCC_PCLK2Config(uint32_t RCC_HCLK); // 配置PCLK2开关
// 获取RCC状态
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);

// 开启外设时钟
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
void RCC_APB2PeriphResetCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void RCC_APB1PeriphResetCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);

CPU频率调节

Cortex-M3就是所说的CPU,它是挂载到AHB总线上的,配置HCLK就行

CPU频率,通常以Hz为单位,表示处理器每秒钟可以执行的周期数,一个周期可以执行一条简单汇编指令。

image.png

上面这个for循环,每执行依次循环,消耗6个周期,全部执行完需要消耗12000000*6 = 72000000个周期,如果时钟频率为72MHz的话,执行完这个for循环就需要1s

系统时钟初始状态

  • SYSCLK=HSI
  • 分频器的系数都是1
  • 所有关键节点的频率都是8M
  • 大部分片上外设的时钟处于关闭状态

所以初始状态下CPU的时钟频率是8MHZ,但是执行main函数前模板工程会执行SystemInit初始化单片机的频率设置为最高值72MHZ

image.png

把SystemInit注释掉后,时钟恢复默认值,这个时候用for循环表示1s(8000000/6=1333333):

for(i=0; i<1333333; i++); // 延迟1s

设置Flash参数

为什么要设置?

  1. 写的所有程序都放在flash里面,CPU从flash中获取代码并执行
  2. 当SYSCLK改变的时候,Flash参数需要做相应的改变
  3. Flash参数改变必须在SYSCLK<=8MHz的时候改变

设置步骤:

  1. 开启指令预取(CPU从flash取一条指令执行的时候,会再预取下一条需要执行的指令,达到无缝执行)
  2. 设置flash访问延迟(flash是通过总线矩阵和cpu连接的,cpu和总线都是高速设备,flash是一个低速外设,当SYSCLK速度超过flash的时候需要停下来等待flash)

注:(只有SYSCLK<8MHz的时候才能设置)

  • 当SYSCLK<=24MHz时,不需要等待
  • 当SYSCLK<=48MHz时,需要等待1个周期
  • 当sYSCLK<=72MHz时,需要等待2个周期

配置时钟树 启动前注释SystemInit

#include "stm32f10x.h" 
#include "LED.h"

int main()
{
	// 开启指令预取、设置等待延迟
	FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);
	FLASH_SetLatency(FLASH_Latency_2);
	
	// 开启HSE
	RCC_HSEConfig(RCC_HSE_ON);
	while(RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET);
	
	// 配置PLL
	RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); // 8 * 9 = 72MHz
	RCC_PLLCmd(ENABLE);
	while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
	
	// 配置SYSCLK
	RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
	
	// 产生HCLK, PCLK1, PCLK2
	RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB 	72MHz
	RCC_PCLK1Config(RCC_SYSCLK_Div2);// APB1	36MHz
	RCC_PCLK2Config(RCC_SYSCLK_Div1);// APB2	72MHz
	
	// 关闭HSI
	RCC_HSICmd(DISABLE);
	
	led_init();
	uint32_t i = 0;
	while(1)
	{
		led0_on();
		for(i=0; i<12000000; i++); // 延迟1s
		led0_off();
		for(i=0; i<12000000; i++); // 延迟1s
	}
}

5. 中断

为什么需要中断?

对单片机系统来说,中断至关重要。比如要检测按键是否按下,如果没有中断,则需要循环的方式不断的去检测按键对应的IO口的电平,这是比较耗费CPU的时间的。如果要检测的更多的话,CPU有可能会导致阻塞。有了中断事情就变的简单了,主程序不需要循环不断的去检测按键,当有按键按下的时候,CPU 执行被打断,去执行按键处理程序就行了。当没有按键按下的时候,CPU 完全可以正常执行代码,丝毫不受任何的影响。

5.1 stm32中断体系架构

Cortex-M3内核支持256个中断,其中包含了16个内核中断和240个外部中断,并且具有 256 级的可编程中断设置。 一般情况下,芯片厂商会对Cortex-M3的中断进行裁剪。

STM32F103系列70个中断有10个内核中断和60个可编程的外部中断。

image.png

5.2 NVIC

NVIC (Nested vectored interrupt controller 嵌套向量中断控制器)和处理器核的接口紧密相连,可以实现低延迟的中断处理和高效地处理中断。NVIC管理着包括内核异常,外部中断等所有中断。由NVIC决定哪个中断的处理程序交给CPU来执行。

每一个外部中断都可以被使能或者禁止,并且可以被设置为挂起状态或者清除状态。处理器的中断可以是电平形式的,也可以是脉冲形式的,这样中断控制器就可以处理任何中断源。16个IO的中断与PVD(电源电压检测),RTC(实时时钟),USB,以太网检测这20个外部中断会通过EXTI来控制,然后交给NVIC。其他中断都是直接交给NVIC来处理。

image.png

中断优先级

NVIC为了方便管理中断,通过给每个中断设置优先级。NVIC用4个位来控制优先级

把优先级分为两种:抢占优先级响应优先级(子优先级)

  • 优先级值越小,优先级越高
  • 如果不设置优先级,则默认优先级为0
  • 先比较抢占优先级,抢占优先级高的可以打断抢占优先级低的
  • 若抢占优先级一样,再比较响应优先级。但是响应优先级不会导致中断嵌套
  • 若抢占优先级一样的同时挂起,则优先处理响应抢占优先级高的
  • 若挂起的优先级(抢占和响应)都一样,则查找中断向量表,值小的先响应

NVIC对优先级分了5组,在程序中先对中断进行分组,而且分组只能分一次,若多次分,只有最后一次生效

image.png

中断向量表

Flash存储器内部从地址0开始的一段区域,按照中断号排列,每4个字节存储一个中断响应函数的地址

image.png

灰色是内核中断,其他的是外设中断 image.png

5.3 EXTI

EXTl(ExternalInterrupt/Event Controller)-外部中断/事件控制器

stm32的众多片上外设之一,能够检测外部输入信号的变化边沿并由此产生中断,或软件触发中断

工作原理

image.png

每个GPIO外设有16个引脚,所以进来16根线;如果每个引脚占用一个通道,那EXTI的16个通道是不够用的,所以GPIO和EXTI中间会有一个AFIO中断引脚选择的电路模块,这个AFIO就是一个数据选择器(对于PA0\PB0\PC0这些,通过AFIO选择之后只有其中一个能接到EXTI的通道0上),然后通过AFIO选择后的16个通道,就能接到了EXTI边沿检测及控制电路上,同时下面这4个蹭网的外设(PVD\PTC\USB\ETH)也是并列接进来的,这些加起来就组成了EXTI的20个输入信号,然后经过EXTI电路之后,分为了两种输出,也就是中断响应和事件响应(上面接到了NVIC用来触发中断,下面有20条输出线路到了其它外设,也就是事件响应)

image.png
  • 中断:是一种硬件机制,用于通知‌CPU发生了某些紧急情况,需要立即处理
  • 事件:不会触发中断,而是触发别的外设操作,属于外设之间的联合工作。外部中断的信号不会通向CPU而是通向其它外设,用来触发其它外设的操作,比如触发ADC转换、触发DMA等

内部电路

EXTI的右边就是20根输入线,然后输入线首先进入边沿检测电路,在上面的上升沿寄存器和下降沿寄存器可以选择是上升沿触发还是下降沿触发或者两个都触发,接着硬件触发信号和软件中断寄存器的值就进入到这个或门的输入端(也就是任意一个为1,或门就可以输出1),然后触发信号通过这个或门后就兵分两路,上一路是触发中断的,下一路是触发事件的:

  1. 上一路是触发中断首先会置一个挂起寄存器(挂起寄存器相当于一个中断标志位,可以读取这个寄存器判断是哪个通道触发的中断,如果挂起寄存器置1,它就会继续向左走和中断屏蔽寄存器共同进入一个与门(与门实际上就是开关控制作用,中断屏蔽寄存器给1那另一个输入就是输出,也就是允许中断;中断屏蔽寄存器给0,那另一个输入无论是什么,输出都是0,相当于屏蔽了这个中断),然后是NVIC中断控制器
  2. 接着就是下一路的选择是触发事件,首先也是一个事件屏蔽寄存器进行开关控制,最后通过一个吗,脉冲发生器到其它外设(脉冲发生器就是给一个电平脉冲,用来触发其它外设的动作)

补充:框图最上面两个就是外设接口和APB总线,我们可以通过总线访问这些寄存器

image.png

5.3 对射式红外传感器计次

image.png
#include "stm32f10x.h" 

uint16_t countsensor_count; // 计数器

void CountSensor_init(void)
{
	// 1. 时钟配置
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // GPIOB 外设
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // AFIO 外设
	// (EXTI 外设时钟默认开启,且不可被关闭)
	
	// 2. 配置GPIO
	GPIO_InitTypeDef GPIO_initstruct;
	GPIO_initstruct.GPIO_Mode = GPIO_Mode_IPU; //对于EXTI来说,模式可以选择 浮空输入|上拉输入|下拉输入(可以看参考手册中的外设GPIO配置)
	GPIO_initstruct.GPIO_Pin = GPIO_Pin_14;
	GPIO_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_initstruct);
	
	// 3. 配置AFIO(库函数在stm32f10x_gpio.h)
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14); // 选择gpiob的pin14作为外部中断源
	
	// 4. 配置EXTI(库函数在stm32f10x_exti.h)
	EXTI_InitTypeDef EXTI_InitStruct;
	EXTI_InitStruct.EXTI_Line = EXTI_Line14; // 连接PB14号口的第14个中断线路
	EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式
	EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 因为上面是GPIO_Mode_IPU设置为高电平,所以触发中断是下降
	EXTI_InitStruct.EXTI_LineCmd = ENABLE;
	EXTI_Init(&EXTI_InitStruct);
	
	// 5. 配置NVIC(NVIC是内核外设,库函数在misc.h)
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 优先级分组 2位抢占 2位响应, 一个工程只需要执行执行一次
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; // 指定中断通道,external line[10-15]合并
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //选择了分组2,抢占优先级和响应优先级的范围都是0-3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 因为这个程序只有一个,所以中断优先级的配置可以随意
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
}

// 中断函数,每个中断通道对应一个通道函数,并且函数名固定
// 中断函数不需声明,它是自动执行的
void EXTI15_10_IRQHandler(void)
{
	// 一般都是先进行一个中断标志位的判断,确保是想要的中断源触发的函数,
	// 因为这个函数EXTI10到EXTI15都能进来,所以要先判断一下是不是想要的EXTI14进来的
	if(EXTI_GetFlagStatus(EXTI_Line14) == SET)
	{
		countsensor_count++;
		EXTI_ClearITPendingBit(EXTI_Line14); //每次中断函数结束后,都应该清除一下中断标志位, 不然会一直触发
	}
}

// 红外传感器接pb14,遮挡一次计数加一
uint16_t countsersor_get(void)
{
	return countsensor_count;
}

6. 定时器

定时器是专门负责定时功能的片上外设,定时触发中断,本质上是一个计数器

stm32的定时器拥有

  • 计数器:用来执行计数定时的寄存器,每来一个时钟,计数器加1
  • 预分频器:可以对计数器的时钟进行分频,让计数更加灵活
  • 自动重装寄存器:是计数的目标值,计多少个时钟申请中断

定时器可以对输入的时钟进行计数,如果主频是72HZ,计数72次就说明是一秒了。上面三个寄存器都是16位的,在72MHz计数时钟下可以实现最大59.65s的定时(72M/65536/65536取倒)。

预分频值(PSC)、自动重装载值(ARR)
定时器的计数频率 = 时钟频率 / (PSC + 1)
最大定时时间 = (ARR + 1)/ 定时器的计数频率
即,最大定时时间 = (ARR + 1) * (PSC + 1) / 时钟频率

定时器不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能

6.1 定时器类型

类型编号总线功能
高级定时器TIM1、TIM8APB2拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能
通用定时器TIM2、TIM3、TIM4、TIM5APB1拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能
基本定时器TIM6、TIM7APB1拥有定时中断、主模式触发DAC的功能

STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4

基本定时器

下面这三个构成了最基本的计数计时电路,这一块电路就叫做时基单元

内部时钟的来源是RCC_TIMxCLK,频率值是系统的主频72MHz,所以通向时基单元的计数基准频率就是72MHz

  1. 进入时基单元首先是预分频器(PSC),它可以对72MHz的计数时钟进行预分频
  2. 计数器,对预分频后的计数时钟进行计数,计数时钟每来一个上升沿,计数器的值加1
  3. 自动重装寄存器存的是我们写入的计数目标,在运行的过程中,计数值不断自增,自动重装载是固定的目标,当计数值等于自动重装值时,那它就会产生一个中断信号,并且清零计数器
image.png

通用定时器

中间最核心的部分,还是时基单元,对于通用定时器而言,计数器的计数模式就不止向上计数一种了(向上自增),通用定时器和高级定时器支持向上计数模式、向下计数模式和中央对齐模式(先向上自增,计到重装值,申请中断,然后再向下自减,减到0,再申请中断)。

image.png

对于通用定时器,时钟源可以选择内部时钟(72MHz)或者外部时钟

外部时钟的选择有如下四种:

第一个外部时钟就是来自TIMx_ETR引脚上的外部时钟,可以在TIM2的ETR引脚也就是PA0上接一个外部方波时钟,然后配置一下内部的极性选择、边沿检测和预分频器电路,再配置一下输入滤波电路,这两块电路可以对外部时钟进行一定的整形,兵分两路,上面一路ETRF进入触发控制器,紧跟着就可以选择作为时基单元的时钟(外部时钟模式2,如图红线);另一路与其他信号通过一个数据选择器输出TRGI(Trigger In,触发输入)(外部时钟模式1,如图黄线),它主要是作为触发输入来使用的,这个触发输入可以触发定时器的从模式。

image.png

第二个外部时钟可以是来自其他定时器的信号ITR,主模式的输出TRGO可以通向其他定时器,实际上通向的就是ITR引脚,通过这一路就可以实现定时器级联的功能。如上如黄线所示,ITR0到ITR3分别来自其他4个定时器的TRGO输出,这就是ITR和定时器的连接关系。例如,可以先初始化TIM3,然后使用主模式把它的更新事件映射到TRGO上,接着再初始化TIM2,选择ITR2对应的就是TIM3的TRGO,然后后面再选择时钟为外部时钟模式1,这样TIM3的更新事件就可以驱动TIM2的时基单元,也就是实现了定时器的级联

image.png

第三个外部时钟可来自TIMx_CH1的TI1_ED,CH1引脚的边沿,即从CH1引脚连接的输入捕获模块获得时钟,ED意为Edge,意为通过这一路的时钟,上升沿和下降沿均有效。

image.png

第四个外部时钟可来自TIMx_CH1的TI1FP1和来自TIMx_CH2的TI2FP2

编码器接口

image.png

它可以读取正交编码器的输出模型

输出比较

通用定时器结构图的右下角即为定时器的输出比较功能的结构。有四个输出通道,分别对应CH1到CH4的引脚,可以用来输出PWM波形,驱动电机

image.png

输入捕获

通用定时器的左下角即为输入捕获电路的结构图,它同输出比较功能一样有四个通道,对应CH1到CH4。可以用于测量输入方波的频率。因为输入捕获和输出比较不能同时使用,故中间的捕获/比较寄存器是输入捕获和输出比较电路共用的,CH1到CH4的引脚也是共用的

image.png

6.2 定时中断和内外时钟源选择

定时中断基本结构

image.png

时基单元运行时序

缓冲(影子)寄存器

带黑色阴影的寄存器,都是有影子寄存器这样的缓冲机制,包括预分频器,自动重装载寄存器和捕获比较寄存器。

为了保证能适用于多种多样的情况,故对时序运行过程中突然手动更改寄存器对时序的影响作了严谨的设计。这里引入缓冲(影子)寄存器,主要目的就是同步,即可以让寄存器设定的某些目标值的变化和更新事件同时发生,防止在运行途中更改造成错误。

这个缓冲寄存器是否启用,可以自己设置。

预分频器时序

image.png
  • 第一行是CK_PSC是预分频器的输入时钟,这个时钟在不断运行
  • 下面的CNT_EN是计数器使能,高电平计数器正常运行,低电平计数器停止
  • 再下面是CK_CNT是计数器时钟既是预分频器的时钟输出也是计数器的时钟输入

开始时,计数器未使能,计数器时钟不运行;然后使能后,前半段,当计数器使能信号CNT_EN变为高电平后的下一个CK_PSC的高电平,定时器时钟CK_CNT接收CK_PSC。且此时预分频器的分频系数为1,PSC = 0,预分频器完成一分频,计数器时钟等于预分频前的时钟,即,CK_PSC = CK_CNT;后半段,预分频系数变为2,计数器时钟变为预分频前时钟的一半。在计数器时钟的驱动下,下面的计数器寄存器也跟随时钟的上升沿不断自增;当计数器寄存器的值依次递增达到0xFC后立即跳变为0x00,说明重装载寄存器ARR设计的目标计数值就是0xFC,此时电路产生一个更新事件脉冲信号UEV,并产生中断信号,计数值清0,这就是一个计数周期的工作流程。

最下面的三行时序,描述的是预分频寄存器的一种缓冲机制,也就是这个预分频寄存器实际上是有两个:一个是倒数第三行的预分频控制寄存器,供读写用并不直接决定分频系数;另一个是倒数第二行的预分频缓冲寄存器(影子寄存器),才是真正起作用的寄存器,在更新事件信号之前在TIMx_PSC中写入新数值,将预分频器的分频系数从1改为2,但是由于缓冲寄存器的存在,CK_CNT不会立即变为CK_PSC / 2,而是在下一次更新中断产生的同时,由预分频缓冲器(影子寄存器)修改分频系数为2,PSC = 1。由预分频计数器时序可以看到,预分频的分频功能实际上也是通过计数器来实现的。当分频系数变为2后,预分频计数器按0、1、0、1依次计数,每当预分频计数器回到0时,预分频器输出信号,CN_CNT输出一个脉冲。

计数器时序

image.png

在计数中途,若突然将自动加载寄存器计数目标由F5改成了36,下面影子寄存器才是真正起作用的,它还是F5,所以现在计数的目标还是计到F5,产生更新事件,同时,要更改的36才被传递到影子寄存器,在下一个计数周期这个更改的36才有效,所以引入影子寄存器的目的实际上是为了同步,就是让值的变化和更新事件同步发生,防止在运行途中更改造成错误。

若不用影子寄存器的话,更改TIMx_ARR寄存器的值有一种不严谨情况:当F5改到36立即生效,但此时计数器已经到了F1,已经超过36了,F1只能增加,但它的目标值却是36比F1小,此时计数器寄存器的值只能递增,故该寄存器会一直递增到最大值0xFFFF之后回到0x0000,再依次递增,再加到36,才能产生更新。

定时器触发中断示例程序

定时器计次(内部时钟)

#include "stm32f10x.h"                  // Device header
 
/*
定时器初始化
对应定时中断结构图
 
第一步,RCC开启时钟,定时器的基准时钟和整个外设的工作时钟就都打开了
第二步,选择时基单元的时钟源,对于定时中断就选择内部时钟源
第三步,配置时基单元,包括预分频器、自动重装载器、计数模式等,参数用结构体配置
第四步,配置输出中断控制,允许更新中断输出到NVIC
第五步,配置NVIC,在NVIC中打开定时器中断通道并分配一个优先级
第六步,运行控制,使能计数器,当定时器使能后,计数器就开始计数了,当计数器更新时,触发中断
最后再写一个中断函数,中断函数每隔一段时间就能自动执行一次
 
*/
void Timer_Init(void)  //定时中断初始化代码
{
	//初始化tim2,也就是通用定时器
	//使用APB1的开启时钟函数,TIM2是APB1总线的外设
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
	
	//选择时基单元的时钟,选择内部时钟;若不调用这个函数,系统上电也是默认是内部时钟
	TIM_InternalClockConfig(TIM2);
	
	//配置时基单元 
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;  //指定时钟分频
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式
	/*
	定时1s也就是定时频率为1Hz,定时频率=72M/ (PSC + 1) / (ARR + 1) = 1s =1Hz,
	那就可以PSC给7200,ARR给10000(1MHz等于10^6Hz),然后两个参数再减1
	在这里预分频是对72M进行7200分频,得到的就是10k的计数频率,
	在10k的频率下,计10000个数,就是1s的时间
	*/
	TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;  //ARR
	TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;  //PSC
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;  //重复计数器的值
	TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
	
	/*
	在TIM_TimeBaseInit函数的最后,会立刻生成一个更新事件,来重新装载预分频器和重复计数器的值
	预分频器有缓冲寄存器,我们写入的PSC和ARR只有在更新事件时才会起作用
	为了让写入的值立刻起作用,故TIM_TimeBaseInit函数的最后手动生成了一个更新事件
        
	但是更新事件和更新中断是同时发生的,更新中断会置更新中断标志位,手动生成一个更新事件,就相当于在初始化时立刻进入更新函数执行一次
	在开启中断之前手动清除一次更新中断标志位,就可以避免刚初始化完成就进入中断函数的问题
	*/
	TIM_ClearFlag(TIM2,TIM_FLAG_Update);
	
	//使能中断
	TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //开启更新中断到NVIC的通道
	
	//NVIC中断
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//NVIC优先级分组
	NVIC_InitTypeDef NVIC_InitTyStructure;
	NVIC_InitTyStructure.NVIC_IRQChannel = TIM2_IRQn;
	NVIC_InitTyStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitTyStructure.NVIC_IRQChannelPreemptionPriority = 2;//抢占优先级
	NVIC_InitTyStructure.NVIC_IRQChannelSubPriority = 1;//响应优先级
	NVIC_Init(&NVIC_InitTyStructure); 
	
	//启动定时器
	TIM_Cmd(TIM2,ENABLE);//当产生更新时,就会触发中断
}
/*
中断函数模版
void TIM2_IRQHandler(void) //当定时器产生更新中断时,这个函数就会自动被执行
{
	//检查中断标志位
	if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)
	{
	//执行相应的用户代码
		Num ++;
		TIM_ClearITPendingBit(TIM2,TIM_IT_Update);//清除标志位
	}
}
*/

外部时钟源

用光敏传感器手动模拟一个外部时钟,定义一个当外部时钟触发10次(预分频之后的脉冲)后触发"定时"中断

在TIM2的ETR引脚也就是PA0上接一个外部时钟源,选择外部时钟模式2

Timer.h

#include "stm32f10x.h"                  // Device header
 
void Timer_Init(void)  //定时中断初始化代码
{
	//初始化tim2、gpioa
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	//通过ETR引脚的外部时钟模式2配置(不设置预分频、不反向(高电平或上升沿有效)、不使用滤波器)
	TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x00);
	
	//配置时基单元 
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;  
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; 
	TIM_TimeBaseInitStructure.TIM_Period = 10 - 1;  //ARR, 计10次触发中断
	TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;  //PSC, 不设置预分频
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; 
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
	
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);

	//NVIC中断
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); 
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	NVIC_InitTypeDef NVIC_InitTyStructure;
	NVIC_InitTyStructure.NVIC_IRQChannel = TIM2_IRQn;
	NVIC_InitTyStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitTyStructure.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitTyStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitTyStructure); 
	
	//使能TIM2
	TIM_Cmd(TIM2,ENABLE);
}

uint16_t Timer_getCounter(void)
{
	return TIM_GetCounter(TIM2);
}

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
 
uint16_t Num;
 
int main(void)
{
	OLED_Init();	//初始化OLED
	Timer_Init();    //初始化定时器
	
	OLED_ShowString(1,1,"Num:");
	OLED_ShowString(2,1,"CNT:");
	
	
	while(1)
	{
		OLED_ShowNum(1,5,Num,5);
		OLED_ShowNum(2,5,TIM_GetCounter(TIM2),5);//CNT计数器值的变化情况,没遮挡一次CNT+1(变化范围是ARR从0一直到自动重装值(10-1))
	}
 
}

void TIM2_IRQHandler(void)
{
	//检查中断标志位
	if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)
	{
		Num ++;// 当CNT的值等于ARR的时候,触发中断,NUM+1
		TIM_ClearITPendingBit(TIM2,TIM_IT_Update);//清除标志位
	}
}

6.3 输出比较

Output Compare,简称OC

它最主要的功能是可以通过比较计数器CNT和捕获/比较寄存器(Capture/Compare Register)CCR值的关系,来输出电平进行置1、置0的翻转操作,用于输出一定频率和占空比的PWM波形

每个高级定时器和通用定时器都拥有4个输出比较的通道,可以同时输出4路PWM波形,且高级定时器的前3个通道额外拥有死区生成电路和互补输出的功能(用于驱动三相无刷电机)。4个输出比较通道都有独立的CCR寄存器,但是它们共用同一个CNT计数器。

image.png

PWM

PWM(Pulse Width Modulation),即脉冲宽度调制,PWM波形是一个数字输出信号,是由高低电平组成的,是一种对模拟电平信号进行数字编码的方法。在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域。也就是说,使用PWM波形,是用来等效地实现一个模拟信号的输出

PWM的秘诀是:天下武功,唯快不破! 需要注意的是:只有在具有惯性的系统中,才能用PWM对模拟信号进行编码

  • 以LED为例:GPIO的输出信号只能是数字信号,不能控制led的亮度,可以通过以下的方法实现:让LED不断点亮、熄灭、点亮、熄灭,当点亮、熄灭的频率足够大时,由于LED的余晖和人眼的视觉暂留效应,LED就会呈现出一个中等亮度。当调控点亮和熄灭的时间比例时就能让LED呈现出不同的亮度级别。
  • 对于电机调速也类似:在高频率下不断让电机交替通断,由于电机断电后不会立刻停止,而是由于惯性转动后停下,电机的速度就能维持在一个中等速度。

高低电平跳变的数字信号可以被等效地表示为中间虚线所表示的模拟量

image.png

PWM参数

image.png

Ts代表一个高低电平变换周期的时间

  • 频率 :f = 1 / Ts(周期的倒数就是频率);变换越快=频率越大(PWM的频率越快,它等效模拟的信号就越平稳,不过同时性能开销就越大)
  • 占空比:q=Ton/Ts(Ton是高电平的时间,Ts是一个周期的时间,q就是高电平时间相对于整个周期时间的比例)占空比决定了PWM等效出的模拟电压的大小,一般用百分比表示。
  • 分辨率:占空比的变化步距,分辨率就是占空比变化的精细程度。即,占空比最小能以百分之多少的精度变化,它的值可以是1%、0.1%。分辨率的大小要看实际项目的需求定,如果既要高频率,又要高分辨率,就需要硬件电路要有足够的性能,要求不高的情况下,1%的分辨率就足够使用了。

输出比较通道

左边是CNT和CCR比较的结果,右边是输出比较电路,最后通过TIM_CH1输出到GPIO引脚上,然后下面还有三个同样的单元,分别输出到CH2、CH3、CH4

image.png

CNT计数器和CCR1第一路的捕获/比较寄存器进行比较,当CNT = CCR1或者CNT > CCR1时,输出模式控制器就会收到一个信号,输出模式控制器就会改变它输出的OC1REF的高低电平(可以配置多种模式)。OC1REF信号兵分两路:一路以将REF信号映射到主模式控制器的TRGO上,去触发其他外设的功能。另一路通往一个极性选择电路,通过控制TIMx_CCER寄存器的值(0或1),可以选择是否将REF信号翻转(写0信号就会往上走,就是信号电平不翻转;写1信号就会往下走,信号通过一个非门取反,输出的信号就是输入信号高低电平反转的信号,这就是极性选择,选择是不是要把高低电平反转一下),之后通往输出使能电路,可以控制是否输出,最后通往OC1引脚,即TIMx_CH1通道的引脚(在引脚定义表中即可找到具体的GPIO口)

输出模式控制器的执行逻辑

输出比较拥有8种工作模式 ,其对应了输出模式控制器的执行逻辑

image.png
  • 冻结:CNT = CCR时维持原状态,实际上此时REF与CNT和CCR都无关,即CNT和CCR无效,REF保持为上一个状态。在输出PWM波形时,如果要暂停波形输出,且对暂停时的高低电平没有要求,就可以设置为这个模式。
  • 匹配时置无效/有效电平:CNT = CCR时REF置无效/有效电平。这两个模式不适合输出连续变化的波形。如果想定时输出一个“一次性”的信号,则可以考虑这两个模式。有效电平和无效电平是高级定时器中的表述,与关断、刹车等功能配合表述的,这里表述的比较严谨。在这里为了理解方便,可以直接认为有效电平就是高电平,无效电平就是低电平。
  • 匹配时电平翻转:CNT = CCR时REF电平翻转。这个模式可以方便地输出一个频率可调,占空比稳定为50%的PWM波形。比如:你设置CCR为0,那CNT每次更新清0时,就会产生一次CNT=CCR的事件,这就会导致输出电平翻转一次,每更新两次,输出为一个周期并且高电平和低电平的时间是始终相等的,也就是占空比始终为50%,当你改变定时器更新频率时,输出波形的频率也会随之改变。改变计数器的更新频率时,输出波形的频率 = 更新频率 / 2(因为更新两次输出才为一个周期,这就是匹配时电平翻转的用途)
  • 强制为无效/有效电平:与冻结模式类似。如果想暂停波形输出,并且在暂停期间保持低电平或者高电平,可以考虑这两个模式。
  • PWM模式1/2:可以用于输出频率和占空比都可调的PWM波形。主要使用的模式。一般只使用PWM模式1向上计数 image.png

参数计算公式如下所示:

PWM频率:即计数器的更新频率 Freq = CK_PSC / (PSC + 1) / (ARR + 1)

PWM占空比:Duty = CCR / (ARR + 1)

PWM分辨率:即占空比变化的步距 Reso = 1 / (ARR + 1),以上定义的分辨率是占空比最小的变化步距。ARR越大,CCR的变化范围就越大,分辨率就越高。(占空比变化的越细腻越好)

PWM驱动程序

PWM驱动LED呼吸灯

LED正极接在PA0引脚,负极接在GND的驱动,这样高电平点亮,低电平熄灭,正极性的驱动方法,这样的话观察更直观一点,就是占空比越大LED越亮,占空比越小LED越暗

pwd_led.c

#include "stm32f10x.h"                  // Device header
 
/*
pwm初始化函数基本步骤(参考PWM基本结构图)
第一步,RCC开启时钟,把要用的TIM外设和GPIO外设的时钟打开
第二步,配置单元,包括时钟源选择和时基单元
第三步,配置输出比较单元,包括CCR值、输出比较模式、极性选择、输出使能这些参数
第四步,配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置,Pwm和GPIO的对应关系可以参考引脚定义表
第五步,运行控制,启动计数器
*/
 
void pwm_init(void)
{
	//1.使能TIM2和GPIOA时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	//引脚重映射内容,将PA0引脚重映射到PA15,将下面GPIO改为PA15其它不动
//	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//引脚重映射;引脚重映射(TIM2的CH1本来是挂载在PA0引脚的,现在我想在其他引脚使用TIM2的CH1通道
//	GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE);//参考手册AFIO。将PA0引脚重映射到PA15,第一个参数可以是GPIO_PartialRemap1_TIM2或GPIO_FullRemap_TIM2
//	GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);//取消调试端口复用JTAG,PA15端口默认使用JTAG调试端口,需要关闭;SWJ就是SWD和JTAG两种调试方式;若想用PA15\PB3\PB4三个引脚做GPIO使用,先打开AFIO再将JTAG复用解除
 
	//2.初始化时基单元
	//选择内部时钟
	TIM_InternalClockConfig(TIM2);
	
	//3.配置时基单元 
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;  //指定时钟分频
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式
	/*
	公式:
	PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
	PWM占空比:Duty = CCR / (ARR + 1)
	PWM分辨率:Reso = 1 / (ARR + 1)
	若PWM波形为频率为1KHz,占空比为50%,分辨率为1%
	CK_PSC=72MHz
	代入公式:
	Freq =1000Hz=72MHz / 720 / 100
	Duty = 50% = 50 / 100
	Reso = 1% = 1 / 100
	因此:PSC=719,ARR=99,ARR=50
	*/
	TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;  //ARR 周期
	TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;  //PSC 预分频器
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;  //重复计数器的值
	TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);

	TIM_ClearFlag(TIM2, TIM_FLAG_Update);
	
	//4.初始化输出比较单元(通道)
	TIM_OCInitTypeDef TIM_OCInitStructure;
	TIM_OCStructInit(&TIM_OCInitStructure);//给结构体赋初始值;若不想把所有成员都列一遍赋值,就可以先用这个函数赋一个初始值,再更改想改的值
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//设置输出比较的模式
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;//设置输出比较的极性, 有效电平为高电平,REF波形直接输出
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//设置输出使能(输出状态)
	TIM_OCInitStructure.TIM_Pulse = 50;//设置CCR
	TIM_OC1Init(TIM2, &TIM_OCInitStructure);//使用PA0口对应是第一个输出比较通道;在TIM2的OC1通道上就可以输出PWM波形了
	
	//5.初始化GPIO
	GPIO_InitTypeDef GPIO_InitStructure;		//结构体变量名GPIO_InitStructure
	//复用推挽输出;PWM波形通过引脚输出,使用定时器来控制引脚,输出数据寄存器将被断开,输出控制权将转移给片上外设(这里片上外设引脚连接的就是TIM2的CH1通道)
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 根据引脚定义表,TIM2_CH1_ETR对应PA0
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //默认50mhz输出
	GPIO_Init(GPIOA,&GPIO_InitStructure);		//使用的是地址传递		
	
	//6.启动定时器
	TIM_Cmd(TIM2,ENABLE);//PWM波形就能通过PA0输出了
}
 
 
//让LED呈现呼吸灯的效果,那就是不断更改CCR的值就行了
//在运行过程更改CCR,使用函数TIM_SetCompare1封装用来单独更改通道1的CCR值
 
//TIM_SetCompare1封装
void PWM_SetCompare1(uint16_t Compare1)
{
	TIM_SetCompare1(TIM2,Compare1);
	
}

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM_LED.h"
 
uint8_t i;
 
int main(void)
{
	OLED_Init();	//初始化OLED
	pwm_init();
 
	while(1)
	{
		//不断调用PWM_SetCompare1函数,更改CCR的值,实现LED呼吸灯的效果
		for(i=0;i<=100;i++)
		{
			PWM_SetCompare1(i);//设置CCR寄存器的值
			Delay_ms(10);
		}
		for(i=0;i<=100;i++)
		{
			PWM_SetCompare1(100-i);
			Delay_ms(10);
		}
	}
}

PWM驱动舵机

舵机的主要组成部分为伺服电机,所谓伺服就是服从信号的要求而动作。在信号来之前,转子停止不动;信号来到之后,转子立即运动。因此我们就可以给舵机输入不同的信号,来控制其旋转到不同的角度。舵机接收的是PWM信号,当信号进入内部电路产生一个偏置电压,触发电机通过减速齿轮带动电位器移动,使电压差为零时,电机停转,从而达到伺服的效果。简单来说就是给舵机一个特定的PWM信号,舵机就可以旋转到指定的位置。

舵机接收的PWM信号频率为50HZ,即周期为20ms。当高电平的脉宽在0.5ms~2.5ms之间时舵机就可以对应旋转到不同的角度。

image.png

PWN驱动直流电机 ...

6.4 输入捕获

输入捕获(Input Capture,IC)模式下,当通道输入引脚出现指定电平跳变瞬间(可以定义为上升沿、下降沿),当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数(脉冲间隔和频率差不多、电平持续时间和占空比也是互相对应的关系)

输入捕获模块可以配置为PWMI(PWM输入)模式和主从触发模式。PWMI模式是PWM的输入模式,专门用来同时测量PWM波形的频率和占空比的。主从触发模式可以实现对频率或占空比的硬件的全自动测量。把这两个功能结合起来,测量频率和占空比就是硬件全自动执行,软件不需进行任何干预,也不需进中断,需要测量的时候,直接读取CCR寄存器就行了,使用非常方便且极大地减轻了软件的压力。

对于同一个定时器,输入捕获和输出比较只能使用其中一个,不能同时使用

输入捕获电路

image.png

(异或门为了三相无刷电机服务的,暂时不看) 输入信号来到了输入滤波器和边沿检测器(极性选择)。输入滤波器可以对信号进行滤波,避免一些高频的毛刺信号误触发;边沿检测器就是和外部中断一样,可以选择高电平触发或者低电平触发,当出现指定的电平时,边沿检测电路就会触发后续电路执行动作。设计了两套滤波和边沿检测电路,第一套电路经过滤波和极性选择得到TI1FP1,输入给通道1的后续电路。第二套电路,经过另一个滤波和极性选择,得到TI1FP2,输入给通道2的后续电路,同理,下面TI2信号进来,也经过两套滤波和极性选择得到TI2FP1输入通道1和TI2FP2输入通道2,进行交叉连接的目的是两个:

  1. 一个通道灵活切换两个引脚,可以灵活切换后续捕获电路的输入,
  2. 两个通道同时捕获一个引脚,可以把一个引脚的输入,同时映射到两个捕获单元,这也是PWMI模式的经典结构,实现两个通道(IC)对一个引脚(CH)进行捕获,就可以同时测量频率和占空比。可以选择各自独立连接,也可以选择进行交叉连接。

然后来到了预分频器。每个通道各有一个预分频器,可以选择对前面的信号进行分频,每来一个触发信号,CNT的值就会向CCR转运一次,转运的同时会发生一个捕获事件,这个事件会在状态寄存器置标志位,同时也可以产生中断,如果需要在捕获的瞬间,处理一些事情的话,就可以开启这个捕获中断,比如可以配置上升沿触发捕获,每来一个上升沿,CNT转运到CCR一次,又因为CNT计数器是由内部的标准时钟驱动的,所以CNT的数值可以用来记录两个上升沿之间的时间间隔(周期)再取倒数就是测周法测量的频率了,在一次捕获后将CNT清零(可以用主从触发模式来自动完成CNT清零)

image.png

TI1FP1信号和TI1F_ED边沿信号,都可以通向从模式控制器,比如TI1FP1信号的上升沿触发捕获,还可以同时触发从模式,这个从模式里就有电路,可以自动完成CNT的清零。从模式就是完成自动化操作的利器 (主从触发模式,即主模式、从模式和触发源选择三个功能的简称。主模式可以将定时器内部的信号映射到TRGO引脚,用于触发其他外设的操作;从模式可以接收其他外设或自身外设的一些信号,用于触发自己的一些操作;触发源选择,即选择从模式的触发信号源功能,也可以认为它是从模式的一部分)

频率测量方法

测量频率,有两种方法可以选择:

  • 测频法:定时器中断,并记录捕获次数
  • 测周法:捕获中断,并记录定时器次数
image.png

测频法

频率的定义就是,1s内出现了多少个重复的周期,那频率就是多少Hz

可以定义闸门时间闸门时间T=1s(不是必须为1s),则在一秒中得到的上升沿的个数(每来一个上升沿就是完整的一个周期的信号个数)就是频率

比如对射式红外传感器计次,每来一个上升沿计次+1,再用一个定时器,定一个1s的定时中断,在中断里,每隔1s取一下计次值,同时清0,为下一次做准备,这样每次读取的计次值就直接是频率;对应定时器外部时钟的代码,也是如此,每隔1s取一下计次,就能实现测频法测量频率的功能了

测周法

两个上升沿内,以标准频率fc计次,fc=72M/(psc+1),得到N(就是读取CCR的值

则测量频率𝑓𝑥为:𝑓𝑥 = 𝑓𝑐 / 𝑁

测周法的基本思想是:周期的倒数就是频率。如果我们能用定时器测量出一个周期的时间(相邻上升沿或相邻下降沿的间隔时间)取倒数即得到测量频率。

捕获信号的两个上升沿,然后测量一下两个上升沿之间持续的时间,但是实际上,并没有一个精度无穷大的秒表来测量时间,测量时间的方法,实际上也是定时器计次,我们使用一个已知的标准频率fc的计次时钟来驱动计数器,从一个上升沿开始计数器从0开始一直计到下一个上升沿停止,计一个数的时间是1/fc,计N个数时间就是N/fc也就是周期,再取倒数就得到了频率fx,输入捕获模块采用测周法进行测量。

输入捕获测量频率

输入捕获模式测频率的基本结构图

image.png

首先,配置时基单元,启动寄存器,则CNT就会在预分频之后的时钟驱动下不断自增。测周法用CNT来计数,间接实现计时的功能。经过预分频后的时钟频率,就是测周法的标准频率fc。之后,GPIO输入一个待测的方波信号,经过经过滤波器和边沿检测选择TI1FP1为上升沿触发,之后数据选择器选择直连通道,分频器选择不分频。当TI1FP1出现上升沿之后,CNT的值就会被CCR1转运捕获;同时触发源选择模块选择TI1FP1为触发信号,从模式选择复位操作,触发CNT清零(先后顺序是:先转运CNT的值到CCR,再触发从模式给CNT清零。或者是非阻塞的同时转移:CNT的值转移到CCR,同时0转移到CNT里面去,总之是先捕获,再清零)。当电路不断工作时,CCR1中的值始终是最新一个周期的计数值,即测周法的计次数 N。所以,当我们想读取信号的频率时,只需要读取CCR1得到N,再计算fc/N就得到频率了。当不需要读取时,整个电路全自动的测量,不需要占用任何软件资源。

示例程序

  • PA0口输出1khz频率,50%占空比的待测信号;
  • PWM模块将待测信号输出给PA0,PA0然后通过导线输入到PA6(PA6是TIM3的通道1)
  • 通道1通过输入捕获模块测量得到频率,然后在主循环里不断刷新显示频率

PWM.c 输出PWM待测信号

#include "stm32f10x.h"                  // Device header

//一般可以根据分辨率的要求先确定ARR,PSC决定频率,CCR决定占空比
 
void pwm_init(void)
{
	//1.打开时钟,选择内部时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	//2.初始化时基单元
	TIM_InternalClockConfig(TIM2);
	
	//3.配置时基单元 
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;  //指定时钟分频
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式
	/*
	公式:
	PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
	PWM占空比:Duty = CCR / (ARR + 1)
	PWM分辨率:Reso = 1 / (ARR + 1)
	*/
	TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;  //ARR 周期
	TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;  //PSC 预分频器
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;  //重复计数器的值
	TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);
	
	//4.初始化输出比较单元(通道)
	TIM_OCInitTypeDef TIM_OCInitStructure;
	TIM_OCStructInit(&TIM_OCInitStructure);
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
	TIM_OCInitStructure.TIM_Pulse = 50;
	TIM_OC1Init(TIM2, &TIM_OCInitStructure);
	
	//5.初始化GPIO
	GPIO_InitTypeDef GPIO_InitStructure;		
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // // 在PA0口输出PWM波形
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//6.启动定时器
	TIM_Cmd(TIM2,ENABLE);
}

//在运行过程更改CCR,进而改变占空比
void PWM_SetCompare1(uint16_t Compare1)
{
	TIM_SetCompare1(TIM2,Compare1);
}
 
//在初始化之后单独修改PSC,进而改变PWM频率
//可以通过改变ARR和PSC,但是改变ARR会影响占空比,这里只改变PSC
void PWM_setPSC(uint16_t prescaler)
{
	//调用库函数里单独写入PSC的函数,在tim.h中
	//写入PSC,第二个参数是写入PSC的值,直接将外层函数的prescaler参数传进去,
	// 第三个参数是重装模式(还是影子寄存器、预装载这个问题,就是写入的值是立刻生效还是在更新事件生效),都可以,这里选立即生效
	TIM_PrescalerConfig(TIM2, prescaler, TIM_PSCReloadMode_Immediate); 
}

IC.c 输入捕获代码

#include "stm32f10x.h"                  // Device header

void ic_init(void)
{
	//1.打开时钟,选择内部时钟,选择TIM3,因为选择TIM2输出PWM
	//使用APB1的开启时钟函数,TIM3是APB1总线的外设
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//打开时钟
 
	//2.初始化GPIO,查看引脚定义表TIM3的CH1和CH2对应PA6和PA7
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	//3.配置时基单元,让CNT计数器在内部时钟的驱动下自增运行
	TIM_InternalClockConfig(TIM3);
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;  //指定时钟分频
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,向上计数
	/*公式:	
	PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
	PWM占空比:Duty = CCR / (ARR + 1)
	PWM分辨率:Reso = 1 / (ARR + 1)           */
	TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR周期,最好要设置大一些防止计数溢出,16位的计数器满量程计数是65535
	TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC预分频器,标准频率就是72M/72=1MHz;这个值决定了测周法的标准频率fc,72M/预分频就是计数器自增的频率就是计数标准频率;需要根据信号频率的分步范围来调整
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);


	//4.初始化输入捕获单元
	TIM_ICInitTypeDef TIM_ICInitStructure;
	TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//选择通道,使用TIM3的通道1
	TIM_ICInitStructure.TIM_ICFilter = 0xF;//配置输入捕获的滤波器,数越大,滤波效果越好,每个数值对应的采样频率和采样次数在参考手册里有,若信号有毛刺和噪声就可以增大滤波器参数可以有效避免干扰
	TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//对应边沿检测、极性选择部分,可以选择上升沿触发/下降沿触发/上升沿和下降沿都触发
	TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;//分频器,触发信号分频器,不分频就是每次触发都有效,2分频就是每隔一次有效一次,以此类推
	TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;//选择触发信号从哪个引脚输入,对应配置数据选择器的。可以选择直连通道/交叉通道/TRC引脚
	TIM_ICInit(TIM3,&TIM_ICInitStructure);

	//5.配置触发源选择,配置TRGI的触发源为TI1FP1
	TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);//触发源选择TI1FP1

	//6.配置从模式,为Reset
	// 主模式的输出TRGO通往其他定时器或者DAC,ADC,是为了控制其他外设
	// 而这里是作为输入来测频率和占空比,不需要控制其他外设,所以不用配置主模式
	TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset); //从模式选择Reset

	//7.启动定时器,调用TIM_Cmd
	TIM_Cmd(TIM3, ENABLE); 	
	/*
	CNT会在内部时钟的驱动下不断自增,即使没有信号过来,它也会不断自增
	有信号来的时候,CNT就会在从模式的作用下自动清零并不会影响测量
	初始化之后,整个电路就能全自动测量了,当我们想查看频率时,需要读取CCR进行计算,所以需要在下面写一个函数
	*/
}

uint32_t IC_GetFreq(void)
{
	//使用测周法的公式,fc=72M/(psc+1),目前psc=72-1,所以fc=1MHz
	//返回的是最新一个周期的频率值(单位是HZ) = 1MHz(1000000) / N(就是读取CCR的值)
	return 1000000 / (TIM_GetCapture1(TIM3) + 1);	
}

PWMI模式算测占空比

PWMI就是PWM输入模式

image.png

首先TI1FP1配置上升沿触发,触发捕获和清零CNT,正常的捕获周期,再来一个TI1FP2,配置为下降沿触发,通过交叉通道去触发通道2的捕获单元(最开始上升沿CCR1捕获同时清零CNT,之后CNT一直加,然后在下降沿时刻触发CCR2捕获,这时CCR2的值就是CNT从上升沿到下降沿的计数值也就是高电平期间的计数值,CCR2捕获并不触发CNT清零,所以CNT继续加,直到下一次上升沿,CCR1捕获周期并CNT清零,这样执行之后CCR1就是一整个周期的计数值,CCR2就是高电平期间的计数值,用CCR2/CCR1就是占空比,以上就是PWMI模式使用两个通道来捕获频率和占空比的思路。另外也可以两个通道同时捕获第一个引脚的输入)

#include "stm32f10x.h"                  // Device header

void ic_init(void)
{
	//1.打开时钟,选择内部时钟,选择TIM3,因为选择TIM2输出PWM
	//使用APB1的开启时钟函数,TIM3是APB1总线的外设
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//打开时钟
 
	//2.初始化GPIO,查看引脚定义表TIM3的CH1和CH2对应PA6和PA7
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	//3.配置时基单元,让CNT计数器在内部时钟的驱动下自增运行
	TIM_InternalClockConfig(TIM3);
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;  //指定时钟分频
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,向上计数
	/*公式:	
	PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
	PWM占空比:Duty = CCR / (ARR + 1)
	PWM分辨率:Reso = 1 / (ARR + 1)           */
	TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR周期,最好要设置大一些防止计数溢出,16位的计数器满量程计数是65535
	TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC预分频器,标准频率就是72M/72=1MHz;这个值决定了测周法的标准频率fc,72M/预分频就是计数器自增的频率就是计数标准频率;需要根据信号频率的分步范围来调整
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);


	//4.初始化输入捕获单元
	TIM_ICInitTypeDef TIM_ICInitStructure;
	TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//选择通道,使用TIM3的通道1
	TIM_ICInitStructure.TIM_ICFilter = 0xF;//配置输入捕获的滤波器,数越大,滤波效果越好,每个数值对应的采样频率和采样次数在参考手册里有,若信号有毛刺和噪声就可以增大滤波器参数可以有效避免干扰
	TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//对应边沿检测、极性选择部分,可以选择上升沿触发/下降沿触发/上升沿和下降沿都触发
	TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;//分频器,触发信号分频器,不分频就是每次触发都有效,2分频就是每隔一次有效一次,以此类推
	TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;//选择触发信号从哪个引脚输入,对应配置数据选择器的。可以选择直连通道/交叉通道/TRC引脚
	TIM_ICInit(TIM3,&TIM_ICInitStructure);
	
	// 配置PWMI
	//方法1,:将通道初始化部分复制一份,结构体定义不需复制
	//TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;//改为通道2
	//TIM_ICInitStructure.TIM_ICFilter = 0xF;
	//TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling;//改为下降沿触发
	//TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
	//TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_IndirectTI;//交叉输入
	//TIM_ICInit(TIM3,&TIM_ICInitStructure);
	
	//方法2:使用TIM_PWMIConfig函数,可快捷地把电路配置成PWMI模式的标准结构,这个函数只支持通道1和2不支持通道3和4,和方法1的效果是一样的,只需传入一个通道的参数就行了,在函数里会自动把剩下的一个通道初始化成相反的配置(比如已经传入了通道1、直连、上升沿,那函数里就会顺带配置通道2、交叉、下降沿)
	TIM_PWMIConfig(TIM3,&TIM_ICInitStructure);


	//5.配置触发源选择,配置TRGI的触发源为TI1FP1
	TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);//触发源选择TI1FP1

	//6.配置从模式,为Reset
	// 主模式的输出TRGO通往其他定时器或者DAC,ADC,是为了控制其他外设
	// 而这里是作为输入来测频率和占空比,不需要控制其他外设,所以不用配置主模式
	TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset); //从模式选择Reset

	//7.启动定时器,调用TIM_Cmd
	TIM_Cmd(TIM3, ENABLE); 	

}

uint32_t IC_GetFreq(void)
{
	//使用测周法的公式,fc=72M/(psc+1),目前psc=72-1,所以fc=1MHz
	//返回的是最新一个周期的频率值(单位是HZ) = 1MHz(1000000) / N(就是读取CCR的值)
	return 1000000 / (TIM_GetCapture1(TIM3) + 1);	
}

uint32_t IC_GetDuty(void)
{
	//高电平的计数值存在CCR2里,整个周期的计数值存在CCR1里,用CCR2/CCR1就能得到占空比了
	//显示整数的话,给它乘100,这样返回值的范围就是0-100,对应占空比0%-100%
	return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1);
}

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
 
int main(void)
{
	OLED_Init();
	pwm_init();
	ic_init();
	
	OLED_ShowString(1,1,"Freq:00000Hz");
	OLED_ShowString(2,1,"Duty:00%");
	
	
	/*
	PA0口输出1khz频率,50%占空比的待测信号;
	PWM模块将待测信号输出给PA0,PA0然后通过导线输入到PA6(PA6是TIM3的通道1)
	通道1通过输入捕获模块测量得到频率,然后在主循环里不断刷新显示频率
	*/
	PWM_setPSC(720-1); //频率=72M/(psc+1)/(arr+1)   //频率=72M/720/100 =1khz
	PWM_SetCompare1(50);//占空比=ccr/(ARR+1)      //占空比=50/100 = 50%

	while(1)
	{
		OLED_ShowNum(1,6,IC_GetFreq(),5);//不断刷新显示频率
		OLED_ShowNum(2,6,IC_GetDuty(),2);//不断刷新占空比
	}
}

6.5 编码器接口

编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度

每个高级定时器和通用定时器都拥有1个编码器接口

也可以使用触发外部中断,在中断函数里手动进行计次。编码器接口自动计次与触发外部中断手动计次相比较而言,使用编码器接口的好处就是节约软件资源。如果使用外部中断计次,当电机高速旋转时,编码器每秒产生上千上万个脉冲,程序就得频繁进中断。进中断之后,完成的任务又只是简单的加一减一,软件资源就被这种简单而又低级的工作给占用了。所以对于这种需要频繁执行且操作又简单的任务,一般都会设计一个硬件电路模块来自动完成。编码器接口就是用来自动给编码器进行计次的电路,每隔一段时间取一下计次值,就能得到编码器旋转的速度了。

正交编码器

正交编码器(输出两个相位相差90°的方波信号),它一般有两个信号输出引脚,一个是A相,一个是B相,当编码器的旋转轴转起来,A和B相会输出方波信号,转的越快,方波的频率就越高。当正转时,A相提前B相90度,反转时,A相滞后B相90度

image.png

使用正交信号相比较单独定义一个方向引脚的优势:

  • 正交信号精度更高:因为A、B相都可以计次,相当于计次频率提高了一倍。
  • 正交信号可以抗噪声:因为正交信号,两个信号必须是交替跳变的,所以可以设计一个抗噪声电路。如果一个信号不变,另一个信号连续跳变,也就是产生了噪声,那这时计次值是不会变化的。

编码器接口工作流程

初始化之后,CNT初始值为0,然后编码器右转,CNT就++,右转产生一个脉冲,CNT就加一次。比如右转产生10个脉冲后停下来,那么这个过程CNT就由0自增到10,停下来。编码器左转,CNT就--,左转产生一个脉冲,CNT减一次。比如编码器再左转产生5个脉冲,那CNT就在原来10的基础上自减5,停下来。

这个编码器接口其实就相当于是一个带有方向控制的外部时钟。它同时控制着CNT的计数时钟和计数方向。这样CNT的值就表示了编码器的位置。

image.png

编码器接口有两个输入端,分别要接到编码器的A相和B相。两个网络标号TI1FP1和TI2FP2,对应输入部分电路的TI1FP1和TI2FP2,这里编码器接口的两个引脚借用了输入捕获单元的前两个通道。所以最终编码器的输入引脚就是定时器的CH1和CH2两个引脚。信号的通路是,CH1通过输入滤波器和边沿检测器从TI1FP1通向编码器接口;CH2通过输入滤波器和边沿检测器从TI2FP2通向编码器接口。CH3和CH4与编码器接口无关

image.png

编码器接口输出部分其实就相当于从模式控制器,控制CNT的计数时钟和计数方向。这里的输出执行流程按照上一部分图中相位和边沿对应的表格,若出现边沿信号,并且对应另一相的状态为正转,则控制CNT自增,否则控制CNT自减。这里72MHz内部时钟和时基单元初始化时设置的计数方向并不会使用,因为此时计数时钟和计数方向都是处于编码器接口托管的状态,计数器自增和自减受编码器控制。

image.png image.png

中间展示了正交编码器抗噪声的原理,在这个状态中TI2没有变化,但是TI1却跳变了好几次,这不符合正交编码器的信号规律,正交信号,两个输出交替变化,就像人走路一样,先左腿迈一步,后右腿迈一步,左右腿交替向前迈,而这里的状态就相当于右腿没动,左腿连续走了好几步。显然这个相当于左腿的动作信号是一个毛刺信号。通过上表中正交信号的逻辑就可以把这种噪声滤掉

旋转编码器测速

编码器电路初始化后,CNT就会随着编码器旋转而自增自减;直接读出CNT值就能测量编码器的位置;测量编码器的速度和方向就需要每隔一段固定的闸门时间取出一次CNT然后再把CNT清零这就是测频法测量速度了

通过定时器定时执行 TIM2_IRQHandler 中断服务程序,在定时器中断处理函数中读取编码器的值,并将其存储在 speed 变量中,然后在主循环中利用 OLED 显示器显示速度值。

image.png

Encoder.c

#include "stm32f10x.h"                  // Device header

//编码器旋转控制CNT自增自减	
//编码器初始化函数,编码器电路初始化后,CNT就会随着编码器旋转而自增自减;直接读出CNT值就能测量编码器的位置;
//测量编码器的速度和方向就需要每隔一段固定的闸门时间取出一次CNT然后再把CNT清零这就是测频法测量速度了

void Encoder_init(void)
{
	/*
	第一步,RCC开启时钟,开启GPIO和定时器的时钟
	第二步,配置GPIO,需将PA6和PA7配置成输入模式
	第三步,配置时基单元,预分频器一般选择不分频,ARR一般给最大值655535,只需要CNT执行计数就行了
	第四步,配置输入捕获单元,这里只有滤波器和极性两个参数有用,后面的参数没有用到,与编码器无关
	第五步,配置编码器接口模式,直接调用一个库函数
	最后,调用TIM_Cmd,启动定时器
	*/
	
	//1.打开时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	//2.初始化GPIO
	GPIO_InitTypeDef GPIO_InitStructure;		
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入,与外部模块保持默认电平一致(上拉与下拉的选择原则);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;	
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 
	GPIO_Init(GPIOA,&GPIO_InitStructure);

	//不需要初始化时基单元内部时钟函数,因为编码器接口会托管时钟,编码器接口就是一个带方向控制的外部时钟
	//TIM_InternalClockConfig(TIM3);
	
	//3.配置时基单元 
	/*
	PWM 频 率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
	PWM占空比:Duty = CCR / (ARR + 1)
	PWM分辨率:Reso = 1 / (ARR + 1)
	*/
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;  //指定时钟分频
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,这个参数也是没有作用的,计数方向也是被编码器接口托管的
	TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;  //ARR 周期 ,满量程计数,这样计数的范围是最大的而且方便换算成负数(65535取反就变成-1)
	TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;  //PSC 预分频器,不分频,编码器的时钟直接驱动计数器
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;  //重复计数器的值
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);	//初始化TIM3
	
	//4.配置输入捕获单元(通道),编码器接口只使用了通道1和2的滤波器和极性选择
	TIM_ICInitTypeDef TIM_ICInitStructure;
	TIM_ICStructInit(&TIM_ICInitStructure);//防止结构体中出现不确定值可能造成问题,最好用StructInit给结构体赋一个初始值
	//通道1
	TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;	
	TIM_ICInitStructure.TIM_ICFilter = 0xF;	//滤波器为0xF
	//TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;	//电平极性为上升沿,这里的上升沿参数代表的是高低电平极性不反转,因为编码器接口始终都是上升沿、下降沿都有效
	TIM_ICInit(TIM3, &TIM_ICInitStructure);
	//通道2
	TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;	
	TIM_ICInitStructure.TIM_ICFilter = 0xF;	
	//TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
	TIM_ICInit(TIM3, &TIM_ICInitStructure);

	//5.配置编码器接口(需要在TIM_ICInit函数之后,否则TIM_ICInit覆盖TIM_EncoderInterfaceConfig函数的配置)
	// 第二个参数:编码器模式TI1和TI2都计数
	// 第三、四个参数:IC1的极性和IC2的极性,和上面配置捕获单元的TIM_ICPolarity是同样的效果,上面的可以不写,Rising就是不反向
	TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
	
	//6.启动定时器
	TIM_Cmd(TIM3,ENABLE);
}

int16_t Encoder_Get(void)
{
	//测速,在固定的匝门时间读一次CNT然后把CNT清零,读取的就是在闸门时间内的计次数
	int16_t temp;
	temp = TIM_GetCounter(TIM3);//读取CNT
	TIM_SetCounter(TIM3, 0);//CNT清零	
	return temp;
}

Timer.c 参考6.2

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
#include "Encoder.h"
 
int16_t speed;

int main(void)
{
	OLED_Init();
	Timer_Init();
	Encoder_init();
	
	OLED_ShowString(1,1,"speed:");

	while(1){
		OLED_ShowSignedNum(1, 7, speed, 5);// SignedNum方便显示负数
	}
}

// 使用定时器中断每隔一秒读取一次CNT的值
void TIM2_IRQHandler(void)
{
	//检查中断标志位
	if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET){
		speed = Encoder_Get();   //定时器每隔1s读取一下速度,存在speed变量里
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);//清除标志位
	}
}