STM32·江科大(上)

笔记中所用动图来源——B站up:keysking

一、简介

9c8f7ec84b944af2bda6996e4997bc72.png

1.片上资源/外设

cc8560f026194e76b6382f4a143f5f9e.png

2.命名规则 

69fed9f106674f409c40412dd095b65b.png

3.系统结构

a3e983af2bba42ecb9a8981a46bad3d7.png

(1) Cortex-M3引出三条总线,分别为ICode(指令总线)——加载程序指令、DCode(数据总线)——加载数据(如常量和调试数据)、System总线,ICode和DCode主要是链接Flash闪存的,Flash里存储的是编写程序。

(2)SRAM——存储程序运行时的变量数据

(3)AHB系统总线(先进高性能总线 Advanced High Performance Bus)——挂载主要外设,如复位和时钟控制

(4)APB(先进外设总线 Advance Peripheral Bus)——链接一般的外设

由于AHB和APB的总线协议、总线速度、还有数据传送格式的差异,连接时中间需要加两个桥接,来完成数据的转换和缓存。AHB总体性能高于APB,APB1性能高于APB2,APB2一般和AHB同频率,都为72MHZ,APB1一般为36MHZ。因此APB2所连接的都是外设中稍微重要的部分(如GPIO端口和一些外设的1号:UASRT1、SPI1、TIM1(高级定时器)、TIM8(高级定时器)等)

(5)DMA(传输单位模块)——搬运速度快,一般用于块设备,例如磁盘

4.引脚定义

fa9b59e29de34a82a659698e5eb0b3b6.png

(1)颜色说明

橙色为电源相关引脚

蓝色是最小系统引脚

绿色是IO口、功能口引脚

(2)表格说明

a. I/O口说明——I/O口所能容忍的电压,FT代表能容忍5V电压,没有则代表只能容忍3.3V电压,如果没有FT的需要接5V电平,就需要加装电平转换电路

b. 主功能——上电后默认的功能,一般和引脚名称相同,如果不同,引脚的实际功能是主功能而不是引脚名称的功能

c. 默认复用——是I/O口上同时连接的外设功能引脚

d. 重定义功能——如果有两个功能同时复用在一个I/O口上,此时两个功能都需要用到,这时可以将其中一个复用功能重映射到其他端口上(前提是这个重定义功能的表里有对应的端口)

5.启动配置 

b85eb763534e4fbdb2fe115af0eac7a8.png 作用:指定程序开始运行的位置

 6.最小系统电路

ba7fbd2eda074a74923b7e3ddadda033.png

二、新建工程 

        目前STM32的开发方式主要有基于寄存器的方式、基于标准库(库函数)的方式和基于HAL库的方式,基于寄存器的方式与51单片机开发方式一致,通过配置寄存器来控制内部线路的链接,来达到我们想要的功能,这种方式最底层、最直接、效率更高,但由于STM32的结构复杂、寄存器太多,故基于寄存器的方式不推荐;基于库函数的方式是使用ST官方提供的封装好的函数,通过调用这些函数来间接配置寄存器,这种方式既能满足对寄存器的配置,对开发者也比较友好,有利于提高开发效率;基于HAL库的方式可以用图形化界面快速配置STM32,适合快速上手STM32的情况,但该方法隐藏了底层逻辑,对STM32不能形成很深的理解。

 1.库函数文件夹

(1)_htmresc目录

        只包含图片 

(2) Libraries目录

        里面包含的就是库函数文件,建立工程时会用到

(3)Project目录

        里面是官方提供的工程示例和模板,以后使用库函数的是时候可以参考 

(4)Utilities目录

        是STM32官方评估板的相关例程,评估板就是官方用STM32做的一个小电路板,用来测评STM32

(5)Release_Notes.html和stm32f10x_stdperiph_lib_um.chm目录

        一个是库函数的发布文档(关于版本的说明),一个是使用手册(有库函数的使用教程)

2.建立基于标准库的工程 

(1)建立存放工程文件夹 

        新建方式与51一致:建立存放文件夹 -> 建立文件夹并命名2-1 STM32工程模板 -> 给工程文件命名Project -> 选择型号(STM32F103C8)

 (2)添加工程所需的必要文件

         打开固件库文件夹 -> Libraries -> CMSIS -> CM3 -> DeviceSupport -> ST -> STM32F10x -> startup -> arm

        arm中的文件就是STM32的启动文件,STM32的程序就是从启动文件开始执行的。复制这些文件至工程模板文件夹中新建的一个文件夹Start(自建),再将STM32F10x中stm32f10x.h(外设寄存器描述文件,作用跟51单片机中的头文件类似,用来描述STM32中有哪些寄存器和它对应的地址)、system_stm32f10x.c、system_stm32f10x.h(两个system文件是用来配置时钟的;STM32的主频72MHz,就是system文件里的函数配置的),将这三个文件也复制到Star文件夹中

        由于STM32是内核和内核外围的设备组成,且内核的寄存器描述和外围设备的描述文件不在一起,因此还需添加一个内核寄存器的描述文件:CM3 -> CoreSupport 里面的两个Core_cm3文件就是内核寄存器的描述文件,同样粘贴至Star文件夹下

        回到Keil,点击选中Source Group ,再次单机进行重命名,改为Start,右键选择添加已经存在的文件,先将创建Start文件中启动文件(后缀为md.s)添加至此,再将里面的.c和.h文件全部添加进来

65cc0955feb64faea455f9cb61e3cf03.png

(3)添加头文件路径

        魔术棒 -> C/C++ -> Include Paths -> 点击旁边的三点键 -> 新建路径 -> 点击三点键 -> 将Start的路径添加至此

(4)建立main函数 

        返回SMT32工程模板 -> 创建User文件夹 -> 回到Keil中,右击Target,添加组 -> 重命名为User -> 右击User,添加新文件 -> 选择C文件,命名为main -> 修改路径到User文件路径

(5)关于报错

        报4个error和19个警告,在魔法棒那里点ARM Compiler下拉框选V5.06

(6)结果

0a6b8385eb77460ba51d570296288184.png         当前工程还没有添加STM32的库函数,所以仍是一个基于寄存器开发的工程

3.利用库函数点灯 

        打开工程文件夹 -> 新建Library文件,用来存放库函数 -> 打开固件库文件夹 -> Libraries -> STM32标准外设驱动 -> src 里面的文件便是库函数的源文件,其中misc是内核库函数,其他是内核外设库函数

b3f435220e924012a4311cd46491b4d4.png

        打开固件库inc,里面是库函数的头文件,同样复制至Library文件中

17a00638b49c4d16977518a43e3f3838.png

         返回Keil -> 右击Target -> 添加组 -> 重命名为Library -> 右击Library,添加已经存在的文件 -> 打开Library,全选添加

        但此时库函数不能直接使用,需要添加一个文件:固件库 -> Project -> STM32Template,此时可以看到conf.h和两个it.c it.h结尾的文件,conf(configuration)文件用来配置库函数头文件的包含关系,另外还有用来参数检查的库函数定义,这是所有库函数都需要的;两个it文件(interrupt)文件是用来存放中断函数的,将这三个文件复制至User中,再到Keil中,将三个文件添加至User组中,最后需要宏定义,打开头文件 -> 找到编译语句并复制 -> 魔法棒 -> C/C++ -> 粘贴至Define -> include Paths三点键 -> 添加User和Library路径

70aeb7aa892442f2a4775a58713df593.png

fa8b7e5d043f49168e9e57e79bd9dbbb.png

         此时就可以通过库函数来进行点灯操作:

(1)配置使能时钟

        通过RCC_APB2PeriphClockCmd()函数来启用时钟不自动提示的点击 右边扳手 Text Completion  勾选Symbols after

3ca89dd50c7f49df86f85814e4e19633.png

        右击函数跳转至函数定义,可以看到简介和参数说明

05295f4c574d44ed98fa62974386b2d4.png

         复制红框文字,作为函数的第一个参数,再看第二个参数,NewState的值可以是ENABLE或者DISABLE,复制ENABLE作为第二个参数

3ee4bee67b7c4aed83cddd055f4926aa.png

        此时GPIOC的外设时钟就配置好了 

(2)配置端口模式 

         用GPIO_Init()函数

b4fd2573df264c01a152fbf036cb5e97.png

        右击跳转函数定义, 可以看到这个函数介绍是根据GPIO_Init结构体的参数来配置GPIO

9c2d26812c6d433caad172002f3a0dbe.png

        第一个参数GPIOx,其中x可以从A到G,选择你要配置哪个GPIO。我们是需要PC13口的LED,所以第一个参数就是GPIOC;

        第二个参数是GPIO_InitTypeDef的结构体,需要先定义一个结构体,命名为GPIO_InitStructure,然后附上结构体参数,引出三个参数,分别是GPIO模式、GPIO端口、GPIO速度

f7a7542c00f54254a11a8641bbe61bf6.png

        右击Mode跳转定义可以看见介绍说: 该参数可以是GPIOMode_TypeDef中的一个值

f324bcec51344d3b81066517aadf05cf.png

        继续右击GPIOMode_TypeDef跳转定义,可以看到是一个枚举,GPIOMode就是这里其中一个值

3fa346d9bd734e758f3bafb6c2a82d9a.png

        选择复制Out_PP(Push-Pull),这就是通用推挽输出,粘贴至参数位置,配置完成

3a8f0d8e5faa4b8b8f66bd5f94119f5d.png

         继续配置Pin,右击打开定义,双击member项,进入定义

25cf931b48c1430a937ebe0ce0597926.png         后续操作同第一个参数一致,全部配置完成即可

295418c31cb741c7872713c00a411005.png        此时,结构体变量就完成了,然后就可以填GPIO_Init的第二个参数,对照参数可知,第二个参数是一个指向结构体的指针

9c2d26812c6d433caad172002f3a0dbe.png

所以我们需要传递结构体的地址,先复制结构体的名字,粘贴到参数二位置,并加上取地址符&,至此GPIO模式配置就完成了
 

e7ed793b70814ee087b45cbc01b480ad.png

        STM32配置方式是一致的,多用就会慢慢熟练的

        接下来就是设置高低电平,来控制LED的亮灭 

#include "stm32f10x.h"                  // Device header

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_SetBits(GPIOC, GPIO_Pin_13);        //设置为高电平
//	GPIO_ResetBits(GPIOC, GPIO_Pin_13);		//设置为低电平
	while (1)
	{
		
	}
}

4.补充

(1)启动文件

        Start中的启动文件有很多类型,至于类型的选择要根据芯片的型号选择

dd89aa9ec8ac491aadf19866661083f5.png

        型分类如下图3bce402a32c44c798dac5b9525faba35.png

       根据前述介绍,STM32是STMF103系列,Flash为64k,因此选择MD结尾的启动文件

(2)新建工程步骤总结

        •建立工程文件夹,Keil中新建工程,选择型号

        •工程文件夹里建立Start、Library、User等文件夹,复制固件库里面的文件到工程文件夹

        •工程里对应建立Start、Library、User等同名称的分组,然后将文件夹内的文件添加到工程分组里

        •工程选项,C/C++,Include Paths内声明所有包含头文件的文件夹

        •工程选项,C/C++,Define内定义USE_STDPERIPH_DRIVER

        •工程选项,Debug,下拉列表选择对应调试器,Settings,Flash Download里勾选Reset and Run

(3)工程架构

4a885640b3534d3caae3f4c88171edbe.png

三、GPIO输出

1.简介

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

        可配置为8种输入输出模式

        引脚电平:0V~3.3V,部分引脚可容忍5V(可在部分端口输入5V电压,也认为是高电平,但对于输出而言,最大只能输出3.3V,因为供电就只有3.3V),具体可以容忍5V的端口可参考STM32引脚定义,I/O口电平标有FT的既是

ed7c6a8fc76e4b0f87f6ef7ce82757ed.png

        输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等

        输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等

2.GPIO基本结构 

        在STM32中,所有的GPIO都是挂载在APB2外设总线上的,其中GPIO外设名称是按照GPIOA、GPIOB、GPIOC...等来命名的,每个GPIO外设共有16个引脚,编号从0~15,那么一般把GPIOA的第0号引脚称为PA0,接着第1号就是PA1...以此类推。每个GPIO模块内,主要包含了寄存器和驱动器,寄存器就是一段特殊的存储器,内核可以通过APB2总线对寄存器进行读写。由于STM32是32位单片机,所以STM32内部寄存器都是32位的,但是端口只有16位,因此只有低16位有对应的端口,高16位没有被用到驱动器是用来增加信号的驱动能力

0a37d9100ace4c289080b2d1450b5900.png

 3.GPIO位结构

d3b99716be20401e9fea8edb4c358b40.png

        整体结构可以被分为两个部分,上面是输入部分,下面是输出部分 

ba2296ef1923449890a98788c711e737.png

