**定时器:**
定时器就是计数器,定时器除了能实现普通的定时功能,还能实现捕获脉冲宽度,计算PWM占空比,输出PWM波形,编码器计数等...

在STM32的F10系列芯片中,最有有8个定时器可以使用,其中TIM6与TIM7是基本定时器,它们只有简单的定时功能(以及一个触发DAC的功能);TIM2~5是通用定时器;TIM1和8是高级定时器,一般用于电机领域
基本定时功能:

可以看到只要有一个稳定的方波信号,每当有一个上升沿到达计数器,寄存器上存储的值就会+1,例如100Hz就是1秒输出一百个脉冲,所以计数器的值到100就是1秒,那哪来的稳定方波信号呢,答案就在我们上一小节的介绍时钟源&时钟树 这里,只要我们将APB1定时器设为72MHz,那么进入基本定时器的脉冲频率就是72MHz
但我们可以看到计数器只有2字节大小,最多只能计65536次,远远不够72M,这怎么办呢?那我们就可以用到我们的预分频器了,例如当预分频器设为n-1时,那它就是n次分频,也就是说当它接收到n次上升沿信号时,才会输出一次方波信号,这跟计数器原理很相似,不过计数器是+1,预分频器是输出一个方波信号。
知道了计时,那么如何实现定时功能呢?
自动重装载寄存器:
它的值就是实时监控计数器的值是否与自己的相同,当计数器记得与自己相同的值时,便将计数器的值重置为0,并且可以触发一次定时器更新中断,通知STM32定时时间到了,如果我们想定时m个脉冲,就需要设置自动重装载寄存器的值为m-1
接下来我们就开始配置CubeMX吧:
首先点击Timers,注意我们使用的STM32f103c8t6只有四个定时器,即1个高级定时器TM1,和剩下的三个通用定时器 ,以定时器TIM4为例子,选择TIM4,然后到TIM4页面把interna Clock(内部时钟)勾上

如果想选择TIM2,则是将时钟源(Clock Source)选择为internal Clock(内部时钟)

接下来是定时器参数配置:

这里的计算公式是(想要设置的定时时间=脉冲频率(72MHz)/PRS(预分频器值)/CCR(自动重装载寄存器值) ),所以我们设置预分频器的值,让72MHz变成10000Hz,然后设置自动重装载值,计数一万次,就实现了定时一秒的效果:
注意这里的两个值,因为是从0开始,因此-1!按照上面这样设置,计数器里的值从0数到9999的时候就会从0开始重新计数。
接下来是时钟源选择:
我们按照上一小节:
外部高速时钟源(HSE)设置为晶振(Crystal/Ceramic Resonator)提高精度


这里补充一个图:

这张图可以看到哪个外设搭载了哪根时钟线,可以看到我们通用定时器都搭载在APB1上,而APB1外设时钟线为32MHz,不过经过倍频器,我们的APB1定时器时钟线升为了72MHz。
**接下来就是代码:**
c
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); // 使能接收中断
HAL_TIM_Base_Start(&htim4);
uint16_t Count=0;
while (1)
{
/* USER CODE END WHILE */
Count=__HAL_TIM_GET_COUNTER(&htim4);
sprintf(Message,"Count:%d",Count);
HAL_UART_Transmit_IT(&huart1,(uint8_t*)Message,strlen(Message));
HAL_Delay(99);
/* USER CODE BEGIN 3 */
}
这是一个每100毫秒,串口发送一次计数器值的函数,**HAL_TIM_Base_Start(&htim4);函数是定时器启动函数, __HAL_TIM_GET_COUNTER(&htim4);是获取计数器值的函数 **
**注意:**延时100ms(HAL_Delay函数有个小bug,想要准确延时100ms,参数需要填99)后发送一次计数器值
除了上面的一个宏定义,还有下面这几个:
c
/* 设置计数器的值
* 第一个参数为句柄,第二个参数为值
*/
__HAL_TIM_SetCounter
/* 获得重装载值
* 只有一个参数,传入句柄
*/
__HAL_TIM_GetAutoreload
/* 设置重装载值
* 句柄+值
*/
__HAL_TIM_SetAutoreload
/* 设置预分频值
* 句柄+值
*/
__HAL_TIM_SET_PRESCALER

**知识点补充:**
可以看到,预分频器和自动重装载 都有一个影子寄存器的结构,而真正工作的是影子寄存器,我们通过 __HAL_TIM_SET_PRESCALER 宏将新的预分频值给到预分频寄存器后,直到计数器的值归零后,才会把新的预分频值给它的影子寄存器
不过自动重装载寄存器的影子寄存器,在我们设置完后,也是在下个周期(计数器的值归零)后生效 ,这样的好处是不会影响当前的计数周期。不过重装载寄存器可以自己控制是否开启影子寄存器机制,默认是关闭影子寄存器的 ,来到CubeMX,这个选项设置成Enable就是打开:

要注意的是:如果我们没启动重装载寄存器的影子寄存器机制,那么当我们将重装载寄存器的数值调小时,可能会小于当前计数器的值,这样的话计数器会数到最大值65535才会触发更新,回到0开始下一次计数。
不过不要忘记我们的初心是实现定时,这就涉及到了定时器的更新中断 ,所谓更新中断,就是每次计数器的值到与重装载值相同时,将计数器值清0重新开始计时,触发的中断,CubeMX设置如下:

选择NVIC,勾上打开定时器TIM4的全局中断功能。
然后用到我们的定时器周期结束中断回调函数:
c
uint8_t Data[]="Hello"
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim==&htim4)
{
HAL_UART_Transmit_IT(&huart1,Data,sizeof(Data));
}
}
这样就实现了每秒发送一次hello,注意开启定时器函数要改成HAL_TIM_Base_Start_IT(&htim4);
最后我们会围绕这个来教学:

**外部时钟与循迹模块:**
上节我们了解了预分频器的时钟源来自STM32时钟树上挂载的APB的时钟线,因为APB时钟线在STM32内部,所以我们也称这个脉冲信号为定时器的"内部时钟"信号 ,那我们能不能对来自GPIO口的信号进行计数呢?这当然可以

触发控制器 是用来触发预分频器进而触发计数器进行计数的,那么我们就将GPIO口接入触发控制器就可以实现对外部信号进行计数。

STM32并没有直接将GPIO口接入进来,F103芯片上,每个定时器(本芯片只有TIM1~4 )有4个输入通道,其输入的信号称为TI1到TI4(TIM Input) ,其中TI3和TI4并没有接入到触发控制器,因此先忽略;STM32为每个通道都配套了输入滤波器和边沿检测器 ,边沿检测器可以检测输入信号的边沿并输出一个脉冲 (可检测上升沿、下降沿、双边沿);对于通道1(TI1)来说,其边沿检测器输出的脉冲信号有两个(TI1FP1和TI1FP2),通道2(TI2)也有两个(TI2FP1和TI2FP2),其中TI1FP2和TI2FP1 后面才会用到,因此先忽略;TI1FP1和TI2FP2来到了触发控制器前,另外通道1(TI1)上还有一根称为TI1_ED 的信号也来到触发控制器前(TI1FP1和TI2FP2可以选择三种边沿触发方式,TI1_ED只能由双边沿触发产生脉冲 ),这三个信号会连接到触发器(即只能选择一个作为输入信号) ,触发器又接入到触发控制器中一个叫做从模式选择器 的结构;除了来自通道1和通道2的这三根信号,触发器还有一个独立的外部时钟信号 ,外部触发器 ETR (External Trigger),ETR经过极性选择、边沿检测、预分频以及输入滤波 ,其中边沿检测只能检测上升沿 ,但是极性选择可以将电平的极性翻转 ,因此极性选择和边沿检测配合就能检测上升沿和下降沿 ;ETR最终进入到触发器。被触发器选择后进入从模式控制器的这条路我们称之为** "外部时钟模式1" **,而ETR除了能进入触发器外,还能独立直接进入触发控制器(注意不是从模式控制器),这条独享的路称之为"外部时钟模式2",为何要如此设置后面会讲到 。

循迹模块:

模块上有一个电位器(变阻器),可以通过小螺丝刀调节其阻值,进而调节此模块的检测阈值,如下:

当红外光强度大于阈值时(黄色的线),模块上的小灯就会亮起,同时模块上的DO引脚会输出低电平 ,红外光小于阈值时相反,灯灭,DO 输出高电平 :

因此,模块面前有可以反射红外光的物体时,DO引脚输出低电平,当模块面前没有物体,或者有黑色物体吸收了红外光时,DO引脚输出高电平,所以假如我们在传送带边缘铺上一圈黑白间隔条纹,运动起来时模块的DO引脚会输出方波信号,将此信号接入定时器中,我们就可以对黑白交替的次数进行计数
**外部时钟模式2:**
接下来我们开始配置CubeMX:来到CubeMX,这次先不要设置HCLK为72MHz,而是保持在8MHz(跟我们的滤波器有关,后面补充),打开Timers,这次我们使用TIM2,这是因为由于该芯片引脚有限,TIM3、4没有引出外部触发器ETR的引脚,进入TIM2的设置,选择时钟源(Clock Source)为ETR2

