STM32

STM32 C语言

在STM32中short占16位,而int占32位。具体如下。stdint关键字列举了在程序中使用的变量名来代替关键字

一,基本介绍

1,认识

2,外设

片上资源/外设

上图深颜色的是位于Cortex-M3内核里面的外设,剩下的是内核外的外设。以上为STM32F1整个系列的外设,并不是所有型号都有这些外设,比如此款STM32F103C8T6就没有后面4个外设,具体查看手册

Systick是内核里面的一个定时器 ,主要用来给操作系统提供定时服务(STM32可以加入操作系统,如FreeRTOS,UCOS等),如果使用操作系统就需要Systick提供定时来进行任务切换的功能。RCC 可以对系统时钟进行配置,还可以使能各模块时钟 ,STM32中外设在上电情况下默认是没有时钟的,不给时钟的话操作外设是无效的,外设也不会工作(目的是为了降低功耗) ,所以在操作外设之前必须要先使能它的时钟(RCC完成时钟的使能)。TIM 最常用的外设,分为高级定时器,通用定时器,基本定时器三种类型,常用的是通用定时器,它不仅可以完成定时中断任务 ,还可以完成测频率,生成PWM波形,配置成专用的编码器接口等功能。USART既支持异步串口,也支持同步串口,我们说的UART是指异步串口。RTC实时时钟在STM内部完成年月日时分秒的计时功能,还可以接外部备用电池,即使掉电也能正常运行

3,命名规则

4,引脚定义

标红色的是电源相关的引脚,蓝色的是最小系统相关的引脚,标绿色的是IO口,功能口这些引脚优先使用加粗的IO口。I/O口电平表示它能容忍的电压,FT表示5V,没有FT的只能容忍3.3V电压。主功能表示上电之后默认的功能,一般和引脚名称相同。默认复用功能就是IO口上同时连接的外设功能引脚,这个配置IO口时可以选择通用IO口或者是复用功能。重定义功能是如果有两个功能同时复用在了一个IO口上,而且也确实需要用到这两个功能,那就可以把其中一个复用功能重映射到其他端口上。

第一个引脚 VBAT是备用电池供电的引脚,在这个引脚可以接3V电池,当系统电源断电时,备用电池可以给内部RTC时钟和备份寄存器提供电源。2号引脚 是IO口或者侵入检测或者RTC,IO口可以根据程序输入高低电平,侵入检测可以用来做安全保障的功能(比如产品安全性比较高,可以在外壳加一些防拆的触点,然后接上电路到这个引脚上,如果有人强行拆开设备,触点断开,这个引脚的电平变化就会触发STM32侵入信号,然后清空数据来保证安全),RTC引脚可以用来输出RTC校准时钟,RTC闹钟脉冲或者秒脉冲。3,4号引脚 是IO口或者接32.768KHz的RTC晶振。5,6号引脚 接系统的主晶振,一般为8MHz,芯片内有锁相环电路,可以对这个8MHz频率进行倍频,最终产生72MHz的频率,作为系统的主时钟。7号 NRST是系统复位引脚,N表示是低电平复位。8,9号引脚 是内部模拟部分的电源,比如ADC,RC振荡器等,VSS是负极,接GND,VDD是正极。接3.3V。10~19号引脚 都是IO口,其中PA0还兼具了WKUP功能,可以用于唤醒处于待机模式的STM32。20号引脚 是IO口或者BOOT1引脚,BOOT引脚用来配置启动模式。21,22 也为IO口。23,24号 的VSS_!和VDD_!都是系统的主电源口,VSS为负极,VDD为正极,另外35,36 VSS_2,VDD_2,VSS_3,VDD_3都是系统的主电源口,STM32内部采用分区供电,所以供电口比较多,使用时,VSS接地,VDD接3.3V即可。25~33 都是IO口。34号,37~40号 这些是IO口或者调试端口,用来调试程序和下载程序,STM32支持SWD和JITAG两种调试方式,SWD需要两根线,分别是SWDIO和SWCLK,JTAG需要5根线,分别是JTMS,JTCK,JTDI,JTDO,NJTRST。此教程使用STLINK下载调试程序,STLINK使用的是SWD的方式,所以只需要PA13和PA14这两个IO口,剩下的PA15,PB3,PB4可以作为普通IO口使用,但需要在程序中配置,否则不会作为IO口。41~43,45,46 都是IO口。44号BOOT0也是用作启动配置的

5,内部系统介绍

Cortex-M3引出三条总线,分别为ICode指令总线,Dcode数据总线,System系统总线,ICode和Dcode主要用来连接Flash闪存(存储编写的程序),System总线连接SRAM,FSMC等。ABH总线挂载外设。DMA相当于CPU小秘书,主要做大量的数据搬运,将其连接在总线矩阵上,它可以和CPU一样拥有总线控制权,用于访问外设。

6,启动配置

启动配置的作用就是指定程序开始运行的位置。一般情况下程序都是在Flash程序存储器开始执行。但是在某些情况下,我们也可以让程序在别的地方执行,用于完成特殊功能。比如系统存储器启动模式,一般是在串口下载程序时使用,其里面存放的是STM32中的一段BootLoader程序(作用就是接收串口的数据,然后刷新到主闪存中),什么时候使用串口下载那?当STM32芯片的5个调试端口都被我们设置成IO口时,这就需要用到串口方式下载程序。内置SRAM启动主要用来程序调试

最后一句话的意思是BOOT只在上电后第四个上升沿之前有效,之后就无所谓了。对于20号引脚BOOT1当 第四个上升沿过去之后,它就变成了IO口功能。

7,最小系统电路

二,新建STM32工程

全过程如下:
• 建立工程文件夹, Keil 中新建工程,选择型号
• 工程文件夹里建立 Start 、 Library 、 User 等文件夹,复制固件库里面的文件到工程文件夹
• 工程里对应建立 Start 、 Library 、 User 等同名称的分组,然后将文件夹内的文件添加到工程分组里
• 工程选项, C/C++ , Include Paths 内声明所有包含头文件的文件夹
• 工程选项, C/C++ , Define 内定义 USE_STDPERIPH_DRIVER
• 工程选项, Debug ,下拉列表选择对应调试器, Settings , Flash Download 里勾选 Reset and Run

目前STM32开发方式主要有基于寄存器的方式,基于标准库也就是库函数的方式和基于HAL库的方式。基于寄存器的方式 与开发51单片机一样,用程序直接配置寄存器来达到我们想要的功能(最底层最直接,效率更高)。但是由于STM32结构复杂,寄存器太多,所以基于寄存器的方式不推荐;基于库函数的方式 是使用官方提供的封装好的函数,通过调用这些函数来间接配置寄存器,由于ST对寄存器封装的比较好,所以这种方式既能满足对寄存器的配置,又能提高开发效率;基于HAL库的方式可以直接用图形化界面快速配置STM32,比较适合快速上手STM32的情况。

我们采用库函数的方式

使用库函数的方式我们需要准备一个STM32库函数的压缩包

首先新建一个存放工程的文件夹在桌面,然后打开Keil 5新建工程选择刚才新建的工程文件夹,再此文件夹内再新建一个文件夹,用于存放本次的工程。然后选择好芯片型号。之后根据以下路径找到所需文件:Libraries-CMMSIS-CM3-DeviceSupport-ST-STM32F10x-startup-arm找到的文件如下图。这些文件就是STM32的启动文件,STM32程序就是从启动文件开始执行的。将这些文件全部复制,放到文件工程文件夹下(可以在工程文件夹下新建一个start文件存放,显得更规矩)

补充:前面说过的启动文件有很多类型,选择哪一个要根据芯片型号来选择 ,以下为型号分类。如果用的是STM32F100就选择带VL的启动文件,然后再根据Flash大小选择LD MH 还收是HD。如果使用STM32F105/107的型号,直接选择CL的启动文件即可

然后回到STM32F10x文件下,如下,stm32f10x.h就是STM32外设寄存器描述文件,与51单片机头文件REGX52.H一样,剩下的两个system文件主要是用来配置时钟的,STM32主频72MHz,就是System文件的函数配置的,将这三个文件赋值,也粘贴到start文件下。

因为STM32是内核和内核外围设备组成的,而且这个内核寄存器描述和外围设备描述文件不在一起,所以还要添加一个内核寄存器的配置文件。打开CM3-CoreSupport,如下,这两个文件就是内核的寄存器描述,将这俩文件复制下来,也粘贴到start问价夹下。到此为止,工程的必要文件就复制完成了

然后回到Keil,将工程下的文件夹改名为start

然后右键选择Add Existing Files...

打开start文件

添加启动文件后缀为md.s的(只能添加这一个启动文件),然后剩下的.c和.h文件都添加进来

这样start文件夹文件就添加好了

最后我们还要再工程选项里添加上这个文件的头文件路径

把start文件路径添加进来即可

在工程文件下新建文件夹,然后回到Keil

右键添加组,

将组改名为刚才新建文件夹的名

然后再此文件夹下新建一个main.c文件

下面的路径要修改成刚才新建的工程下文件夹的路径

修改成如下

然后就可以开始编程了

注意:点击下图所示右上角小扳手,将编码格式Encoding改为UTF-8,防止中文乱码

编程完后,按照如下方式将烧录工具和STM32最小系统板接好

然后配置调试器

我们用的是STLINK,所以修改为ST-Link

再点击右侧Setting按钮,将Reset and Run给勾上(勾上后下载程序后会立马复位并执行。否则每次下载之后还要按一下复位按键才执行)

接下来为工程添加库函数。打开工程文件夹,新建文件命名为Library来存放库函数

在如下所示路径中找到库函数文件,misc是内核的库函数,其他为内核外设库函数

将其全部复制到刚才创建的Library文件下 ,然后再打开inc文件夹(如下图)可以看到库函数头文件,也复制粘贴到Library下

然后回到Keil软件添加组,并将添加的组修改名称为Library

右键组,添加已经存在的文件。将刚才创建的Library文件夹内的文件都添加进来

但是对于库函数来说,还不能直接使用。还需要再添加一个文件

按如下路径找到所需文件

stm32f10x_conf.h用来配置库函数头文件的包含关系。下面的两个后缀为it的文件用来存放中断函数

将这三个文件复制下来,粘贴到工程User目录下

然后回到Keil,在User组里将刚才的文件添加进去

在头文件右键打开

找到下方语句,它表示必须定义USE_STDPERIPH_DRIVER,下面的stm32f10x_conf.h才有效,复制USE_STDPERIPH_DRIVER