(1)输入部分 

        a.I/O口

 17536aa330754e8ab4ce9ad27c2f7ba8.png

        如图,I/O引脚处连接两个保护二极管,用作对输入电压进行限幅,一个接VSS(0V),一个接VDD(3.3V)。当输入电压大于3.3V时,上面的二极管导通,电流流向VDD,而不会流入设备内部,避免过高的电压对内部这些电路产生伤害。当输入电压小于0V时(此电压是相对于VSS的电压,所以可以有负电压),下面的二极管被导通,电流从VSS流出I/O引脚,也不会进入设备内部。当输入电压在0~3.3v之间,两个二极管都不会被导通。

        b.上拉/下拉电阻

3e155bc60a134252ae8039c86e884335.png

         如图,分别连接一个上拉电阻和一个下拉电阻,这两个开关是可以通过程序进行配置的,当上面导通,下面断开,就是上拉输入模式。当下面导通,上面断开,就是下拉输入模式。若两个都断开,就是浮空输入模式。

        上拉和下拉的作用:为了给输入一个默认的输入电平。对于一个数字端口,输入不是高电平就是低电平。事实上,如果什么都不接,相当于浮空输入模式,引脚的输入电平极易受到外界干扰而变化。为了避免这种引脚悬空而导致的输入数据的不稳定,就需要加上上拉电阻或者下拉电阻。当接入上拉电阻,此时引脚悬空,还有上拉电阻来保证引脚的高电平。因此上拉输入又可称为默认高电平输入模式,同理也有低电平输入模式。该上拉电阻和下拉电阻的阻值都比较大,是一种弱上拉和弱下拉,目的是尽量不影响正常的输入操作

         c.肖特基触发器

a56acd1133f04659a5c658e38abb3a6d.png

         由于外界输入的虽然是数字信号,但实际情况下,可能会产生各种失真,因此该模块的作用是对输入电压进行整形,执行逻辑为:若输入电压大于某一阈值,输出就会瞬间升为高电平,如果输入电压小于某一阈值,输出就会瞬间降为低电平。(如图红线是整形后的波形图)

41bd7e723e0c445b9a3352241831d5d5.png

        由肖特基触发器整形后的波形就可以直接写入输入数据寄存器中,再用程序读取输入数据寄存器对应某一位的数据就可知道输入的电平

        片上外设有模拟输入和服用功能输入。模拟输入连接到ADC上,因为ADC需要接受模拟量,故接在肖特基触发器前复用功能输入是连接到其他需要读取端口的外设上(如串口输入引脚等),这根线接收的是数字量,故接在肖特基触发器后

(2) 输出部分

        a.输出寄存器和复用功能输出

f49ac2ee24a2469dad588f83f692b254.png

         数字部分由输出数据寄存器或片上外设控制,两种控制方式通过数据选择器接到输出控制部分。若选择输出数据寄存器进行控制,就是普通I/O口输出,写该寄存器的某一位就可以操作对应某个端口。位设置/清除寄存器可以单独操作输出数据寄存器的某一位,而不影响其他位。由于输出数据寄存器同时控制16个端口,并且这个寄存器只能整体读写,所以想单独控制某一个端口而不影响其他端口,可采取特殊的操作方式:

        第一种方式:先读取这个寄存器,然后用按位与和按位或的方式更改某一位,最后再将更改后的数据写回去,在C语言中就是&=和|=的操作,该方法较麻烦,效率不高,不推荐使用

        第二种方式:通过设置位设置/位清除寄存器,若我们要对某一位进行置1的操作,在位设置寄存器的对应位写1即可,剩下不需要操作的位写0,此时自动将输出数据寄存器对应位置为1,而剩下写0的位则保持不变。同理想对某一位清0,就在清零寄存器的对应位置写1即可

        b.MOS管

a9900e5357db49ab86a1df1e3bb6fb4d.png

         开关负责将I/O口接到VDD或者VSS,此时有推挽、开漏或关闭三种输出模式

        推挽模式P-MOS、N-MOS均有效 ,数据寄存器为1时,上管导通,下管断开,输出直接接到VDD,就是输出高电平,反之输出低电平。该模式下,高低电平均有较强的驱动能力,故该模式也称强推输出模式。总的来说,STM32对I/O口具有绝对控制权,高低电平都由STM32说的算。

        开漏模式P-MOS无效,只有N-MOS工作,数据寄存器为1时,下管断开,此时相当于输出断开,也就是高阻模式。数据寄存器为0时,下管导通,输出直接到VSS,也就是输出低电平。该模式下,只有低电平有驱动能力,高电平没有驱动能力。此模式可作为通信协议的驱动方式(I2C通信的引脚,就是使用开漏模式),在多机通信的情况下,该模式可以避免各个设备间的相互干扰,另外开漏模式还可以用于输出5V的电平信号,如在I/O口接一个上拉到5V的电源,当输出低电平时,由内部N-MOS直接接VSS,当输出高电平时,由外部的上拉电阻拉高到5V

          关闭模式:两个MOS管都无效,也就是输出关闭,端口的电平由外部信号来控制

4.GPIO模式 

        通过配置GPIO的端口配置寄存器,端口可以配置成以下8种模式

c1024eaf654a4abda6e1ccca41906955.png

(1)浮空 / 上拉 / 下拉输入 

5fe413a21dfe48518bd04cb59b6e633e.png

        由图可知,此时输出是断开的,端口只能输入不能输出。在I/O引脚部分,当接VDD3.3V,又外接一个5V电源,此时二极管会被导通,并产生较大的电流,因此上端二极管需要进行一些处理。

(2)模拟输入 

8cfce9f285bf4cbd8251717317f7e4ce.png        由图可知,肖特基触发器和输出驱动均处于关闭状态,是由引脚直接接入片上外设

(3)开漏 / 推挽输出 6b8ba33739b045c9845fa6e8fed857ac.png

        由图可知,输入和输出都是有效的,这是因为一个端口只能有一个输出,但可以有多个输入

(4)复用开漏 / 推挽输出 

533c3d2662ba4a288d2ea7e44704f57e.png

        由图可知,通用输出是没有连接的,引脚控制权在片上外设,由片上外设控制

四、LED和蜂鸣器介绍

1.简介

        LED:发光二极管,正向通电点亮,反向通电不亮

        有源蜂鸣器内部自带振荡源,将正负极接上直流电压即可持续发声,频率固定

        无源蜂鸣器内部不带振荡源,需要控制器提供振荡脉冲才可发声,调整提供振荡脉冲的频率,可发出不同频率的声音

2.硬件电路

         LED电路:

bad6aa7a37d1440ba468fa8475bcab19.png低电平驱动电路

3b5412d77b974f9e8ac7fd4258b1a4b8.png高电平驱动电路

        蜂鸣器电路 :使用三极管开关的驱动方案

9c2f4c6ef2234b0692fbb50819b22d8d.png

        PNP三极管驱动电路:三极管左端是基极,带箭头的是发射极,剩下的是集电极。基极给低电平,三极管就会导通,通过3.3V和GND就可以给蜂鸣器提供驱动电流了。基极给高电平,三极管截止,蜂鸣器没有电流

4b14f17dd12044ca94d2eca759b14059.png

        NPN三极管驱动电路:驱动逻辑与PNP是相反的,高电平接通,低电平断开

        注:PNP三极管最好接上,NPN三极管最好接下 ,因为三极管的通断,是需要在发射极和基极直接产生一定的开启电压,也就是箭头后的电压要高于箭头指向的电压就导通。

 3.实战

(1)点亮LDE

        a.接线图8ba051c503a346ceb5b3246d44b6c420.png

        如图所示,采用的是低电驱动电路

        b. 操作STM32的GPIO的三个步骤:

        1.使用RCC开启GPIO时钟

        2.使用GPIO_Init函数初始化GPIO 

        3.使用输入或者输出的函数控制GPIO口

        c. RCC库函数

        打开Library中的RCC.h文件,下拉即可看到其所有的库函数,本教程中常用的只有以下三个 

void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);

/*
RCC AHB外设时钟控制
RCC APB2外设时钟控制
RCC APB1外设时钟控制
*/

        右击跳转函数定义,可知,AHB、APB2、APB1操作都是一样的,第一个参数选择外设,第二个参数选择使能或失能

         d. GPIO库函数

        打开Library中的GPIO.h文件,下拉可以看到GPIO所有的库函数,目前只需了解如下库函数即可

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);
//给结构体变量赋一个默认值
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);
//读取整个输出寄存器
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);
//根据第三个参数的值,来设置指定端口的高低电平
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
//可以同时对16个端口进行写入操作

        e. 代码实现 

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

int main(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE); //设置GPIO时钟
	
	GPIO_InitTypeDef GPIO_IniteStructure;             //定义结构体变量
	GPIO_IniteStructure.GPIO_Mode = GPIO_Mode_Out_PP; //选择推挽输出模式
	GPIO_IniteStructure.GPIO_Pin = GPIO_Pin_0;        //选择0号引脚
	GPIO_IniteStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA , &GPIO_IniteStructure);    //GPIO初始化函数,调用已定义的结构体变量
	
	
	while(1)                                    //接线时选择的是低电平点亮
	{
		GPIO_ResetBits(GPIOA , GPIO_Pin_0);                //两种写法选一即可
        GPIO_WriteBit(GPIOA , GPIO_Pin_0 , Bit_RESET );
	}
}

(2)LED闪烁

        原理同点灯类似,不再赘述

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

int main(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE);
	
	GPIO_InitTypeDef GPIO_IniteStructure;
	GPIO_IniteStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_IniteStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_IniteStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA , &GPIO_IniteStructure);
	
	
	while(1)
	{
		GPIO_WriteBit(GPIOA , GPIO_Pin_0,Bit_RESET);
		Delay_ms(500);
		GPIO_WriteBit(GPIOA , GPIO_Pin_0,Bit_SET);
		Delay_ms(500);
	}
}

(3)LED流水灯

        a. 接线图

8fc924a0c92f4e45929995e9393b4f8e.png

        如图所示,LED都选择低电平驱动电路

        b. 配置说明

	GPIO_IniteStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2;

        该配置方法可以一次打开多个端口,具体原理可以右击打开函数定义

#define GPIO_Pin_0                 ((uint16_t)0x0001)  /*!< Pin 0 selected */
#define GPIO_Pin_1                 ((uint16_t)0x0002)  /*!< Pin 1 selected */
#define GPIO_Pin_2                 ((uint16_t)0x0004)  /*!< Pin 2 selected */
#define GPIO_Pin_3                 ((uint16_t)0x0008)  /*!< Pin 3 selected */
#define GPIO_Pin_4                 ((uint16_t)0x0010)  /*!< Pin 4 selected */
#define GPIO_Pin_5                 ((uint16_t)0x0020)  /*!< Pin 5 selected */
#define GPIO_Pin_6                 ((uint16_t)0x0040)  /*!< Pin 6 selected */
#define GPIO_Pin_7                 ((uint16_t)0x0080)  /*!< Pin 7 selected */
#define GPIO_Pin_8                 ((uint16_t)0x0100)  /*!< Pin 8 selected */
#define GPIO_Pin_9                 ((uint16_t)0x0200)  /*!< Pin 9 selected */
#define GPIO_Pin_10                ((uint16_t)0x0400)  /*!< Pin 10 selected */
#define GPIO_Pin_11                ((uint16_t)0x0800)  /*!< Pin 11 selected */
#define GPIO_Pin_12                ((uint16_t)0x1000)  /*!< Pin 12 selected */
#define GPIO_Pin_13                ((uint16_t)0x2000)  /*!< Pin 13 selected */
#define GPIO_Pin_14                ((uint16_t)0x4000)  /*!< Pin 14 selected */
#define GPIO_Pin_15                ((uint16_t)0x8000)  /*!< Pin 15 selected */
#define GPIO_Pin_All               ((uint16_t)0xFFFF)  /*!< All pins selected */

        由图可知,Pin_0对应0x0001,对应二进制为0000 0000 0000 0001,同理其余两个引脚也可化为相应二进制,将他们相或,就可以得到0000 0000 0000 0111,此时多个端口被置为1。为了方便起见,我们就将所有端口打开

	GPIO_IniteStructure.GPIO_Pin = GPIO_Pin_All;

        c. 代码实现

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

int main(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE);
	
	GPIO_InitTypeDef GPIO_IniteStructure;
	GPIO_IniteStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_IniteStructure.GPIO_Pin = GPIO_Pin_All;
	GPIO_IniteStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA , &GPIO_IniteStructure);
	
	
	while(1)
	{
		GPIO_Write(GPIOA , ~0x0001); //0000 0000 0000 0001
		Delay_ms(500);
		GPIO_Write(GPIOA , ~0x0002); //0000 0000 0000 0010
		Delay_ms(500);
		GPIO_Write(GPIOA , ~0x0004); //0000 0000 0000 0100
		Delay_ms(500);
		GPIO_Write(GPIOA , ~0x0008); //0000 0000 0000 1000
		Delay_ms(500);
		GPIO_Write(GPIOA , ~0x0010); //0000 0000 0001 0000
		Delay_ms(500);
		GPIO_Write(GPIOA , ~0x0020); //0000 0000 0010 0001
		Delay_ms(500);
		GPIO_Write(GPIOA , ~0x0040); //0000 0000 0100 0001
		Delay_ms(500);
		GPIO_Write(GPIOA , ~0x0080); //0000 0000 1000 0001
		Delay_ms(500);
	}
}