选择ETR2(外部触发器)后就可以看到PA0引脚被标记为了TIM2_ETR,也就是说我们可以通过PA0 引脚将循迹模块DO引脚输出的方波信号引入到定时器

接下来是ETR2的参数配置:

这些参数保持默认就好了,预分频的存在是因为最终输入到触发器的ETR信号最快只能是内部时钟(APB1/APB2)频率的1/4,因此通过这个分频器可以把速度降下来,但是我们本次的输入信号并不快,所以就不设置了
**下面是获取计数器值代码:**
c
HAL_TIM_Base_Start(&htim2); //开启定时器
OLED_Init();
char Message[20];
uint16_t Count=0;
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
Count=__HAL_TIM_GET_COUNTER(&htim2); //获取定时器值
sprintf(Message,"Count:%d",Count); //拼接字符串
OLED_ShowString(1,1,Message);
/* USER CODE BEGIN 3 */
}

通过OLED屏幕,可以发现,这个计数器值变化的有的快,这是因为经过黑白边缘,因为各种原因产生非常多的抖动脉冲,所以我们要消除这些抖动脉冲才能正常计数,那怎么消除呢,这就用到了输入滤波器

大多是情况下,抖动脉冲的宽度都远远小于真正的脉冲宽度,所以此滤波器都会按照一定的采样频率f检验每个经过它的脉冲,如果N次检验后脉冲还没有结束,就判断这次脉冲有效,否则不行,接下来我们进入CubeMX进行检验吧:

我们可以通过配置Clock Filter(滤波器)来配置f和n,大多数情况下都选择最大值15。

我们可以看到采样频率f的值与内部时钟有关的,内部时钟频率越大,采样频率越快,太快就会导致正常脉冲宽度太窄,所以我们采用的是8MHz内部时钟频率。
现在来实现一开始要的效果,测传送带的速度,需要在CubeMX设置自动重装载值为10,并且开启中断,我直接给出代码(作为参考,重点是学习外部时钟):
c
在这里插入代码片
**外部时钟模式1:**
首先我们关闭时钟源的选项,然后从模式(Slave Mode)选择为外部时钟模式1(External Clock Mode 1) ,然后再选择到底是使用触发器的哪一条路输入,这里我们选择ETR1(外部触发器1)

可以看到Clock设置变成了Trigger(触发器设置) ,不过参数还是一样的,这里触发器过滤器(Filter)还是选择15

接着就保存并生成代码,这里只是底层硬件设置做了改动,而代码逻辑没有变化,所以不需要更改代码。
接着触发器源我们试试TI1_ED吧


可以看到TI1_ED的触发器设置只有过滤器,因为TI1_ED只能是双边沿触发的产物,没有极性选择了。
接着是TI2FP2

这里可以看到极性选择有三种,上升沿触发,下降沿触发,以及双边沿触发,不过要注意这里的引脚变成了PA1
定时器从模式
上小节我们了解到定时器的输入通道TI1、TI2和ETR(外部触发器)都可以在经过触发器选择后,引入到从模式控制器中,在CubeMX中我们只需要将从模式(Salve Move)选择外部时钟模式1 ,选择一个触发源 ,就可以将GPIO口信号引入到定时器中 ,驱动计数器进行计数。

但我们也可以看到从模式 选择除了外部时钟模式1 还有另外三种模式,外部时钟模式1的功能是给定时器提供计数的信号 ,而这三种模式的功能,则是控制定时器的工作状态 ,它们分别是:复位模式,门模式,以及触发模式 ,下面我们就来认识一下吧。

复位模式
它可以对定时器的计数状态** 进行复位 ,假设从模式被配置成复位模式,由于 从模式控制器不再是外部时钟模式1(而是复位模式),也就不能为定时器提供计数信号**,所以我们需要使用另外的时钟源,如果单纯的计时,可以使用内部时钟,想对外部信号进行计数,也可以使用外部触发器ETR ,通过外部时钟模式2接入定时器 (这就是为何ETR可以独辟蹊径,跳过触发器,直接接到触发控制器的原因,即在从模式控制器被占用时,还可以从ETR引入外部信号 )。 回到正题,假设我们这是使用内部时钟源,预分频器设置为0,自动重装载寄存器设置为5,另外从模式的触发源选择为TIFP1上升沿触发 ;那么在定时器正常的计数过程中(0~5),如果TI1输入了一个带上升沿的信号,就会从TI1FP1输出一个脉冲 ,进入到从模式控制器 ,此时从模式控制器就会执行复位模式的功能:将定时器进行复位,所谓复位操作其实与自动重装载一样,也是进行更新事件(计数器清零、设置对应的影子寄存器、触发定时器更新中断(如果开启了中断))。
下面我们就开始配置CubeMX吧