如下操作,在Define后粘贴刚才复制的

下面的头文件目录也要将刚才的User和Library路径给添加上

实现点灯操作

首先配置使能时钟,库函数里有一个函数来开启时钟,输入RCC_APB2PeriphClockCmd(,会自动出现要写的参数,这时我们可以右键进入到函数定义,在函数定义上方选择要用的参数。第一个参数是选择外设,第二个是选择新的状态

第一个参数选择RCC_APB2Periph_GPIOC,第二个参数选择ENABLE。这样GPIOC的外设时钟就配置好了

第二步是配置端口模式,用GPIO_Init函数。此函数有两个参数,第一个是选择哪个GPIO,第二个是参数的结构体。进入到函数定义,根据提示配置函数即可,因为我们用PC13口的LED,所以第一个参数写GPIOC。第二个结构体参数,我们根据注释新建一个GPIO_InitTypeDef变量,变量名为GPIO_InitStructure,然后利用GPIO_InitStructure.来调用结构体参数并配置,如下图

结构体变量有Mode,Pin,Speed三个参数

配置这三个参数,右键进入定义,根据下图红圈内的继续ctrl+f查找定义地方

就可以找到定义好的各种模式。我们选择GPIO_Mode_Out_PP(通用推挽输出)

最后,利用函数GPIO_SetBits来设置端口高电平来点灯。函数GPIO_ResetBits可以将端口置为低电平

最终代码

cpp 复制代码
#include "stm32f10x.h"
int main(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
    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;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
    GPIO_ResetBits(GPIOC. GPIO_Pin_13);
    while(1)
    {
    }
}

调试功能:

注意:要修改程序需要退出调试模式重新编译。

点击下图红圈所示进入调试模式

进入调试界面如下,左边红圈为寄存器组和状态标志位等信息。上方黄圈为C语言翻译成的汇编程序

上方从左到右依次为复位,全速运行(程序一直运行至断点),停止全速运行。单步运行,跳过当前行单步运行,跳出当前函数单步运行,跳至光标指定行单步运行

如下图所示,可以查看STM32的所有外设,并进行查看

三,GPIO输出

1,GPIO基本介绍

GPIO叫做通用输入输出,可配置8种输入输出模式。引脚电平在0~3.3V之间,部分标注FT的可容忍5V(即可在端口输入5V,而端口最大输出电压为3.3V)。输出模式下可控制端口输出高低电平,用以驱动LED,控制蜂鸣器,模拟通信协议输出时序等。输入模式下可读取端口高低电平或电压,用于读取按键输入,外接模块电平信号输入,ADC电压采集,模拟通信协议接收数据等

2,GPIO总体结构

左边的为APB2外设总线,也就是之前介绍STM32系统结构时所说的挂载外设的APB2,STM32中,所有的GPIO都是挂载在APB2外设总线上的。GPIO外设的名称按照GPIOA,GPIOB,GPIOC来命名的,每个GPIO外设共有16个引脚(编号从0到15,PA0~PA15)。每个GPIO模块内,主要包含了寄存器和驱动器,寄存器是一种特殊的存储器,内核可以通过APB2总线对寄存器进行读写。寄存器每一位对应一个引脚,因为STM32为32位单片机,所以STM32内部的寄存器都是32位的,但端口只有16位,所以寄存器只有低16位有作用,高16位不使用。

3,GPIO位结构

整体可以分为两个部分,上面为输入部分,下面为输出部分。中间画虚线框的是驱动器部分.

输入部分:从IO引脚开始,接了两个保护二极管,这两个保护二极管可以对输入电压进行限幅,上面接VSS为3.3V,下面接VDD为0V,如果输入电压比3.3V还要高,那上方二极管就会导通,输入电压产生的电流就会直接流入VDD,而不会流入内部电路;如果输入电压比0V还要低,下方二极管就回导通,电流就会直接从VSS直接流出去,而不从内部电路汲取电流,可以保护内部电路;如果输入电压在0~3.3V之间,那两个二极管都不会导通,这时二极管对电路没有影响。上拉电阻和下拉电阻的开关可以通过程序配置,如果上面导通下面断开就是上拉输入模式(默认为高电平的输入模式),反之为下拉输入模式(默认为低电平的输入模式),如果两个都断开就是浮空输入模式,上拉和下拉作用是为了给输入提供一个默认的输入电平,同时也是为了避免引脚悬空导致的输入数据不稳定。这两个上拉和下拉电阻阻值比较大,是一种弱上拉和弱下拉,目的是尽量不影响正常的输入操作。下一个是施密特触发器(图片有误),它的作用就是对输入电压进行整形,执行逻辑是如果输入电压大于某一阈值,输出就会瞬间升为高电平;如果输入电压低于某一阈值,输出就会瞬间降为低电平,可以有效避免信号由于波动造成的信号抖动现象,接下来经过施密特触发器整形的波形就可以直接写入输入数据寄存器了,我们再用程序读取输入数据寄存器对应某一位的数据,就可以知道端口的输入电平了。最后上面还有两路线路是连接到片上外设的一些端口,模拟输入是接到ADC上的,因为ADC需要接受模拟量所以在施密特触发器前接收。另一个复用功能输入是连接到其他需要读取端口的外设上的。

输出部分:数字部分由输出数据寄存器或片上外设控制,两种控制方式通过数据选择器接到输出控制部分。如果选择通过输出数据寄存器进行控制就是普通的IO口输出,写这个数据寄存器的某一位就可以操作对应的某个端口了,它前面的位设置/清楚寄存器,这个可以用来单独操作输出数据寄存器的某一位而不影响其他位(因为输出数据寄存器需要同时控制16个端口,并且这个寄存器只能整体读写,所以需要采用特殊的操作方式只修改某一位) ,如果我们要对某一位进行置1操作,在位设置寄存器的对应位写1即可,剩下位写0,这样它内部电路会自动将输出数据寄存器中对应位置为1,而剩下写0的位则保持不变;如果想对某一位清0,就在位清除寄存器对应位写1即可。输出控制后接到了两个MOS管,这个MOS管就是一种电子开关,信号控制导通和关闭,开关负责将IO口接到VDD或VSS,这里可以选择推挽,开漏或关闭三种输出模式;推挽输出模式下P-MOS和N-MOS均有效,当数据寄存器为1时,上管导通下管断开输出直接接到VDD就是输出高电平,数据寄存器为0时输出低电平,这种模式下高低电平均有较强的驱动能力,所以推挽输出也可以叫强推输出模式,推挽输出模式下,STM32对IO口有绝对控制权。开漏输出模式下P-MOS是无效的,只有N-MOS在工作,数据寄存器为1时,下管断开,这是输出相当于断开,也就是高阻模式;数据寄存器为0时,下管导通,输出直接接到VSS,也就是输出低电平。这个开漏模式可以作为通信协议的驱动方式,比如I2C通信的引脚就是使用开漏模式,在多机通信情况下,这个模式可以避免各个设备的相互干扰;另外,开漏输出可以用于输出5V电平信号,比如在IO口外接一个上拉电阻到5V的电源,当输出低电平时,由内部VSS的N-MOS直接接VSS,输出高电平时由外部上拉电阻拉高至5V。

4,GPIO八种工作模式

通过上述对位结构的介绍,只要对GPIO的端口配置寄存器,端口就可以配置成以下8种模式

输出模式下输入模式是有效的,而输入模式下输出模式是断开的,这是因为一个端口可以有多个输入但是只能有一个输出

STM32上电后如果不初始化,默认为浮空输入模式

复用推挽输出和复用开漏输出和普通的开漏输出推挽输出差不多,只是复用的输出引脚电平由片上外设控制,电路图如下

5,GPIO控制函数使用

函数:以下函数可以在stm32f10x_gpio.h头文件中查看

void GPIO_DeInit(GPIO_TypeDef* GPIOx);//调用此函数后所指定的GPIO外设就会被复位

void GPIO_AFIODeInit(void);//可以复位AFIO外设

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);//初始化GPIO口,需要先定义一个结构体变量,然后给结构体赋值,最后调用这个函数

void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct);//可以把结构体变量赋一个默认值

//以下四个用来读取IO口状态

uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//用来读取输入寄存器某一个端口的输入值

uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);//读取整个输入寄存器,返回值为16位数据,每一位代表一个端口值

uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//以下两个函数用来读取输出数据寄存器的某一位,它并不是用来读取端口的输入数据,一般用于输出模式下,用来看一下自己输出的是什么

uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);

//以下四个用来GPIO写入

void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//将指定引脚设置为高电平

void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//将指定引脚设置为低电平

void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);//指定端口并根据BitVla的值设置指定端口BitVal可以写成Bit_RESET(低电平),Bit_SET(高电平)

void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);//根据PortVal对16个端口同时写入

void GPIO_PinLockConfig函数:用来锁定GPIO配置的,调用这个函数,参数指定某个引脚,这个引脚的配置就会被锁定,防止意外更改。

//以下为GPIO8种工作模式

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;

6,示例:LED灯点亮

Delay为自己编写的函数,可以直接使用

cpp 复制代码
#include "stm32f10x.h"
#include "delay.h"
int main(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//也可以按位或同时使能多个时钟
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    //因为每个引脚都占有1位,所以要同时初始化多个引脚的话,只需要逻辑或即可
    //如:GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2
    //如果要同时初始化PA的所有引脚可以用GPIO_Pin_All
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    while(1)
    {
        GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);
        Delay_ms(500);
        GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);
        Delay_ms(500);        
    }
}

四,GPIO输入

1,传感器模块简介

传感器元件(光敏电阻/热敏电阻/红外接收管等)的电阻会随外界模拟量的变化而变化,通过与定值电阻分压即可得到模拟电压输出,再通过电压比较器进行二值化即可得到数字电压输出。其电路如下

中间电路R1,N1组成分压电路,N1为传感器变化电阻,传感器变化,AO(IN)点电压 随之变化。IN+作为U1电压比较器的输入端(U1内部为电压比较器,输入电压IN+,IN-进行比较,如果大于IN-则输出D0为高电平,反之为低电平)。IN-输出端为R2可调电阻,以得到不同电压输出IN-,相当于调整阈值。

2,硬件电路

下图按下按键PA0接收到低电平,松开按键引脚处于悬空,这种悬空状态电平是不确定的。为了防止干扰,需要将此引脚设置为上拉输入模式,保证按键不按下时引脚处于高电平状态

下图接法,PA0引脚设置为浮空输入和上拉输入都可以