(4)蜂鸣器

        a. 接线图

265a36fc490f4dfca7da44d2ea11f13f.png

        b.  配置原理

        由前述实践,该原理就很容易理解,只需要注意,套件中的蜂鸣器是低电平有效的,置0响,置1不响

        c. 代码实现

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

int main(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE);
	
	GPIO_InitTypeDef GPIO_IniteStructure;
	GPIO_IniteStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_IniteStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_IniteStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB , &GPIO_IniteStructure);
	
	
	while(1)
	{
		GPIO_ResetBits(GPIOB , GPIO_Pin_12);
		Delay_ms(100);
		GPIO_SetBits(GPIOB , GPIO_Pin_12);
		Delay_ms(100);
		GPIO_ResetBits(GPIOB , GPIO_Pin_12);
		Delay_ms(100);
		GPIO_SetBits(GPIOB , GPIO_Pin_12);
		Delay_ms(700);
	}
}

五、GPIO输入

1.按键介绍

        按键:常见的输入设备,按下导通,松手断开

        按键抖动:由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动

        抖动最简单的解决办法就是加一段延时,将抖动时间跳过即可

ca40599901fc4945919f7eb626f024fd.png

4b505a861e274779aea7415813b420e7.png

 2.传感器模块

(1)简介

        传感器模块:传感器元件(光敏电阻/热敏电阻/红外接收管等)的电阻会随外界模拟量的变化而变化,但是电阻的变化不易观察,因此通常会通过与定值电阻进行串联分压即可得到模拟电压输出,再通过电压比较器进行二值化即可得到数字电压输出

462dcd8882744741ae5193978286e86a.png

(2)电路分析 

988a9d68307e4bc393697426d87f9cee.png

         首先看第一部分,N1是传感器元件代表的可变电阻,其阻值可以根据环境的光线、温度等模拟量进行变化,其中R1是和N1进行分压的定值电阻,C2是一个滤波电容,给中间的电压输出进行滤波,用来滤除干扰,保证输出电压波形的平滑,保证电路稳定一般在电路中遇到一端接地,一端接入电路的电容都可以考虑是否是滤波电容。分析电路:当N1的阻值减小时,GND下拉作用增强,AO端的电压会被拉低,极端情况下,当N1阻值为0时,AO输出被完全下拉,变为0V。当N1的阻值增大时,下拉作用会减弱,AO端由于R1的上拉作用电压会升高,极端情况下,当N1阻值无穷大,相当于断路,AO端的电压被完全上拉至VCC。在N1和R1的分压下,AO输出的就是模拟电压,见总图,AO的电压直接通过排针输出

e9252bd66c5f437e82b567a752f804f4.png

        其次该模块还支持数字输出(对AO进行二值化输出),二值化是通过LM393芯片完成,LM393是一个电压比较器芯片,内置两个独立的电压比较电路。C1是电压的滤波电容。这个电压比较器实际上就是一个运算放大器,如图,同向电压输入端IN+与AO相接,为模拟电压端,反向电压输入端IN-与电位器相接,这个电位器的接法也是分压电阻的原理,移动电位器,IN-就会生成一个可调的阈值电压,将两个电压进行比较,最终输出结果就是DO,以数字电压输出

379494864f6e421fbf02412981837de3.png

         运算放大器当做比较器时:

(1)当同向输入电压大于反向输入电压时,输出会瞬间升高为最大值,也就是输出VCC

64779fdddb264ee2b98b7f33a7323916.png

 (2)当同向输入电压小于反向输入电压时,输出会瞬间降低为最小值,也就是GND

42df20dbdd6a44ab8ee49f696047f77d.png

         LED2可以指出DO的输出电平,低电平点亮,高电平熄灭,R5为上拉电阻,这是为了保证默认输出为高电平(若没有上拉或者下拉电阻,会导致该输出端成为浮空状态)

547292f07c414e339cd2fb1e31940ea8.png

 (3)硬件电路

        如图所示,给出了按键连接的四种方式,通常情况下都会使用下接方式(是电路设计的习惯和规范),注意分析浮空输入状态

8fe0a598d2874ff6a5f0c9e36f02df19.png

3.按键控制LED

(1)GPIO库函数的使用

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);
//读取整个输出寄存器
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
//锁定GPIO配置,参数指定某个引脚,锁定该引脚,防止意外篡改

(2)代码实现

        a. main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"

uint8_t KeyNum=0;	//定义全局变量,与Key.c中的局部变量不矛盾

int main(void)
{
	LED_Init();		//初始化LED
	Key_Init();		//初始化按键
	
	while(1)
	{
		KeyNum = Key_GetNum();
			if(KeyNum == 1)
				LED1_Turn();	//状态取反
			if(KeyNum == 2)
				LED2_Turn();	//状态取反
	}
}

        b. Key.c

#include "stm32f10x.h" // Device header
#include "Delay.h"

void Key_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB , &GPIO_InitStructure);
}

uint8_t Key_GetNum(void)	//有返回值,用unsigned char类型接收
{
	uint8_t KeyNum = 0;		//无按键按下时,KeyNum为0
	if(GPIO_ReadInputDataBit(GPIOB , GPIO_Pin_1) == 0)	//读取输入寄存器的值,按下按键为0
	{
		Delay_ms(20);		//按键消抖
		while(GPIO_ReadInputDataBit(GPIOB , GPIO_Pin_1) == 0);	//检测松手
		Delay_ms(20);		//按键消抖
		
		KeyNum = 1;
	}	
	if(GPIO_ReadInputDataBit(GPIOB , GPIO_Pin_11) == 0) //读取输入寄存器的值,按下按键为0
	{
		Delay_ms(20);		//按键消抖
		while(GPIO_ReadInputDataBit(GPIOB , GPIO_Pin_11) == 0);	//检测松手
		Delay_ms(20);		//按键消抖
		
		KeyNum = 2;
	}
	return KeyNum;
}

        c. Key.h

#ifndef __KEY_H__
#define __KEY_H__

void Key_Init(void);
uint8_t Key_GetNum(void);

#endif

        d. LED.c

#include "stm32f10x.h"                  // Device header

void LED_Init(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_1 | GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA , &GPIO_InitStructure);
	
	GPIO_SetBits(GPIOA , GPIO_Pin_1 | GPIO_Pin_2);
}

void LED1_ON(void)
{
	GPIO_ResetBits(GPIOA , GPIO_Pin_1);
}

void LED1_OFF(void)
{
	GPIO_SetBits(GPIOA , GPIO_Pin_1);
}	

void LED1_Turn(void)	//LED1状态取反
{
	if(GPIO_ReadOutputDataBit(GPIOA , GPIO_Pin_1) == 0)	//如果LED1处于点亮状态
	{
		GPIO_SetBits(GPIOA , GPIO_Pin_1);	//熄灭LED1
	}
	else
	{
		GPIO_ResetBits(GPIOA , GPIO_Pin_1);	//点亮LED1
	}
}

void LED2_ON(void)
{
	GPIO_ResetBits(GPIOA , GPIO_Pin_2);
}

void LED2_OFF(void)
{
	GPIO_SetBits(GPIOA , GPIO_Pin_2);
}		

void LED2_Turn(void)	//LED2状态取反
{
	if(GPIO_ReadOutputDataBit(GPIOA , GPIO_Pin_2) == 0)	//如果LED2处于点亮状态
	{
		GPIO_SetBits(GPIOA , GPIO_Pin_2);	//熄灭LED2
	}
	else
	{
		GPIO_ResetBits(GPIOA , GPIO_Pin_2);	//点亮LED2
	}
}

        e. LED.h

#ifndef __LED_H__
#define __LED_H__

void LED_Init(void);
void LED1_ON(void);
void LED1_OFF(void);
void LED2_ON(void);
void LED2_OFF(void);
void LED2_Turn(void);
void LED1_Turn(void);

#endif

        f. Delay.c

#include "stm32f10x.h"

/**
  * @brief  微秒级延时
  * @param  xus 延时时长,范围:0~233015
  * @retval 无
  */
void Delay_us(uint32_t xus)
{
	SysTick->LOAD = 72 * xus;				//设置定时器重装值
	SysTick->VAL = 0x00;					//清空当前计数值
	SysTick->CTRL = 0x00000005;				//设置时钟源为HCLK,启动定时器
	while(!(SysTick->CTRL & 0x00010000));	//等待计数到0
	SysTick->CTRL = 0x00000004;				//关闭定时器
}

/**
  * @brief  毫秒级延时
  * @param  xms 延时时长,范围:0~4294967295
  * @retval 无
  */
void Delay_ms(uint32_t xms)
{
	while(xms--)
	{
		Delay_us(1000);
	}
}
 
/**
  * @brief  秒级延时
  * @param  xs 延时时长,范围:0~4294967295
  * @retval 无
  */
void Delay_s(uint32_t xs)
{
	while(xs--)
	{
		Delay_ms(1000);
	}
} 

        g. Delay.h

#ifndef __DELAY_H
#define __DELAY_H

void Delay_us(uint32_t us);
void Delay_ms(uint32_t ms);
void Delay_s(uint32_t s);

#endif

4. 光敏传感器控制蜂鸣器

(1)接线图

c91fc35ad9bd4e529cd60579b4e54384.png        接好后会发现,指示灯都是亮灯状态。当遮住光线时,输出指示灯熄灭,代表输出高电平,未遮光时,输出指示灯点亮,代表输出低电平,也就是说,未遮光时,光敏传感器向单片机输入低电平,遮光时,光敏传感器向单片机出入高电平

(2)代码实现 

        a. main.c文件

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "BUZZER.h"
#include "LightSensor.h"


int main(void)
{
	BUZZER_Init();
	LightSensor_Init();
	
	while(1)
	{
		if(LightSensor_Get()== 1)	//光敏传感器输入1
		{
			BUZZER_ON();			//打开蜂鸣器
		}
		else
		{
			BUZZER_OFF();			//关闭蜂鸣器
		} 
	}
}

        b. BUZZER.c文件

#include "stm32f10x.h"                  // Device header

void BUZZER_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB , &GPIO_InitStructure);
	
	GPIO_SetBits(GPIOB , GPIO_Pin_12);
}

void BUZZER_ON(void)
{
	GPIO_ResetBits(GPIOB , GPIO_Pin_12);
}

void BUZZER_OFF(void)
{
	GPIO_SetBits(GPIOB , GPIO_Pin_12);
}	

void BUZZER_Turn(void)	//蜂鸣器状态取反
{
	if(GPIO_ReadOutputDataBit(GPIOB , GPIO_Pin_12) == 0)	//如果蜂鸣器打开
	{
		GPIO_SetBits(GPIOB , GPIO_Pin_12);	//关闭蜂鸣器
	}
	else
	{
		GPIO_ResetBits(GPIOB , GPIO_Pin_12);	//打开蜂鸣器
	}
}

        c. BUZEER.h文件

#ifndef __BUZZER_H__
#define __BUZZER_H__

void BUZZER_Init(void);
void BUZZER_ON(void);
void BUZZER_OFF(void);
void BUZZER_Turn(void);

#endif

        d. LightSensor.c文件

#include "stm32f10x.h"                  // Device header

void LightSensor_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE); 
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB , &GPIO_InitStructure);
}

uint8_t LightSensor_Get(void)        //读取光敏电阻输入值
{
	return GPIO_ReadInputDataBit(GPIOB , GPIO_Pin_13);
}

        e. LightSensor.h 

#ifndef __LIGHTSENSOR_H__
#define __LIGHTSENSOR_H__

void LightSensor_Init(void);
uint8_t LightSensor_Get(void);

#endif

六、LOED调试工具

 1.调试方式

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

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

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

2.OLED简介

        OLED(Organic Light Emitting Diode):有机发光二极管

        OLED显示屏:性能优异的新型显示屏,具有功耗低、相应速度快、宽视角、轻薄柔韧等特点

        0.96寸OLED模块:小巧玲珑、占用接口少、简单易用,是电子设计中非常常见的显示屏模块

        供电:3~5.5V,通信协议:I2C/SPI,分辨率:128*64

7608aae30cac40cdb9e956d86f0871fb.png

 3.硬件电路

9cedfe18a0794fe4ad27832e7a29943a.png

4.OLED驱动函数 

073be6ea8d9d4784a0de3dfc7fa84ca2.png

