案例代码及相关资料下载链接:
链接:https://pan.baidu.com/s/1hsIibEmsB91xFclJd-YTYA?pwd=jauj
提取码:jauj
1 定时器综述
1.1 定时器简介
TIM(Timer)定时器,最基本功能就是定时触发中断:对输入的时钟进行计数,并在计数值达到设定值时触发中断。由此可以得出定时器本身就是一个计数器,而当输入信号是一个准确可靠的时钟的时候,而当它对这个基准时钟进行计数的时候,就相当于计时。
STM32单片机中的定时器有由16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz(在STM32单片机中,定时器的基准时钟是72MHz)计数时钟下可以实现最大59.65s(计算公式如下:T=1/CK_CNT_OV = CK_PSC / (PSC + 1) / (ARR + 1))的定时,同时定时器与定时器之间支持级联,两个定时器级联就可以有8000多年的计时。
STM32单片机中的定时器不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。而定时器也根据复杂度与应用场景的不同分为以下三种类型:
以上定时器的编号是STM32中所有系列的集合并不是所有型号都有以上八种定时器,例如本开发板STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4。所以定时器的具体资源要根据自身开发板的数据手册来看。并且从上表可得定时器的功能是向下兼容的,高一级的定时器拥有低一级定时器的全部功能。
1.2 定时器的结构
1.2.1 定时中断结构
定时中断时定时器最基本的功能,为了讲解清楚,用基本定时器的结构图来讲解(如下图所示)
首先来了解接到时基单元是由计数器、预分频器、自动重装寄存器这三个寄存器组成的,而定时器产生中断的过程就是通过时基单元来完成的。首先时基单元的输入是由内部时钟(也由外部时钟会在内外时钟选择时讲到)直接输入的,而内部时钟是RCC上的TIMXCLK给的,一般都为72MHz。
内部时钟进入时基单元之后首先接PSC预分频器,操作这个寄存器可以对输入的内部时钟进行预分频,寄存器置0为不分频,置1进行二分频,置2进行三分频。这个预分频器是十六位寄存器(0~65535),所以最大可以进行65536分频。由此可以得到输出频率计算公式:输出频率=输入频率/n+1。而预分频器分频的作用就是为了得到自身需要的定时时间。
计数器:计数器是对PSC预分频后的计数时钟进行计数,(向上计数模式为例)计数时钟每来一个上升沿计数器就加1,计数器也是一个16位的寄存器,所以计数器的计数范围也是从0计到65535。如果加到65535再继续加的话就会回到0重新往上加。而当计数器的值达到设定的目标的值时就会产生中断,完成定时中断的任务。
自动重装寄存器:这个寄存器就是用来存储产生中断的目标值的,也是十六位寄存器,存的是写入的计数目标(固定值),当计数器加到这一固定值时,就说明计数达到目标值,会给一个中断标志同时会清零计数器的值。如果是产生一个中断,那么就会传向NVIC,配置好NVIC后NVIC传给CPU这样定时器的更新中断就会得到CPU的响应。如果是更新事件就是会产生一个事件同时就不会产生中断。
了解完以上定时中断的工作模式来看一下部分寄存器工作时的时序图:
预分频工作的时序图:
上图为预分频器的参数从1变成2时的时序图,CK_PSC 为预分频器的输入时钟,CNT_EN为计数器的使能,高电平时计数器正常运行,低电平时计数器停止运行。CK_CNT计数器时钟是预分频器的输出时钟也是计数器的输入时钟,这里最主要讲的就是预分频器的控制寄存器与缓冲寄存器,由时序图可以看出当预分频控制器从0变成1时其他时序并未发生改变,只有当预分频的缓冲控制器改变后其他时序图才开始改变,这样是为了让一段计数周期的前半周期与后半周期的计数频率相同,所以预分频缓冲器是真正控制分频的寄存器,而预分频控制寄存器是来操作缓冲器的一个寄存器。
其他寄存器中也有类似这样的结构,大家可以自行去看手册中的内容来学习。
再补充一个关于计数器计数模式的知识点:
向上计数模式:在向上计数模式中,计数器从0计数到自动加载值(TIMx_ARR计数器的内容),然后重新从0开始 计数并且产生一个计数器溢出事件。
向下计数模式:在向下模式中,计数器从自动装入的值(TIMx_ARR计数器的值)开始向下计数到0,然后从自动 装入的值重新开始并且产生一个计数器向下溢出事件。
中央对齐模式:计数器从0开始计数到自动加载的值(TIMx_ARR寄存器)−1,产生一个计数器 溢出事件,然后向下计数到1并且产生一个计数器下溢事件;然后再从0开始重新计数。 在这个模式,不能写入TIMx_CR1中的DIR方向位。它由硬件更新并指示当前的计数方向。
1.2.2 定时器时钟的选择
对于内部时钟的选择和上述定时器中断基本一致就不再详解,主要来看如何对外部时钟进行选择,而外部时钟定时器一共有两种模式:外部时钟源模式1和外部时钟源模式2 。
外部时钟源模式2:
放大的结构图如上所示,这个外部时钟来自TIMx_ETR上的引脚,而TIMx_ETR引脚则是复用在了PA0这个GPIO口上,我们在PA0上接一个外部方波时钟,然后再配置内部的极性选择,边沿检测和预分频电路,再配置输入滤波电路,这两个电路时为了对外部时钟输入方波型号进行整形(对输入的波形进行滤波,,它可以避免一些高频的毛刺信号造成误触发 )。然后通过ETRF进入触发控制器,再接着就可以选择作为时基单元的时钟了,如果想在ETR外部引脚提供时钟,或者想对ETR时钟进行计数,把这个定时器当做计数器来使用的话,就可以配置这一路电路。在STM32中,这一路叫做外部时钟模式2。
外部时钟源模式1:
放大的结构图如上图所示:结合本图和通用定时器的结构图来看ETR引脚既可以接外部时钟源模式1也可以接外部时钟源模式2,外部时钟模式1接ETR的作用与外部时钟源模式2作用一致(具体如何接在外部时钟源模式2中有写)。外部时钟源模式1也可以接ITR信号这一部分信号来自于其他定时器,从通用定时器结构图来看,主模式输出的TRGO可以通向其他定时器,接其他定时器时引脚接到了定时器的ITR上,ITR0~ITR3是其他四个定时器的TRGO输出。具体连接方式如下图表78所示:
而这一路外部时钟模式主要来实现定时器的级联的功能。
1.2.3 定时器输出比较
OC(Output Compare)输出比较,输出比较可以通过比较CNT与CCR寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形,定时器的输出比较功能是通用定时器与高级定时级特有的,每个高级定时器和通用定时器都拥有4个输出比较通道。注意高级定时器的前3个通道额外拥有死区生成和互补输出的功能,这个主要用来驱动三相无刷电机,先暂不做了解,我们主要来学习通用定时器的输出比较。
定时器输出比较是对PWM的波形进行控制的那么我们首先来了解一下什么是PWM:PWM(Pulse Width Modulation)脉冲宽度调制,在具有惯性(改变的瞬间不会消失而会暂时存留一段时间,就像跑步一样从跑步状态到立定状态不会瞬间完成。)的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域,而PWM信号就是一个数字输出信号,是由高低电平组成的,他是用来等效的输出一个模拟信号。
PWM具体输出信号如下图所示:
PWM参数:频率 = 1 / TS(周期的倒数),占空比 = TON / TS(高电平时间与整个周期时间的比例),分辨率 = 占空比变化步距(占空比最小变化的跨度)。
输出比较的具体结构
在通用定时器中是标红这一部分:
在输出比较的结构图里面,从最左边开始,输出模式控制器这里接的是CNT计数器和CCR1的捕获/比较寄存器。当CNT > CCR1或者CNT = CCR1时,会给输出模式控制器传输一个信号,输出模式控制器就会改变它输出OC1REF的高低电平,REF信号(相当于一个参考信号)指的就是输出信号的高低电平,REF信号可以输出到主模式控制器,把REF信号映射到由TRGO上,但是更多情况下是直接给到下面这个通道,这里有一个CC1P寄存器,是一个极性的选择,当寄这个寄存器置0时,参考信号REF与输出信号一致,当寄这个寄存器置1时,经过一个非门,给参考信号反转一下,那么参考信号REF与输出信号相反,最后输出的信号会在GPIO口上显示出,例如与PWM通道1复用的GPIO口是PA0。
输出比较有以下几种模式:冻结
(1)CNT=CCR时,REF保持为原状态
(2)匹配时置有效电平 CNT=CCR时,REF置有效电平
(3)匹配时置无效电平 CNT=CCR时,REF置无效电平
(4)匹配时电平翻转 CNT=CCR时,REF电平翻转
(5)强制为无效电平 CNT与CCR无效,REF强制为无效电平
(6)强制为有效电平 CNT与CCR无效,REF强制为有效电平
(7)PWM模式1 向上计数:CNT<CCR时,REF置有效电平,CNT≥CCR时,REF置无效电平,向下计数:CNT>CCR时,REF置无效电平,CNT≤CCR时,REF置有效电平(通过对CCR与CNT的控制就可以来控制占空比得到不同的PWM波形。)
(8)PWM模式2 向上计数:CNT<CCR时,REF置无效电平,CNT≥CCR时,REF置有效电平,向下计数:CNT>CCR时,REF置有效电平,CNT≤CCR时,REF置无效电平。
以上八种模式最常使用的是PWM输出比较的模式,其他模式一般在特定场景中使用。
接下来是PWM的参数计算方法,在我们对输出比较进行配置时会经常用到:
PWM模式1,采用向上计数法:的图像化黄线:ARR(周期),红线:CNT(计数)蓝线:CRR。(CNT<CCR时,REF置有效电平(高电平),CNT≥CCR时,REF置无效电平(低电平))。
PWM参数计算方法如下:
PWM频率: Freq = CK_PSC(内部时钟预分频) / (PSC + 1) / (ARR + 1)
PWM占空比: Duty = CCR / (ARR + 1)
PWM分辨率: Reso = 1 / (ARR + 1)
1.2.4 定时器输入捕获
简介:
输入捕获,即Input Capture,英文缩写为IC。输入捕获模式下,当通道输入引脚出现指定电平跳变(可以定义为上升沿、下降沿)时,当前CNT的值将被锁存到CCR中(这就是“捕获”的含义),可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数。每个高级定时器和通用定时器都拥有4各输入捕获通道,且二者没有区别。
输入捕获模块可以配置为PWMI(PWM输入)模式和主从触发模式。PWMI模式是专门用来同时测量PWM波形的频率和占空比的。主从触发模式可以实现对频率或占空比的硬件的全自动测量,不占用软件资源,可以减轻CPU的压力。
原理图:
输入捕获的通道与输出比较的通道是一致的,所以在通用定时器里的具体结构与输出比较一致。
来看输入捕获的具体原理图:
输入捕获通道的工作原理:输入信号首先进入输入滤波器和边沿检测器(输入滤波器与边沿检测器前文有提到不再赘述),当触发指定的电平时,边沿检测电路就会触发,然后用CC1P来选择极性,最终得到TI1FP1触发信号(也有TI1FP2de 触发信号连接到通道2),通过数据选择器进入通道1的捕获电路,进入捕获单元后,通过CCIS数据选择器对数据进行选择,然后经过一个ICPS来配置预分频器,最后对CC1E进行控制选择输出使能或失能,如果选择使能就可以控制CCR对CNT进行捕获操作了。捕获信号同时会触发一个事件,这个事件会在状态寄存器置标志位,可以触发中断。CR对CNT进行捕获之后,需要对CNT进行一次清0操作,这样每次捕获得到的值才是测周法(下文有讲解)两个上升沿(下降沿)之间的时间间隔。这个清0操作,就需要用到主从触发模式来自动完成。由输入捕获通道1的详细框图可得:经过滤波和极性选择的TI1FP1信号和经过滤波的边沿信号TI1F_ED都可以通向从模式控制器,之后便可以通过硬件电路自动完成CNT的清0操作(用来测频率)。
主从触发模式:
即主模式、从模式和触发源选择三个功能的简称,主模式可以将定时器内部的信号映射到TRGO引脚,用于触发其他外设的操作;从模式可以接收其他外设或自身外设的一些信号,用于触发自己的一些操作(定时器的运行);触发源选择,即选择从模式的触发信号源功能,也可以认为它是从模式的一部分。(如下图所示)
下面是通过触发源选择来配置主模式功能:
还有配置触发源信号来控制从模式:
本节会用到从模式的复位功能。
PWMI的基本结构:
首先,配置时基单元,启动寄存器,则CNT就会在时钟驱动下不断自增。我们使用CNT来计数,间接实现计时的功能。经过预分频后的时钟频率,就是测周法的标准频率f c 。之后,GPIO输入一个待测的方波信号,经过TI1FP1为上升沿触发,之后数据选择器选择直连通道,分频器选择不分频。当TI1FP1出现上升沿之后,CNT的值就会被CCR1捕获;同时触发源选择模块选择TI1FP1为触发信号,从模式选择复位操作。则TI1FP1信号完成两个操作:先捕获CNT的值到CCR1中然后清零CNT(与输入捕获基本一致),将一个引脚的信号映射到两个输入捕获单元,将TI1FP2配置为下降沿触发,就可以同时测量PWM波形的频率和占空比了,如下图所示,占空比:
D u t y = C C R 2 /C C R 1 × 100 %
频率测量有以下几种方式:
测频法:在闸门时间T内,对上升沿计次,得到N,则频率 f_x=N / T(测量高频信号)。
测周法:两个上升沿内,以标准频率fc计次,得到N ,则频率 f_x=f_c / N(测量低频信号)。
中界频率:测频法与测周法误差相等的频率点 f_m=√f_c / T
1.2.5 定时器编码器接口
简介:
Encoder Interface 编码器接口
编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减(所以接到编码器通道时不用时基单元部分来计数),从而指示编码器的位置、旋转方向和旋转速度,每个高级定时器和通用定时器都拥有1个编码器接口(资源较少),两个输入引脚借用了输入捕获的通道1和通道2(虽然借用但用Cube配置的时候不是配置输入捕获,到时候会教如何具体配置,如果用标准库写的话就是配置输入捕获通道)。
从原理上看,编码器接口输出的信号就相当于一个带有方向控制的外部时钟,同时控制CNT的计数时钟和计数方向。这时,CNT的值就代表了编码器的位置。如果每隔一段时间将CNT的值取出并清0,我们就能得到编码器在这一段时间内的旋转速度(注意,不是旋转速率,这里的速度只与这段时间的长短和始末位置有关),思路与测频法测正交脉冲的频率相似,只不过这里的计数器的计数方向由编码器接口决定。
正交编码器:
两个输出引脚输出的方波信号相位相差90°的编码器称为正交(增量)编码器。一个方波信号相对于另一个方波信号超前或滞后90°代表正传或反转。
要测量编码器的旋转方向,还可以使用一个引脚输出方波表示旋转角度和速度,另一个引脚输出旋转方向:正传输出高电平,反转输出低电平。但相较于这一种方案,使用正交编码器的优势在于:正交信号精度更高。正交信号的A、B相都可以计次,且相位相差90°,相当于计次频率(计数器的变化频率)提高了一倍。
正交信号可以在一定程度上免除噪声的干扰。对于正交信号而言,两个信号必须交替跳变,CNT的值才会自增或自减。如果一个信号不变,另一个信号连续跳变,则CNT的值是不会变化的。
编码接口的基本结构与工作模式:
编码器接口借用了定时器的CH1和CH2通道,与CH3和CH4无关。且输入捕获前端的输入滤波和极性选择部分也有使用。编码器接口的输出部分,相当于一个从模式控制器,控制CNT的自增和自减。此时72Hz内部时钟和时基单元初始化时设置的计数方向都处于失效的状态,因为此时的计数时钟和计数方向都处于编码器接口托管的状态。 所以当一个定时器被配置为编码器接口模式,这个定时器就很难完成其他工作了,因为编码器接口直接控制时基单元的时钟来源。但是在编码器接口模式下ARR是有效的,一般将ARR设置为最大量程,这样利用补码的特性,很容易得到负数。
编码器接口的设计逻辑为:把A相和B相的所有边沿作为计数器的计数时钟,出现边沿信号时,就计数自增或自减,此时增减由另一相的状态确定。当然,也可以仅在一相进行计数,忽略另一相,只不过这样会牺牲一部分计数的精度。
计数方向与编码器信号的关系:
以下是正转时正交编码器的信号:
根据计数方向与编码器信号的关系图与此图可以看出,当正转时计数器都是向上计数的,而反转时正好相反,根据这些特性我们就可以有效的测出编码的速度与方向。
具体例子:
在计数时如果极性选择不反向的话TI1FP1和TI2FP2均不反相时,计数器的操作实例如下图所示。下图同时也展示了编码器接口的抗噪声原理:当一个信号不变,另一个信号连续跳变(非正交编码器的信号),计数器就会来回加减摆动,最终计数值不变。(具体如下图所示)
而反相的作用是当编码器模块的使用过程中,如果数据的加减方向反了,就可以将任意一个引脚反相,反转计数方向得到自身需要的计数模式:具体如下图所列举(TI1反向时):
定时器编码器接口部分的介绍就到此为止,以上就是关于定时器的全部内容,接下来做具体实验来加深印象。
2 有关定时器的实验内容
在写代码之前先了解HAL中提供的关于定时器中断的函数:
HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);//打开定时器中断。
__HAL_TIM_CLEAR_FLAG (TIM_HandleTypeDef *htim, TIM_SR_UIF);//清除更新中断标志
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)//定时中断回调的函数。
__HAL_TIM_GET_COUNTER ( IM_HandleTypeDef *htim) //返回计数器寄存器的值。
__HAL_TIM_SetCounter (TIM_HandleTypeDef *htim,uint16_t Num);给计数器重新赋值。
HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, TIM_CHANNEL_x);//初始化输出比较通道
__HAL_TIM_SET_COMPARE(TIM_HandleTypeDef*htim,TIM_CHANNEL_X, uint16_t Compare);//设置默认的占空比值(compare)
HAL_TIM_IC_Start_IT (TIM_HandleTypeDef*htim,TIM_CHANNEL_X);//初始化输入捕获通道
HAL_TIM_ReadCapturedValue(TIM_HandleTypeDef*htim ,TIM_CHANNEL_X )//读取CCR的值
__HAL_TIM_PRESCALER (TIM_HandleTypeDef*htim,uint16_t Prescaler );//单独设置预分频的值。
HAL_TIM_Encoder_Start(TIM_HandleTypeDef*hti,TIM_CHANNEL_ALL);
//初始化编码器接口。
__HAL_TIM_IS_TIM_COUNTING_DOWN(TIM_HandleTypeDef*htim)://返回 0 或 1 ,代表转动方向
2.1 定时器中断:
实验内容如下:
利用定时器中断和OLED显示屏来显示计时,每一秒记一次。
接线图如下:
实物图如下:
关于STM32CubeMX的配置如下:
在Timer中选择定时器2来进行配置,1时钟源选择内部时钟,2预分频设置为分7200频,3计数器模式设置为向上计数模式,4自动重装寄存器设置为10000,5内部时钟不分频,6给计数器使能。
在NVIC里打开定时器2的中断
给NVIC的分组模式设为分组2,给OLED显示屏的引脚设置为输出模式,剩下配置不变生成工程即可。
代码部分:
Timer部分代码:
#include "main.h"
#include "tim.h"
#include "gpio.h"
extern uint16_t Num;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim==&htim2)
{
Num++;//定时器每隔一秒发生一次中断,响应中断后计数加一
}
}
uint16_t Timer_GetCounter(void)
{
return __HAL_TIM_GET_COUNTER ( &htim2 );//获取计数器寄存器的值
}
主函数部分代码:
OLED_Init();
HAL_TIM_Base_Start_IT(&htim2);//打开定时器2
OLED_ShowString (1,1,"Count:");
OLED_ShowString (2,1,"Cnt:");
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
OLED_ShowNum (1,7,Num ,5);//秒表定时器一秒发生一次中断加一
OLED_ShowSignedNum (2,7,Timer_GetCounter () ,5);//得到寄存器内部的值
}
写完这些代码下载程序后发现,秒表的计时是从1开始而不是从0开始,这是因为库函数中初始化过程中为了要让预分频器立刻起作用要操控预分频器的缓冲寄存器,库函数中给了更新事件的代码让预分频缓冲寄存器立刻起作用,同时也产生了中断标志位这样就导致了CPU一开始运行就进入了中断,为了弥补这一操作,我们需要在TIM初始化的函数中给他把更新中断的标志清除,这样单片机就会,从主程序开始执行。具体操作如下:
光标右键定时器初始化函数让他跳转到定义的.c文件里
在初始化函数的末尾加上__HAL_TIM_CLEAR_FLAG ( &htim2, TIM_SR_UIF);//清除更新中断标志 即可。
2.2 定时器外部时钟
实验内容:
用对射式红外传感器模块来手动模拟一个外部时钟的信号,然后每十个信号对其进行一次计数,来做一个计数器。实验现象:OLED屏幕上会显示计数器的值与计次。
接线图如下:
实物图如下:
首先配置STM32CubeMX
在Timer中选择定时器2来进行配置,1时钟源选择外部时钟ETR,2预分频设置为分1频,3计数器模式设置为向上计数模式,4自动重装寄存器设置为10,5内部时钟不分频,6给计数器使能。
剩余配置与2.1的配置相同,保存配置后打开工程:
开始手写代码部分;
Timer部分代码:
#include "main.h"
#include "tim.h"
#include "gpio.h"
extern uint16_t Num;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim==&htim2)
{
Num++;//定时器每隔一秒发生一次中断,响应中断后计数加一
}
}
uint16_t Timer_GetCounter(void)
{
return __HAL_TIM_GET_COUNTER ( &htim2 );//获取计数器寄存器的值
}
主函数部分代码:
OLED_Init();
HAL_TIM_Base_Start_IT(&htim2);//打开定时器2
OLED_ShowString (1,1,"Count:");
OLED_ShowString (2,1,"Cnt:");
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
OLED_ShowNum (1,7,Num ,5);//秒表定时器一秒发生一次中断加一
OLED_ShowSignedNum (2,7,Timer_GetCounter () ,5);//得到寄存器内部的值
}
这里同样会遇到开始计数时为1而不是0的现象,只需要给TIM初始化时,给一个清除更新中断的标志即可。
做完两个实验发现我们手写的代码是一致的,这是由于Cube帮我们写好了初始化,实际上外部时钟初始化的时候由于要用到GPIO口PA0所以我们要将PA0进行初始化,不过Cube已经帮我们配好,所以不需要配置,并且观察定时器回调函数发现,在HAL库中我们不用清除中断标志,当中断执行完成之后,会自动回退到主程序,这也是Cube帮我们已经写好了原因,所以Cube在帮助代码这一块体现了极大的便捷,但是我们也要明白这些是如何得来的,不然对学习的帮助不会很大。
2.3 PWM驱动LED的呼吸灯
实验内容:
通过定时对GPIO口来输出PWM波形,通过不断改变占空比来实现分辨率为1,频率为1000Hz的LED呼吸灯,通过下面公式计算得到参数
PWM = CK_PSC(内部时钟预分频) / (PSC + 1) / (ARR + 1)=1000 ,
PWM占空比: Duty = CCR / (ARR + 1)(CCR时刻改变)
PWM分辨率: Reso = 1 / (ARR + 1)=1。
接线图如下:
实物图如下 :
首先配置STM32CubeMX
在Timer中选择定时器2来进行配置,1时钟源选择内部时钟,2选定定时通道1,3预分频设置为分720-1频,4计数器模式设置为向上计数模式,5自动重装寄存器设置为100-1,6内部时钟不分频,7给计数器使能。
接下来对PWM通道进行设置给默认即可Mode :PWM模式1,Pulse(占空比值) :0,Fast Mode PWM脉冲快速模式 :不使能,PWM 极性: 设置为高电平(有效电平)。
其他Cube工程的设置不变,如果想用OLED来进行调试可以将PB8与PB9设置为输出模式。
代码部分:
PWM模块代码:
void PWM_Init(void)
{
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);//打开PWM输出模式
}
void PWM_SetCompare1(uint16_t Compare)
{
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, Compare);//设置默认的占空比值
}
主函数模块:
/* USER CODE BEGIN 2 */
OLED_Init();
PWM_Init ();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
for(i=0;i<100;i++)
{
PWM_SetCompare1 (i);//设置占空比从暗到亮
HAL_Delay(10);
}
for(i=0;i<100;i++)
{
PWM_SetCompare1 (100-i);//从亮到暗
HAL_Delay(10);
}
/* USER CODE END WHILE */
2.4 PWM驱动舵机
硬件电路:
舵机是一种根据输入PWM信号占空比来控制输出角度的装置
输入PWM信号要求:周期为20ms,高电平宽度为0.5ms~2.5ms
实物图如下:
拆分图如下:
由上图可以看出舵机主要由程序图里面的这些零件组成,可以看出舵机并不是一种单独的电机,它内部是由直流电机驱动的。他里面还有个控制电路板,是一个电机的控制系统,大概的执行逻辑是PWM信号输入到控制板,给控制板一个指定的目标角度,如果大于目标角度,电机就会反转,如果小于目标,电机就会正转,最终输出输出轴固定的在指定角度,这就是舵机输出的具体工作流程,但不管怎么样,我们只需要知道输入一个PWM波形,输出轴固定在一个角度就好。对于舵机的实际运用通常是来控制机器人或者机械臂的关节。
电路图如下所示:
具体实验如下:
实验内容:通过按键来控制输出不同的PWM信号,改变信号的占空比将PWM信号给到舵机的控制板然后驱动舵机转动。
接线图如下:
实物图如下:
首先配置STM32CubeMX
在Timer中选择定时器2来进行配置,1时钟源选择内部时钟,2选定定时通道2,3预分频设置为分72-1频,4计数器模式设置为向上计数模式,5自动重装寄存器设置为20000-1,6内部时钟不分频,7给计数器使能。
接下来对PWM通道进行设置给默认即可Mode :PWM模式1,Pulse(占空比值)为0,Fast Mode PWM脉冲快速模式设置为不使能,PWM 极性设置为高电平(有效电平)与上述基本一致,本节实验需要用到按键和OLED所以要对按键和OLED显示屏的GPIO继续配置。(按键代码和OLED的代码可以移植以前的代码拿来用,所以宏定义时与之前一致会更便捷的进行代码移植)。
代码部分:
PWM代码模块:
#include "main.h"
#include "tim.h"
#include "gpio.h"
void PWM_Init(void)
{
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);//开启PWM通道2
}
void PWM_SetCompare2(uint16_t Compare)
{
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, Compare);//设置默认的占空比值
}
Servo代码模块
#include "main.h"
#include "tim.h"
#include "gpio.h"
#include "PWM.h"
void Servo_Init(void)//直流电机初始化
{
PWM_Init();
}
void Servo_SetAngle(float Angle )//赋值更便捷,用到数学知识
{
PWM_SetCompare2(Angle / 180 * 2000 + 500);
}
主函数代码模块:
OLED_Init();
Servo_Init ();
OLED_ShowString (1,1,"Angle:");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
KeyNum =Get_KeyNum ();
if(KeyNum==1)//按键按下加30的角度
{
Angle+=30;
if(Angle>180)
{
Angle=0;
}
}
Servo_SetAngle (Angle );
OLED_ShowNum (1,7,Angle,3);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
2.5 PWM驱动直流电机
硬件电路部分:
直流电机是一种将电能转换为机械能的装置,有两个电极,当电极正接时,电机正转,当电极反接时,电机反转。
直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作TB6612是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向。(驱动电压5V,开发板的VCC电压为3.3V所以需要接到STlink上)。
直流电机与驱动电路TB6612实物图如下:
具体接线电路如下:
以上名称的具体含义如下:
具体实验如下:
具体实验内容:
通过按键来输出不同的PWM信号,改变占空比来实现对直流电机线圈转速的操纵,同时可以控制正反转,控制正转就是给输入控制端的电平给一高一低即可,反转给输入控制段电平相比正转而言设置相反的高低电平即可。
接线图:
实物图:
对于STM32的工程配置:
配置过程与上述过程基本一致,把PWM通道改成3即可,同时与直流电机相连的IO口配置为输出模式,其他模式的配置与PWM驱动舵机的配置基本一致。
代码部分:
PWM代码模块:
#include "main.h"
#include "tim.h"
#include "gpio.h"
void PWM_Init(void)
{
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);//开启PWM通道3
}
void PWM_SetCompare3(uint16_t Compare)
{
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, Compare);//设置默认的占空比值
}
Motor模块(直流电机)
#include "main.h"
#include "tim.h"
#include "gpio.h"
#include "PWM.h"
void Motor_Init(void)//直流电机初始化
{
PWM_Init();
}
void Motor_SetSpeed(int16_t Speed )
{
if(Speed>0)//正转动
{
HAL_GPIO_WritePin (Motor1_GPIO_Port ,Motor1_Pin,GPIO_PIN_RESET );
HAL_GPIO_WritePin (Motor1_GPIO_Port ,Motor2_Pin,GPIO_PIN_SET );
PWM_SetCompare3(Speed );
}
else//反转动
{
HAL_GPIO_WritePin (Motor1_GPIO_Port ,Motor2_Pin,GPIO_PIN_RESET );
HAL_GPIO_WritePin (Motor1_GPIO_Port ,Motor1_Pin,GPIO_PIN_SET );
PWM_SetCompare3(-Speed );//由于该函数为无符号参数所以要加负号取反
}
}
主函数模块:
OLED_Init();
Motor_Init ();
OLED_ShowString (1,1,"Speed:");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
KeyNum =Get_KeyNum ();
if(KeyNum==1)
{
Speed+=20;//按键每按下一次转速增加20
if(Speed>100)
{
Speed=-100;
}
}
Motor_SetSpeed(Speed);//驱动转速增加(改变占空比)
OLED_ShowNum (1,7,Speed,3);//显示转速与正转反转
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
2.6 输入捕获测定频率与占空比
PWMI模式就是对输入捕获模式的进一步配置,所以直接来配置PWMI模式来测频率与占空,将PA0设置为输出模式,输出不同的PWM占空比,然后将PA6设置为PWMI来接收来自PA0的信号,并且通过输入捕获模式来得到PA6端口具体的频率与占空比,与PA0端口进行比较观察是否一致。
实验如下:
接线图:
实物图如下:
实验内容如下:
配置STM32CubeMX工程
首先用定时器2来输出一个PWM信号:在Timer中选择定时器2来进行配置,1时钟源选择内部时钟,2选定定时通道2,3预分频设置为分720-1频,4计数器模式设置为向上计数模式,5自动重装寄存器设置为100-1,6内部时钟不分频,7给计数器使能。(与上一节操作一致)
接下来用定时器来配置PWMI输入捕获通道(将PWM输出的信号)给输入到这里来:
1从模式配置为复位,2选择内部时钟源,3打开通道1与通道2配置为输入捕获模式,4(时基单元的配置)预分频配置为72,自动重装寄存器配置为最大65536(防止计数溢出) , 5(输入捕获通道配置)通道1的边沿触发器选择上升沿触发,分频选择不分频,滤波器参数配置为0xf,通道2只需要将边沿改为下降沿即可沿即可。
程序调试用OLED显示屏来调试所以对于OLED的引脚要进行配置(PB8与PB9改为输出模式即可)。其余配置不变,保存配置生成工程即可。
代码部分:
PWM部分代码:
#include "main.h"
#include "tim.h"
#include "gpio.h"
void PWM_Init(void)
{
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);//打开PWM输出模式
}
void PWM_SetCompare1(uint16_t Compare)
{
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, Compare);//设置默认的占空比值
}
void PWM_SetPrescaler(uint16_t Prescaler)//单独改变预分频
{
__HAL_TIM_PRESCALER (&htim2,Prescaler );
}
Inputcapture部分代码:
#include "main.h"
#include "tim.h"
#include "gpio.h"
void IC_Init(void )
{
HAL_TIM_IC_Start_IT (&htim3,TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT (&htim3,TIM_CHANNEL_2);//打开PWMI模式
}
uint32_t IC_GetFreq(void)
{
// 测周法标准频率为1MHz
return 1000000 / (HAL_TIM_ReadCapturedValue (&htim3 ,TIM_CHANNEL_1 )+1);//计算的是PA6的频率
}
/**
* @brief 获取测得的占空比
* @param 无
* @retval 测得的占空比(百分之)
**/
uint32_t IC_GetDuty(void)
{
// return TIM_GetCapture2(TIM3) * 100 / TIM_GetCapture1(TIM3);
return (HAL_TIM_ReadCapturedValue (&htim3 ,TIM_CHANNEL_2 )+1) * 100 / (HAL_TIM_ReadCapturedValue (&htim3 ,TIM_CHANNEL_1 )+1);//PA6的占空比
}
主函数部分代码:
OLED_Init();
IC_Init();
PWM_Init();
PWM_SetPrescaler(720 - 1);//改变PA0的预分频(频率)
// Freq = 72M / (PSC + 1) / (ARR + 1),这里 ARR + 1 == 100
PWM_SetCompare1(40);//改变PA0的占空比
// Duty = CCR / (ARR + 1)
OLED_ShowString(1, 1, "Freq:00000Hz");
OLED_ShowString(2, 1, "Duty:000%");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
OLED_ShowNum(1, 6, IC_GetFreq(), 5);
OLED_ShowNum(2, 6, IC_GetDuty(), 3);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
对于主函数这部分代码里改变PA0占空比和频率的代码可以放进定时器里这样可以实时检测输入信号的频率和占空比的改变。
2.7 编码器接口测速
实验内容:
利用定时器的编码器接口,旋转编码按钮,根据旋转编码器的正交信号来控制计数器的加减(正转加反转减),通过OLED显示屏来获取编码器旋转的速度与正反(正负号表示)。
接线图:
实物图:
首先配置STM32CubeMX的工程:
本章节需要用到定时器定时中断来测速,而当定时器用作编码器时,其他功能不能照常使用,所以需要一个定时器用来做中断,一个用来做编码器接口:
给定时器2配置为每隔一秒进一次中断,记得在NVIC里打开定时器2的中断:
给定时器配置为编码器接口:1模式配置为编码器接口接收正交编码的形式时基单元的配置维持默认(预分频为0,自动重装值为65535),
2采用A,B相都计数的模式(精度更高)。
3采用极性不反转的模式(上升沿在这里的意思时极性不反转)。(通道1和2配置相同)
4滤波器的参数为0xF.(通道1和2配置相同)
这里截图只截了通道1的,通道2也要配置。
其余配置不变,由于GPIO口引脚复用所以对于PA0,PA6与PA7不用专门配置,然后将OLED的数据线与时钟线PB9与PB8配置为输出模式即可。然后保存并打开工程。
代码部分:
Timer部分代码:
#include "main.h"
#include "tim.h"
#include "gpio.h"
#include "Encoder.h"
extern int16_t Speed;
void Timer_Init()//使能定时器中断
{
HAL_TIM_Base_Start_IT (&htim2);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim==&htim2)
{
Speed = Encoder_Get();//定时器每隔一秒发生一次中断,响应中断后计数加一
}
}
Encoder部分代码:
#include "main.h"
#include "gpio.h"
#include "tim.h"
#include "Timer.h"
uint16_t Encoder_Count;
void Encoder_Init(void )
{
HAL_TIM_Encoder_Start (&htim3, TIM_CHANNEL_ALL);//初始化编码器接口
}
uint16_t Timer_GetCounter(void)
{
return __HAL_TIM_GET_COUNTER ( &htim3 );//获取计数器寄存器的值
}
uint16_t Encoder_Get(void)
{
int16_t Temp;
Temp = Timer_GetCounter() ;
__HAL_TIM_SetCounter (&htim3,0);//中断后清空计数器的值
return Temp ;
}
主函数部分代码:
Timer_Init();
OLED_Init();
Encoder_Init();
OLED_ShowString (1,1,"Speed:");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
OLED_ShowSignedNum (1,7,Speed,5);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}