下图PA0要配置为下拉输入模式

下图PA0配置为下拉输入和浮空输入都可以

STM32与传感器的接法如下。DO数字输出接PA0。AO为模拟输出,暂时不用

3,按键控制

五,OLED调试工具

1,常用的调试方式

串口调试:通过串口通信,将调试信息发送到电脑端,电脑使用串口助手显示调试信息

显示屏调试:直接将显示屏连接到单片机,将调试信息打印到显示屏上

Keil调试模式:借助Keil的调试模式,可使用单步运行,设置断点,查看寄存器及变量等功能

2,基本介绍

OLED叫做有机发光二极管。OLED显示屏:性能优异的新型显示屏,具有功耗小,响应速度快,宽视角,轻薄柔韧等特点

供电:3V~5V 通信协议:I2C(如下图,四针脚一般用I2C,7针脚一般用SPI) 分辨率:128*64

3,硬件电路

四针脚OLED。SCL和SDA为I2C通信引脚,应该接到单片机I2C通信引脚上,而STM32我们用GPIO口模拟的I2C通信。

7针脚OLED。除了VCC和FND,剩下的引脚是SPI通信协议,也可以用GPIO口模拟通信协议

4,OLED驱动函数

一下图为OLED实物图和屏幕坐标图

六,中断系统

EXIT外部中断是众多能产生中断的外设之一。本节主要介绍外部中断

1,基本介绍

• 中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得 CPU 暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行

• 中断优先级:当有多个中断源同时申请中断时, CPU 会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源

• 中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断, CPU 再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回
ARM体系结构中,中断通常由外设或者外部输入产生,有时也可以软件触发,如定时器溢出,按键输入,串口数据到达等

2,STM32中断

STM32F1系列最多为70个可屏蔽中断通道(即中断源)(10个内核中断和60个外部中断),包含EXTI(外部中断),TIM(定时器),ADC(模数转换器),USART,SPI,I2C,RTC等多个外设。使用NVIC统一管理中断,每个中断通道都有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级

下图为STM32的中断,灰色的是内核的中断(一般用不到)。第一个Reset为复位中断,当产生复位事件时,程序就会自动执行复位中断函数。非灰色部分就是STM32的外设中断了,比如第一个WWDG窗口看门狗用来检测程序运行状态的中断(程序卡死了,没有及时喂狗,窗口看门狗就会申请中断,使程序跳到看门狗中断程序里);PVD电源电压监测,如果供电电压不足,PVD电路就会申请中断。EXTI0~EXTI4,EXTI9_5和EXTI15_!0即为本节要学的外部中断的中断资源。

下表的地址就是指的中断向量。STM32中断向量表是一个存储中断处理函数地址的数组,位于Flash区的起始地址。每个数组元素对应一个中断源,其地址指向相应的中断服务程序。当中断发生时,处理器会根据中断号查找向量表,然后跳转到对应的中断服务程序执行。中断向量表的作用是解决中断函数地址不固定与中断必须跳转到固定地方执行程序之间的矛盾。由于编译器每次编译都会为中断函数随机分配地址,但硬件要求中断必须跳转到固定位置上,因此,中断向量表就作为这样一个固定的地址列表,其中包含了中断函数的地址及跳转到这些地址的程序。当中断发生时,处理器会跳转到这个固定的中断向量表,然后根据其中的信息跳转到相应的中断处理函数,从而执行中断。

下图为STM32中断框图

3,NVIC基本结构

NVIC名字叫做嵌套中断向量控制器,在STM32中统一分配中断优先级和管理中断的。NVIC是一个内核外设,是CPU小助手。NVIC有很多输入口,中断线路都可以接过去;NVIC只有一个输出口,它根据每个中断优先级分配中断的先后顺序。

当一个中断请求到达时,NVIC会确定其优先级并决定是否应该中断当前执行的程序,以便及时响应和处理中断请求。

NVIC支持256个中断(16内核和240外部)。支持256优先级,允许裁剪。

NVIC第一个作用是中断使能/失能控制,第二个是中断优先级控制(将内核中断和外部中断一起拿过来做中断判断),第三个作用是优先级分组控制,通过AIRCR寄存器对优先级进行分组,分组排序完后进入CPU

4,NVIC优先级分组

为了处理不同形式的优先级,STM32的NVIC可以对优先级进行分组,分为抢占优先级和响应优先级。

如果一个中断的抢占优先级高于正在执行的中断,那么它就可以打断当前中断,优先得到执行,数值越小,优先级越高。如果两个中断同时到达且它们的抢占优先级相同,那么响应优先级高的中断首先得到响应,数值越小,优先级越高。当多个中断同时发生时,执行顺序首先由抢占优先级决定,如果抢占优先级相同,则进一步由响应优先级决定,如果响应优先级也相同,则最终由自然优先级决定。在中断嵌套的情况下,高抢占优先级的中断可以打断低抢占优先级的中断,但高响应优先级的中断不能打断低响应优先级的中断(当它们具有相同抢占优先级时)

NVIC的中断优先级由优先级寄存器IPR的4位(0~15)决定(对应16个优先级,值越小优先级越高)(IPR有8位,实际中只使用高4位),这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级,怎么切分又由AIRCR寄存器控制(8到10位)

下图为5种分组方式,程序中自己选择。整个程序中只分一次(一般在init函数中分)

5,NVIC寄存器

中断使能寄存器,如果某一位写1,则这一位对应的中断就允许通过,一个寄存器有32位,32X8=256正好对应了前面所说的NVIC支持256个中断。中断失能寄存器,如果某一位写1,则这一位对应的中断就不允许通过。NVIC属于内核,其寄存器属于内核寄存器,需要查看STM32F10xx Cortex-M3手册,如下查看ISER寄存器

中断优先级寄存器IPR一个寄存器有8位,刚才说过,只使用高4位,而且通过寄存器AIRCR又分成了两组。共有240个寄存器,正好对应了之前说的NVIC支持256个中断(16内核和240外部)中的240个外部寄存器,因为内核优先级已将硬件确定下来了

如下图,AIRCR只关注红圈的3位

6,NVIC相关函数介绍

NVIC库函数如下,常用的是四个HAL_NVIC_EnableIRQ(使能功能),HAL_NVIC_DisableIRQ(失能功能),HAL_NVIC_SetPriorityGrouping(优先级分组),HAL_NVIC_SetPriority(优先级)。

7,NVIC配置方法

设置中断分组 -> 设置中断优先级->使能中断

设置中断分组一般在HAL_Init函数中进行,函数如下

分组方式由高亮NVIC_PRIORITYGROUP_2决定,它的取值如下

8,EXTI

(1)简介

• EXTI ( Extern Interrupt )外部中断
• EXTI 可以监测指定 GPIO 口的电平信号,当其指定的 GPIO 口产生电平变化时, EXTI 将立即向 NVIC 发出中断申请,经过 NVIC 裁决后即可中断 CPU 主程序,使 CPU 执行 EXTI 对应的中断程序
• 支持的触发方式:上升沿 / 下降沿 / 双边沿(上升沿和下降沿都可以触发) / 软件触发(比如执行一段程序触发一次)
• 支持的 GPIO 口:所有 GPIO 口(即任意GPIO口都可以当作外部中断的引脚),但相同的 Pin 不能同时触发中断(比如PA0和PB0不能同时作为中断引脚,PA1,PB1,PC1不能同时作为中断引脚)
• 通道数: 16 个 GPIO_Pin ,外加 PVD 输出、 RTC 闹钟、 USB 唤醒、以太网唤醒(ETH,只有互联型才有)
• 触发响应方式:中断响应 / 事件响应(STM32对外部中断额外增加的功能,当外部中断检测到引脚电平变化时,正常的流程是选择触发中断,但是也可以选择触发一个事件,那么中断信号就不会通向CPU,而是通向其他外设,用来触发其他外设的操作,事件中断属于外设之间的联合工作)

(3)EXTI基本结构

最左边为GPIO口的外设,每个外设16个引脚,所以接16跟线。由于EXTI只有16个GPIO通道,但这里每个GPIO外设都有16个引脚,如果每个引脚都占用一个通道,那显然16个通道就不够用了,所以在这里会有一个AFIO中断引脚选择的电路模块(数据选择器),它可以在前面3个GPIO外设选择其中一个连接到后面的EXIT通道里(所以前面说的相同的Pin不能同时触发中断),同时下面4个外设PVD RTC USB等也是并列接进来的,这些加起来就组成了EXTI的20个输入信号,然后经过EXTI电路后,分为了两种输出,上面的接到NVIC是用来触发中断的(20路输入应该有20路中断输出,输出太多比较占用NVIC通道资源,所以就把外部中断9~5,和15~10给分到一个通道里,也就是说外部中断的9~5触发同一个中断函数,15~10也触发同一个中断函数,编程时在这两个中断函数里需要再根据标志位区分到底是哪个中断进来的),下方有20条通道到了其他外设,就是我们刚才说的事件响应。

(3)AFIO复用IO口

下图为上述AFIO选择中断引脚(数据选择器)的结构图,

AFIO主要用于引脚复用功能的选择和重定义。在STM32中AFIO主要完成两个任务:复用功能引脚重定义,中断引脚选择。

(4)EXTI框图

边缘检测电路检测上升沿下降沿,如果上升沿触发选择寄存器打开就可以检测上升沿,如果下降沿触发选择寄存器打开就可以检测下降沿,同时打开那就都可以检测到。经过或门后,分两路走,往上面走就是所谓的中断,请求挂起寄存器就是标志位,中断屏蔽寄存器如果置0则中断无效,为1则中断可用。往下走为事件。

常用的就是中断屏蔽寄存器,请求挂起寄存器,上升沿触发选择寄存器,下降沿触发选择寄存器。

(5) EXTI寄存器

中断屏蔽寄存器共20位,其中MR19只用于互联网产品。每个位对应每个引脚(16个GPIO+PVD,USB,RTC,ETH)

也是有20个位。某个位写1表示允许某跟线上升沿触发。

检测到中断,则对应位置1.

(6)EXTI函数

配置上升沿下降沿寄存器和中断屏蔽寄存器都是在HAL_GPIO_Init函数里面

HAL_GPIO_EXTI_IRQHandler通过查询挂起寄存器来查看那条线触发了中断

(7)编码器简介

• 旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向
• 类型:机械触点式 / 霍尔传感器式 / 光栅式

编码器硬件电路

A相输出和B相输出接到STM32两个引脚上

9,实例

(1)初始化配置