函数 函数fe2f97358c1f4e43aa30a17ff48b46f4.png

5.简单显示 

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"

int main(void)
{
	OLED_Init();
	
	OLED_ShowChar(1,1,'A');
	OLED_ShowString(1,3,"HelloWord!");
	OLED_ShowNum(2,1,12345,5);
	OLED_ShowSignedNum(2,7,-66,2);
	OLED_ShowHexNum(3,1,0xAA55,4);
	OLED_ShowBinNum(4,1,0xAA55,16);
	
	while(1)
	{
		
	}
}

 七、EXTI外部中断

 1.中断系统

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

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

        中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回

 2.中断执行流程

252cf0ed23b1467cb13b35ff1c5e1007.png

 3.STM32中断

        有68个可屏蔽中断通道,也就是有68个中断源,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设使用NVIC统一管理中断每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级

        由于程序中的中断函数的地址是由编译器来分配的,并不固定。但由于硬件的限制,在中断跳转时,只能跳到固定地址执行程序,为了能让硬件跳转到一个不固定的中断函数中,此时就需要在内存中定义一个地址的列表,该列表中的地址是固定的,中断发生后,就跳到这个地址,在改地址中,由编译器加上一条跳转到中断函数的代码,此时中断就可以跳转到任意位置了。该中断地址的列表就是中断向量表

4.NVIC基本结构 

31cf19ed561542ab82d38f7b796f6f40.png

        STM32中,NVIC是用来统一分配中断优先级和管理中断的

5.NVIC优先级分组 

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

        响应优先级:当前已经有中断正在执行,同时后面还排着若干中断,此时来了一个优先级最高的中断,等待正在执行的中断完成后,该最高优先级中断不用等待直接执行,总的来说这种可以插队的优先级就叫响应优先级

        抢占优先级当前已经有中断正在执行,同时后面还排着若干中断,此时来了一个优先级最高的中断,不等正在执行的中断完成,直接让其等待,转而执行最高优先级的中断,总的来说这种决定是否可以中断嵌套的优先级就叫抢占优先级

        为了将优先级再区分为抢占优先级和响应优先级,就需要对16个优先级进行分组。NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级。抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队,也就是数值小的优先响应

a261b08c728a4ac281ee5083875d1f3f.png

        该分组方式在程序中由编程者决定,因此在配置优先级时,需要注意抢占优先级和响应优先级的取值范围

 6.EXTI简介

        EXTI(Extern Interrupt)外部中断

        EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序

        支持的触发方式:上升沿/下降沿/双边沿/软件触发

        支持的GPIO口:所有GPIO口都可触发中断,但相同的Pin不能同时触发中断,如PA0和PB0

        通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒,共20个

        触发响应方式:中断响应/事件响应(中断响应是正常的流程,引脚电平变化触发中断,事件响应不会触发中断而是触发别的外设操作,属于外设之间的联合工作)

7.EXTI基本结构 

a0f31f046e704046858d71dea45cdc41.png

        最左侧为GPIO口的外设, 每个GPIO外设有16个引脚,而EXTI模块只有16个GPIO通道,如果每个引脚占用一个通道,这16个通道肯定不够分,因此会有一个AFIO中断引脚选择模块,他可以在所有GPIO的16个引脚中,选择其中一个连接到EXTI通道中,也就是将所有GPIO的16个引脚分为16组,每组只能选其一,连接到EXTI相应编号的通道上,如GPIOA_Pin_0或者GPIOB_Pin_0或者GPIOC_Pin_0......连接到EXTI的0号通道,所以在前述中说明相同的Pin不能同时触发中断。然后通过AFIO选择后的16个通道,就接到EXTI边沿检测及控制电路上,同时接入的还有四个“蹭网外设”。经过EXTI电路后,分为两种输出,其中有一部分接入NVIC,用来触发中断(如图,本来20路输入应有20路中断输出,但ST公司将EXTI的9~5和15~10接在了一路中,9~5触发同一个中断函数,15~10触发同一个中断函数,因此在编程时,要在这两个函数中通过标志位来区分是哪个中断。剩下的20路接入其他外设用来触发其他外设操作,也就是事件响应)

8.AFIO复用IO口

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

d7f3759addc2494a84ffdf598b52560d.png

 9.EXTI框图

050a8c968eb3452fac99d36b14a80565.png

       EXTI右侧的输入线,即为20根输入线(16+4),进入边沿检测电路,边沿检测电路主要读取20条输入线的电平,根据电平来检测上升沿或下降沿。上升或下降沿寄存器就是使能作用,比如上升沿寄存器0号置1,0号输入线再输入高电平则触发0号边沿电路的上升沿。通过上升沿寄存器和下降沿寄存器可以选择上升沿触发选择或者下降沿触发选择或者两个都选,选择完成后触发信号同软件中断进入或门,只要有1就会输出1,因此在前述EXTI简介中说支持上升沿 / 下降沿 / 双边沿 / 软件触发这几种触发方式。接着兵分两路,上路触发中断,下路触发事件,也就对应着EXTI基本结构中的进入NVIC或其他外设。

        触发中断会首先置一个挂起寄存器,相当于一个中断标志位,可以读取该寄存器从而判断是哪个通道发起的中断,若中断挂起寄存器置1,就会继续向后,与中断屏蔽寄存器一同进入一个与门,此时若中断寄存器为1,就会输出该中断,也就是允许中断,反之则会屏蔽该中断。

        事件中断首先也是一个事件屏蔽寄存器进行开关控制,置1则允许中断,否则屏蔽中断,接着通过脉冲发生器的电平脉冲,触发其他外设动作

10.EXTI使用场景

        对于STM32来说,想要获取的信号是外部驱动的很快的突发信号,如旋转编码器的输出信号,红外遥控接收头的输出,我们随时会操作他们,因此这些外设会突然发出很多脉冲信号,这个信号是突发的,且是外部驱动的,STM32想要及时接收,就需要中断来解决。但对于按键来说,虽然也是外部驱动的突发事件,但是并不推荐外部中断来读取按键,因为外部中断不好处理按键抖动和松手问题,在中断函数中写消抖和检测松手很大概率会引起卡死。而且按键信号也并不是转瞬即逝的,所以要求不高可以在主程序中循环读取,其次也可采用定时器中断读取的方式,这样既可以做到后台读取按键值、不阻塞主程序,也可以很好的处理按键抖动和松手检测的问题(这部分内容对应51视频的24c02记时器编程的后30分钟内容

11.旋转编码器的介绍 

        旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向 

        类型:机械触点式/霍尔传感器式/光栅式

1.类型简介

(1)光栅式编码器

05c55a1361cd4875b971895b38ad60b3.png

         该编码器是使用对射式红外传感器来测速,配合光栅编码盘,当编码盘转动时,红外传感器的红外光就会出现遮挡、透过、遮挡、透过这样的现象,对应模块输出的电平就是高低电平交替的方波,方波个数就代表转过的角度,方波频率表示转速,因此我们可以通过外部中断捕获方波边沿,以此判断位置和速度,但该模块只有一路输出,正反转输出波形无法区分。因此该测速方法只能测位置和速度,不能测量旋转方向

(2)机械触点式——旋转编码器

46b52887b6cb442aa30de5205bd8d7b4.png

c1d5aa97432c4c138e86503f7bb4ef8d.png         由图可以看出,内部使用金属触点进行通断,看到编码盘,是类似于光栅的只不过是金属触点,旋转时,依次接通和断开两边的触点,值得注意的是,该金属盘的位置是经过设计的,也就是说金属盘上的触点不是轴对称的,旋转的时候就会先后碰到电路板上的左右触点,它能让两侧触点的通断产生一个90度的相位差,由于是采用触点接触的形式,因此不适合测高速旋转

        正转时,左边引脚A的波形要提前于右边引脚B的波形90°,同理反转时,右边引脚A的波形要提前于左边引脚B的波形90°,这样正反转就可以区分开了。这种相差90°的波形也称作正交波形

226716a2a8c04d27869b645e6bb77e4a.png

(3)霍尔传感器式编码器(具体参考手册)

3289605e45aa43759291c50cc743e118.png

2.旋转编码器硬件电路

03fce26e04af4f7eae18de5385456312.png

        由图,两个触点中间接地,两侧接有R1和R2两个上拉电阻,触点未导通时,默认输出A端口的就是高电平,当触点被导通时,就会被GND下拉为低电平,通过A端口输出的就是低电平。R3和R4为两个限流电阻,为了防止模块引脚电流过大而破坏电路,C1,C2为滤波电容

aa23b26a3e5b48418ea0f6f7a3e13810.png

        接线时,注意 A,B的Pin脚不要接重,如PA0和PB0

12.对射式红外传感器&旋转编码器计次

9b44c8d88774483b844f337891ec9388.png

        设计思路: 对于外部中断的配置,根据EXTI的基本结构图可知,只需将外部中断从GPIO到NVIC这一路的外设模块配置好即可。

        第一步,配置RCC。将所涉及的外设时钟都打开

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

        第三部,配置AFIO。选择GPIO的路数,连接到后面的EXTI

        第四部,配置EXTI。选择边沿触发方式,如上升沿、下降沿或双边沿。并且选择触发响应方式,可选择中断响应和事件响应

        第五步,配置NVIC。给与中断一个合适 优先级

a0f31f046e704046858d71dea45cdc41.png

1.库函数

(1)AFIO库函数 

void GPIO_AFIODeInit(void);
//复位AFIO外设,调用时会清除AFIO外设
void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
void GPIO_EventOutputCmd(FunctionalState NewState);
//这两函数用来配置AFIO事件输出功能
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);
//用来进行引脚重映射  参数一:重映射方式  参数二:设置新状态
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
//配置AFIO数据选择器,用来选择中断引脚
void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface);
//配置以太网

(2)EXTI库函数

void EXTI_DeInit(void);
//清除EXTI配置,恢复成上电默认状态
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
//和GPIO功能一致
void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct);
//和GPIO功能一致
void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line);
//软件触发外部中断,,参数给一个指定中断线,若只需要外部引脚触发中断,则不用该函数
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);
//获取指定标志位是否置1,建议主程序中使用
void EXTI_ClearFlag(uint32_t EXTI_Line);
//对置1的标志位进行清除,建议主程序中使用
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
//获取中断标志位是否被置1,建议中断程序中使用
void EXTI_ClearITPendingBit(uint32_t EXTI_Line)
//清除中断挂起标志位,建议中断程序中使用

(3)NVIC库函数

void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
//用来中断分组,参数是中断分组的方式
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
//根据结构体里的指定参数初始化NVIC
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset);
//设置中断向量表
void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState);
//系统低功耗配置

2.代码实现

(1)main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"

int main(void)
{
	/*模块初始化*/
	OLED_Init();			//OLED初始化
	CountSensor_Init();		//计数传感器初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "Count:");	//1行1列显示字符串Count:
	
	while (1)
	{
		OLED_ShowNum(1, 7, CountSensor_Get(), 5);		//OLED不断刷新显示CountSensor_Get的返回值
	}
}

(2)CountSensor.c

#include "stm32f10x.h"                  // Device header

uint16_t CountSensor_Count;				//全局变量,用于计数

/**
  * 函    数:计数传感器初始化
  * 参    数:无
  * 返 回 值:无
  */
void CountSensor_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//开启GPIOB的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);		//开启AFIO的时钟,外部中断必须开启AFIO的时钟
	
	/*GPIO初始化*/
	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);						//将PB14引脚初始化为上拉输入
	
	/*AFIO选择中断引脚*/
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//将外部中断的14号线映射到GPIOB,即选择PB14为外部中断引脚
	
	/*EXTI初始化*/
	EXTI_InitTypeDef EXTI_InitStructure;						//定义结构体变量
	EXTI_InitStructure.EXTI_Line = EXTI_Line14;					//选择配置外部中断的14号线
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;					//指定外部中断线使能
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;			//指定外部中断线为中断模式
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;		//指定外部中断线为下降沿触发
	EXTI_Init(&EXTI_InitStructure);								//将结构体变量交给EXTI_Init,配置EXTI外设
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
																//即抢占优先级范围:0~3,响应优先级范围:0~3
																//此分组配置在整个工程中仅需调用一次
																//若有多个中断,可以把此代码放在main函数内,while循环之前
																//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;		//选择配置NVIC的EXTI15_10线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设
}

/**
  * 函    数:获取计数传感器的计数值
  * 参    数:无
  * 返 回 值:计数值,范围:0~65535
  */
uint16_t CountSensor_Get(void)
{
	return CountSensor_Count;
}

