前言
由于网上关于定时器捕获的资料比较少,所以这一篇会比较啰嗦一点,说的详细一些。
简介
在stm32f103c8这个单片机中,只有1个高级定时器TIM1,和3个通用定时器TIM2~4;前后一章讲了用通用定时器输出PWM的课题,这章就用高级定时器做输入捕获吧。
输入捕获高级定时器和通用定时器差不多,只是高级定时器会多两个步骤,分的比较细一些,相对来说更能让人理解清晰。
高级定时器是一个 16 位的可以向上、向下、居中计数的定时器,可以定时,可以输出比较,可以输入捕捉,还可以做三相电机互补输出,像做BLDC的驱动,就必须要求单片机需要有3个高级定时器。
输入捕获
输入捕获可以用来测信号的周期、频率、脉宽等。
截屏手划的,不直望见谅。T1,T2,t1,t2表示各点定时器计数值
周期:T = (T2-T1)*(时钟频率/分频系数);
频率:f = 1/T;
脉宽 = (t1-T1)*(时钟频率/分频系数);//周期包括相邻一组高低,脉宽只包括一个高电平
※注意
要注意的是,周期/脉宽可能比较大,在定时器数完16位,也就是从0数到65535之后,周期/脉宽末点还没到,所以我们需要开启定时器的重装载,然后重装载了多少次都要加进去。(类似于我们的手表,下午2点等于14点加各12);在这就要加重装载值,溢出多少次加多少次重装载值。
比如说
周期:T2-T1 = 溢出次数n*重装载值x +(T2-T1)这里的T2-T1可以是负数,负数代表计数值比重装载值小。
类似于10进制从8数到2:
8,9,0,1,2。
T2-T1=10*1+(2-8)= 4,计数4次。
想明白这点之后,接下来,如果我们在第一次检测到边沿的时候,给定时器的计数值清零。这时候是不是就相当于T1永远是0点。
所以T2-T1 = 溢出次数n*重装载值x + T2。
搞清楚之后,就全都明了了。
后面为了简单,就都用周期代替,脉宽实际上与周期是一样的,只有在程序中需要翻转一下检测方式,说到程序的时候会提示。
偷到一张图,对着这张图看会对溢出了解的清楚一点,采集到一次上升沿,清空计数值:
建立工程
搞明白之后,直接建立工程。
模式
TIM1 -> 内部时钟源 -> 输入直接捕获模式
查看规格书,需要使用的是定时器TIM1, TIM1挂在APB2上。
时钟树
查看时钟树,APB2 timer clocks -> 64MHz,
参数设置
预分频器:63
计数方向:向上
重装载值:65535
内部时钟分频:不分
重复计数:0
自动重装载:使能
主从:失能
触发事件类型:复位
捕获方式:直接捕获
分频:不分
滤波:不滤
解释一下:
预分频器:设置成64-1,这样设置的好处是,计数器每数一个数,就是1us,怎么算的可以去翻前面嘀嗒定时器延时篇,T=1/(F/(预分频系数+1))=10^-6s=1us。
重装载值:一般需要测周期这种,是越大越好,越大能测得的周期就越大,65535是最大值。
下降沿:根据情况而定选择,因为我是用按键来仿真的,按键电路在下面。
直接捕获方式:在上面通道项已经选择了,这里只能选这一项。直接捕获和间接捕获的主要区别在于是否对信号进行额外的处理或操作。直接捕获保留原始信号,间接捕获则对信号进行了一定的预处理或优化。选择哪种方式取决于具体的需求和应用场景。
预分频器:这里指输入分频,选择几分频就是要检测几次信号才能触发,如果是4分频上升沿检测,就是要检测到4次上升沿信号才会触发。
电路
按键仿真信号源电路:
中断
打断中断:不使能
更新中断:使能
触发和换向中断:不使能
捕获中断:使能
基本定时器通常不具备捕获功能,通用定时器只有一个全局中断,高级定时器有以上四个中断。
如果使用的通用定时器,只需要勾上全局中断就行了。
开启中断之后来到NVIC,这里就可以看到刚才开启的中断了,优先级分抢占优先级和子优先级,由4bit控制,可随意分配,我这给的是抢占3bit,所以子优先级自然而然就是1位了。(前面外部中断有说过中断优先级)
这里更新中断和捕获中断设置成一样就好了,这里是都设置成抢占1级,子优先0级。
配置完后,生成程序。
代码
生成的库函数
打开看一下stm32cubeMX生成的程序。双击打开启动文件startup_stm32f103xb.s,这里有所有的中断函数。startup_stm32f103xb.s文件第102行,后面也写了注释。顺序也是按照stm32cube里面一样,第一个:打断中断,第二个:更新中断,第三个:触发和换向中断,第四个:捕获中断。
以更新中断为例,右键跟踪。
102 DCD TIM1_BRK_IRQHandler ; TIM1 Break
103 DCD TIM1_UP_IRQHandler ; TIM1 Update
104 DCD TIM1_TRG_COM_IRQHandler ; TIM1 Trigger and Commutation
105 DCD TIM1_CC_IRQHandler ; TIM1 Capture Compare
函数内只有一个中断服务函数,继续跟踪。
void TIM1_UP_IRQHandler(void)
{
/* USER CODE BEGIN TIM1_UP_IRQn 0 */
/* USER CODE END TIM1_UP_IRQn 0 */
HAL_TIM_IRQHandler(&htim1);
/* USER CODE BEGIN TIM1_UP_IRQn 1 */
/* USER CODE END TIM1_UP_IRQn 1 */
}
接下来这个函数有点长,这里就不列出来了。大致意思就是,有四个捕获通道,如果对应通道检测到捕获事件,并且回调函数没有被禁用,则调用回调函数。
接下来跟踪回调函数。HAL库的回调函数大致都是这种类型。函数内部是空的,前面有一个弱定义符号__weak,前面提到过,表示可以重新定义一个一样名字的函数,并且不会报错。
__weak void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(htim);
/* NOTE : This function should not be modified, when the callback is needed,
the HAL_TIM_OC_DelayElapsedCallback could be implemented in the user file
*/
}
然后我们就是要自己编写更新中断和捕获中断服务函数了。了解里面原理的可以直接按下面步骤找到服务函数(前面提到过,不多讲。)
捕获步骤
首先,先来编写捕获中断服务函数,刚刚上面说了,捕获需要做的步骤是:
捕获到第一次下降沿 -> 清空计数值 -> 等待第二次捕获 -> 捕获到第二次下降沿 -> 读取计数值 -> 退出等待下一次捕获。
不过单片机是机器,是死的,他不知道什么时候捕获到第一次,什么时候捕获完成了,所以需要设定两个变量,称作标志位,一个是捕获到第一次的标志位,一个是捕获完成的标志位。
※,定时器的计数器如果不关闭就进行清零的话,是清不掉的。之前测试过一次,也不清楚是清楚成功了又数上去了,还是压根没有清除掉,所以在计数器清零前加一个停止计数,后面加一个开始计数的步骤。
相应库函数或寄存器
然后,先找到相对应的库函数或寄存器:
捕获下降沿 -> 捕获中断回调函数HAL_TIM_IC_CaptureCallback()
获取捕获计数值 -> 库函数HAL_TIM_ReadCapturedValue()
TIM1计数器 -> CNT寄存器
TIM1计数器计数启停控制寄存器 -> CR1控制寄存器第0位控制计数启停,1开,0停。
寄存器通过查规格书得到,如下:
编写回调函数
现在,所有的龙珠都收集齐了,可以召唤神龙了,看代码:
uint8_t Capture_finish_sign; //捕获完成的标志
uint8_t Capture_edge_sign; //捕获到边沿的标志
uint16_t Capture_val; //捕获完成时的计数值
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM1) //进入到这个函数,就代表捕获到了下降沿
{
if(Capture_finish_sign == 0) //如果捕获未完成
{
if(Capture_edge_sign) //已经捕获到一次下降沿了
{
Capture_edge_sign = 0; //清空第一次捕获下降沿标志位
Capture_finish_sign = 1; //捕获完成状态
Capture_val = HAL_TIM_ReadCapturedValue(&htim1, TIM_CHANNEL_1); //读取计数器当前计数值
}
else //没有捕获到过下降沿
{
Capture_count = 0; //清空上一次溢出次数
Capture_val = 0; //清空上一次捕获的计数值
Capture_edge_sign = 1; //第一次捕获到上升沿标志
TIM1->CR1 &= ~TIM_CR1_CEN; //定时器停止计数
TIM1->CNT = 0; //定时器计数清0
TIM1->CR1 |= TIM_CR1_CEN; //定时器开始计数
}
}
}
}
然后编写更新中断回调函数:
uint8_t Capture_count; //捕获溢出次数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM1)
{
if(Capture_finish_sign == 0) //未捕获完成
{
if(Capture_edge_sign) //已经捕获到了第一次下降沿
{
if(Capture_count == 0xff) //溢出次数超过最大次数0xff次
{
Capture_finish_sign = 1; //退出捕获
Capture_val = 0xffff; //此时返回的溢出次f数0xff,计数器数值0xffff,表示计数超载,需要增大Capture_count字节数
}
else
{
Capture_count++; //溢出次数加1
}
}
}
}
}
主函数调用
最后在主函数中先要开启TIM1的捕获中断和更新中断,然后调用回调函数:
/* USER CODE BEGIN 2 */
HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_1); //以中断方式开启捕获
__HAL_TIM_ENABLE_IT(&htim1, TIM_IT_UPDATE); //开启更新中断
// HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
int i;
while (1)
{
if(Capture_finish_sign) //捕获完成
{
if((Capture_count == 0xff) && (Capture_val == 0xffff)) //捕获超载
{
//error
}
else //正常捕获
{
Cycle = Capture_count * 0xffff + Capture_val; //周期us
}
//捕获的周期值用一个数组显示出来,就可以不用再接外设,用keil仿真直接看了,
if(i<200)
T[i++] = Cycle;
else
i=0;
Capture_finish_sign = 0; //转换完成,捕获完成标志清零。
}
}
测量脉宽
如果是需要测量脉宽,还需要在捕获回调函数中加一个翻转,捕获到上升沿,翻转成下降沿捕获,捕获到下降沿,翻转成捕获上升沿,具体如下:
※注意,翻转检测边沿的时候,需要先复位。
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM1) //进入到这个函数,就代表捕获到了下降沿
{
if(Capture_finish_sign == 0) //如果捕获未完成
{
if(Capture_edge_sign) //已经捕获到一次下降沿了
{
Capture_edge_sign = 0; //清空第一次捕获下降沿标志位
Capture_finish_sign = 1; //捕获完成状态
Capture_val = HAL_TIM_ReadCapturedValue(&htim1, TIM_CHANNEL_1); //读取计数器当前计数值
TIM1->CCER &= ~(0x01<<13); //改为上升沿捕获
}
else //没有捕获到过下降沿
{
Capture_count = 0; //清空上一次溢出次数
Capture_val = 0; //清空上一次捕获的计数值
Capture_edge_sign = 1; //第一次捕获到上升沿标志
TIM1->CR1 &= ~TIM_CR1_CEN; //定时器停止计数
TIM1->CNT = 0; //定时器计数清0
TIM1->CCER |= 0x01<<13; //改为下降沿捕获
TIM1->CR1 |= TIM_CR1_CEN; //定时器开始计数
}
}
}
}
结语
最后,其实检测脉宽还有一个办法,跟踪边沿的宏过去就可以发现,虽然stm32cube上没有这一项,但是HAL库的库函数中有一个上下边沿都检测的宏TIM_INPUTCHANNELPOLARITY_BOTHEDGE
所以,是不是只需要生成程序之后,把初始化函数中的上升沿改成双边沿就行了,/手动滑稽,不知道是不是stm32cube跟HAL之间的一个bug,具体如下:
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_FALLING; --->>>
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_BOTHEDGE;