本文以STM32F405单片机的TIM4为例,介绍如何用一个定时器的四个通道独立地输出四路PWM脉冲。
STM32单片机的定时器的PWM模式和输出比较翻转(Toggle)模式都能输出PWM脉冲。PWM模式下,一个定时器的只能输出四路频率相同(占空比可以不同)的PWM波。然而在实际应用中可能需要单片机输出PWM控制多个电机,使用不同的定时器输出PWM脉冲比较浪费硬件资源,这时候就可以采用输出比较 Toggle 模式使一个定时器的四个通道独立(频率和占空比都可以不同)地输出四路PWM脉冲。
输出比较 Toggle 模式介绍
将定时器配置成输出比较 Toggle 模式后,使定时器向上计数,当定时器的计数寄存器(CNT)中的数值等于捕获比较寄存器(CCRx,x对应通道编号)时,对应的引脚输出电平发生反转,同时状态寄存器中的CCxIF中断标志位置1,如果使能了捕获比较中断,则单片机进入中断服务程序。输出比较 Toggle 模式的时序请见下图:
输出比较 Toggle 模式输出PWM脉冲
在定时器输出PWM脉冲的应用中,定时器是在不停地计数。如果需要引脚停止输出PWM脉冲,则失能对应通道的输出和捕获比较中断,详细方法请见下面代码中的TIM4CH1OutControl函数;如果需要引脚输出PWM脉冲,则使能对应通道的输出和捕获比较中断,并且在中断中改变捕获比较寄存器(CCRx)中的值。下面简要介绍定时器输出比较 Toggle 模式输出PWM脉冲的过程。
- 定时器的计数寄存器(CNT)不停地从0到65534计数,计数65535次后从0再计数到65534,循环往复。
- 当计数寄存器(CNT)中的数值等于捕获比较寄存器(CCRx)中的数值时,引脚电平翻转一次,同时进入相应的捕获比较中断服务程序。
- 在中断程序中,将捕获比较寄存器(CCRx)的值读出并加上一个数(与PWM的频率和占空比有关)后,再写入捕获比较寄存器(CCRx)。
- 这样当计数寄存器的数值再次等于捕获比较寄存器的数值时,重复 2、3 过程。这样引脚输出的电平不断翻转,就产生了PWM脉冲。
需要注意的是,单片机进入两次捕获比较中断服务程序才输出一个PWM脉冲。如果通过驱动器控制步进电机时需要计算旋转角度,则进两次中断记一次脉冲,步进电机相应地走一个步进角,脉冲数乘以步进角就能得到步进电机旋转角度。
代码
下面的代码配置STM32F405单片机的TIM4的四个通道输出不同频率的PWM脉冲,占空比均为50%。
/**
* @description: Configure TIM4 CH1-CH4 with toggle mode.
* @param: none.
* @return: none.
*/
void BoardController::Timer4FourChannelPWMInit(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
//TIM4_CH1 CH2 CH3 CH4
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 输出比较复用引脚映射到TIM4
GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_TIM4);// CH1
GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_TIM4);// CH2
GPIO_PinAFConfig(GPIOB, GPIO_PinSource8, GPIO_AF_TIM4);// CH3
GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_TIM4);// CH4
TIM_TimeBaseStructure.TIM_Prescaler = 84-1; // 定时器计数频率 1MHz
TIM_TimeBaseStructure.TIM_Period = 65535-1; // 自动从装载值,定时器从0到65534计数65535次
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_Toggle; // 定时器配置成输出比较翻转(Toggle)模式
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;// 通道引脚默认输出高电平
TIM_OC1Init(TIM4, &TIM_OCInitStructure);
TIM_OC2Init(TIM4, &TIM_OCInitStructure);
TIM_OC3Init(TIM4, &TIM_OCInitStructure);
TIM_OC4Init(TIM4, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM4,TIM_OCPreload_Disable);
TIM_OC2PreloadConfig(TIM4,TIM_OCPreload_Disable);
TIM_OC3PreloadConfig(TIM4,TIM_OCPreload_Disable);
TIM_OC4PreloadConfig(TIM4,TIM_OCPreload_Disable);
// 先失能输出比较通道,失能捕获比较中断,默认不输出PWM,后面有函数使能通道和中断,使单片机输出PWM
TIM_ITConfig(TIM4,TIM_IT_CC1|TIM_IT_CC2|TIM_IT_CC3|TIM_IT_CC4,DISABLE);
TIM_CCxCmd(TIM4,TIM_Channel_1,TIM_CCx_Disable);
TIM_CCxCmd(TIM4,TIM_Channel_2,TIM_CCx_Disable);
TIM_CCxCmd(TIM4,TIM_Channel_3,TIM_CCx_Disable);
TIM_CCxCmd(TIM4,TIM_Channel_4,TIM_CCx_Disable);
TIM_Cmd(TIM4,ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
/**
* @description: Control TIM4 output PWM.
* @param: operation 1 enable output, 0 disable output.
* @return: none.
*/
void BoardController::TIM4CH1OutControl(unsigned char operation)
{// 还有三个函数控制CH2、CH3、CH4,它们和此函数的区别仅在于调用库函数的参数的通道号,不再赘述.
if(!operation)
{// operation = 0 时,失能输出比较通道,失能捕获比较中断.
TIM_CCxCmd(TIM4,TIM_Channel_1,TIM_CCx_Disable);
TIM_ITConfig(TIM4,TIM_IT_CC1,DISABLE);
}
else
{// operation = 1 时,使能输出比较通道,使能捕获比较中断.
TIM_CCxCmd(TIM4,TIM_Channel_1,TIM_CCx_Enable);
TIM_ITConfig(TIM4,TIM_IT_CC1,ENABLE);
}
}
#define TIM4_CNT_FREQ 1000000 // 定时器计数频率 1MHz.
#define TIM4_CH1_PWM_FREQ 400 // CH1 输出频率 400Hz.
#define TIM4_CH2_PWM_FREQ 300 // CH2 输出频率 300Hz.
#define TIM4_CH3_PWM_FREQ 200 // CH3 输出频率 200Hz.
#define TIM4_CH4_PWM_FREQ 100 // CH4 输出频率 100Hz.
// 计算CCRx寄存器的步进值,占空比都是50%,若要改变占空比,则设置不同的步进值.
#define TIM4_CH1_CCR_INC TIM4_CNT_FREQ/TIM4_CH1_PWM_FREQ/2
#define TIM4_CH2_CCR_INC TIM4_CNT_FREQ/TIM4_CH2_PWM_FREQ/2
#define TIM4_CH3_CCR_INC TIM4_CNT_FREQ/TIM4_CH3_PWM_FREQ/2
#define TIM4_CH4_CCR_INC TIM4_CNT_FREQ/TIM4_CH4_PWM_FREQ/2
/**
* @description: TIM4 interrupt service routine.
* @param: none.
* @return: none.
*/
void TIM4_IRQHandler(void)
{
unsigned short ccr;// 因为CCRx寄存器只有低16位有用,所以定义unsigned short型变量.
if(TIM_GetITStatus(TIM4,TIM_IT_CC1) == SET)
{
static unsigned char cnt1 = 0x00;// 记录进入中断的次数.
static unsigned int ch1_pulse_number =0x00;// 记录输出脉冲个数.
TIM_ClearITPendingBit(TIM4,TIM_IT_CC1);
cnt1++;
ccr = TIM4->CCR1;
TIM4->CCR1 = ccr+TIM4_CH1_CCR_INC;// 一直累加就行,让变量自动溢出.
if(cnt1&0x01)//进两次中断完成一个PWM周期.
{
ch1_pulse_number++; // 计算输出脉冲个数.
if(ch1_pulse_number>= target_pulse)
{// 如果输出脉冲个数达到目标脉冲个数,则关闭CH1的PWM输出.
// 变量target_pulse需要根据实际需求计算,这里代码中没有定义.
ch1_pulse_number= 0;
board.TIM4CH1OutControl(0);
}
}
}
if(TIM_GetITStatus(TIM4,TIM_IT_CC2) == SET)
{
static unsigned char cnt2 = 0x00;// 记录进入中断的次数.
static unsigned int ch2_pulse_number =0x00;// 记录输出脉冲个数.
TIM_ClearITPendingBit(TIM4,TIM_IT_CC2);
cnt2++;
ccr = TIM4->CCR2;
TIM4->CCR2 = ccr+TIM4_CH2_CCR_INC;
if(cnt2&0x01)
{
ch2_pulse_number++;
// 计算步进电机旋转角度,可以根据旋转方向再加上正负的定义.
// 代码中 angle 和 step_angle 未定义。
angle = ch2_pulse_number*step_angle;
}
}
if(TIM_GetITStatus(TIM4,TIM_IT_CC3) == SET)
{
static unsigned char cnt3 = 0x00;// 记录进入中断的次数.
static unsigned int ch3_pulse_number =0x00;// 记录输出脉冲个数.
TIM_ClearITPendingBit(TIM4,TIM_IT_CC3);
cnt3++;
ccr = TIM4->CCR3;
TIM4->CCR3 = ccr+TIM4_CH3_CCR_INC;
if(cnt3&0x01)
{
ch3_pulse_number++;
//类似前面的代码,按实际需求写.
}
}
if(TIM_GetITStatus(TIM4,TIM_IT_CC4) == SET)
{
static unsigned char cnt4 = 0x00;// 记录进入中断的次数
static unsigned int ch4_pulse_number =0x00;// 记录输出脉冲个数.
TIM_ClearITPendingBit(TIM4,TIM_IT_CC4);
cnt4++;
ccr = TIM4->CCR4;
TIM4->CCR4 = ccr+TIM4_CH4_CCR_INC;
if(cnt4&0x01)//进两次中断完成一个PWM周期
{
ch4_pulse_number++;
//类似前面的代码,按实际需求写.
}
}
}