/**
  * 函    数:EXTI15_10外部中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
void EXTI15_10_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line14) == SET)		//判断是否是外部中断14号线触发的中断
	{
		/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
		if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
		{
			CountSensor_Count ++;					//计数值自增一次
		}
		EXTI_ClearITPendingBit(EXTI_Line14);		//清除外部中断14号线的中断标志位
													//中断标志位必须清除
													//否则中断将连续不断地触发,导致主程序卡死
	}
}

(3)CountSensor.h

#ifndef __COUNT_SENSOR_H
#define __COUNT_SENSOR_H

void CountSensor_Init(void);
uint16_t CountSensor_Get(void);

#endif

13.旋转编码器计次 

58807196d049486d9d9791051674d6e3.png

1.原理解读

        本次实验方法利用正交波形来判断正反旋转,如图所示,当A为下降沿时,B若为低电平,则判断为反转;当B为下降沿时,若A为低电平,则判断正转

226716a2a8c04d27869b645e6bb77e4a.png

        注:当两个中断同时响应时,抢占优先级谁高谁先中断,当抢占优先级相同时,谁的响应优先级谁高谁先响应(注意:当抢占优先级相同时,低响应优先级已经进入中断函数,高响应优先级不可打断低优先级)

2.代码实现 

(1)main.c 

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Encoder.h"

int16_t Num;			//定义待被旋转编码器调节的变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	Encoder_Init();		//旋转编码器初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "Num:");			//1行1列显示字符串Num:
	
	while (1)
	{
		Num += Encoder_Get();				//获取自上此调用此函数后,旋转编码器的增量值,并将增量值加到Num上
		OLED_ShowSignedNum(1, 5, Num, 5);	//显示Num
	}
}

(2)Encoder.c

#include "stm32f10x.h"                  // Device header

int16_t Encoder_Count;					//全局变量,用于计数旋转编码器的增量值

/**
  * 函    数:旋转编码器初始化
  * 参    数:无
  * 返 回 值:无
  */
void Encoder_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//开启GPIOB的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);		//开启AFIO的时钟,外部中断必须开启AFIO的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);						//将PB0和PB1引脚初始化为上拉输入
	
	/*AFIO选择中断引脚*/
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);//将外部中断的0号线映射到GPIOB,即选择PB0为外部中断引脚
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);//将外部中断的1号线映射到GPIOB,即选择PB1为外部中断引脚
	
	/*EXTI初始化*/
	EXTI_InitTypeDef EXTI_InitStructure;						//定义结构体变量
	EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;		//选择配置外部中断的0号线和1号线
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;					//指定外部中断线使能
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;			//指定外部中断线为中断模式
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;		//指定外部中断线为下降沿触发
	EXTI_Init(&EXTI_InitStructure);								//将结构体变量交给EXTI_Init,配置EXTI外设
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
																//即抢占优先级范围:0~3,响应优先级范围:0~3
																//此分组配置在整个工程中仅需调用一次
																//若有多个中断,可以把此代码放在main函数内,while循环之前
																//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;			//选择配置NVIC的EXTI0线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设

	NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;			//选择配置NVIC的EXTI1线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;			//指定NVIC线路的响应优先级为2
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设
}

/**
  * 函    数:旋转编码器获取增量值
  * 参    数:无
  * 返 回 值:自上此调用此函数后,旋转编码器的增量值
  */
int16_t Encoder_Get(void)
{
	/*使用Temp变量作为中继,目的是返回Encoder_Count后将其清零*/
	/*在这里,也可以直接返回Encoder_Count
	  但这样就不是获取增量值的操作方法了
	  也可以实现功能,只是思路不一样*/
	int16_t Temp;
	Temp = Encoder_Count;
	Encoder_Count = 0;
	return Temp;
}

/**
  * 函    数:EXTI0外部中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
void EXTI0_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line0) == SET)		//判断是否是外部中断0号线触发的中断
	{
		/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
		if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
		{
			if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)		//PB0的下降沿触发中断,此时检测另一相PB1的电平,目的是判断旋转方向
			{
				Encoder_Count --;					//此方向定义为反转,计数变量自减
			}
		}
		EXTI_ClearITPendingBit(EXTI_Line0);			//清除外部中断0号线的中断标志位
													//中断标志位必须清除
													//否则中断将连续不断地触发,导致主程序卡死
	}
}

/**
  * 函    数:EXTI1外部中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
void EXTI1_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line1) == SET)		//判断是否是外部中断1号线触发的中断
	{
		/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
		if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
		{
			if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)		//PB1的下降沿触发中断,此时检测另一相PB0的电平,目的是判断旋转方向
			{
				Encoder_Count ++;					//此方向定义为正转,计数变量自增
			}
		}
		EXTI_ClearITPendingBit(EXTI_Line1);			//清除外部中断1号线的中断标志位
													//中断标志位必须清除
													//否则中断将连续不断地触发,导致主程序卡死
	}
}

(3)Encoder.h

#ifndef __ENCODER_H
#define __ENCODER_H

void Encoder_Init(void);
int16_t Encoder_Get(void);

#endif

八、TIM定时中断

        定时器可以对输入的时钟进行计数(定时器就是一个计数器),并在计数值达到设定值时触发中断

        16位计数器、预分频器、自动重装寄存器(计数目标值,即申请时钟中断个数)的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时

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

        根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型

        为什么在72MHz计数时钟下可以实现最大59.65s的定时?
        72M/65536/65536,得到的是中断频率,然后取倒数,就是59.65秒多,大家可以自己算一下。
        
详细解释:在定时器中,预分频器和计数器都是16位的,所以它们的最大值是65535,而不是65536。预分频器的最大值决定了计数时钟的频率,而计数器的最大值决定了定时器的最大计数周期。因此,如果预分频器和计数器的最大值都设置为65535,那么定时器的最大时间就是72MHz/65536/65536,得到的是中断频率,倒数就是中断时间。(最大值是65536,但计数是从0~65535)

1.定时器类型 

cd514bfa6c994a71b6ea5206fc0a6c58.png

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

  (1)基本定时器

c458b87468794288b2d33a85cc305cb6.png

        这个可编程定时器的主要部分是一个带有自动重装载的16位累加计数器,计数器的时钟通过一个预分频器得到。

        图中折线UI代表中断信号,像这种计数值等于重装值产生的中断一般称为“更新中断”,更新中断之后就会通往NVIC,然后在配置好NVIC的定时器通道,那么定时器的更新中断就能够得到CPU的响应

        折线U代表产生一个事件,对应的时间称为“更新事件”,更新事件不会触发中断,但可以触发内部其他电路的工作

        软件可以读写计数器、自动重装载寄存器和预分频寄存器,即使计数器运行时也可以操作。时基单元包含:

1.预分频寄存器(TIMx_PSC)

2.计数器

3.自动重装载寄存器

(1)预分频器
        预分频可以以系数介于1至65536之间的任意数值对计数器时钟分频,就是
对输入的基淮频率提前进行一个分频的操作。它是通过一个16位寄存器(TIMx-PSC)的计数实现分频。因为TIMx-PSC控制寄存器具有缓冲,可以在运行过程中改变它的数值,新的预分频数值将在下一个更新事件时起作用。
        假设这个寄存器写0,就是不分频,或者说是1分频,这时候输出频率=输入频率=72MHz;如果预分频器写1,那就是2分频,输出频率=输入频率/2=36MHz,所以预分频器的值和实际的分频系数相差了1,
实际分频系数=预分频器的值+1

(2)预分频器时序 

837e6866eb8c4f4abe121edb56d926f5.png

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

CK_CNT:计数器时钟,既是预分频器2的时钟输出,也是计数器的时钟输入。由图可知,计数器使能为高电平时,前半段的预分频计数器系数为1,计数器的时钟等于预分频器前的时钟,后半段预分频器系数变为2,计数器时钟变为预分频器前时钟的一半

CK_PSC:定时器时钟源

        一个计数周期的工作流程:在计数器时钟的驱动下,计数器寄存器也跟随时钟的上升沿不断自增,FC之后,计数值变为0,由此可以推断出,ARR自动重装值即为FC,当计数值和重装值相等,且下一个时钟来临时,计数值才清零,并且产生一个更新事件。

        最后三行时序描述的是预分频寄存器的一种缓冲机制,一个是预分频控制寄存器,供读写使用,并不直接决定分频系数;还有一种是缓冲寄存器(影子寄存器),该寄存器才是真正起作用的寄存器。如在某事刻,将预分频寄存器由0改为1,如果在此时立刻改变时钟分频系数,就会导致前后频率不同,但由于预分频缓冲器的存在,即使是在中间突然改变了分频系数,但该变化不会立刻生效,而是会等到本次计数周期结束时,产生了更新事件,预分频控制寄存器的值才会被传递到预分频缓冲寄存器中才生效。预分频器内部实际上也是靠计数器来分频的,当预分频缓冲寄存器中值为0时,计数器就一直为0,直接输出原频率;当预分频值为1时,计数器就以010101计数,每次回到0时,就会输出一个脉冲,使得计数寄存器电平发生跳变,预分频器的值和实际的分频系数之间有一个数的偏移,于是就有了如图的计数器计数频率公式(PSC-预分频值  CK_PSC-定时器时钟源)

(3)计数器时序

b2f18b04838244d58d5dfb5c4632d4f6.png

CK_INT:内部时钟 

分析同预分频器时序同理,不再赘述。需注意的是,更新中断标志(UIF)置1时,就要申请中断,中断响应后,需要在中断程序中手动清0

(6)计数器无预装时序/计数器有预装时序 

0898f37e796e4a0aa851b4c3bd3459f3.png

aa7e3e3bac484096b8b1c87f465119ef.png

        如图所示,无预装时序没有影子寄存器,当自动加载寄存器中的计数目标由FF变为36时,计数器寄存器会直接加到36才清零,同时,更新事件并且更新中断标志。有预装时序有影子寄存器,当计数目标变为36时,计数器不会立刻改变上限,仍是F5之后清零,并且更新事件,此时才会将更新后的计数目标传递到影子寄存器中,到下一个计数周期才按36为计数目标计数。由此可以看出,引入影子寄存器的目的,实际上是为了同步,就是让值的变化和更新事件同步发生,防止在运行途中更改造成错误

(7)RCC时钟树

8827991623194c18a013b98018397da5.png

        如图,由红线划分 ,左边的为时钟产生电路,右边的为时钟分配电路 ,中间SYSCLK为系统时钟72MHz。

        1.在时钟产生电路中,有四个中断源:内部8MHz高速RC振荡器、外部4-16MHz高速石英晶体振荡器(即晶振,一般为8MHz)、外部32.768KHz(一般是给RTC提供时钟)、内部40KHz低速RC振荡器(给看门狗提供时钟)。

        前两个高速晶振给系统提供时钟,APB1、APB2、AHB的时钟都是来源这两个高速晶振,其中,内部和外部的8MHz都可以用,区别是外部的石英震荡器比内部RC振荡器更加稳定,因此一般都用外部晶振。

        SystemInit函数中配置时钟时,首先会启用内部8MHz为系统时钟,并以8MHz暂时运行,然后再启用外部时钟,外部8MHz进入PLL锁相环进行倍频,经过倍频9倍,得到72MHz,等到锁相环输出稳定后,选择锁相环输出为系统时钟,这样就将系统时钟由8MHz切换为72MHz。此种方法可以解决一个问题:如果外部晶振出现问题,会导致自己预设时钟和实际时钟满了大概10倍,这就是系统时钟无法切换到72MHz,只以8MHz的内部时钟运行结果

        CSS是时钟安全系统,负责切换时钟,他可以监测外部时钟的运行状态,一旦外部时钟失效,他就会将外部时钟切换为内部时钟,保证系统时钟的运行,防止程序卡死

        2.在时钟分配电路中,72MHz会进入AHB总线,AHB总线中有个预分频器,在SystemInit里配置的分配系数为1,即AHB的时钟就是72MHz,然后进入APB1总线,此时配置分频系数为2,也就是APB1总线时钟为72MHz/2=36MHz。

        在图中,APB1总线后还有一条分路,若APB1预分频系数=1,频率不变,否则频率*2,此时APB1预分频系数为2,因此此时的频率应该是36MHz*2=72MHz,然后这一路是单独为定时器2~7开通,因此定时器2~7的频率又变为了72MHz。因此,无论是高级定时器还是通用定时器还是基本定时器,他们的内部基准时钟都是72MHz

        APB2中分频系数为1,因此APB2和AHB时钟都为72MHz,分路中有与门输入,下面的外设时钟使能就是程序中的RCC_PAPB2/1PeriphClockCmd的作用

      内部时钟驱动定时器

(2)通用定时器

9dee1a12e386435283ad1790b8288de1.png

         如图带一个黑色阴影的寄存器,都是有影子寄存器这样的的缓冲机制的,包括预分频器,自动重装寄存器和下面的捕获比较寄存器,所以计数的这个ARR自动重装寄存器,也是有一个缓冲寄存器的,并且这个缓冲寄存器是用还是不用,是可以自己设置的

        对于通用计时器来说,中间最核心的部分仍然是时基单元,但是对于通用计时器而言,计数器的计数模式包括向上计数,向下计数,中央对齐

dcaca798b16e4579abbff79574546d6b.png

48f9c5658eef495084bbd61dcdc82ec3.png

bd5c3878a31a4bea9486f81fbd065ecc.png

 1.内外时钟源选择和主从触发模式结构

8d08867ba7174dd5a3d39ce2d60b4fee.png

         通用计时器中,时钟源既可以选择内部72MHz时钟,还可以选择外部时钟。

        第一个外部时钟来自TIMx_ETR引脚的外部时钟,对应在PA0上,可以在PA0上接一个外部方波时钟,然后配置内部的极性选择、边沿检测和预分频器电路,再配置好输入滤波电路,经配置好的电路整型后的滤波信号一路通过ETRF进入触发控制器就可以作为时基单元的时钟,此时该电路称为“外部时钟模式2”。除了外部ETR引脚可以提供时钟外,TRGI也可以提供时钟(主要用作触发输入使用),该输入可以触发定时器的从模式,此时该电路称为“外部时钟模式1”,该模式可以由ETR提供,也可由ITR0、ITR1...来提供,这部分的时钟信号来自其他定时器,从右边可以看出,主模式输出TRGO可通向其他定时器,此时就可以接到其他定时器的ITRx引脚上来了

0afe3f48c485455285af30432f57b77c.png        如图为具体连接方式,TIM2的ITR0接在了TIM1的TRGO上,同理以此类推,通过这一路就可以实现定时器级联

b381f5b3d3914d05a858a8a66da4077e.png

        比如先初始化TIM3,然后使用主模式将其更新事件映射到TRGO上,接着再初始化TIM2,选择ITR2,对应就是TIM3的TRGO,然后再选择时钟模式1,此时TIM3的更新事件就可以驱动TIM2的时基单元,也就实现了定时器的级联

        同样的,还可以选择TI1F_ED(ED是Edge沿边的意思),这里连接的是输入捕获单元的CH1引脚,也就是从CH1引脚捕获时钟,该时钟上升沿和下降沿均有效,即获取CH1边沿

1d4d5c45e98c4a50a403fed1755032ca.png

         最后,还可以由TI1FPx获取时钟,TI1FP1连接的是CH1的时钟,TI2FP2连接的是CH2的时钟

71a9971db8174eadb5a393fec86a53b1.png

总的来说计数器时钟可由下列时钟源提供:

  • 内部时钟(CK_INT)
  • 外部时钟模式1:外部输入脚(TIx)
  • 外部时钟模式2:外部触发输入(ETR)
  • 内部触发输入(ITRx):使用一个定时器作为另一个定时器的预分频器,如可以配置一个定时器Timer1而作为另一个定时器Timer2的预分频器。

        边沿检测器工作原理

2.定时器编码器接口

        该接口可以读取正交编码器的输出波形,详细笔记见后续

3.主模式输出

        该部分电路可以将内部的一些事件映射到TRGO引脚上,用于触发其他定时器、DAC或ADC

 4.输出比较电路

        共有4个通道,分别对应CH1到CH4,可用于输出PWM波形,驱动电机

29b68ca8e30a48b5a2e15e77ae160c56.png

 5.输入捕获电路

        共有4个通道,用于测量输入方波频率

cf3f5f1c7dc34b75a7e79e88d14a2fd6.png

6.捕获 / 比较寄存器 

       是输入捕获和输出比较电路共用的,因为这两者不能同时使用,所以该寄存器是共用的,引脚也是共用的。当使用输入捕获时,就是捕获寄存器;当使用输出比较时,他就是比较寄存器,在输出比较模块,这块电路会比较CNT和CCR的值,CNT计数自增,CCR是我们写入的值。当CNT大于CCR、小于CCR或等于CCR时,输出引脚会对应置1、置0、置1、置0,这样就可以输出一个电平不断跳变的PWM波形

4d33296a56e149d5a5c543c419b0e3a7.png

7.滤波器工作原理

        在一个固定的时钟频率f下进行采样,如果连续N个采样点都为相同的电平,那就代表输入信号稳定,输出采样值。如果采样值不全都相同,说明信号有抖动,此时保持上一次的输出,或者直接输出低电平,这样就保证输出信号在一定程度上的滤波,这里采样频率f和采样点N都是滤波器的参数,频率越低,采样点数越多,滤波效果越好,但是相应的信号延迟就会越大

 2.定时中断基本结构

d10a550e61ce495e8303354086325fa9.png

         从时基单元出发的中断信号会先在状态寄存器中置一个中断标志位,这个标志位会通过中断输出控制,到NVIC申请中断,之所以有中断输出控制,是因为该定时器模块有很多地方都要申请中断,如果需要就允许,如果不需要就禁止

        更详细的内容移步手册13~15章

3.定时器定时中断&定时器外部中断代码

f68aeaed97c845eca28eae5e229f3181.png

(1)定时器定时中断

         1.初始化定时器步骤

d10a550e61ce495e8303354086325fa9.png

        第一步:RCC开启时钟。打开时钟后,定时器的基准时钟和整个外设的工作时钟会同时打开

        第二步:选择时基单元的时钟源。对于定时中断,选择内部时钟源

        第三部:配置时基单元。结构体配置

        第四部:配置中断输出控制。允许更新中断输出到NVIC

        第五步:配置NVIC。在NVIC中打开定时器中断的通道,并分配一个优先级

        第六步:运行控制

        整个模块配置完成后还需使能计数器

        2.定时器库函数 

void TIM_DeInit(TIM_TypeDef* TIMx);
//恢复配置
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
//时基单元初始化
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
//给结构体变量赋默认值
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
//使能计数器,也就是运行控制
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
//使能中断输出信号,也就是中断输出控制 参数1:选择时钟 参数2:选择中断输出 参数3:配置状态
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
//内部时钟
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
//选择ITRx其他定时器时钟 参数2:选择接入的其他定时器
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,uint16_t TIM_ICPolarity, uint16_t ICFilter);
//选择TIx捕获通道时钟  参数2:选择TIx具体某个引脚 参数3:输入极性 参数4:输入滤波器
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,uint16_t ExtTRGFilter);
//选择ETR通过外部时钟模式1输入的时钟  参数2:外部触发预分频器,提前对外部时钟再做一个分频 参数3、4:极性和滤波器
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, 
uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
//选择ETR通过外部时钟模式2输入的时钟 参数2:外部触发预分频器,提前对外部时钟再做一个分频 参数3、4:极性和滤波器
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);
//单独写预分频值  参数2:要写的预分频值 参数3:写入模式,预分频器有一个缓冲器,写入值在更新事件发生后才有效,可以选择在更新事件生效,也可以选择在写入后手动产生一个更新事件让值立即生效
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);
//改变计数器计数模式  参数2:选择新的计数器模式
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);
//用于获取标志位和清除标志位

3.代码实现 

(1)mian.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:");			//1行1列显示字符串Num:
	
	while (1)
	{
		OLED_ShowNum(1, 5, Num, 5);			//不断刷新显示Num变量
	}
}

/**
  * 函    数:TIM2中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)		//判断是否是TIM2的更新事件触发的中断
	{
		Num ++;												//Num变量自增,用于测试定时中断
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);			//清除TIM2更新事件的中断标志位
															//中断标志位必须清除
															//否则中断将连续不断地触发,导致主程序卡死
	}
}

(2)Timer.c

#include "stm32f10x.h"                  // Device header

/**
  * 函    数:定时中断初始化
  * 参    数:无
  * 返 回 值:无
  */