模块初始化函数,在模块初始化函数中写入中断配置,配置外部中断参照下图(从GPIO到NVIC这一路出现的外设模块都配置好)

cs 复制代码
#include"stm32f10x.h"

void CountSensor_Init(void)
{

}

第一步:配置RCC,把这里涉及的外设的时钟都打开(EXTI和NVIC的时钟是一直打开的,不需要再开启。NVIC是内核外设不需要开启时钟)

cs 复制代码
#include"stm32f10x.h"

void CountSensor_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APBPeriph_GPIOB, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APBPeriph_AFIO, ENABLE);
}

第二步:配置GPIO,选择端口为输入模式

cs 复制代码
#include"stm32f10x.h"

void CountSensor_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APBPeriph_GPIOB, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APBPeriph_AFIO, ENABLE);
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
}

像这种其他外设使用GPIO的情况,如果不清楚该配置为什么模式,可以参考手册,如下

第三步:配置AFIO,选择一路GPIO连接到后面的EXTI

AFIO外设官方并没有给它分配专门的库函数文件,它的库函数是和GPIO在一个文件里的(stm32f10x.gpio文件)

所得函数如下

void GPIO_AFIODeInit函数: 用来复位AFIO外设,调用这个函数,AFIO配置会全部清除

void GPIO_EventOutputConfig函数和GPIO_EventOutputCmd函数:这两个函数用来配置AFIO的事件输出功能

void GPIO_PinRemapConfig函数和GPIO_EXTILineConfig函数:这两个函数比较重要,前者用来进行引脚重映射,第一个参数选择重映射方式,第二个参数是新的状态;后者就是本次要使用的函数,调用这个函数就可以配置AFIO数据选择器,来选择我们想要的中断引脚

void GPIO_ETH_MediaInterfaceConfig函数:和以太网有关

可以看到GPIO_EXTILineConfig函数第一个参数可以是GPIO_PortSourceGPIOx(x为ABCD等),第二个参数GPIO_PinSourcex(x从0到15)

cs 复制代码
#include"stm32f10x.h"

void CountSensor_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APBPeriph_GPIOB, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APBPeriph_AFIO, ENABLE);
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//代表我们使用PB14为中断引脚
}

第四步:配置EXTI,选择边沿触发方式,比如上升沿下降沿或者双边沿;选择触发响应方式,中断响应或事件响应

void EXTI_DeInit函数:调用它就可以把EXTI的配置都清除,恢复成上电默认的状态

void EXTI_Init函数:调用这个函数就可以根据EXTI_InitTypeDef* EXTI_InitStruct这个结构体的参数配置EXTI外设

void EXTI_StructInit函数:调用这个函数可以把参数传递的结构体变量赋一个默认值。前面这三个参数,基本所有外设都有

void EXTI_GenerateSWInterrupt函数:这个函数是用来软件触发外部中断的,调用这个函数,参数给一个指定的中断线,就能软件触发一次这个外部中断

下面四个函数,也是库函数的模板函数,很多模块都有这四个函数。这四个参数主要是查看状态寄存器的,当程序想看标志位时就可以用这四个函数。前两个是在主程序里查看或清除标志位;后两个是在中断函数里查看或清除标志位。本质上这四个函数都是对状态寄存器的读写,只不过下面两个函数只能读写与中断有关的标志位,并且对中断是否允许做出了判断,而上面两个函数只是一般的读写标志位,无额外处理,能不能触发中断的标志位都能读取(中断里用上面两个也是没问题的)

FlagStatus EXTI_GetFlagStatus函数可以获取指定的标志位是否被置1了。void EXTI_ClearFlag函数可以对置1的标志位进行清除。

有的标志位比较紧急,在置标志位后会触发中断,在中断函数里如果想查看标志位和清除标志位,那就用EXTI_GetITStatus函数获取中断标志位是否被置1.EXTI_ClearITPendingBit函数清除中断挂起标志位

我们使用EXTI_Init配置,右键跳转到定义。可以看到与GPIO初始化类似,使用结构体初始化。

所以首先定义结构体EXTI_InitTypeDef EXTI_InitStructure,然后用.引出成员

  • EXTI_InitStructure.EXTI_Line:指定我们要配置的中断线,定义如下。我们要用PB14,所以选择第14个线路,也就是EXTI_Line14
  • EXTI_InitStructure.EXTI_LineCmd:指定选择的中断线的新状态。可以是ENABLE和DISABLE
  • EXTI_InitStructure.EXTI_Mode:指定外部中断线的模式,可以看到共两个模式,第一个是中断模式,第二个是事件模式
  • EXTI_InitStructure.EXTI_Trigger:指定触发信号的有效边沿。Rising为上升沿触发,Falling为下降沿触发。
cs 复制代码
#include"stm32f10x.h"

void CountSensor_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APBPeriph_GPIOB, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APBPeriph_AFIO, ENABLE);
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//代表我们使用PB14为中断引脚
    
    //EXTI初始化配置
    EXTI_InitTypeDef EXTI_InitStructure;
    EXTI_InitStructure.EXTI_Line = EXTI_Line14;
    EXTI_InitStructure.EXTI_Trigger = ENABLE;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_LineCmd = EXTI_Trigger_Falling;
    EXTI_Init(&EXTI_InitStructure);
}

第五步:配置NVIC。给我们这个中断选择一个合适的优先级

void NVIC_SetVectorTable函数:设置中断向量表,此函数用的不多

void NVIC_SystemLPConfig函数:系统低功耗配置 ,此函数用的不多

void NVIC_PriorityGroupConfig函数:用来中断分组,参数为中断分组方式。右键跳转定义,可以看到下图注释的几种模式(pre-emption priority就是抢占优先级)。注意:分组方式整个芯片只能用一种,所以这个分组代码整个工程只需要执行一次

void NCIV_Init函数:根据结构体里面指定的参数初始化NVIC

**NVIC_InitStructure.NVIC_IRQChannel:**下图翻译:指定中断通道来开启或关闭,这个参数可以是IRQn_Type里的一个值。(对于完整的STM32中断通道列表,请参考stm32f10x.h文件)。意思就是IRQn_Type的定义不在这个文件,要到stm32f10x.h中找。

可以ctrl+F搜索,在STM32F10X_MD中找到如下图所示EXTI15_10_IPQn(STM32的EXTI10到EXTI15都是合并到了这个通道)

NVIC_InitStructure.NVIC_IRQChannelCmd: 指定中断通道是使能还是失能,参数为ENABLE或DISABLE

**NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority和 NVIC_InitStructure.NVIC_IRQChannelSubPriority:**指定所选通道的抢占优先级和响应优先级。根据分组填写0-15数字,也可以在定义中查看,如下

cs 复制代码
#include"stm32f10x.h"

void CountSensor_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APBPeriph_GPIOB, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APBPeriph_AFIO, ENABLE);
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//代表我们使用PB14为中断引脚
    
    //EXTI初始化配置
    EXTI_InitTypeDef EXTI_InitStructure;
    EXTI_InitStructure.EXTI_Line = EXTI_Line14;
    EXTI_InitStructure.EXTI_Line = ENABLE;
    EXTI_InitStructure.EXTI_Line = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Line = EXTI_Trigger_Falling;
    EXTI_Init(&EXTI_InitStructure);

    //配置NVIC
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreeptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);
}

(2)中断函数

中断函数在startup_stm32f10x_md.s启动文件中,找一下可以看到定义的中断向量表,其中以IRQHandler结尾的即为中断函数名字

cs 复制代码
#include"stm32f10x.h"

void CountSensor_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APBPeriph_GPIOB, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APBPeriph_AFIO, ENABLE);
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//代表我们使用PB14为中断引脚
    
    //EXTI初始化配置
    EXTI_InitTypeDef EXTI_InitStructure;
    EXTI_InitStructure.EXTI_Line = EXTI_Line14;
    EXTI_InitStructure.EXTI_Line = ENABLE;
    EXTI_InitStructure.EXTI_Line = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Line = EXTI_Trigger_Falling;
    EXTI_Init(&EXTI_InitStructure);

    //配置NVIC
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreeptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);
}

void EXTI15_10_IRQHandler(void){//中断函数都是无参无返回值的
                                //中断函数不需要声明
    //这个函数EXTI10~15都能进来,所以要先判断一下是不是我们想要的EXTI14进来
    //EXTI_GetITStatus()函数。之前在stm32f10x_exti.h文件中讲过
    //看一下EXTI14的中断标志位是否为1。返回值为SET或者RESET
    if(EXTI_GetITStatus(EXTI_Line14)==SET){
        //执行中断程序代码
        //中断程序结束后一定要调用一下清除中断标志位的函数,因为中断标志位为1,程序就会跳转到中 
        //断函数,不清除中断标志位它就会一直申请中断
        EXTI_ClearITPendingBit(EXTI_Line14);
    }
}

在中断内最好不要执行耗时过长的代码 ,中断函数要简短快速。另外,最好不要在中断函数和主函数内调用相同的函数或者操作同一个硬件,尤其是硬件相关的函数;在实现功能时,在中断里操作变量或者标志位,当中断返回时再对变量进行操作,这样既能保证中断函数简短迅速,又能保证不产生冲突的硬件操作。

七,TIM定时器

STM32中功能最强大,结构最复杂的外设之一------定时器

  • 分为以下四个部分:

1,定时器基本定时功能:定一个时间,让定时器每隔这个时间触发一次中断,实现每隔一段时间执行一段程序。定时器可以对输入的时钟进行计数,并在计数值到达设定值时触发中断,定时器实际上就是一个计数器。在STM32中定时器的基准时钟一般都是主频72MHz(如果对72MHz计72个数,那么1MHz也就是1us的时间)

2,定时器输出比较功能:最常见用途就是产生PWM波形,用于驱动电机等设备

3,定时器输入捕获功能:使用输入捕获这个模块来实现测量方波频率的例子

4,定时器编码器接口:使用这个编码器接口,能够更加方便地读取正交编码器的输出波形

STM32拥有16位计数器,预分频器(16位),自动重装寄存器(16位)的时基单元,在72MHz计数时钟下可以实现最大59.65s地定时。这里计数器就是用来执行计数定时的一个寄存器,每来一个时钟,计数器加一;预分频器可以对计数器的时钟进行分频,让计数更灵活;自动重装寄存器就是计数的目标值,就是我想要计多少个时钟申请中断。上述三个部分构成了定时器最核心的部分,称为时基单元。如果预分频器设置最大,自动重装也设置最大,那定时器最大定时时间为59.65s,接近一分钟(72Mhz/65535/65535,得到中断频率,再取倒数)。除此之外,STM32定时器支持级联模式,也就是一个定时器输出作为另一个定时器输入,这样加一起,最大定时时间就是59.65X65535X65535。

