电子说
单片机开发中,电机的控制与定时器有着密不可分的关系,无论是直流电机,步进电机还是舵机,都会用到定时器,比如最常用的有刷直流电机,会使用定时器产生PWM波来调节转速,通过定时器的正交编码器接口来测量转速等。
本篇先介绍 定时器的基础知识 ,然后对照这些知识介绍一下 定时器输出PWM的基本原理 ,以及 编程实现与代码分析 。
首先来看一下定时器的基础介绍。
1
1 定时器基础知识
以STM32F4为例,一共有14个定时器:
这里列举高级定时器的特性,在此基础上,对比添加其与通用定时器的不同之处:
基本定时器 (TIM6、TIM7)的功能比较单一,所具有的功能如下:
使用定时器,一般需要配置如下:
先来看一下 定时器的原理框图 ,对定时器的内部原理有一个整体直观的感受:
从上图可以看出,计数器的时钟源可以为:
TIMx_ETR
引脚得到一般使用RCC的内部时钟CK_INT
,也即定时器时钟TIMxCLK
,经APB1或APB2预分频器后分频提供。
关于定时器时钟源的具体细节,可以来看一下 STM32F4的时钟树 :
从STM32F4的内部时钟树可知:
另外:
当APB1和APB2分频数为1的时候,各定时器的时钟就是对应的APB1或APB2的时钟;
如果APB1和APB2 分频数不为1 ,那么各定时器的时钟就是对应的APB1或APB2的时钟的 2倍 ;
由于库函数中 APB1 预分频的系数默认是 2,所以,所以TIM1、TIM8TIM11的时钟为APB2时钟的两倍即 168MHz ,TIM2TIM7、TIM12~TIM14的时钟为APB1的时钟的两倍即 84MHz 。
由于定时器时钟的提供的可以频率较高,计数器不需要这么高的频率来计数,所以会进行降频,使用一个合适的低频时钟来计数。
定时器时钟经过PSC 预分频器之后,即 CK_CNT
,用来驱动计数器计数。PSC 是一个16 位的预分频器,可以对定时器时钟TIMxCLK
进行 1~65536 之间的任何一个数进行分频。
具体计算方式为:CK_CNT=TIMxCLK/(PSC+1)
。
比如,使用STM32F4的通用定时器2(TIM2CLK为APB1的时钟的两倍即 84MHz ),PSC设置为83,则计数时钟为84MHz/(83+1)=1MHz
,即1ms计一个数。
计数器 CNT 是一个 16 位的计数器,只能往上计数,最大计数值为 65535。当计数达到自动重装载寄存器的时候产生更新事件,并清零从头开始计数。
自动重装载寄存器 ARR 是一个 16 位的寄存器,这里面装着计数器能计数的最大数值。当计数到这个值的时候,如果使能了中断的,定时器就产生溢出中断。
2
定时器输出PWM原理
如下图是PWM输出的原理示意图:
假设定时器工作模式设置为向上计数 PWM模式,且当 CNT=CCRx 时输出 0,则:
因此,改变 CCRx 的值,就可以改变 PWM 输出的 占空比 ,改变 ARR 的值,就可以改变 PWM 输出的 周期(频率) ,这就是利用定时器输出PWM 的基本原理。
3
定时器常用的寄存器
使用定时器来输出PWM时,需要对其寄存器进行相应的设置。定时器的寄存器有好多个,这里先介绍几个与输出PWM相关的几个寄存器,其它是寄存器以后用到时再介绍。
控制寄存器,就是来设置定时的工作模式:
位 15:10 保留,必须保持复位值。
位 9:8 CKD :时钟分频 (Clock division)此位域指示定时器时钟 (CK_INT) 频率与数字滤波器所使用的采样时钟(ETR、TIx)之间的分频比,
位 7 ARPE :自动重载预装载使能 (Auto-reload preload enable)
位 6:5 CMS :中心对齐模式选择 (Center-aligned mode selection),包括1种边沿对齐模式与3种中心对齐模式
位 4 DIR :计数器方向 (Direction),0为 递增计数 ,1为 递减计数 。
注:当定时器配置为中心对齐模式或编码器模式时,该位为只读状态。
位 3 OPM :单脉冲模式 (One-pulse mode)
位 2 URS:更新请求源 (Update request source)
此位由软件置 1 和清零,用以选择 UEV 事件源。
位 1 UDIS:更新禁止 (Update disable)此位由软件置 1 和清零,用以使能/禁止 UEV 事件生成。
位 0 CEN:计数器使能 (Counter enable),0为禁止计数器,1为使能计数器
只有事先通过软件将 CEN 位置 1,才可以使用外部时钟、门控模式和编码器模式。而触发模式可通过硬件自动将 CEN 位置 1。在单脉冲模式下,当发生更新事件时会自动将 CEN 位清零。
这些通道可用于 输入(捕获模式)或输出(比较模式)模式 。通道方向通过配置相应的 CCxS 位进行定义。此寄存器的所有其它位在输入模式和输出模式下的功能均不同。对于任一给定位
因此,必须注意同一个位在输入阶段和输出阶段具有不同的含义。
这里仅先介绍输出模式下的功能:
位 15 OC2CE :输出比较 2 清零使能 (Output compare 3 clear enable)
位 14:12 OC2M[2:0] :输出比较 2 模式 (Output compare 2 mode)
位 11 OC2PE :输出比较 2 预装载使能 (Output compare 2 preload enable)
位 10 OC2FE :输出比较 2 快速使能 (Output compare 2 fast enable)
位 9:8 CC2S[1:0] :捕获/比较 2 选择 (Capture/Compare 2 selection)参考下面的CC1S通道1
位 7 OC1CE :输出比较 1 清零使能 (Output compare 3 clear enable)
OC1CE:输出比较 1 清零使能 (Output Compare 1 Clear Enable)
位 6:4 OC1M :输出比较 1 模式 (Output compare 1 mode)一共可配置位7种模式,这里仅介绍2种:
位 3 OC1PE :输出比较 1 预装载使能 (Output compare 1 preload enable)
位 2 OC1FE :输出比较 1 快速使能 (Output compare 1 fast enable)
此位用于加快触发输入事件对 CC 输出的影响(仅当通道配置为 PWM1 或 PWM2 模式时,OCFE 才会起作用)。
位 1:0 CC1S[1:0] :捕获/比较 1 选择 (Capture/Compare 1 selection)
此位域定义通道方向(输入/输出)以及所使用的输入。
00:CC1 通道配置为输出。
01:CC1 通道配置为输入,IC1 映射到 TI1 上。
10:CC1 通道配置为输入,IC1 映射到 TI2 上。
11:CC1 通道配置为输入,IC1 映射到 TRC 上。此模式仅在通过 TS 位(TIMx_SMCR 寄存器)选择内部触发输入时有效
注:仅当通道关闭时(TIMx_CCER 中的 CC1E = 0),才可向 CC1S 位写入数据。
计数器的功能很单一,就是计数:
预分频器的功能也很单一,就是分频:
位 15:0 PSC[15:0] :预分频器值 (Prescaler value)
计数器时钟频率 CK_CNT
等于 fCK_PSC / (PSC[15:0] + 1)
。
PSC 包含在每次发生更新事件时要装载到实际预分频器寄存器的值。
自动重装载寄存器的功能也很单一,就是保存一个数,在计数满的时候,重新开始计数
位 15:0 ARR[15:0] :自动重载值 (Auto-reload value)
ARR 为要装载到实际自动重载寄存器的值。
当自动重载值为空时,计数器不工作。
自动重装载寄存器的功能也很单一,也是保存一个数,用于与当前的CNT进行比较,注意 TIM2 和 TIM5是32位计数。
以CCR1寄存器(一共有CCR1~CCR4这4个通道)为例:
TIMx_CCMR
寄存器中的OC1PE
位来使能预装载功能,写入的数值会被直接传输至当前寄存器中。否则只在发生更新事件时生效(拷贝到实际起作用的捕获/ 比较寄存器1)。实际捕获/比较寄存器中包含要与计数器 TIMx_CNT
进行比较并在 OC1 输出上发出信号的值。4
4代码实现与分析
上面介绍了定时器的基础知识与PWM的输出原理,下面就来实际看一下,如何编写对应的代码(以STM32F407为例)。
定时器的初始化,因为需要用到对应的引脚输出PWM,因此要先初始化 GPIO引脚 ,然后,还要初始化定时器的 时基 (计数的时钟)以及 输出通道 (用于配置PWM的输出模式)。
这里用到的是定时器3,根据STM32F407的数据手册“3 Pinouts and pin description”中的“Table 9. Alternate function mapping”复用引脚说明表,可以看到定时器3通道1对应的引脚位A6:
因此程序中对A6引脚可以这样配置,注意一定要 配置引脚的复用功能 :
GPIO_InitTypeDef GPIO_InitStructure; /*引脚配置 结构体*/
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); //使能PORTA时钟
GPIO_PinAFConfig(GPIOA,GPIO_PinSource6,GPIO_AF_TIM3); /*GPIOA6复用为定时器3*/
/*复用引脚配置*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //GPIOA6
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; /*复用功能*/
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //速度100MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化PA6
时基初始化,主要是配置定时器的 计数频率 ( psc )和自动重装置值(每次计数的周期, arr ),比如TIM3_PWM_Init(500-1,84-1);
(关于psc与arr的知识点,可以再回顾一下上面1.3节的知识)
这里将arr的值设置为500,即计数器每计够500个数就会重新从0开始计数,这个500再乘以计数器计数的周期,就是PWM真正的周期, 那计数器计数的频率是多少呢 (频率的倒数为周期)?
这里将psc的值设置为84-1,即TIM3的输入频率为84MHz再将频率降低1/84,即使用1MHz的频率计数(1s能计1,000,000个数,也即1us计1个数),那么PWM的真正周期就是500*1us=500us(0.5ms)
,通过改变占空比的值( ccr ),就可以调节PWM的输出占空比。
时基初始化配置如下:
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; /*时基 结构体*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); //TIM3时钟使能
/*时基初始化*/
TIM_TimeBaseStructure.TIM_Period=arr; /*ARR 自动重装载值(周期),例如500*/
TIM_TimeBaseStructure.TIM_Prescaler=psc; /*PSC 定时器分频,例如84*/
TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; /*时钟分割*/
TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; /*向上计数模式*/
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure); /*初始化定时器3*/
最后一句的时基初始化,起始就是对定时的寄存器进行配置,该函数的内部实现如下:
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct)
{
uint16_t tmpcr1 = 0;
tmpcr1 = TIMx->CR1;
if((TIMx == TIM1) || (TIMx == TIM8)|| /*高级定时器TIM和TIM8*/
(TIMx == TIM2) || (TIMx == TIM3)||(TIMx == TIM4) || (TIMx == TIM5)) /*通用定时器中的TIM2~TIM5*/
{
/* 设置为计数器模式 */
tmpcr1 &= (uint16_t)(~(TIM_CR1_DIR | TIM_CR1_CMS));
tmpcr1 |= (uint32_t)TIM_TimeBaseInitStruct->TIM_CounterMode;
}
if((TIMx != TIM6) && (TIMx != TIM7)) /*基本定时器TIM6和TIM7无此功能*/
{
/* 设置时钟分频 */
tmpcr1 &= (uint16_t)(~TIM_CR1_CKD);
tmpcr1 |= (uint32_t)TIM_TimeBaseInitStruct->TIM_ClockDivision;
}
/* 配置CR1寄存器 */
TIMx->CR1 = tmpcr1;
/* 配置ARR寄存器,设置自动重转载值 */
TIMx->ARR = TIM_TimeBaseInitStruct->TIM_Period ;
/* 配置PSC寄存器,设置预分频值 */
TIMx->PSC = TIM_TimeBaseInitStruct->TIM_Prescaler;
if ((TIMx == TIM1) || (TIMx == TIM8)) /*高级定时器TIM和TIM8*/
{ /* 配置RCR寄存器,设置重复计数值 */
TIMx->RCR = TIM_TimeBaseInitStruct->TIM_RepetitionCounter;
}
/* 生成一个更新事件来立即重新加载预分频器和重复计数器(仅针对高级定时器TIM1和TIM8)值 */
TIMx->EGR = TIM_PSCReloadMode_Immediate;
}
输出通道初始化,主要是配置输出的一些参数,这里主要关注 TIM_OCMode (模式)与 TIM_OCPolarity (极性),这两个参数是配合使用的:
PWM模式1
TIMx_CNT时通道1为有效电平,否则为无效电平;
向下计数时,一旦TIMx_CNT>TIMx_CCR1
时通道1为无效电平,否则为有效电平。
`
PWM模式2
向上计数时,一旦TIMx_CNT时通道1为无效电平,否则为有效电平;<>
向下计数时,一旦
TIMx_CNT>TIMx_CCR1
时通道1为有效电平,否则为无效电平。
`
`
这里的有效电平又是什么意思呢?怎么算有效电平?它就是通过极性来配置的:
输出High模式:有效电平为高电平
输出Low模式:有效电平为低电平
对比着再来看这张图:
当CNT的计数值小于CCR时,即t1这个时间段,输出有效电平(TIM_OCMode_PWM1模式),而有效电平是高电平(极性为TIM_OCPolarity_High),所以PWM的IO逻辑在t1这个时间段输出了高电平。
输出通道的配置如下:
TIM_OCInitTypeDef TIM_OCInitStructure; /*输出通道 结构体*/
/*输出通道初始化,初始化TIM3 Channel1 PWM模式*/
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; /*选择定时器模式:TIM脉冲宽度调制模式1*/
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; /*输出极性:TIM输出比较极性高*/
TIM_OC1Init(TIM3, &TIM_OCInitStructure); //根据指定的参数初始化外设TIM3 OC1
TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); /使能TIM3在CCR1上的预装载寄存器/
TIM_ARRPreloadConfig(TIM3,ENABLE);/ARPE使能:使能控制寄存器CR的第8位:ARPR, Auto-reload preload enable/
TIM_Cmd(TIM3, ENABLE); /使能TIM3:使能控制寄存器CR的第0位:CEN, counter enable/
关于配置CCMR1、CCER寄存器
CCMR1:
CCER:
TIM_OC1Init
函数对应于输入通道的初始化,其实就是操作CCMR1
、CCER
等寄存器:
void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct)
{
uint16_t tmpccmrx = 0, tmpccer = 0, tmpcr2 = 0;
TIMx->CCER &= (uint16_t)~TIM_CCER_CC1E;/* 关闭通道1: 复位CC1E位 */
tmpccer = TIMx->CCER;/* 获取 TIMx CCER 寄存器的值 /
tmpcr2 = TIMx->CR2; / 获取 TIMx CR2 寄存器的值 */
tmpccmrx = TIMx->CCMR1;/* 获取TIMx CCMR1 寄存器的值 /
tmpccmrx &= (uint16_t)~TIM_CCMR1_OC1M; / 复位输出比较模式OC1M位 /
tmpccmrx &= (uint16_t)~TIM_CCMR1_CC1S;
tmpccmrx |= TIM_OCInitStruct->TIM_OCMode;/ 设置为输出比较模式 */
tmpccer &= (uint16_t)~TIM_CCER_CC1P; /* 复位输出极性CC1P /
tmpccer |= TIM_OCInitStruct->TIM_OCPolarity; / 设置输出极性 /
tmpccer |= TIM_OCInitStruct->TIM_OutputState; / 设置输出状态 */
if((TIMx == TIM1) || (TIMx == TIM8)) /高级定时器的特殊配置/
{
//省略。。。
}
TIMx->CR2 = tmpcr2; /* 写数据到TIMx的CR2寄存器 /
TIMx->CCMR1 = tmpccmrx; / 写数据到TIMx的CCMR1寄存器 /
TIMx->CCR1 = TIM_OCInitStruct->TIM_Pulse;/ 设置CCR1寄存器 /
TIMx->CCER = tmpccer; / 写数据到TIMx的CCER寄存器 */
}
4.2 动态改变占空比
占空比是通过修改CCR寄存器的值进行修改的,如果定时器初始化时只设置了1次CCR的值,那么会输出恒定占空比的PWM波;如果在定时器运行的时候,动态修改CCR的值,则可以实现PWM占空比的动态调整。
如下程序,实现了每隔10ms对占空比进行一次修改,每次将高电平计数值增加5,当增大道500(占空比100%)时,再逐渐减小到0(占空比0%),不断循环。
u16 led0pwmval=0;
u8 dir=1;
TIM3_PWM_Init(500-1,84-1); //84M/84=1Mhz的计数频率,重装载值500,所以PWM频率为 1M/500=2Khz.
while(1) //实现比较值从0-500递增,到500后从500-0递减,循环
{
delay_ms(10);
if(dir)
{
led0pwmval+=5; //dir==1 led0pwmval递增
}
else
{
led0pwmval-=5; //dir==0 led0pwmval递减
}
if(led0pwmval>500)
{
dir=0; //led0pwmval到达500后,方向为递减
}
if(led0pwmval==0)
{
dir=1; //led0pwmval递减到0后,方向改为递增
}
TIM_SetCompare1(TIM3,led0pwmval); /*CCR 修改比较值(占空比)*/
}
5
测试效果
将程序下载到板子,我用的一块STM32F407的板,A6引脚上接了一个LED灯,实际效果的LED逐渐变亮,再逐渐变暗,依次循环。
再通过逻辑分析仪来查看实际的输出波形,如下图,测得的pwm周期0.5ms(频率2kHz),与软件中设定的一致。
在某一时刻,脉宽55us。
在另一时刻,脉宽0.365ms,即实现了PWM脉宽的动态调整。
`
全部0条评论
快来发表一下你的评论吧 !