void Timer_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
	
	/*配置时钟源*/
	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
	
	/*时基单元初始化*/
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;		//时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;	//计数器模式,选择向上计数
	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,配置TIM2的时基单元	
	
	/*中断输出配置*/
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);						//清除定时器更新标志位
																//TIM_TimeBaseInit函数末尾,手动产生了更新事件
																//若不清除此标志位,则开启中断后,会立刻进入一次中断
																//如果不介意此问题,则不清除此标志位也可
	
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);					//开启TIM2的更新中断
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
																//即抢占优先级范围:0~3,响应优先级范围:0~3
																//此分组配置在整个工程中仅需调用一次
																//若有多个中断,可以把此代码放在main函数内,while循环之前
																//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;				//选择配置NVIC的TIM2线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;	//指定NVIC线路的抢占优先级为2
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设
	
	/*TIM使能*/
	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
}

/* 定时器中断函数,可以复制到使用它的地方
void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
	{
		
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}
*/

(3)Timer.h

#ifndef __TIMER_H
#define __TIMER_H

void Timer_Init(void);

#endif

 注:(1) PC和ARR按照以下公式计算39b581eb15b9430d855af5ae6369d96b.png

        (2)在Timer.c中,若在TimeBaseInit函数后不清除中断标志位,则会导致计数器从1开始计数,也就是说,在函数初始化后就已经进入了一次中断函数。此时右击打开TimeBaseInit函数定义可以发现:

353f598e0fc44c788fdc656a4ac5f30e.png

         在前面我们了解到预分频器有一个缓冲寄存器,只有在更新事件时,我们写入的值才会起作用。而此时在后面手动生成了一个更新事件:TIMx -> EGR = TIM_PSCReloadMode_Immediate,这就使得我们写入的值会立刻起作用。但这就导致了:更新事件和更新中断是同时发生的,而更新中断会置更新中断标志位,一但程序初始化完成后,更新中断就会立刻进入中断函数。这就是从1开始计数的原因

        解决方法:既然是更新中断同更新事件一起发生,并且会置中断标志位,此时只需在初始化完成后,将中断标志位清除即可,即在TimeBaseInit函数后加TIM_ClearFlag函数即可

 (2)定时器外部时钟

a5819471604a475a8d7cc997f7bb9712.png

        1.代码实现

(1)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:");			//1行1列显示字符串Num:
	OLED_ShowString(2, 1, "CNT:");			//2行1列显示字符串CNT:
	
	while (1)
	{
		OLED_ShowNum(1, 5, Num, 5);			//不断刷新显示Num变量
		OLED_ShowNum(2, 5, Timer_GetCounter(), 5);		//不断刷新显示CNT的值
	}
}

/**
  * 函    数:TIM2中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)		//判断是否是TIM2的更新事件触发的中断
	{
		Num ++;												//Num变量自增,用于测试定时中断
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);			//清除TIM2更新事件的中断标志位
															//中断标志位必须清除
															//否则中断将连续不断地触发,导致主程序卡死
	}
}

(2)Timer.c

#include "stm32f10x.h"                  // Device header

/**
  * 函    数:定时中断初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数配置为外部时钟,定时器相当于计数器
  */
void Timer_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
	
	/*GPIO初始化*/
	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);						//将PA0引脚初始化为上拉输入
	
	/*外部时钟配置*/
	TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F);
																//选择外部时钟模式2,时钟从TIM_ETR引脚输入
																//注意TIM2的ETR引脚固定为PA0,无法随意更改
																//最后一个滤波器参数加到最大0x0F,可滤除时钟信号抖动
	
	/*时基单元初始化*/
	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的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;				//预分频器,即PSC的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;			//重复计数器,高级定时器才会用到
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);				//将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元	
	
	/*中断输出配置*/
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);						//清除定时器更新标志位
																//TIM_TimeBaseInit函数末尾,手动产生了更新事件
																//若不清除此标志位,则开启中断后,会立刻进入一次中断
																//如果不介意此问题,则不清除此标志位也可
																
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);					//开启TIM2的更新中断
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
																//即抢占优先级范围:0~3,响应优先级范围:0~3
																//此分组配置在整个工程中仅需调用一次
																//若有多个中断,可以把此代码放在main函数内,while循环之前
																//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;				//选择配置NVIC的TIM2线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;	//指定NVIC线路的抢占优先级为2
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设
	
	/*TIM使能*/
	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
}

/**
  * 函    数:返回定时器CNT的值
  * 参    数:无
  * 返 回 值:定时器CNT的值,范围:0~65535
  */
uint16_t Timer_GetCounter(void)
{
	return TIM_GetCounter(TIM2);	//返回定时器TIM2的CNT
}

/* 定时器中断函数,可以复制到使用它的地方
void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
	{
		
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}
*/

(3)Timer.h

#ifndef __TIMER_H
#define __TIMER_H

void Timer_Init(void);
uint16_t Timer_GetCounter(void);

#endif

九、TIM输出比较