1,定时器分类

STM32不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。根据复杂度和应用场景,分为高级定时器,通用定时器和基本定时器。如下图,高级定时器连接着性能更高的APB2总线。高级定时器额外功能主要是为三相无刷电机驱动设计的

STM32F103C8T6定时器资源为TIM1,TIM2,TIM3,TIM4,也就是一个高级定时器和三个通用定时器,无基本定时器 。

(1)基本定时器

其中下面的自动重装载寄存器,计数器和预分频器最重要,预分频器之前,连接的就是基准计数时钟的输入,由于基本定时器只能选择内部时钟,所以可以认为上面的线直接连接到了内部时钟CK_INT,内部时钟来源为RCC_TIMxCLK,这里的频率值一般都是系统的主频72MHz。预分频器可以对72MHz的计数时钟进行预分频,比如预分频器这个寄存器写0就是不分频(输出频率等于输入频率=72MHz),写1就是二分频(输出频率等于输入频率/2=36MHz),写2就是三分频,以此类推,所以预分频器的值和实际分频系数相差为1.由于这个预分频器是16位的,所以最大值可以写65535.也就是65536分频。自动重装寄存器用于存储目标值,当计数值等于自动重装寄存器的目标值,也就是计时时间到了,计数器就会产生一个中断信号并且清零计数器。像这种计数值等于自动重装值产生的中断我们一般把它叫做更新中断,这个更新中断之后就会通往NVIC,我们再配置好NVIC定时器通道,那定时器更新中断就可以得到CPU响应了。

除了上面说到的定时中断原理,主模式触发DAC功能(主从触发模式,它能让内部硬件在不受程序的控制下实现自动运行)。主模式触发DAC:在我们使用DAC时,可能会用DAC输出一段波形,那就需要每隔一段时间触发一次DAC,让它输出下一个电压点;正常思路是使用定时器中断,但是这样会使主程序处于频繁被中断的状态,影响主程序运行和其他中断响应,所以定时器就设计了一个主模式,使用这个主模式可以把定时器的更新事件映射到触发输出TRGO的位置,然后TRGO直接接到DAC的触发转换引脚上,这样定时器的更新就不需要再通过中断来触发DAC转换了,整个过程不需要软件参与,实现硬件的自动化,这就是主模式的作用。

(2)通用定时器

中间最核心部分还是时基单元,结构与基本定时器一样。但是通用定时器计数方式不止向上计数(从0开始增加直到记到重装值)一种,还支持向下计数模式和中央对齐模式(先从0递增,记到重装值,申请中断,然后在向下自减,减到0,申请中断),常用向上计数模式。

时基单元上面的部分就是内外时钟源选择和主从触发模式的结构,通用定时器时钟源不仅可以选择内部的72MHz的时钟,还可以选择外部时钟,第一个外部时钟就是来自TIMx_ETR引脚上的外部时钟,ETR引脚可以在引脚定义图中找到,如下。这里我们选择TIM2_ETR引脚,也就是PA0,在PA0上接一个外部方波时钟,然后配置一下内部的极性选择,边沿检测和预分频器电路,再配置一下滤波电路,滤波后的信号兵分两路,上面一路ETRF进入触发控制器,紧跟着就可以选择作为时基单元的时钟了,在STM32中,TIMx_ETR这一路也叫做"外部时钟模式2"。除了外部ETR引脚可以提供时钟外,下面还有一路TRGI可以提供时钟,这一路主要用作触发输入来使用,这个触发输入可以触发定时器的从模式(触发模式和从模式后续会讲到),本次讲的是触发输入作为外部时钟使用的情况,把TRGI当作外部时钟的输入来看,这一路就叫做"外部时钟模式1",通过这一路的外部时钟第一个就是ETR引脚的信号(ETR引脚既可以通过上面一路作为时钟,也可以通过下面一路当作时钟,这两种情况等价);第二个是ITR信号,这一部分的时钟信号是来自其他定时器的,从触发控制器右边可以看出,主模式的输出TRGO可以通过其他定时器,这个通向其他定时器就就接到了ITR引脚上了,ITR0~ITR3分别来自其他4个定时器的TRGO输出,具体连接方式可以查看手册,如下。通过ITR这一路就可以实现定时器级联,比如我们可以先初始化TIM3,然后使用主模式把它的更新事件映射到TRGO上,接着再初始化TIM2,这里选择ITR2(对应着TIM3的TRGO),然后再选择时钟为外部时钟模式1。第三个可以选择TI1F_ED(ED是边沿的意思),这里连接的是输入捕获单元的CH1引脚,也就是从CH1引脚获得时钟,ED是边沿也就是从这一路获得的时钟上升沿和下降沿均有效。最后还可以通过TI1FP1和TI2FP2获得,TI1FP1连接CH1引脚的时钟,TI2FP2连接CH2引脚的时钟。总结来说,外部时钟模式1的输入可以是ETR引脚,其他定时器,CH1引脚的边沿,CH1引脚和CH2引脚。对于时钟输入而言最常用的还是内部的72MHz,如果要使用外部时钟首选ETR引脚外部时钟模式2的输入,其他输入是为了满足特殊场景设立的,比如T1FP1,T2FP2,TI1F_ED为了测频率,输入捕获而设计

通用定时器下面部分共分为两块电路

红框内为输出比较电路,总共4个通道,分别对应CH1到CH4的引脚,可以用于输出PWM波形和驱动电机

蓝框内是输入捕获电路,也是四个通道,对应CH1到CH4的引脚,可以用于测量输入方波的频率等

中间的比较/捕获寄存器是输入捕获和输出比较电路共用的。因为输入捕获和输出比较不能同时使用,所以这里的寄存器和引脚都是共用的。关于这部分电路后续讲解,本节讲定时中断和内外时钟源选择。

(3)高级定时器

高级定时器与通用定时器相比,左上红框部分一样,主要改动的是剩下的部分。

首先是蓝框所示部分,申请中断的地方增加了一个重复次数计数器,有了这个计数器之后,就可以实现每隔几个计数周期,才发生一次更新事件和更新中断,相当于对输出信号又做了一次分频,所以对于高级定时器来说,最大定时时间为59.65X65535。剩下的部分就是高级定时器对输出比较模块的升级。

DTG(Dead Time Generate)为死区生成电路,右边的输出引脚由原来的一个变为两个互补的输出,可以输出一对互补的PWM波,这些电路是为了驱动三相无刷电机的,因为三相无刷电机的驱动电路一般需要三个桥臂,每个桥臂2个大功率管控制,总共需要6个大功率管开关来控制,所以这里的输出PWM引脚的前三路就变成了互补的输出,而第四路没什么变化。为了防止互补输出的PWM驱动桥臂时,在开关切换的瞬间可能因为器件的不理想造成短暂的直通现象,所以前面加上了死区生成电路,在开关切换的瞬间,产生一定时长的死区,让桥臂上下管全都关断,防止直通现象。

最后一部分绿框内就是刹车输入功能,这个是为了给电机驱动提供安全保障的,如果外部引脚TIMx BKIN产生了刹车信号或者内部时钟失效产生了故障,那么控制电路就会自动切断电机的输入,防止意外发生。

2,定时中断基本结构

运行控制:控制寄存器的一些位,比如启动停止,向上或向下计数等。时基单元左边是为其提供时钟的部分,而可以选择RCC提供的内部时钟,也可以选择ETR引脚提供的外部时钟2,也可以选择触发输入作为外部时钟,即外部时钟模式1,还有一个编码器模式,一般是编码器独用的模式,普通时钟用不到这个。

产生的中断信号会在状态寄存器里置一个中断标志位,这个中断会通过中断输出控制,到NVIC中申请中断。因为定时器模块有很多地方都要申请中断(可以看定时器结构图),这些中断都要经过中断输出控制,如果需要这个中断就允许,否则禁止,简单来说,中断输出控制就是一个中断输出的允许位

3,时序图 (时基单元运行)

(1)预分频器时序

CK_OSC是预分频器的输入时钟,可以在定时器分类的结构图中看到CK_PSC从时钟连接到了预分频器。选内部时钟的话一般是72MHz。

CNT_EN计数器使能,高电平计数器正常运行,低电平计数器停止。

CK_CNT,计数器时钟 。在定时器分类的结构图中也能看到,位于预分频器和CNT计数器中间。它既是预分频器的时钟输出,也是计数器的时钟输入。可以看到计数器使能关闭,计数器不使能,使能开启后,前半段分频器位为1,则分频器输入时钟等于计数器输入时钟,后半段预分频器变为2,则分频器输入时钟就变为了计数器输入时钟的两倍

在计数器时钟的驱动下,计数器寄存器也跟随时钟的上升沿不断自增

更新事件的下面三个时序,是预分频器寄存器的一种缓冲机制,预分频寄存器实际上是两个 。一个是预分频器控制寄存器,供读写使用,不直接决定分频系数;另外一个缓冲寄存器才是真正起作用的寄存器,比如在某个时刻把预分频寄存器由0改为1,如果此刻立刻改变时钟分频系数就会导致在一个计数周期内前半部分和后半部分频率不一样,使用缓冲寄存器,当分频系数改变时这个变化不会立刻生效,而是会等本次计数周期结束时,产生了更新事件,预分频控制寄存器的值才会传递到缓冲寄存器里生效,过程可以看上图时序。预分频器内部实际上也是靠计数来分频的,当预分频值为0时,计数器就一直为0,当预分频值为1时,计数器就0,1,0,1,0,1这样计数,在回到0时输出一个脉冲
• 计数器计数频率: CK_CNT = CK_PSC / (PSC + 1)

(2)计数器时序

内部时钟分频因子为2就是分频系数为2

CK_INT:内部时钟72MHz

CNT_EN:时钟使能

计数到0036时,计数器溢出,产生一个更新事件脉冲,另外还会置一个更新中断位UIF,这个标志位只要置1就会去申请中断,中断响应后需要中断程序手动清零。当CK_CNT再来一个上升沿时计数器寄存器清零。


• 计数器溢出频率: CK_CNT_OV = CK_CNT / (ARR + 1)

= CK_PSC / (PSC + 1) / (ARR + 1)

1,计数器无预装时序

有预装时序与无预装时序区别就是有无像预分频缓冲器一样的缓冲器。通过ARPE位就可以选择是否使用预装功能

