STM32 定时器输出比较翻转模式
此项功能是用来控制一个输出波形,或者指示一段给定的的时间已经到时。
当计数器与捕获/比较寄存器的内容相同时,输出比较功能做如下操作:
● 将输出比较模式(TIMx_CCMRx寄存器中的OCxM位)和输出极性(TIMx_CCER寄存器中的CCxP位)定义的值输出到对应的引脚上。在比较匹配时,输出引脚可以保持它的电平
(OCxM=000)、被设置成有效电平(OCxM=001)、被设置成无效电平(OCxM=010)或进行翻转(OCxM=011)。
● 设置中断状态寄存器中的标志位(TIMx_SR寄存器中的CCxIF位)。
● 若设置了相应的中断屏蔽(TIMx_DIER寄存器中的CCxIE位),则产生一个中断。
● 若设置了相应的使能位(TIMx_DIER寄存器中的CCxDE位,TIMx_CR2寄存器中的CCDS位选择DMA请求功能),则产生一个DMA请求。
TIMx_CCMRx中的OCxPE位选择TIMx_CCRx寄存器是否需要使用预装载寄存器。
● 设置中断状态寄存器中的标志位(TIMx_SR寄存器中的CCxIF位)。
● 若设置了相应的中断屏蔽(TIMx_DIER寄存器中的CCXIE位),则产生一个中断。
● 若设置了相应的使能位(TIMx_DIER寄存器中的CCxDE位,TIMx_CR2寄存器中的CCDS位选择DMA请求功能),则产生一个DMA请求。
TIMx_CCMRx中的OCxPE位选择TIMx_CCRx寄存器是否需要使用预装载寄存器。
在输出比较模式下,更新事件UEV对OCxREF和OCx输出没有影响。
同步的精度可以达到计数器的一个计数周期。输出比较模式(在单脉冲模式下)也能用来输出一个单脉冲。
输出比较模式的配置步骤:
1.选择计数器时钟(内部,外部,预分频器)
2.将相应的数据写入TIMx_ARR和TIMx_CCRx寄存器中
3.如果要产生一个中断请求和/或一个DMA请求,设置CCxIE位和/或CCxDE位。
4.选择输出模式,例如:必须设置OCxM=’011’、OCxPE=’0’、CCxP=’0’和CCxE=’1’,当计数器CNT与CCRx匹配时翻转OCx的输出管脚,CCRx预装载未用,开启OCx输出且高电平有效。
重要:这句话的含义就是代表了第一次比较触发前的输出电平,或者说是触发时是上升沿还是下降沿
具体看下面的文章2
5.设置TIMx_CR1寄存器的CEN位启动计数器
TIMx_CCRx寄存器能够在任何时候通过软件进行更新以控制输出波形,条件是未使用预装载寄存器OCxPE=’0’,否则TIMx_CCRx影子寄存器只能在发生下一次更新事件时被更新)
文章1:http://news.eeworld.com.cn/mcu/article_2016101130330.html
STM32的定时器还有一个模式叫做输出比较翻转模式。这种模式,顾名思义,可以翻转电平,但是条件是:当计数值达到比较值时,才会在对应的通道引脚翻转原先的电平。利用这个特点,我们可以在引脚上生成PWM波。
下面就讲讲如何利用这个“翻转”这个特点,来输出PWM波。还是基于我自己的规工程。
1、工程的修改
1)这里用到了定时器,所以需要将stm32f10x_tim.h添加到STM32F10x_StdPeriod_Driver工程组中。
2)打开stm32f0x_conf.h文件,将其中原先被屏蔽的语句:#include "stm32f10x_tim.h"的注释去掉。
3)新建OCToggle.c与OCToggle.h两个文件,分别保存在BSP文件夹里下的src与inc中,然后在将OCToggle.c添加到BSP工程组。
2、OCToggle.c与OCToggle.h文件程序的编写
首先是引脚的初始化。我使用TIM2,所以需要初始化TIM2对应的引脚PA0、PA1、PA2、PA3这四个引脚将它们配置成复用推挽输出,代码如下:
/*************************************************************
Function : OCToggle_GPIO_Init
Description: 输出比较翻转模式下定时器对应通道引脚初始化
Input : none
return : none
*************************************************************/
static void OCToggle_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//定时器各通道引脚配置成复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
接下去当然是要配置定时器了。它的代码如下:
/*************************************************************
Function : OCToggle_TIM2_Init
Description: 输出比较翻转模式定时器2初始化
Input : none
return : none
*************************************************************/
static void OCToggle_TIM2_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//初始化TIM2时钟
/* -------------------------------------------------------
TIM_OCMode_Toggle模式计数值等于比较值翻转电平
在PA0引脚输出频率为72M/CCR1_Val/2=732Hz,占空比为50%的PWM波
在PA1引脚输出频率为72M/CCR2_Val/2=1099Hz,占空比为50%的PWM波
在PA2引脚输出频率为72M/CCR3_Val/2=2197Hz,占空比为50%的PWM波
在PA3引脚输出频率为72M/CCR4_Val/2=4395Hz,占空比为50%的PWM波
---------------------------------------------------------*/
TIM_TimeBaseStructure.TIM_Period = 65535;//定时器计数周期
TIM_TimeBaseStructure.TIM_Prescaler = 1 - 1;//预分频
TIM_TimeBaseStructure.TIM_ClockDivision = 1 - 1;//时钟不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//增计数
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);//初始化定时器
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_Toggle;//输出比较主动模式
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//输出使能
TIM_OCInitStructure.TIM_Pulse = CCR1_Val;//设置比较值(跳变值)
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;//有效电平为高电平
TIM_OC1Init(TIM2, &TIM_OCInitStructure);//初始化输出比较寄存器
TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Disable);//关闭预转载
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//输出使能
TIM_OCInitStructure.TIM_Pulse = CCR2_Val;//设置比较值(跳变值)
TIM_OC2Init(TIM2, &TIM_OCInitStructure);//初始化输出比较寄存器
TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Disable);
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//输出使能
TIM_OCInitStructure.TIM_Pulse = CCR3_Val;//设置比较值(跳变值)
TIM_OC3Init(TIM2, &TIM_OCInitStructure);//初始化输出比较寄存器
TIM_OC3PreloadConfig(TIM2, TIM_OCPreload_Disable);
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//输出使能
TIM_OCInitStructure.TIM_Pulse = CCR4_Val;//设置比较值(跳变值)
TIM_OC4Init(TIM2, &TIM_OCInitStructure);//初始化输出比较寄存器
TIM_OC4PreloadConfig(TIM2, TIM_OCPreload_Disable);
TIM_ITConfig(TIM2, TIM_IT_CC1 | TIM_IT_CC2 | TIM_IT_CC3 | TIM_IT_CC4, ENABLE);//清除中断标志
TIM_Cmd(TIM2, ENABLE);//打开定时器2
}
不对定时器做任何分频,并且让它满计数,即计数周期为65535。接下去再设置个通道的的工作模式为输出比较翻转模式,设置通道1的比较值为CCR1_Val、CCR3_Val、CCR3_Val、CCR4_Val,它们的值在OCToggle.h中定义。然后打开个通道的输出比较事件的中断。最后在打开定时器。这样的话,定时器这段就配置完成了。
既然打开了中断,则需要配置下中断,设置TIM2的中断优先级为1,代码如下:
/*************************************************************
Function : OCToggle_Int_Init
Description: 输出比较翻转模式中断初始化
Input : none
return : none
*************************************************************/
static void OCToggle_Int_Init(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//中断优先级为1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
还要编写一个总函数:OCToggle_Init()函数将上面的初始化相关代码都在这个函数中调用,代码如下:
/*************************************************************
Function : OCToggle_Init
Description: 输出比较翻转模式初始化
Input : none
return : none
*************************************************************/
void OCToggle_Init(void)
{
OCToggle_GPIO_Init();
OCToggle_TIM2_Init();
OCToggle_Int_Init();
}
接下去OCToggle.h的代码:
#ifndef __OCTOGGLE_H__
#define __OCTOGGLE_H__
#include "stm32f10x.h"
#define CCR1_Val 49152
#define CCR2_Val 32768
#define CCR3_Val 16384
#define CCR4_Val 8192
void OCToggle_Init(void);
#endif
这里之所以将CCR1_Val的值在这个h文件中宏定义,而不在c文件中直接定义成变量,原因是为了在其他文件中调用方便,如果定义成变量的话,在其他文件还要用extern关键字来声明,比较麻烦!
3、stm32f10x_it.c文件的修改
TIM2的中断服务函数的程序如下:
/*************************************************************
Function : TIM2_IRQHandler
Description: 定时器2中断服务程序
Input : none
return : none
*************************************************************/
void TIM2_IRQHandler(void)
{
static u16 capture = 0;
if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET)//通道1检测到比较事件
{
TIM_ClearITPendingBit(TIM2, TIM_IT_CC1);//清除标志位
capture = TIM_GetCapture1(TIM2);
TIM_SetCompare1(TIM2, capture + CCR1_Val);//重新设置比较值
}
if (TIM_GetITStatus(TIM2, TIM_IT_CC2) != RESET)//通道1检测到比较事件
{
TIM_ClearITPendingBit(TIM2, TIM_IT_CC2);//清除标志位
capture = TIM_GetCapture2(TIM2);
TIM_SetCompare2(TIM2, capture + CCR2_Val);//重新设置比较值
}
if (TIM_GetITStatus(TIM2, TIM_IT_CC3) != RESET)//通道1检测到比较事件
{
TIM_ClearITPendingBit(TIM2, TIM_IT_CC3);//清除标志位
capture = TIM_GetCapture3(TIM2);
TIM_SetCompare3(TIM2, capture + CCR3_Val);//重新设置比较值
}
if (TIM_GetITStatus(TIM2, TIM_IT_CC4) != RESET)//通道1检测到比较事件
{
TIM_ClearITPendingBit(TIM2, TIM_IT_CC4);//清除标志位
capture = TIM_GetCapture4(TIM2);
TIM_SetCompare4(TIM2, capture + CCR4_Val);//重新设置比较值
}
}
在这个中断服务程序中,每当检测到输出比较时事件时,通道对应的引脚就会自动翻转电平,然后我们在中断中,重新设置它的比较值,在原来的计数值的基础上再增加与计数值相同的计数值作为下一次的比较值。这样的话,就会形成占空比为50%的PWM波。以通道1为例解释下。通道1原来设置的计数值为49152,当定时器的计数值达到这个数时,就翻转通道1对应的输出引脚PA0翻转电平,然后在设置新的计数值为49152+49152,因为定时器只有16位,会溢出,所以下一个比较值为32768,这样的话,如果49152计数值时间内引脚输出高电平的话,下一个49152计数值时间内就会输出低电平,形成频率为72M/49152/2=732Hz,占空比为50%的PWM波。同样的,通道2会输出频率为1099Hz,占空比为50%的PWM波;通道3输出频率为2197Hz,占空比为50%的PWM波;通道4输出频率为4395Hz,占空比为50%的PWM波。
4、main函数的编写
main函数很简单,只是调用一些初始化函数:
/*************************************************************
Function : main
Description: main入口
Input : none
return : none
*************************************************************/
int main(void)
{
BSP_Init();
OCToggle_Init();
PRINTF("\nmain() is running!\r\n");
while(1)
{
LED1_Toggle();
Delay_ms(1000);
}
}
5、测试
用示波器的探头分别连接引脚PA0、PA1、PA2、PA3。可以看到下面的现象:
连接PA0可以检测到频率为732Hz,占空比为50%的PWM波。如下图所示:
连接PA1可以检测到频率为1099Hz,占空比为50%的PWM波。如下图所示:
连接PA2可以检测到频率为2197Hz,占空比为50%的PWM波。如下图所示:
连接PA3可以检测到频率为4395Hz,占空比为50%的PWM波。如下图所示:
文章2:OCx与OCxREF和CCxP之间的关系
http://blog.sina.com.cn/s/blog_3ba262a10101esd1.html
初学STM32,我这个地方卡了很久,现在终于有些明白了,现在把我的理解写下与大家共享,如果有不对的地方,还请指出。
OCxREF就是一个参考信号,并且约定:
OCxREF=1,称OCxREF有效。反之,OCxREF=0,称OCxREF无效;
‘1’电平(高电平)称为OCxREF的有效电平,‘0’ 电平(低电平)称为OCxREF的无效电平。
——依据参考手册:The output stage generates an intermediate waveform which is then used for reference:OCxRef (active high). The polarity acts at the end of the chain.
(翻译)输出阶段产生一个中间波形OCxRef(高有效)作为参考。输出信号的极性体现在信号链的末端。
现在解释几个名词之间的关系:
然后来理解输出比较的几个模式(PWM模式是输出比较模式的特例)
查看TIMx_CCMR1寄存器的OC1M域,有如下定义(摘自最新版的参考手册)
翻译如下:
000:冻结——输出比较寄存器TIMx_CCR1中的内容与计数器TIMx_CNT中的内容之间的比较对输出无影响。(此模式用于时基的生成)
001:当匹配时,设置通道1为有效电平。当计数器TIMx_CNT中的内容与捕捉/比较寄存器1(TIMx_CCR1)中的内容相匹配时,强行拉高OC1REF 信号。
010:当匹配时,设置通道1为无效电平。当计数器TIMx_CNT中的内容与捕捉/比较寄存器1(TIMx_CCR1)中的内容相匹配时,强行拉低OC1REF 信号。
011:翻转——当TIMx_CNT= TIMx_CCR1时,OC1REF信号取反。
100:强制无效电平——强行拉低OC1REF 信号。
101:强制有效电平——强行拉高OC1REF 信号。
110:PWM模式1——向上计数模式中,只要TIMx_CNT< TIMx_CCR1,通道1有效,反之无效。向下计数模式中,只要TIMx_CNT> TIMx_CCR1,通道1无效(OC1REF=0),反之有效(OC1REF=1)。
110:PWM模式2——向上计数模式中,只要TIMx_CNT< TIMx_CCR1,通道1无效,反之有效。向下计数模式中,只要TIMx_CNT> TIMx_CCR1,通道1有效,反之无效。
我用红色标出了提到有效、无效的地方。不难发现,有效与无效分别对应OC1REF=1和OC1REF=0。这正是我们先前约定的结果。
到此,不同模式下输出比较的结果对OC1REF信号的影响已经很清楚了,但是最终的输出信号是OC1,并不是OC1REF。而且前面有一句话(输出信号的极性体现在信号链的末端)还未做解释。
到底OC1REF与OC1之间有何秘密呢?我们来看下面这个图:
显然,我们只关心红色圈内的信号与方框内的寄存器位以及信号在它们之间是如何传播的。
oc1ref从输出模式控制器(Output mode controller)开始,分为两路,上面一路至主模式控制器(To the master mode controller),这里我们不关心它的去向,我们关心的是下面一路,下面一路在进入双路开关之前又被分成了两路——一路是原信号,一路是原信号的非。显然TIMx_CCER中的CC1P位用来控制这个开关,CC1E位控制着整条信号链的通断。
当CC1P=0时(CC1E=1):
很显然,OC1与OC1REF的关系只受CC1P的影响(CC1E=1)
然而参考手册上对CC1P位是这么描述的:
CC1P=0时:OC1高电平有效
CC1P=1时:OC1低电平有效
根据本文开篇的名词解释,可以这么理解:
CC1P=0时:OC1有效电平是高电平
CC1P=1时:OC1有效电平是低电平
这时就迷惑了,这个高电平有效和低电平有效是啥意思呢?
我们从头分析(整个过程CC1E=1,OC1的输出是允许的):
①假定OC1REF有效(OC1REF=1),那么从OC1REF到OC1的整条信号链上的信号都是有效信号,我们称OC1输出了有效信号。
那这个有效信号是高电平还是低电平呢?
这就是由CC1P决定的:
②假定OC1REF无效(OC1REF=0),那么从OC1REF到OC1的整条信号链上的信号都是无效信号,我们称OC1输出了无效信号。
无效信号的高电平和低电平也是由CC1P决定:
用一张表来总结上述过程:
OC1REF |
CC1P |
功能 |
OC1 |
描述 |
0 |
0 |
OC1高电平有效 |
0(低电平) |
无效 |
1 |
OC1低电平有效 |
1(高电平) |
无效 |
|
1 |
0 |
OC1高电平有效 |
1(高电平) |
有效 |
1 |
OC1低电平有效 |
0(低电平) |
有效 |
显然,OC1REF决定了OC1输出电平是否有效,而CC1P决定了有效电平的极性。
我们抽出上表的后四列:
CC1P |
功能 |
OC1 |
描述 |
0 |
OC1高电平有效 |
0(低电平) |
无效 |
1 |
OC1低电平有效 |
1(高电平) |
无效 |
0 |
OC1高电平有效 |
1(高电平) |
有效 |
1 |
OC1低电平有效 |
0(低电平) |
有效 |
我们将表按1、2列合并
CC1P |
功能 |
OC1 |
描述 |
0
|
OC1高电平有效 |
0(低电平) |
无效 |
OC1高电平有效 |
1(高电平) |
有效 |
|
1
|
OC1低电平有效 |
0(低电平) |
有效 |
OC1低电平有效 |
1(高电平) |
无效 |
现在很清楚了,从上表中可以清楚地看到CC1P对OC1有效极性的控制。即,OC1的极性只有与CC1P指定的有效极性一致,OC1才能是有效的(绿色部分)。这样就解释了“输出信号的极性体现在信号链的末端”这句话。
然而这条链还未结束,还有个CC1E呢。当然,它就是一个OC1输出使能位而已。
但细心的你可能会发现,参考手册上对CC1E位有这样的描述:
OCx = OCxREF + Polarity
这个式子告诉我们OCx与OCxREF和Polarity(极性,即CCxP位)的关系。
我们上面提到了它们的关系,是分了两种情况(CC1P=0和CC1P=1)表示的,这个式子帮我们将上面关系归纳成了一个。这个式子怎么得来的?
回忆一下数字电路里面的半加器(就是不进位的加法),真值表如下:
OCxREF |
Polarity |
OCx |
0(无效) |
0(高有效) |
0(无效) |
0(无效) |
1(低有效) |
1(无效) |
1(有效) |
0(高有效) |
1(有效) |
1(有效) |
1(低有效) |
0(有效) |
我们写逻辑函数(按黄色部分写):
注意:前面的“+”号表示半加运算(不进位加法),其实是逻辑上的“异或”。
OC1连接到TIMx_CH1上,而TIMx_CH1是复用的。可在参考手册上定时器功能复用部分找到。