1.输出比较简介

        OC(Output Compare)输出比较

        IC(Input Capture)输入捕获

        CC(Capture/Compare)输入捕获和输出比较单元

        输出比较可以通过比较CNT(计数器)与CCR(捕获/比较寄存器)寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形(详细介绍转至通用定时器的捕获 / 比较寄存器

        每个高级定时器和通用定时器都拥有4个输出比较通道

        高级定时器的前3个通道额外拥有死区生成和互补输出的功能

2.PWM波形简介

        PWM(Pulse Width Modulation)脉冲宽度调制,PWM波形是一个数字输出信号,也是由高低电平组成的连续变化的电平信号。使用PWM波形,是用来等效地实现一个模拟信号的输出。该特征就可以应用于实现LED呼吸灯,当点亮、熄灭的频率足够大时,就会呈现出中等亮度,当我们调控这个点灯和熄灭的时间比例时,就能让LED呈现出不同的亮度级别。对于电机调速也是一样,设置一个很快的频率,给电机通电、断电、通电、断电,那么电机的速度就能维持在一个中等速度。

         在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域。值得注意的是PWM的应用场景必须是一个惯性系统,在LED熄灭时,由于余辉和人眼视觉暂留现象,LED不会立马熄灭,而是有一定惯性,过一小段时间才会熄灭。电机也是同理,当电机断电时,电机的转动不会立马停止,而是有一定惯性,过一会才停。这样具有惯性的系统,才能使用PWM

ee2283d29717495492f86cf6c73fa167.png

        由上图可知,这种高低电平跳变的数字信号 ,可以等效为中间虚线表示的模拟量。当上面电平时间长,下面电平时间短,那么等效的模拟量就向上偏移,反之模拟量向下偏移

PWM参数:

        频率 = 1 / TS(一个高低电平变换周期的时间)频率越快,划分越细,曲线越平滑        

       占空比 = TON / TS 即高电平占整个周期的比例,如占空比为50%,就是一个方波,占空比为20%,就是高电平占20%,低电平占80% 。占空比越大,等效的模拟电压越趋近于高电平,反之越趋向低电平,该等效关系一般来说是线性的,如高电平为5V,低电平为0V,50%占空比就等效于中间电压2.5V

       分辨率 = 占空比变化步距,比如有的占空比只能是1%、2%、3%这样以1%的步距跳变,那么分辨率就为1%,因此分辨率就是占空比变化的精细程度

47b45034d5f3433fa1f0bb84ba66c61f.png

 3.输出比较通道(通用)

00ad0bd2388a4e4a80ebbb46eada3597.png

输出模式控制器的8种状态

983e01f16310434bb73163fce9ba22eb.png

        如图125,左边计数器和捕获输入寄存器进行比较,当CNT>=CCR1时, 就会给输出模式控制信器传一个信号,输出模式控制器根据如表格所示,需要哪个模式,就通过TIMx_CCMR1寄存器来进行配置

(1)输出模式

        冻结模式时,CNT和CCR可以理解为无效,REF(reference)参考信号量维持为原状态。这个模式可以暂停输出正在输出的PWM波,此时高低电平也维持为暂停时刻的状态保持不变

        匹配时置有效电平 / 无效电平为字面含义,可以理解为CNT=CCR时,REF为高电平 / 低电平,该模式只能翻转单次,不适合连续输出

        匹配时电平翻转,该模式可以方便地输出一个频率可调,占空比为50%的PWM波形,如设置CCR为0,那CNT每次更新清0时,就会产生因此CNT=CCR事件,这就会导致输出电平翻转一次,每更新两次,输出为一个周期,如下图。并且高电平和低电平的时间是始终相等的,也就是占空比始终为50%。当改变定时器更新频率时,输出波形的频率也会随之改变,由于计数器更新两次才是一个PWM周期,因此两者之间的关系是:

        输出波形频率=更新频率 / 2

088e9396ddbd45b982b8a751e4179c61.png

         PWM模式1 或 2,该模式可用于输出频率和占空比都可调的PWM波形,是我们主要使用的模式。这个情况比较多,一般都只使用向上计数,PWM模式2实际上就是PWM模式1输出的取反(改变PWM模式1和PWM模式2,就只是改变了REF电平的极性而已),是因为REF输出之后还有一个极性的配置,所以使用PWM模式1的正极性和PWM模式2的反极性最终的输出是一样的。所以使用的话,我们可以只使用PWM模式1,并且是向上计数,这一种模式就行了。

5c951e5597814c2abfdc295017989fef.png

        CCR和CNT不断进行比较,以PWM模式1为例,如上图的波形图,CNT从0自增,低于CCR时,为低电平输出,高于CCR时,跳变为低电平,达到ARR时,计数器CNT清零,此时会低于CCR,电平又跳变为高电平,依次循环。由此可知CCR增高,占空比增大CCR减小,占空比减小,此时的REF就是频率可调,占空比也可调的PWM波形,最终再经过极性选择,输出使能,最终通向GPIO口,这样就完成了PWM波形的输出

a. PWM的参数计算。

        PWM频率:由图可知,PWM的周期对应着计数器的更新周期,因此,PWM频率=计数器溢出频率

        PWM占空比:PWM的周期对应CNT从0~99,共100个数,也就是ARR+1,PWM的高电平对应CNT,从0增加到30,但此时,当CNT达到30 后,就已经跳变为低电平,因此高电平对应的是0~29共30个数,也就是CCR

        PWM分辨率:也就是占空比变化的步距,CCR始终在0~ARR之间,CCR=ARR+1时,占空比正好为100%,若CCR再增大,占空比仍然是100%,因此CCR的变化范围取决于ARR的值ARR越大,CCR的范围越大,对应分辨率越大。由于此时定义的分辨率是占空比最小的变化步距,所以值要越小越好

b534e74f0b0745209383ea0450203d0d.png

 4.舵机

(1)简介 

        舵机是一种根据输入PWM信号占空比来控制输出角度的装置

        输入PWM信号要求:周期为20ms,高电平宽度为0.5ms~2.5ms

43300cbf25484685a64bf1118dded855.png

b4a8c2a9f917486890967037971f9bdb.png

d579fe90b76148a58d5b166fb5fd0036.png

         舵机工作流程:PWM信号输入到控制板,给控制板一个指定目标角度,然后电位器检测输出轴的当前角度,比当目标角度大就反转,比目标角度小就正转,最终使输出轴固定在指定角度

 (2)硬件电路73d1205947c14101aa4335bc6e1b7cea.png0b537031feb647a6923dc672cc11970e.png

由于电机是是大功率器件,因此在正极处需要接一个5V的电源 

5.直流电机及驱动

(1)简介

        直流电机是一种将电能转换为机械能的装置,有两个电极,当电极正接时,电机正转,当电极反接时,电机反转

        直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作

        TB6612是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向

05f2e4b14d4843c99c2d7ffae65125c4.png

        最右图为H桥型电路,两边电路为推挽电路,如左侧推挽电路,上管导通,下管断开,左边输出就是接在VM的电机电源正极,反之下管接在PGND电源负极,右边分析同理,因此两路推挽电路就可控制电流的流动方向,也就可以控制电机正反转

(2)硬件电路 

08ca2158f10447539d2ef82d9102b053.png 

        VM的电压一般和电机的额定电压保持一致,比如电机是5V,VM就接5V,电机是7.2V,VM就接7.2V

        VCC要和控制器的电源保持一致,如使用STM32,是3.3V器件,那么VCC就接3.3V

        AO1、AO2、BO1、BO2就是两路电机的输出 ,可以像图中所示接两个电机。AO1和AO2是A路的两个输出,他的控制端就是上面PWMA、AIN2和AIN1,这三个引脚控制下面A路的一个电机,同理B路也是,图中已用灰色路径标出。所以上面三个引脚直接接在GPIO口即可,其中PWMA引脚接PWM信号输出端,其他两个引脚可以接任意两个普通GPIO口这三个引脚给一个低功率的控制信号,驱动电路就会从VM汲取电流,输出到电机,这样就完成了低功率的控制信号控制大功率设备的目的了。BO1和BO2同理

        STBY为待机控制引脚,如果接GND,芯片不工作,处于待机状态,如果接逻辑电源VCC,芯片就正常工作

        如表格所示,IN1、IN2如果都接高电平,O1、O2都为低电平,这样两个输出没有电压差,电机不会转动;IN1低、IN2高,PWM给高,则反转,给低则不转,若PWM是一个连续的高低电平信号,这样电机就可以反转、停止、反转、停止...若PWM频率足够快,那么电机就可以连续稳定地反转了,并且速度取决于PWM信号的占空比;其余分析同理

 6.PWM驱动LED呼吸灯

(1)接线图

a49c7e30258a4b81a290904c17e3bcd1.png

(2)步骤 

        第一步,RCC开启时钟,将后续所用到的TIM外设和GPIO外设时钟打开

        第二步,配置时基单元,包括前面的时钟源选择

        第三部,配置输出比较单元 ,包括CCR的值、输出比较模式、极性选择、输出使能这些参数

        第四部,配置GPIO,将PWM对应的GPIO口,初始化为复用推挽输出配置 

        第五步,运行控制,启动计数器 

(3)库函数

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);
//配置强制输出模式,用于在运行中,想要暂停输出波形,并强制输出高电平或低电平
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);
//用来配置CCR寄存器预装功能,预装功能就是影子寄存器,写入的值不会立即生效而是更新事件才生效
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);
//用来配置快速使能
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);
//清除REF信号,对应手册外部事件时清除REF信号有介绍
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);
//用于单独设置输出比较极性,带N就是高级定时器里互补通道的配置,OC4无互补通道
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);
//选择输出比较模式,用于单独更改输出比较模式的函数
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);
//用来单独更改CCR寄存器值的函数,运行时可以更改占空比(重要)
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
//仅高级定时器使用,在使用高级定时器输出PWM时,需要调用这个函数使能主输出,否则PWM不能正常输出

4)结构体说明 

GPIO_IintStructure.GPIO_Mode = GPIO_Mode_AF_PP;

        此时能注意到,GPIO的模式选择了复用推挽模式,选择该模式是因为在普通开漏 / 推挽输出下,引脚的控制权来自输出数据寄存器,若想让定时器来控制引脚,就需要用到复用开漏 / 推挽输出,如下图所示,复用开漏 / 推挽输出时,输出数据寄存器是断开状态,输出控制权为片上外设

259ad81e59094726a6dc3ef9d5f37327.png

dce9bb11b17f415db0f4ee4a6126f321.png

TIM_OCMode:2d71b2f4f31b40278e9a3b8e001867d4.png

983e01f16310434bb73163fce9ba22eb.png

 TIM_Pulse:也就是设置CCR寄存器的值deccc6561b3d4bf191c83983e69b108c.png

 TIM_OCPolarity:高极性时,极性不翻转,REF波形直接输出,即有效电平是高电平,REF有效时,输出高电平。反之输出低电平

030d6392986943ee8c96632cb9dc03f1.png

 (5)代码实现

a. main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"

uint8_t i;			//定义for循环的变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	PWM_Init();			//PWM初始化
	
	while (1)
	{
		for (i = 0; i <= 100; i++)
		{
			PWM_SetCompare1(i);			//依次将定时器的CCR寄存器设置为0~100,PWM占空比逐渐增大,LED逐渐变亮
			Delay_ms(10);				//延时10ms
		}
		for (i = 0; i <= 100; i++)
		{
			PWM_SetCompare1(100 - i);	//依次将定时器的CCR寄存器设置为100~0,PWM占空比逐渐减小,LED逐渐变暗
			Delay_ms(10);				//延时10ms
		}
	}
}

b. PWM.c

#include "stm32f10x.h"                  // Device header

/**
  * 函    数:PWM初始化
  * 参    数:无
  * 返 回 值:无
  */
void PWM_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
	
	/*GPIO重映射*/
//	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);			//开启AFIO的时钟,重映射必须先开启AFIO的时钟
//	GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);			//将TIM2的引脚部分重映射,具体的映射方案需查看参考手册
//	GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);		//将JTAG引脚失能,作为普通GPIO引脚使用
	
	/*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);							//将PA0引脚初始化为复用推挽输出	
																	//受外设控制的引脚,均需要配置为复用模式		
	
	/*配置时钟源*/
	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
	
	/*时基单元初始化*/
	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_TimeBaseInit,配置TIM2的时基单元
	
	/*输出比较初始化*/
	TIM_OCInitTypeDef TIM_OCInitStructure;							//定义结构体变量
	TIM_OCStructInit(&TIM_OCInitStructure);							//结构体初始化,若结构体没有完整赋值
																	//则最好执行此函数,给结构体所有成员都赋一个默认值
																	//避免结构体初值不确定的问题
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;				//输出比较模式,选择PWM模式1
	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);						//将结构体变量交给TIM_OC1Init,配置TIM2的输出比较通道1
	
	/*TIM使能*/
	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
}

/**
  * 函    数:PWM设置CCR
  * 参    数:Compare 要写入的CCR的值,范围:0~100
  * 返 回 值:无
  * 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
  *           占空比Duty = CCR / (ARR + 1)
  */
void PWM_SetCompare1(uint16_t Compare)
{
	TIM_SetCompare1(TIM2, Compare);		//设置CCR1的值
}

c. PWM.h

#ifndef __PWM_H
#define __PWM_H

void PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);

#endif

注:

(1)库函数中void TIM_OCxInit(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);中TIM_OCxInit的x为1、2、3、4,对应4个输出比较单元,或者说输出比较通道。你需要初始化哪个通道,就调用哪个函数。不同的通道对应的GPIO口也是不一样的,所以这里要按照你GPIO口的需求来。这里使用的是PA0口,对应的就是第一个输出比较通道。

03f6b10d0c8d4c1792abb02d2aeed032.png

(2)你要使用哪个外设,就只能用对应的引脚,不过,但是虽然它是定死的,STM32还是给了我们一次更改的机会的,这就是重定义,或者叫重映射。如果你既要用USART2的TX引脚,又要用TIM2的CH3通道,它俩冲突成,没办法同时用,那我们就可以在这个重映射的列表里找一下,比如TIM2的CH3,那TIM2的CH3就可以从原来的PA2引脚,换到PB10引脚,这样就避免了两个外设引脚的冲突。如果这个重映射的列表里找不到,那外设复用的GPIO就不能挪位置。这就是重映射的功能,配置重映射是用AFIO来完成的

1eac9a9c13f24b2cb841da54db55bd2a.png

(3)输出比较初始化时,并没有全部给出成员的值,但是如果中途想把高级定时器当做通用定时器输出PWM时,那自然就会把TIM_OCXInit的TIM2改成TIM1。这样的话,这个结构体原本没有用到的成员,现在需要使用,但是对于那些成员并没有赋值,那就会导致高级定时器输出PWM出现一些奇怪的问题。所以为了避免程序中出现不确定的因素,把结构体所有的成员都配置完整;或者需要先给结构体成员都赋一个初始值,再修改部分的结构体成员

        使用到的函数就是:

TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct)

 7.PWM驱动舵机

(1)接线图da3a858738cd4cee9cee7b5cc9d13660.png

        SG90舵机有三根线,第一个GND,就是棕色线,接在面包板的GND;第二个5V正极红色线,这里要接5V的电机电源,注意不要把它接在面包板的正极,STM32芯片正极只有3.3V的电压,而且输出功率不太带不动电机,所以要把它接在STLINK的5V输出引脚;第三个引脚,PWM信号橙色线,接在PA1引脚上(这里用的是PA1的通道2)由手册的引脚定义表可知,PA0的复用功能是TIM2_CH1(通道一),PA1的复用功能是TIM2_CH2(通道2)

(2)一个定时器不同通道输出PWM特点

        因为引脚换到了PA1,因此通道需要改为TIM_OC,当然你也可以四个通道全部使用,只需要在代码中加入所有通道即可。因为不同通道是共用一个计数器,因此他们的频率必须是一样的,它们的占空比由各自CCR决定,因此占空比可以各自设定。再者因为计数器更新所有PWM同时跳变,因此他们的相位是同步的

        如驱动多个舵机或者直流电机,使用一个定时器不同通道的PWM就可以了

(3)参数计算

0724c33b0d1e4e29a4c07ba830f902e6.png

        当然,占空比的计算如果还是搞不清楚,那不妨这样理解:舵机的周期是20ms,如果要实现0.5ms的输入脉冲宽度,也就是0.5ms的高电平,这就相当于是高电平占整个周期的1/40,那么再将这1/40带入Duty,就能够求出CCR的值。ARR已经根据PWM频率计算得出了,当然这个值并不是固定值,需要在实际项目中做出更方便实施的值。2.5ms的高电平时间计算同理。

d579fe90b76148a58d5b166fb5fd0036.png

        由于后续要用到舵机角度的封装,因此需要对其角度CCR的值做出映射,拟合后可以由一次函数得出: 

1dd5449a2b904d40a2f62a73a18ab6f1.png

(4)代码部分

(1)main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Servo.h"
#include "Key.h"

uint8_t KeyNum;			//定义用于接收键码的变量
float Angle;			//定义角度变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	Servo_Init();		//舵机初始化
	Key_Init();			//按键初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "Angle:");	//1行1列显示字符串Angle:
	
	while (1)
	{
		KeyNum = Key_GetNum();			//获取按键键码
		if (KeyNum == 1)				//按键1按下
		{
			Angle += 30;				//角度变量自增30
			if (Angle > 180)			//角度变量超过180后
			{
				Angle = 0;				//角度变量归零
			}
		}
		Servo_SetAngle(Angle);			//设置舵机的角度为角度变量
		OLED_ShowNum(1, 7, Angle, 3);	//OLED显示角度变量
	}
}

(2)Servo.c

#include "stm32f10x.h"                  // Device header
#include "PWM.h"

/**
  * 函    数:舵机初始化
  * 参    数:无
  * 返 回 值:无
  */
void Servo_Init(void)
{
	PWM_Init();									//初始化舵机的底层PWM
}

/**
  * 函    数:舵机设置角度
  * 参    数:Angle 要设置的舵机角度,范围:0~180
  * 返 回 值:无
  */
void Servo_SetAngle(float Angle)
{
	PWM_SetCompare2(Angle / 180 * 2000 + 500);	//设置占空比
												//将角度线性变换,对应到舵机要求的占空比范围上
}

(3)Servo.h

#ifndef __SERVO_H
#define __SERVO_H

void Servo_Init(void);
void Servo_SetAngle(float Angle);

#endif

(4)Key.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/**
  * 函    数:按键初始化
  * 参    数:无
  * 返 回 值:无
  */
void Key_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//开启GPIOB的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);						//将PB1和PB11引脚初始化为上拉输入
}

/**
  * 函    数:按键获取键码
  * 参    数:无
  * 返 回 值:按下按键的键码值,范围:0~2,返回0代表没有按键按下
  * 注意事项:此函数是阻塞式操作,当按键按住不放时,函数会卡住,直到按键松手
  */
uint8_t Key_GetNum(void)
{
	uint8_t KeyNum = 0;		//定义变量,默认键码值为0
	
	if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)			//读PB1输入寄存器的状态,如果为0,则代表按键1按下
	{
		Delay_ms(20);											//延时消抖
		while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0);	//等待按键松手
		Delay_ms(20);											//延时消抖
		KeyNum = 1;												//置键码为1
	}
	
	if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0)			//读PB11输入寄存器的状态,如果为0,则代表按键2按下
	{
		Delay_ms(20);											//延时消抖
		while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0);	//等待按键松手
		Delay_ms(20);											//延时消抖
		KeyNum = 2;												//置键码为2
	}
	
	return KeyNum;			//返回键码值,如果没有按键按下,所有if都不成立,则键码为默认值0
}

(5)Key.h

#ifndef __KEY_H
#define __KEY_H

void Key_Init(void);
uint8_t Key_GetNum(void);

#endif

(6)PWM.c

#include "stm32f10x.h"                  // Device header

/**
  * 函    数:PWM初始化
  * 参    数:无
  * 返 回 值:无
  */
void PWM_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);							//将PA1引脚初始化为复用推挽输出	
																	//受外设控制的引脚,均需要配置为复用模式
	
	/*配置时钟源*/
	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
	
	/*时基单元初始化*/
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;     //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
	TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1;				//计数周期,即ARR的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;				//预分频器,即PSC的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;            //重复计数器,高级定时器才会用到
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);             //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
	
	/*输出比较初始化*/ 
	TIM_OCInitTypeDef TIM_OCInitStructure;							//定义结构体变量
	TIM_OCStructInit(&TIM_OCInitStructure);                         //结构体初始化,若结构体没有完整赋值
	                                                                //则最好执行此函数,给结构体所有成员都赋一个默认值
	                                                                //避免结构体初值不确定的问题
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;               //输出比较模式,选择PWM模式1
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;       //输出极性,选择为高,若选择极性为低,则输出高低电平取反
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;   //输出使能
	TIM_OCInitStructure.TIM_Pulse = 0;								//初始的CCR值
	TIM_OC2Init(TIM2, &TIM_OCInitStructure);                        //将结构体变量交给TIM_OC2Init,配置TIM2的输出比较通道2
	
	/*TIM使能*/
	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
}

/**
  * 函    数:PWM设置CCR
  * 参    数:Compare 要写入的CCR的值,范围:0~100
  * 返 回 值:无
  * 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
  *           占空比Duty = CCR / (ARR + 1)
  */
void PWM_SetCompare2(uint16_t Compare)
{
	TIM_SetCompare2(TIM2, Compare);		//设置CCR2的值
}

(7)PWM.h

#ifndef __PWM_H
#define __PWM_H

void PWM_Init(void);
void PWM_SetCompare2(uint16_t Compare);

#endif

8.PWM驱动直流电机

(1)接线图

c8657d633ab149bdaa506fe02837344f.png

 (2)代码实现

(1)main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Motor.h"
#include "Key.h"

uint8_t KeyNum;		//定义用于接收按键键码的变量
int8_t Speed;		//定义速度变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	Motor_Init();		//直流电机初始化
	Key_Init();			//按键初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "Speed:");		//1行1列显示字符串Speed:
	
	while (1)
	{
		KeyNum = Key_GetNum();				//获取按键键码
		if (KeyNum == 1)					//按键1按下
		{
			Speed += 20;					//速度变量自增20
			if (Speed > 100)				//速度变量超过100后
			{
				Speed = -100;				//速度变量变为-100
											//此操作会让电机旋转方向突然改变,可能会因供电不足而导致单片机复位
											//若出现了此现象,则应避免使用这样的操作
			}
		}
		Motor_SetSpeed(Speed);				//设置直流电机的速度为速度变量
		OLED_ShowSignedNum(1, 7, Speed, 3);	//OLED显示速度变量
	}
}

(2)Motor.c

#include "stm32f10x.h"                  // Device header
#include "PWM.h"

/**
  * 函    数:直流电机初始化
  * 参    数:无
  * 返 回 值:无
  */
void Motor_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);		//开启GPIOA的时钟
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);						//将PA4和PA5引脚初始化为推挽输出	
	
	PWM_Init();													//初始化直流电机的底层PWM
}

/**
  * 函    数:直流电机设置速度
  * 参    数:Speed 要设置的速度,范围:-100~100
  * 返 回 值:无
  */
void Motor_SetSpeed(int8_t Speed)
{
	if (Speed >= 0)							//如果设置正转的速度值
	{
		GPIO_SetBits(GPIOA, GPIO_Pin_4);	//PA4置高电平
		GPIO_ResetBits(GPIOA, GPIO_Pin_5);	//PA5置低电平,设置方向为正转
		PWM_SetCompare3(Speed);				//PWM设置为速度值
	}
	else									//否则,即设置反转的速度值
	{
		GPIO_ResetBits(GPIOA, GPIO_Pin_4);	//PA4置低电平
		GPIO_SetBits(GPIOA, GPIO_Pin_5);	//PA5置高电平,设置方向为反转
		PWM_SetCompare3(-Speed);			//PWM设置为负的速度值,因为此时速度值为负数,而PWM只能给正数
	}
}

(3)Motor.h

#ifndef __MOTOR_H
#define __MOTOR_H

void Motor_Init(void);
void Motor_SetSpeed(int8_t Speed);

#endif

(4)PWM.c

#include "stm32f10x.h"                  // Device header

/**
  * 函    数:PWM初始化
  * 参    数:无
  * 返 回 值:无
  */
void PWM_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);							//将PA2引脚初始化为复用推挽输出	
																	//受外设控制的引脚,均需要配置为复用模式
	
	/*配置时钟源*/
	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
	
	/*时基单元初始化*/
	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 = 36 - 1;               //预分频器,即PSC的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;            //重复计数器,高级定时器才会用到
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);             //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
	
	/*输出比较初始化*/ 
	TIM_OCInitTypeDef TIM_OCInitStructure;							//定义结构体变量
	TIM_OCStructInit(&TIM_OCInitStructure);                         //结构体初始化,若结构体没有完整赋值
	                                                                //则最好执行此函数,给结构体所有成员都赋一个默认值
	                                                                //避免结构体初值不确定的问题
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;               //输出比较模式,选择PWM模式1
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;       //输出极性,选择为高,若选择极性为低,则输出高低电平取反
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;   //输出使能
	TIM_OCInitStructure.TIM_Pulse = 0;								//初始的CCR值
	TIM_OC3Init(TIM2, &TIM_OCInitStructure);                        //将结构体变量交给TIM_OC3Init,配置TIM2的输出比较通道3
	
	/*TIM使能*/
	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
}

/**
  * 函    数:PWM设置CCR
  * 参    数:Compare 要写入的CCR的值,范围:0~100
  * 返 回 值:无
  * 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
  *           占空比Duty = CCR / (ARR + 1)
  */
void PWM_SetCompare3(uint16_t Compare)
{
	TIM_SetCompare3(TIM2, Compare);		//设置CCR3的值
}

(5)PWM.h

#ifndef __PWM_H
#define __PWM_H

void PWM_Init(void);
void PWM_SetCompare3(uint16_t Compare);

#endif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值