计数器正在计数时突然更改了自动加载寄存器(自动重装寄存器),由FF改为36,计数的目标值就由FF改为了36

2,计数器有预装时序

在计数器计数时 ,自动加载寄存器用来读写,自动加载影子寄存器真正起作用。突然把计数目标由F5改为36,可以看到自动加载影子寄存器仍为F5,直到此计数周期完成

4,RCC时钟树

RCC时钟树是STM32用来产生和配置时钟的,并且把配置好的时钟发送到各个外设的系统。在程序运行之前还会执行一个SystemInit函数,此函数就是用来配置时钟树的

此时钟树以AHB预分频器为界限,左边为时钟的产生电路,右边为时钟的分配电路。中间的SYSCLK就是系统时钟72MHz。在时钟的产生电路,共有四个时钟源,分别为8MHz HSI RC(内部的8MHz高速RC振荡器),4-16MHz HSE OSC(外部的4-16MHz高速石英晶体振荡器,即晶振,一般接8MHz),LSE OSC 32.768KHz(外部的32.768KHz低速晶振,一般给RTC提供时钟),LSI RC 40 KHz(内部的40KHz低速RC振荡器,给看门狗提供时钟)。上面两个高速晶振,是用来给提供系统时钟的,AHB APB2 APB1的时钟都是来源于这两个高速晶振,这两个高速晶振都是可以用的,只不过外部的石英振荡器比内部的RC振荡器更稳定,所以一般都用外部晶振

在SystemInit函数里,ST配置时钟首先会开启内部时钟,选择内部8MHz为系统时钟,暂时以内部8MHz时钟运行,然后再启动外部时钟,配置外部时钟进入PLL锁相环进行倍频,8MHz倍频9倍得到72MHz,等到锁相环输出稳定后,选择锁相环为系统时钟,这样就把系统时钟从8MHz切换为了72MHz。如果外部晶振损坏,系统就会以8MHz运行,会慢9倍左右。CSS是时钟安全系统,负责切换时钟,可以检测外部时钟运行状态,一旦外部时钟失效,它就会自动把外部时钟切换回内部时钟。

系统时钟72MHz进入AHB总线,AHB总线有分频器,在SystemInit里配置的分配系数为1,那么AHB时钟就是72MHz,然后进入APB1总线,这里的配置分配系数为2 ,所以APB1总线的时钟为36MHz,线路向下红框部分分配给定时器2-7的为72MHz,所以无论是通用定时器,基本定时器还是高级定时器内部基准时钟都是72MHz。蓝框内的外设时钟使能就是我们的RCC_APB2PerphClockCmd作用的地方,使能时钟就是在这个位置写1让左边的时钟能够输出给外设。

5,定时器基本定时功能

初始化定时器

根据下图流程图一一初始化

cs 复制代码
#include "stm32f10x.h"
void Timer_Init(void)
{
    
}

步骤:1,RCC开启时钟(打开后,定时器的基准时钟和整个外设的工作时钟就会同时打开了)

2,选择时基单元的时钟源,对于定时中断我们选择内部时钟源

3,配置时基单元(包括预分频器,自动重装器,计数模式等等,用一个结构体就都可以配置好了)

4,配置输出中断控制,允许更新中断输出到NVIC

5,配置NVIC,在NVIC中打开定时器中断通道,并分配一个优先级

6,运行控制。整个模块配置完成后还要使能一下定时器

7,定时器中断函数

需要用到的定时器库函数

void TIM_DeInit(TIM_TypeDef* TIMx);恢复缺省设置

void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);时基单元初始化。参数一TIM_TypeDef* TIMx选择某个定时器;参数二TIM_TimeBaseInitTypeDef*TIM_TimeBaseInitStruct是结构体,包含了配置时基单元的一些参数

void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);将结构体变量赋一个初值

void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);用来使能计数器(对应运行控制)。TIM_TypeDef* TIMx选择定时器;参数二FunctionalState NewState选择使能还是失能

void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);这个是用来使能中断输出信号的,对应中断输出控制模块。参数一TIM_TypeDef* TIMx选择定时器;参数二uint16_t TIM_IT选择要配置哪个中断输出;参数三FunctionalState NewState选择使能还是失能

以下6个函数对应时基单元的时钟选择部分,可以选择RCC内部时钟,ETR外部时钟,ITRx其他定时器,TIx捕获通道这些

void TIM_InternalClockConfig(TIM_TypeDef* TIMx); 选择内部时钟
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource); 选择ITRx其他定时器的时钟。参数TIM_TypeDef* TIMx选择要配置的定时器, uint16_t TIM_InputTriggerSource选择要接入哪个其他定时器
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_tTIM_TIxExternalCLKSource,
uint16_t TIM_ICPolarity, uint16_t ICFilter);
选择TIx捕获通道的时钟。参数TIM_TIxExternalCLKSource选择TIx具体的某个引脚;uint16_t TIM_ICPolarity选择输入的极性;uint16_t ICFilter选择滤波器。对于外部引脚的波形,一般都会有极性选择和滤波器
**void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);**选择ETR通过外部时钟模式1输入的时钟,参数TIM_ExtTRGPrescaler为外部触发预分频器,这里可以对ETR外部时钟再提前做一个分频,参数选择如下图一;第三个参数是选择外部触发的极性,如下图2,第一个TIM_ExtTRGPolarity是反向,就是低电平或者下降沿有效,第二个是不反向,就是高电平或者上升沿有效;第四个参数外部触发滤波器,这个值必须是0x00到0x0F的一个值,他是来决定滤波的f和n的,具体决定方式如下图


void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
选择ETR通过外部时钟模式2输入的时钟。对于ETR输入的外部时钟而言TIM_ETRClockMode1Config和TIM_ETRClockMode1Config都是等效的,如果不需要触发输入的功能,那两个函数可以互换.
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);这个不是用来选择时钟,单独用来配置ETR引脚的预分频器,极性,滤波器这些参数

在初始化结构体里有很多关键的参数,比如自动重装值和预分频值等,这些参数可能在初始化后还需要改,如果再调用初始化函数来更改比较麻烦,以下函数可以单独更改这些参数。

**void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);**可以用来单独写预分频值。参数uint16_t Prescaler就是要写入的预分频值;参数二uint16_t TIM_PSCReloadMode写入的模式,之前说过,预分频器有一个缓冲器,写入的值是在更新事件发生后生效,这里的写入模式可以选择是在更新事件发生后生效,也可以是在写入后手动产生一个更新事件,让这个值立刻生效。 void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);用来改变计数器的计数模式。 参数uint16_t TIM_CounterMode选择新的计数器模式。 void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);自动重装器预装功能配置,而可以选择有无预装 void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);给计数器写入一个值 void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);给自动重装器写入一个值 uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);获取当前计数器的值 uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);获取当前预分频器的值

以下四个函数用来获取标志位和清除标志位

FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);

void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);

ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);

void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);

1,RCC开启时钟

因为TIM2是APB1总线的外设,所以要使用APB1开启时钟函数

cs 复制代码
#include "stm32f10x.h"
void Timer_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
}
2,选择时基单元的时钟源
cs 复制代码
#include "stm32f10x.h"
void Timer_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    //选择内部时钟作为TIM2时基单元的时钟
    TIM_InternalClockConfig(TIM2);//定时器上电后默认就是使用内部时钟,所以不写这行也行
}
3,配置时基单元

TIM_TimeBaseInitStructure.TIM_ClockDivision:时钟分频。我们之前说过,在定时器的外部信号输入引脚一般都会有一个滤波器,可以滤掉信号的抖动干扰,它的工作原理就是在一个固定的时钟频率f下进行采样,如果连续N个采样点都为相同的电平,那就代表输入信号稳定了,就把这个采样值输出出去,如果这N个采样值不全都相同,那就说明信号有抖动,这时就保持上一次输出或者直接输出低电平也行。这里的采样频率f和采样点N都是滤波器的参数;而采样频率f可以由内部时钟直接而来,也可以是由内部时钟加一个时钟分频而来,分频多少就是有参数ClockDivision来决定的,所以这个参数跟时基单元关系不大,这个参数取值如下,第一个为1分频,也就是不分频,第二个是二分频,第三个是4分频,这里随便选择一个即可

TIM_TimeBaseInitStructure.TIM_CounterMode:计数器模式。可选择的模式如下,分别是向上计数,向下计数和三种中央对齐的模式

以下三个就是时基单元每个关键寄存器的参数了,不过这里没有CNT计数器的参数,这个如果之后需要的话,可以用之前说的SetCounter和GetCounter来操作。其中前两个来决定定时时间,可以根据之前在时序图说过的公式计算要配置为多少,比如说定时一秒。则PSC给7200,ARR再给10000,然后两个参数都减1(因为预分频器和计数器都有1个数的偏差)

TIM_TimeBaseInitStructure.TIM_Period:周期,就是ARR自动重装器的值

TIM_TimeBaseInitStructure.TIM_Prescaler:PSC预分频器的值

TIM_TimeBaseInitStructure.TIM_RepetitionCounter:重复计数器的值,是高级定时器才有的,这里我们不用,直接给0

cs 复制代码
#include "stm32f10x.h"
void Timer_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    //选择内部时钟作为TIM2时基单元的时钟
    TIM_InternalClockConfig(TIM2);//定时器上电后默认就是使用内部时钟,所以不写这行也行

    //配置时基单元    
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
	TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
}
4,配置输出中断控制

用函数TIM_ITConfig来使能中断。参数一为TIM2,参数二可以是以下这些值的任意组合,我们选择第一个TIM_IT_Update更新中断,参数三ENABLE。配置完后就开启了更新中断到NVIC的通路