这里配置是5秒定时,触发一次更新中断
下面是代码:
c
uint8_t Update_Count=0; //每进一次中断加1
uint8_t Trigger_Count=0; //触发器触发一次加1
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) //更新中断回调函数
{
if(htim==&htim2)
{
Update_Count++;
}
if(__HAL_TIM_GET_FLAG(htim,TIM_FLAG_TRIGGER)==SET) //判断触发器标志位,置1说明触发
{
Trigger_Count++;
__HAL_TIM_CLEAR_FLAG(htim,TIM_FLAG_TRIGGER); //标志位清0
}
}
HAL_TIM_Base_Start_IT(&htim2); //开启定时器并使能更新中断
OLED_Init();
char Message[20];
uint16_t Count=0;
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
Count=__HAL_TIM_GET_COUNTER(&htim2); //获取定时器值
sprintf(Message,"Count:%d",Count); //拼接字符串
OLED_ShowString(1,1,Message);
OLED_ShowNum(2,1,Update_Count,3);
HAL_Delay(100);
}
从这个代码,我们就可以观察到OLED屏幕上,5秒时间到,定时器值清0,触发一次更新中断,Update_Count加1,而循迹模块输入一个上升沿,定时器值也会清零,并触发一次更新中断,那怎么判断这个更新中断是复位模式还是自动重装载? ,其实除了触发一次更新中断会置标志位外,从模式控制器在接收到触发信号后,还会设置一个触发器中断标志 ,如下图:

门模式:
保持上面的设置都不变,只不过从模式设置成门模式:当输入信号为高电平(严格讲是检测到上升沿)时,门就打开,时钟信号可以进入到定时器,定时器正常计数;当输入信号为低电平(检测到下降沿)时,门就关闭,定时器暂停计数,输入通道的边沿检测器就可以改变高低电平对门的开关控制。
来到CubeMX,将从模式改为门模式,其他无需修改,这里需要注意的是:门模式下,控制信号出现下降沿或者上升沿,从模式器就会暂停计数器计数或者开启计数器计数,这两个边沿的时刻会将触发器中断标志位置1,但和复位模式不同,只是将标志位置1,并不复位计数器值,也不触发定时器更新中断(不调用回调函数),因此代码这样写:
代码如下:
c
HAL_TIM_Base_Start_IT(&htim2); //开启定时器并使能更新中断
OLED_Init();
uint16_t Count=0;
uint8_t Trigger_Count=0; //触发器触发一次加1
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(__HAL_TIM_GET_FLAG(&htim2,TIM_FLAG_TRIGGER)==SET) //判断触发器标志位,置1说明触发
{
Trigger_Count++;
__HAL_TIM_CLEAR_FLAG(&htim2,TIM_FLAG_TRIGGER); //清除标志位
}
Count=__HAL_TIM_GET_COUNTER(&htim2); //获取定时器值
sprintf(Message,"Count:%d",Count); //拼接字符串
OLED_ShowString(1,1,Message);
OLED_ShowNum(2,1,Update_Count,3);
OLED_ShowNum(3,1,Trigger_Count,3);
HAL_Delay(100);
}
通过OLED屏幕可以看到, 当循迹模块输出低电平时(由高变低),从模式触发并且一直输出一个数,证明计数器暂停了,变为高电平后计数继续,并且也将触发中断标志位置1。
触发模式:
它的作用就是检测到设定的边沿后,让定时器开始计数,不需要改代码,和门模式的代码一样,通过OLED可以看到,循迹模块输出高电平时,定时器开始计数,但输出低电平时,定时器并不会停止计数,但输出高电平时,Trigger_Count会+1,说明当循迹模块输出高电平时会将触发器标志位置1,那怎么停止计数呢?这时我们可以配合单脉冲模式使用(One Pulse Mode),所谓单脉冲模式就是不再开启循环计数,当计数到自动重装载时就会停止计数

知识点补充:
我们可以注意到,每次刚启动程序时,或者按下复位键,Update_Count(触发更新中断)就会+1,这是因为,在调用 MX_TIM2_Init();定时器初始化函数时,会将定时器更新中断标志位置1 ,所以我们在调用HAL_TIM_Base_Start_IT(&htim2); 时,中断机制启动 ,NVIT马上会检测到更新中断标志置1,触发中断,进而回调了更新中断的回调函数,怎么解决呢,其实很简单,只要在HAL_TIM_Base_Start_IT(&htim2);前面,调用一下 __HAL_TIM_CLEAR_FLAG(&htim2,TIM_FLAG_UPDATE);清除标志位函数就行了。
