在使用STM32F103产生固定频率、固定占空比的PWM波时,虽然有官方以及众多开发板提供的例程,但是关于有点问题并没有说的很清晰,并且《STM32F10X参考手册》的中文翻译可能容易造成歧义,所以一开始并没有理解,这里就梳理一下我的理解,如果有误解的情况,希望交流指正。
1. 遇到的问题
先直接上段配置代码,这段代码是产生一个20kHz固定频率,50%固定占空比的方波信号,典型的配置过程,一般来说也不会有什么太多的疑问。但是我逐步了解背后的定时器工作逻辑的时候,就产生了一些疑问,也没有找到合理、清晰的解答。先说明一下,只有通用定时器和高级定时器才有PWM模式,基本定时器没有。
static void PWM_Mode_Config(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 基本定时器配置
TIM_DeInit(TIM3);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 开启定时器时钟,即内部时钟CK_INT=72M
TIM_TimeBaseStructure.TIM_Period = 49; // 自动重装载寄存器的值,累计TIM_Period+1个频率后产生一个更新或者中断
TIM_TimeBaseStructure.TIM_Prescaler = 71; // 时钟预分频数为
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 设置时钟分频系数:不分频(这里用不到)
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 初始化定时器
TIM_ClearFlag(TIM3, TIM_FLAG_Update); // 清除计数器更新标志位
// PWM模式配置
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // 配置为PWM模式1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 使能输出
TIM_OCInitStructure.TIM_Pulse = 25; // 设置初始PWM脉冲宽度为25,实际上就是配置占空比(捕获比较寄存器1,CCR1)
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; // 当定时器计数值小于CCR1_Val时为低电平
TIM_OC2Init(TIM3, &TIM_OCInitStructure ); // 使能通道2
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable ); // 使能预装载(使能CCR1的预装载)
TIM_ARRPreloadConfig(TIM3, ENABLE); // 使能自动重载寄存器ARR的预装载
// 开启TIM3
TIM_Cmd(TIM3, DISABLE); // 使能定时器
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); // 使能定时器中断
BASIC_TIM_NVIC_Config();
}
1.1 什么是清除标志位
TIM_ClearFlag(TIM3, TIM_FLAG_Update); // 清除计数器更新标志位
代码中有这样一条,有人会问函数TIM_ClearFlag()
和函数TIM_ClearITPendingBit()
有什么区别?其实重点在Flag和IT,前者是外设的状态标志,而后者是外设的中断标志。状态标志就是一个外设它有自身的一些标志位(Flag),来表明它处于什么状态,下图就是定时器的状态标记。中断标志就是使能外设的中断后,每次发生一次中断,它会表明发生了什么样的中断,同样中断也有相应的标记。两者分别靠函数TIM_GetFlagStatus()
和函数TIM_GetITStatus()
来获取。
没有使能中断时,是可以读取该外设的状态标志的。同理,串口外设也有此区别。
1.2 模式1和模式2的区别
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // 配置为PWM模式1
模式1
在向上计数时,一旦TIMx_CNT < TIMx_CCR1时通道1为有效电平,否则为无效电平;在向下计数时,一旦TIMx_CNT>TIMx_CCR1时通道1为无效电平(OC1REF=0),否则为有效电平(OC1REF=1)。
模式2
在向上计数时,一旦TIMx_CNT < TIMx_CCR1时通道1为无效电平,否则为有效电平;在向下计数时,一旦TIMx_CNT>TIMx_CCR1时通道1为有效电平,否则为无效电平。
实际上可以看到,模式1和模式2并无本质的区别,只是在同样的有效电平情况下,输出的波形电平相反而已。那么什么又是有效电平?后面有机会单独说明。
1.3 什么是自动重装载和预装载寄存器?
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable ); // 使能预装载(使能CCR1的预装载)
TIM_ARRPreloadConfig(TIM3, ENABLE); // 使能自动重载寄存器ARR的预装载
这个问题才是我写这篇博客的源动力,网上有很多人问,但是回答者没能详细说明,《STM32F10X参考手册》中的描述也有歧义。我只能从别人回答的只言片语中摸索,然后根据自己的理解来解释。下面就看第二节的基本知识。
2. PWM波产生的过程
《STM32F10X参考手册》对此的描述如下:
自动装载寄存器是预先装载的,写或读自动重装载寄存器将访问预装载寄存器。根据在TIMx_CR1寄存器中的自动装载预装载使能位(ARPE)的设置,预装载寄存器的内容被立即或在每次的更新事件UEV时传送到影子寄存器。当计数器达到溢出条件(向下计数时的下溢条件)并当TIMx_CR1寄存器中的UDIS位等于’0’时,产生更新事件。更新事件也可以由软件产生。随后会详细描述每一种配置下更新事件的产生。
我在这个通用定时器框图里面找不到预装载寄存器,这就更增加疑惑了。后来翻了很多问答帖,大概明白了其中的意思,以下是我的总结以及理解。
从框图里面看到自动重装载寄存器(Auto Reload Register,ARR)和捕获/比较寄存器组(Capture/Compare Register,CCR)的框下面有阴影,这就说明这两种寄存器含有影子寄存器(Shadow Register)。影子寄存器实际上才是真正直接参与定时器工作的寄存器,具有即时性。我们配置的ARR和CCR相当于都是上层的寄存器,芯片在工作中,自动将这些上层寄存器的值传递到各自的影子寄存器,从而参与计数或者比较过程。
那么,预装载寄存器在哪里呢?
在参考文献《关于STM32影子寄存器和预装载寄存器和TIM_ARRPreloadConfig》中发现实际上ARR和CCR(可能)在物理底层上是两个寄存器的组合,即预装载寄存器(Preload Register)和影子寄存器。实际上,我们在代码层面,对ARR和CCR赋值,最终传递到直接参与工作的影子寄存器,中间还要经历一个预装载寄存器的传递,它在这里相当于缓存的作用。但是在手册中并没找到确实再物理硬件上存在预装载寄存器的蛛丝马迹(只查到缓存器一说),那么也可以这样理解。所谓的预装载寄存器实际上只是ARR和CCR在他们需要向影子寄存器传递值的时候的一种“功能化”的别称,也就是说在需要传值的时候,ARR和CCR就是各自影子寄存器的预装载寄存器。
预装载寄存器的概念应该是相对于影子寄存器来说的。影子寄存器是即时其作用的,而预装载寄存器的值只有传递到影子寄存器才能起作用,你可以把它理解为一个缓存。就程序员的角度观察,两者共用一个地址,无法直接区别访问,只能通过另外的办法来设置对该地址操作的具体行为。
作为类比,你可以把预装载寄存器理解为内存中的cache,数据写入了cache,却是不一定写入内存的。当然你可以通过MMU设置为写穿(write through)或写回(write back)模式。
那么ARR和CCR各自的影子寄存器的值在什么时候更新呢?
可以立即将值传入或者每次事件更新的时传入,依赖于相关的控制位。
以ARR为例,控制寄存器TIMx_CR1的位7——ARPE(自动重装载预装载允许位 ,Auto-reload preload enable),写“0”时,TIMx_ARR寄存器没有缓冲;写“1”时,TIMx_ARR寄存器被装入缓冲器。也就是说ARPE写1,则影子寄存器立即被更新,否则只有等到每次事件发生时,才更新,这就是TIM_ARRPreloadConfig(TIM3, ENABLE);
的含义。
void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState)
{
/* Check the parameters */
assert_param(IS_TIM_ALL_PERIPH(TIMx));
assert_param(IS_FUNCTIONAL_STATE(NewState));
if (NewState != DISABLE)
{
/* Set the ARR Preload Bit */
TIMx->CR1 |= TIM_CR1_ARPE;
}
else
{
/* Reset the ARR Preload Bit */
TIMx->CR1 &= (uint16_t)~((uint16_t)TIM_CR1_ARPE);
}
}
同理,CCR的影子寄存器也有相应操作。我选择的是TIM3的通道2,其控制寄存器CCMR1的OC2PE位(CCMR1控制通道1和通道2),相应的操作可以对照库函数来了解。
看到有人回复“如果不改变频率和占空比,纯粹输出PWM波,则可以不需要使能预装载”,那是不是意味着以下两句代码可以不写?这需要试一下……
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable ); // 使能预装载(使能CCR1的预装载)
TIM_ARRPreloadConfig(TIM3, ENABLE); // 使能自动重载寄存器ARR的预装载
那这样设计预装载和影子寄存器有什么好处呢?
参考如下,我的简单理解就是将定时器的赋值过程和工作过程独立开。
设计preload register和shadow register的好处是,所有真正需要起作用的寄存器(shadow register)可以在同一个时间(发生更新事件时)被更新为所对应的preload register的内容,这样可以保证多个通道的操作能够准确地同步。如果没有shadow register,或者preload register和shadow register是直通的,即软件更新preload register时,同时更新了shadow register,因为软件不可能在一个相同的时刻同时更新多个寄存器,结果造成多个通道的时序不能同步,如果再加上其它因素(例如中断),多个通道的时序关系有可能是不可预知的。
天呐,终于稍微理顺了,虽然不一定对,而且定时器需要死磕的细节太多,精力有限,慢慢来~~
参考文献:
- 《STM32F10X参考手册》
- 《32位基于ARM微控制器STM32F101xx与STM32F103xx 固件函数库》
- 《关于STM32影子寄存器和预装载寄存器和TIM_ARRPreloadConfig》
- 《为什么TIM_ARRPreloadConfig在我的程序中没有作用 他到底有什么作用?》
- 《STM32定时器预装载寄存器的理解?》