cs 复制代码
#include "stm32f10x.h"
void Timer_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    //选择内部时钟作为TIM2时基单元的时钟
    TIM_InternalClockConfig(TIM2);//定时器上电后默认就是使用内部时钟,所以不写这行也行

    //配置时基单元    
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
	TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);

    //配置输出中断控制
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
}
5,配置NVIC
cs 复制代码
#include "stm32f10x.h"
void Timer_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    //选择内部时钟作为TIM2时基单元的时钟
    TIM_InternalClockConfig(TIM2);//定时器上电后默认就是使用内部时钟,所以不写这行也行

    //配置时基单元    
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
	TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);

    //配置输出中断控制
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

    //配置NVIC
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);
}
6,启动定时器
cs 复制代码
#include "stm32f10x.h"
void Timer_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    //选择内部时钟作为TIM2时基单元的时钟
    TIM_InternalClockConfig(TIM2);//定时器上电后默认就是使用内部时钟,所以不写这行也行

    //配置时基单元    
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
	TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);

    //配置输出中断控制
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

    //配置NVIC
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);

    //使能定时器
    TIM_Cmd(TIM2, ENABLE);
}
7,中断函数
cs 复制代码
#include "stm32f10x.h"
void Timer_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    //选择内部时钟作为TIM2时基单元的时钟
    TIM_InternalClockConfig(TIM2);//定时器上电后默认就是使用内部时钟,所以不写这行也行

    //配置时基单元    
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
	TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);

    //由于预分频器缓冲器只有在更新事件时才会起作用,所以初始化时为了让值立刻起作用
    //就在初始化代码最后手动生成了一个更新事件,这样,预分频器的值才有效。
    //但是更新事件和更新中断是同时发生的,更新中断会置更新中断标志位,当我们一旦初始化完
    //之后,更新中断就会立刻进入,这就是为什么刚一上电就立刻进入中断的原因。
    //解决方法就是在TIM_TimeBaseInit后,NVIC_Init(开启中断)前加一行如下代码,手动清除一下中断标志位
    TIM_ClearFlag(TIM2, TIM_FLAG_Update);

    //配置输出中断控制
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

    //配置NVIC
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);

    //使能定时器
    TIM_Cmd(TIM2, ENABLE);
}

void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
	{
		
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}

6,定时器输出比较功能

(1)基本介绍

输出比较功能主要用来输出PWM波形驱动电机等

OC(Output Compare)输出比较:输出比较可以通过比较CNT计数器与CCR捕获/比较寄存器值的关系,来对输出电平进行置1,置0或翻转的操作,用于输出一定频率和占空比的PWM波形。CNT就是下图红框时基单元里面的计数器,CCR就是蓝框里的捕获/比较寄存器。这块电路会比较CNT和CCR的值,CNT计数自增,CCR是我们给定的一个值,当CNT大于CCR,小于CCR或者等于CCR时就会对应输出0,1,0,1

每个高级定时器和通用定时器都拥有4个输出比较通道,并且高级定时器前3个通道额外拥有死区生成和互补输出的功能

(2)输出比较通道

以下为通用定时器输出比较电路。对应着上图的紫圈内的电路。经过输出比较电路后,最后通过TIM_CH1输出到GPIO引脚上。

下图。左边就是CNT计数器和CCR1第一路的捕获/比较寄存器。当CNT>=CCR1,就会给输出模式控制器传一个信号,输出控制器就会改变它输出oc1ref的高低电平(ref指reference的缩写,意思是参考信号),上面的ETRF输入是定时器的一个小功能,一般不用。接着REF信号可以前往主模式控制器,在这里可以把REF映射到主模式的TRGO输出上去,但是REF主要去向还是下面一路,接着到达TIMxC_CER,这是一个极性选择,给这个寄存器写0,信号就往上走,就是信号电平不翻转(进去啥样,出去啥样),写1信号就会往下走,信号翻转。接着是输出使能电路,选择是否要输出,最后就是OC1引脚,这个引脚九四CH1通道的引脚,在引脚定义表里就可以知道具体哪个GPIO口了

以下为高级定时器的输出比较通道 。右边引脚通常接如图电路。上面是正极,然后是大功率电子开关(MOS管),然后再来一个MOS管,最后接地,MOS管左边为控制极,比如说给高电平右边两根线就导通,低电平就断开,下面MOS管也一样,这就是一个最基本的推挽电路,中间为输出,如果上管导通,下管断开,那输出就是高电平,如果下管导通,上管断开,那输出就是低电平,如果上下管都导通,那就是电源短路,这样是不允许的,如果上下管都断开,那就是高阻态,如果有两个这样的推挽电路,就构成了H桥,就可以控制直流电机正反转了,如果有三个这样的推挽电路,就可以驱动三相无刷电机了。如果直接用单片机来控制的话,就需要两个控制极,并且这两个控制极电平要相反,也就是互补

OC1和OC1N就是两个互补的输出端口,在切换导通状态时,如果上管关断瞬间下管就立刻打开,那可能会因为器件不理想,上管还没完全关断下管就已经导通了,这会导致功率损耗,引起器件发热,所以为了避免这个问题就有了死区生成电路,它会在上管关闭的时候,延迟一小段时间再导通下管。

输出模式控制器工作原理:输出比较8种模式如下,也就是这个输出模式控制器里面的执行逻辑。上图所示的模式控制器的输入是CNT和CCR的大小关系,输出的是REF的高低电平,里面有多种模式可以更加灵活地控制REF输出,这些模式的选择可以通过寄存器TIMx_CCMR1来配置。

第一个模式 冻结,可以理解为CNT和CCR无效,REF保持为原状态,这个模式用于比如正在输出PWM波,突然像暂停一会输出,就可以设置为这个模式,一旦切换为冻结模式后,输出就停止了,并且高低电平也维持为暂停时刻地状态,保持不变。二三四个模式有效电平无效电平可以理解为高低电平,这三个模式都是当CNT与CCR值相等时,执行操作,第四个模式就可以用作波形输出了,比如匹配时电平翻转模式,这个模式可以方便输出一个频率可调,占空比始终为50%的PWM波形,第二第三个模式用途不大。第五,六个模式,强制为无效电平和强制为有效电平,这两个模式和冻结差不多,如果想暂停波形输出,并且想在暂停期间保持低电平或者高电平,那就可以设置为这两个模式。

最后两个模式PWM1和PWM2,PWM模式2是PWM模式1输出的取反,改变PWM模式1和PWM模式2,就只是改变了REF电平的极性而已。在上面的通用定时定时器输出比较通道中讲过,REF输出之后还有一个极性的配置,所以使用PWM模式1的正极性和PWM模式2的反极性最终的输出是一样的,所以只使用PWM1即可

(3) PWM基本结构

右上角坐标,蓝色线为CNT计数的值,黄色线是ARR的值,这个过程中设置一条红色的线,这条红色线就是CCR

舵机的PWM信号要求:周期20ms(50Hz),高电平宽度为0.5ms~2.5ms

电机驱动模块:STBY待机控制脚,如果接GND驱动模块不工作,处于待机状态,如果接逻辑电源VCC就正常工作。

(4)实例

定时器输出比较功能函数介绍

以下四个函数用来配置输出比较模块,OC就是(Output Compare) ,输出比较。用来配置上图的输出比较单元,输出比较单元有4个,对应也有四个函数。第一个参数TIM_TypeDef* TIMx选择一个定时器,第二个参数TIM_OCInitTypeDef* TIM_OCInitStruct结构体就是输出比较的那些参数

void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);

void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);

void TIM_OC3Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);

void TIM_OC4Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);

void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);//给输出比较结构体赋一个默认值

以下四个函数用来配置强制输出模式的。如果在运行过程中想要暂停输出波形并强制输出高或低电平,可以用这个函数。

void TIM_ForcedOC1Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);

void TIM_ForcedOC2Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);

void TIM_ForcedOC3Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);

void TIM_ForcedOC4Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);

以下四个函数用来配置CCR寄存器的预装功能(即影子寄存器)

void TIM_OC1PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);

void TIM_OC2PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);

void TIM_OC3PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);

void TIM_OC4PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);

以下四个函数用来配置快速使能的,这个在功能手册单脉冲模式有介绍。用的不多

void TIM_OC1FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);

void TIM_OC2FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);

void TIM_OC3FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);

void TIM_OC4FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);

在功能手册外部事件清除REF信号一节有介绍。用的不多

void TIM_ClearOC1Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);

void TIM_ClearOC2Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);

void TIM_ClearOC3Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);

void TIM_ClearOC4Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);

以下函数用来单独设置输出比较的极性的。前面带N的就是高级定时器里互补通道的配置,OC4无互补通道,所以就没有OC4N的函数。这里的函数可以设置极性,在结构体的初始化函数也可以设置极性,这两个设置极性的作用是一样的。一般来说,结构体初始化里的参数都会有一个单独函数可以修改

void TIM_OC1PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);

void TIM_OC1NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity);

void TIM_OC2PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);

void TIM_OC2NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity);

void TIM_OC3PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);

void TIM_OC3NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity);

void TIM_OC4PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);

以下两个函数单独修改输出使能参数的

void TIM_CCxCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCx);

void TIM_CCxNCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCxN);

以下函数用来选择输出比较模式

void TIM_SelectOCxM(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_OCMode);

以下四个函数用来单独更改CCR寄存器值的函数。更改占空比就需要这四个函数

void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);

void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);

void TIM_SetCompare3(TIM_TypeDef* TIMx, uint16_t Compare3);

void TIM_SetCompare4(TIM_TypeDef* TIMx, uint16_t Compare4);

以下函数仅高级定时器使用。在使用高级定时器输出PWM时,需要调用这个函数使能主输出,否则PWM将不能正常输出

void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);

初始化PWM

根据下图进行初始化:

  1. RCC开启时钟。把要用的TIM外设和GPIO外设的时钟打开
  2. 配置时基单元。包括时钟源选择
  3. 配置输出比较单元。包括CCR的值,输出比较模式,极性选择,输出使能等参数。在函数里用结构体统一来配置
  4. 配置GPIO。把PWM对应的GPIO口初始化为复用推挽输出的配置
  5. 运行控制。启动计数器,这样就能输出PWM了
1,RCC开启时钟
cs 复制代码
void pwm_init(){
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

}
2,选择内部时钟并配置时基单元
cs 复制代码
 void pwm_init(){   
    //RCC开启时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    //选择内部时钟源
    TIM_InternalClockConfig(TIM2);
	//配置时基单元
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;		//ARR
	TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;		//PSC
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
}
3,配置输出比较单元

不同输出比较单元对应GPIO口也是不一样的,所以按照GPIO口需求来。这里使用PA0口,对应第一个输出比较通道

输出比较单元初始化结构体成员很多,而且很多参数是给高级定时器用的。比如OCN开头的就是给高级定时器用的,所以一般只把需要用的参数列出来即可

TIM_OCInitStructure.TIM_OCMode:输出比较模式,如下图。第一个为冻结模式,第二个Active就是相等时置有效电平,第三个相等时置无效电平,第四个Toggle相等时电平翻转,最后两个就是PWM了

**TIM_OCInitStructure.TIM_OCPolarity:**第一个参数High,高极性,就是极性不翻转,REF波形直接输出,第二个参数Low,低极性,就是REF电平取反

TIM_OCInitStructure.TIM_OutputState:输出使能

TIM_OCInitStructure.TIM_Pulse:用来设计CCR寄存器的值。

cs 复制代码
 void pwm_init(){   
    //RCC开启时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    //选择内部时钟源
    TIM_InternalClockConfig(TIM2);
	//配置时基单元
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	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_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 = 0;		//设置CCR
	TIM_OC1Init(TIM2, &TIM_OCInitStructure);
}
4,配置GPIO

在引脚定义一节里,有一列默认复用功能,这一列就是片上外设的端口和GPIO的连接关系,在这一列可以看到PA0对应的TIM2_CH1_ETR,这说明TIM2的ETR引脚和通道1引脚都是借用了PA0这个引脚的位置,所以说要使用TIM2的OC1也就是CH1通道输出PWM,那它就只能在PA0的引脚上输出,而不能任意选择引脚输出,同样地,如果使用TIM2CH2,那就只能在PA1的端口上输出(或者使用AFIO把它重映射到有TIM2_CH1_ETR的位置)

GPIO引脚模式应设置为复用推挽输出。在前面开漏/推挽输出内部结构图中讲过,对于普通 开漏/推挽输出,引脚控制权是来自于输出数据寄存器的,如果想让定时器控制引脚就要使用复用开漏/推挽输出模式,这个模式下输出数据寄存器断开,输出控制权转移给片上外设

cs 复制代码
 void pwm_init(){   
    //RCC开启时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    //选择内部时钟源
    TIM_InternalClockConfig(TIM2);
	//配置时基单元
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	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_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 = 0;		//设置CCR
	TIM_OC1Init(TIM2, &TIM_OCInitStructure);

    //配置GPIO
    GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;		//GPIO_Pin_15;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
}
5, 启动定时器

TIM_Cmd(TIM2,ENABLE)

cs 复制代码
 void pwm_init(){   
    //RCC开启时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    //选择内部时钟源
    TIM_InternalClockConfig(TIM2);
	//配置时基单元
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	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_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 = 0;		//设置CCR
	TIM_OC1Init(TIM2, &TIM_OCInitStructure);

    //配置GPIO
    GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;		//GPIO_Pin_15;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    //启动定时器
    TIM_Cmd(TIM2, ENABLE);
}
6,补充:引脚重映射

之前说过PA0可以作为TIM2_CH1_ETR引脚功能,但是如果PA0引脚被占用,这时就需要使用引脚重映射功能,如图所示,PA15引脚重定义功能那一列有TIM2_CH1_ETR,就说明TIM2_CH1_ETR可以通过AFIO重映射到这个引脚上,这个引脚就可以输出PWM波形

首先,开启AFIO的时钟。使用void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);函数,这个函数第一个参数可选的重映射方式比较多,如下图为其中一部分。每种方式对应的重映射关系可见功能手册,在AFIO一节

如下图所示为功能手册AFIO重映射部分图像,给出了重映射方式和引脚更改的关系

如下图,TIM的重映射功能和引脚更改的关系,有四种对应关系。如果我们想把PA0改到PA15就可以选择重映像方式1(部分重映像)或者完全重映像。

在重映射方式里找一下,可以看到蓝框所示的部分重映像1,2和完全重映像

增加如下代码理论可以完成重映射。

cs 复制代码
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);

但是PA15上电后默认复用为调试端口JTDI,所以想让它作为普通GPIO或者复用定时器的通道,那还需要先关闭调试端口的复用,也是利用GPIO_PinRemapConfig来关闭,如下图所示,调用以下三个参数,就可以解除调试端口的复用。第一个NoJTRST调用后就可以解除PB4的调试端口,PB4就可以作为正常GPIO口使用;第二个JTAGDisable,这个就是解除JTAG调试端口的复用,在引脚定义里就是PA15,PB3,PB4这三个端口变回GPIO;第三个参数SWJ_Disable,这个参数就是把SWD和JTAG的调试端口全部解除。

用如下代码就可以正常使用PA15。注意:重映射后,初始化GPIO时初始化PA15而不是PA0

cs 复制代码
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);

GPIO_PinRemapConfig(GPIO_Ramp_SWJ_JTAGDisable, ENABLE);

7,定时器输入捕获功能

(1)基本介绍

IC(Input Capture),可以使用输入捕获功能测频率(必须是高低电平信号,高电平3.3V,低电平0V),PWMI模式测频率占空比

输入捕获模式下,当通道输入引脚出现指定电平跳变时(上升沿或下降沿,可以通过程序配置),当前CNT的值将被锁存在CCR中(即把当前CNT的值读出来,写入到CCR中)。可用于测量PWM波形的频率,占空比,脉冲间隔,电平持续时间等参数

每个高级定时器和通用定时器都拥有4个输入捕获通道

可配置PWMI模式,同时测量频率和占空比

可配合主从触发模式,实现硬件的全自动测量

(2)测频率方法

1,测频法

在闸门时间T内,对上升沿进行计次,得到N,频率fx=N/T

测频法适合测量高频信号。计次N小误差大。测频法测量结果更新慢,数值稳定

2,测周法(测周期,取倒数)

两个上升沿内,以标准频率fc计次,得到N,则频率fx=fc/N。这里测量周期的方法还是使用定时器。

测周法适合测量低频信号。测周法测量结果更新快,数据跳变也非常快

频率高低的标准取决于中界频率(测频法与测周法误差相等的频率点)。当待测频率小于中界频率时,测周法误差更小;当待测频率大于中界频率时,,测频法误差更小。

𝑓𝑚=√(fc / T)

(3)输入捕获结构图

首先,左边是四个通道的引脚,引脚进来有一个三输入的异或门,这个异或门输入接到了通道1,2,3端口(异或门执行逻辑:当三个引脚的任何一个有电平翻转时,输出引脚就产生一次电平翻转),之后输出通过数据选择器(梯形框)到达输入捕获通道1(数据选择器如果选择上面的,就是3个通道的异或值;如果选择下面一个,那异或门就没有用,四个通道各用各的引脚。设计这个异或门就是为三相无刷电机服务的,无刷电机有3个霍尔传感器检测转子的位置,可以根据转子位置进行换相,有了异或门就可以在前三个通道接上无刷电机的霍尔传感器,然后这个定时器就作为无刷电机的接口定时器去驱动换相电路工作)。输入信号经过数据选择器来到输入滤波器(对信号进行滤波)和边沿检测器(可以选择高电平触发或者低电平触发),当出现指定电平时,边沿检测电路就会触发后续电路执行动作。另外,实际上它是设计了两套滤波和边沿检测电路,第一套电路经过滤波和极性选择得到TI1FP1输入给通道1的后续电路;第二套电路经过另一个滤波和极性选择得到TI1FP2输入给下面的通道2的后续电路,同理输入捕获通道2也是这样。这样通道1可以同时输入给通道1和通道2,这样做的好处是可以灵活切换后续捕获电路的输入(可以一会以CH1作为输入,一会以CH2作为输入,可以通过数据选择器灵活选择),其次,可以把一个引脚的输入同时映射到两个捕获单元,这也是PWMI模式的经典结构(第一个通道使用上升沿触发来捕获周期,第二个通道使用下降沿触发,用来捕获占空比,两个通道同时对一个引脚进行捕获,就可以同时测量频率和占空比)。另外还有一个TRC信号也可以选择作为捕获部分的输入,这也是为了无刷电机的驱动。

输入信号经过滤波和极性选择后就来到了预分频器,分频之后的通道就可以选择触发捕获电路进行工作了。每来一个触发信号,CNT的值就会向CCR转运一次,转运同时发生一个捕获事件CC1I(如图),这个事件会在状态寄存器置标志位,同时也可以产生中断。比如说使用上升沿触发,则每来一个上升沿CNT值像CCR转运一次,下一个上升沿来了再转运一次,两次值相减就可以得到一个周期的时间,这就是测周法测频率.。(注意每次捕获后,都要把CNT清0,这样下次上升沿再捕获时,取出的CNT才是两个上升沿的时间间隔,这样在一次捕获后自动将CNT清零的步骤可以用主从触发模式自动来完成)

以下为输入捕获通道的详细框图。可以看到TI1F_ED信号和TI1FP1信号都可以通过从模式控制器,比如TI1FP1的上升沿触发捕获,还可以同时触发从模式(从模式里面就有电路可以自动完成CNT的清零)

(4)主从触发模式

主模式可以将定时器内部信号映射到TRGO引脚,用于触发别的外设。从模式就是接收其他外设或自身外设的一些信号,用于控制自身定时器的运行,也就是被别的信号控制。触发源选择就是选择从模式的触发信号源的(可以认为是从模式的一部分),触发源选择,选择一个指定的信号,得到TRGI,TRGI去触发从模式,从模式可以在它的列表里选择一项操作来自动执行。比如想让TI1FP1信号自动触发CNT清零,那触发源选择就可以选中TI1FP1,从模式执行的操作就可以选择Reset操作。

在库函数里这三部分分别对应三个函数

有关这些信号的具体解释可以参考手册,如下

(5)基本结构图

相关推荐
生擒小朵拉1 分钟前
STM32添加库函数
java·javascript·stm32
云伴枫轻舞7 分钟前
我对 OTA 的理解随记,附GD32/STM32例程
stm32·单片机·嵌入式硬件
Aczone281 小时前
硬件(五) 存储、ARM 架构与指令系统
arm开发·嵌入式硬件·架构
LS·Cui1 小时前
单片机按键示例功能
单片机
【ql君】qlexcel2 小时前
MCU上电到运行的全过程
单片机·嵌入式硬件·mcu·启动过程
搞一搞汽车电子2 小时前
S32K3平台eMIOS 应用说明
开发语言·驱动开发·笔记·单片机·嵌入式硬件·汽车
pQAQqa2 小时前
FreeRTOS项目(2)摇杆按键检测
stm32·单片机·嵌入式硬件·freertos
小莞尔3 小时前
【51单片机】【protues仿真】基于51单片机停车场的车位管理系统
c语言·开发语言·单片机·嵌入式硬件·51单片机
一川月白7094 小时前
51单片机---硬件学习(跑马灯、数码管、外部中断、按键、蜂鸣器)
单片机·学习·51单片机·外部中断·蜂鸣器·数码管·跑马灯
月阳羊5 小时前
【硬件-笔试面试题-69】硬件/电子工程师,笔试面试题(知识点:电机驱动电路的反馈电路)
java·经验分享·嵌入式硬件·面试