STM32F401的PWM输出

PWM的说明

PWM有三个关键指标: PWM频率, 占空比, 区分度

对于同一个时钟频率下工作的单片机, 区分度是和PWM工作频率相关的, 因为总频率是固定的, PWM工作频率越高, 留下给区分度的部分就越低, 因此区分度就越低. 对于STM32, 如果时钟是72MHz, 在PWM频率为1KHz时, 区分度为16bit, 在281KHz时, 为8bit, 在4.5MHz时, 就是4bit了.

STM32F4 Timers

STM32的PWM功能是定时器功能的一部分, STM32F4系列完整的定时器是14个

Timer Type Resolution Prescaler Channels MAX INTERFACE CLOCK MAX TIMER CLOCK* APB
TIM1, TIM8 Advanced 16bit 16bit 4 SysClk/2 SysClk 2
TIM2, TIM5 General purpose 32bit 16bit 4 SysClk/4 SysClk, SysClk/2 1
TIM3, TIM4 General purpose 16bit 16bit 4 SysClk/4 SysClk, SysClk/2 1
TIM9 General purpose 16bit 16bit 2 SysClk/2 SysClk 2
TIM10, TIM11 General purpose 16bit 16bit 1 SysClk/2 SysClk 2
TIM12 General purpose 16bit 16bit 2 SysClk/4 SysClk, SysClk/2 1
TIM13, TIM14 General purpose 16bit 16bit 1 SysClk/4 SysClk, SysClk/2 1
TIM6, TIM7 Basic 16bit 16bit 0 SysClk/4 SysClk, SysClk/2 1

F401属于低端系列, 定时器只有一部分, 内置的定时器为

  • 1个高级定时器TIM1
    • 三相 PWM 输出, 4个独立通道(如果正反算两个的话有8个). It has complementary PWM outputs with programmable inserted dead times
  • 7个通用定时器
    • 全功能的: TM2&5, TIM3&4, 4个独立通道 for input capture/output compare, PWM or one-pulse mode output.
    • 普通的: TIM9, TIM10,11. TIM10和TIM11有1个独立通道, TIM9有2个独立通道 for input capture/output compare, PWM or one-pulse mode output.
  • 2个watchdog timers

每个定时器都有对应的通道数, 一般都有CH1 - CH4, 对于TIM1, 还有CH1N - CH4N

关于CH1和CH1N
后者输出相对于前者反相的PWM信号, CH1和CH1N两个通道互补输出. 在设置这两个通道输出的时候如果开启了互补输出, 那么这两个引脚的输出电平始终相反, 也就是一个引脚输出低电平, 另一个引脚自动输出高电平, 反之亦然. 这样的输出方式一般用于电机驱动控制.

STM32F4的TIMx PIN脚输出映射关系

TIM1 TIM2 TIM3 TIM9
CH1 PA8 PA0 PA5 PA15 PA6 PB4 PA2
CH2 PA9 PA1 PB3 PA7 PB5 PA3
CH3 PA10 PA2 PB10 PB0
CH4 PA11 PA3 PB11 PB1
CH1N PB13 PA7
CH2N PB14 PB0
CH3N PB15 PB1

设置PWM输出电平的模式

PWM输出模式的配置主要有两个

1. TIM_OCMode: TIM输出比较和PWM模式

  • TIM_OCMode_Timing 在比较成功后不在对应输出管脚上产生输出, TIM_OCMode_Timing does not produce output on the corresponding output pin after a successful comparison
  • TIM_OCMode_Active
  • TIM_OCMode_Inactive
  • TIM_OCMode_Toggle 计数达到比较值时翻转对应输出管脚上的电平, TIM_OCMode_Toggle is to flip the level on the corresponding output pin after a successful comparison
  • TIM_OCMode_PWM1 常用的模式, CNT < CRRx时为有效电平, CNT > CRRx为无效电平
  • TIM_OCMode_PWM2 与PWM1相反, CNT小于时为无效电平, 高于时为有效电平, 配合TIM_OCPolarity可以做到和PWM1一样的输出

2. TIM_OCPolarity: PWM的有效电平
与TIM_OCMode_PWM1和TIM_OCMode_PWM2配合, TIM_OCPolarity_High表示有效电平是高电平, TIM_OCPolarity_Low是低电平. 一般使用PWM1+HIGH的组合.

上面两个配置结合产生的效果

  • TIM_OCMode_PWM1模式下
    • 设置TIM_OCPolarity_High, TIMx_CNT > TIMx_CCR输出高电平, TIMx_CNT < TIMx_CCR输出低电平
    • 设置TIM_OCPolarity_Low, TIMx_CNT > TIMx_CCR输出低电平, TIMx_CNT < TIMx_CCR输出高电平
  • TIM_OCMode_PWM2模式下
    • 设置TIM_OCPolarity_High, TIMx_CNT > TIMx_CCR输出低电平, TIMx_CNT < TIMx_CCR输出高电平
    • 设置TIM_OCPolarity_Low, TIMx_CNT > TIMx_CCR输出高电平, TIMx_CNT < TIMx_CCR输出低电平

设置PWM频率

设置PWM频率, 即设置PWM完整周期的时钟计数次数. 这个是通过TIM_BaseStruct.TIM_Period(ARR寄存器)设置的, 要设置这个值, 首先你要知道这个值的上限, 即定时器的最大值, 例如 16bit 即 65535, 要计算出PWM频率, 可以这样计算

PWM_frequency = timer_tick_frequency / (TIM_Period + 1)

也可以通过PWM频率倒推时钟周期计数值

TIM_Period = timer_tick_frequency / PWM_frequency - 1

例如, 如果需要的PWM频率为10KHz, 则时钟的周期计数值为

TIM_Period = (84000000 / 10000) - 1; // 8399

如果需要17.57 Khz, 就是

TIM_Period = (SystemCoreClock / 17570 ) - 1;

如果通过这个式子算出来的计数值大于定时器长度(例如超过了65535), 你需要增大 prescaler, 降低系统时钟频率

如果需要在运行时修改, 可以使用函数TIM_PrescalerConfig(TIM2, 35999, TIM_PSCReloadMode_Immediate), 这个函数的作用就是在定时器工作时改变预分频器的值.

设置PWM占空比

设置占空比, 需要通过设置 TIM_Pulse 参数, 这个值就是用于比较的触发值CRR, TIM_OCInitStructure.TIM_Pulse = 100表示触发值为100, 这个值的计算要结合PWM周期总计数值TIM_Period和需要的占空比百分比, 例如

pulse_length = ((TIM_Period + 1) * DutyCycle) / 100 - 1
# 其中DutyCycle是一个百分比, 例如对于TIM_Period为8399, 如果需要25%占空比
pulse_length = (8399 + 1) * 0.25 - 1 = 2099

如果需要在运行时修改, 你可以:

  1. You just write the updated width to the TIMx_CCRy register. Changed the CCR value in the relevant timer register and this did the trick. In my case, the code used to change the duty cycle is 'TIM3 -> CCR4 = {required value}'
  2. 通过调用TIM_SetCompare[x](TIMx, Compare1)这个函数,修改CCR的值,改变输出占空比, 例如TIM_SetCompare1函数名中的数字1代表的是TIMx的通道1, 参数TIMx可以是TIM1, TIM2等, 第二个参数 Compare1, 是用于与TIMx计数值比较的数, 在TIMx达到这个计数值时将根据当前的模式和极性, 进行电平变换. TIM_SetCompareX这个函数有四个, 分别是TIM_SetCompare1, TIM_SetCompare2, TIM_SetCompare3, TIM_SetCompare4. 对应不同的CHx使用, 例如TIMx_CH1使用 TIM_SetCompare1, TIMx_CH2使用TIM_SetCompare2, 等等.
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1) {
	/* Check the parameters */
	assert_param(IS_TIM_LIST8_PERIPH(TIMx));
	/* Set the Capture Compare1 Register value */
	TIMx->CCR1 = Compare1;
}

控制直流马达的方向

控制直流马达的模块有两种, 一种是L293D这种三线输入的模块, AB线的电压差确定转向, E线输入PWM确定转速, 另一种是L298N和L9110s这种双线输入的模块, 根据两根线的电压差决定方向, 根据线上的PWM决定转速. 对于前者, 需要两根GPIO加一根PWM输出, 对于后者, 需要两根PWM输出, PWM加在正向的PIN脚上, 另一个PIN脚PWM设为0.

对于后者的控制代码例子如下

/*
双轴摇杆: PIN脚朝左, X轴左小右大, Y轴上小下大
前进: X中,Y小
后退: X中,Y大
左转: X小,Y中
右转: X大,Y中
*/
void AdjustChannelPuls(u8 axis_x, u8 axis_y) {
  int8_t l, r;
  calc(axis_x, axis_y, &l, &r);
  printf("X:%d, Y:%d, L:%d, R:%d
", axis_x, axis_y, l, r);
  if (l >= 0) {
    Channel1Pulse = CalcPuls(l);
    Channel3Pulse = CalcPuls(0);
  } else {
    Channel1Pulse = CalcPuls(0);
    Channel3Pulse = CalcPuls(-l);
  }
  if (r >= 0) {
    Channel2Pulse = CalcPuls(r);
    Channel4Pulse = CalcPuls(0);
  } else {
    Channel2Pulse = CalcPuls(0);
    Channel4Pulse = CalcPuls(-r);
  }
  TIM_ResetCounter(TIM3);
}

void UpdatePWM(void) {
  TIM_SetCompare1(TIM2, Channel1Pulse);
  TIM_SetCompare2(TIM2, Channel2Pulse);
  TIM_SetCompare3(TIM2, Channel3Pulse);
  TIM_SetCompare4(TIM2, Channel4Pulse);
}

代码

启动对应输出口的定时器, 这里是TIM4

void TM_TIMER_Init(void) {
    TIM_TimeBaseInitTypeDef TIM_BaseStruct;
    
    /* 开启TIM4时钟 */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
    /*    
    TIM4连接的是 APB1 总线, 在F407上时钟是 42MHz, 但是有内部PLL, 将频率翻倍为 84MHz. 注意: 也有定时器是接在 APB2 总线上的, 默认工作在 84MHz, 通过内部PLL翻倍至 168MHz                                                             

    设置预分频 timer prescaller 
    时钟被设置为     timer_tick_frequency = Timer_default_frequency / (prescaller_set + 1)
    在这个例子中, 我们希望使用最大频率, 所以 prescaller 设置为 0, 所以时钟与总线时钟一致, 频率为
    timer_tick_frequency = 84000000 / (0 + 1) = 84000000 
    */    
    TIM_BaseStruct.TIM_Prescaler = 0;
    /* 使用上升沿计数 */
    TIM_BaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
    /*
    设置一个PWM完整周期的时钟计数次数, 首先你要知道定时器的最大值, 在这个例子中是16bit, 即 65535, 要计算出你的PWM频率, 可以这样计算
    PWM_frequency = timer_tick_frequency / (TIM_Period + 1)
    通过这个算式也可以通过PWM频率倒推时钟周期计数值
    TIM_Period = timer_tick_frequency / PWM_frequency - 1
    在这个例子中, 如果需要的PWM频率为10KHz, 则时钟的周期计数值为
    TIM_Period = 84000000 / 10000 - 1 = 8399

    如果通过这个式子算出来的计数值大于定时器长度(例如超过了65535), 你需要增大 prescaler 降低系统时钟频率
    */
    TIM_BaseStruct.TIM_Period = 8399; /* 10kHz PWM */
    /* TIM_ClockDivision的设置不影响PWM频率 */
    TIM_BaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_BaseStruct.TIM_RepetitionCounter = 0;
    /* TIM4 初始化 */
    TIM_TimeBaseInit(TIM4, &TIM_BaseStruct);
    /* TIM4 开始计数 */
    TIM_Cmd(TIM4, ENABLE);
}

初始化PWM 4个通道

void TM_PWM_Init(void) {
    TIM_OCInitTypeDef TIM_OCStruct;
    
    /* 通道的公用配置 */
    
    /* PWM 模式 2 = Clear on compare match 达到预设值时拉低电平 */
    /* PWM 模式 1 = Set on compare match 达到预设值时拉高电平 */
    TIM_OCStruct.TIM_OCMode = TIM_OCMode_PWM2;
    TIM_OCStruct.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCStruct.TIM_OCPolarity = TIM_OCPolarity_Low;

    /*
    要得到期望的占空比(DutyCycle, 一个百分比), 通过这个式子计算定时器触发值
    pulse_length = ((TIM_Period + 1) * DutyCycle) / 100 - 1
    例如
    25%  占空比: pulse_length = ((8399 + 1) * 25) / 100 - 1 = 2099
    50%  占空比: pulse_length = ((8399 + 1) * 50) / 100 - 1 = 4199
    75%  占空比: pulse_length = ((8399 + 1) * 75) / 100 - 1 = 6299
    100% 占空比: pulse_length = ((8399 + 1) * 100) / 100 - 1 = 8399
    
    注意: 如果触发值大于时钟周期总长度 TIM_Period, 这个PWM将一直输出同样的电平
    */
    TIM_OCStruct.TIM_Pulse = 2099; /* 25% duty cycle */
    TIM_OC1Init(TIM4, &TIM_OCStruct);
    TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);
    
    TIM_OCStruct.TIM_Pulse = 4199; /* 50% duty cycle */
    TIM_OC2Init(TIM4, &TIM_OCStruct);
    TIM_OC2PreloadConfig(TIM4, TIM_OCPreload_Enable);
    
    TIM_OCStruct.TIM_Pulse = 6299; /* 75% duty cycle */
    TIM_OC3Init(TIM4, &TIM_OCStruct);
    TIM_OC3PreloadConfig(TIM4, TIM_OCPreload_Enable);
    
    TIM_OCStruct.TIM_Pulse = 8399; /* 100% duty cycle */
    TIM_OC4Init(TIM4, &TIM_OCStruct);
    TIM_OC4PreloadConfig(TIM4, TIM_OCPreload_Enable);
}

初始化GPIO输出

void TM_LEDS_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct;
    
    /* GPIOD 时钟 */
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);

    /* 设置这些PIN脚的功能复用 */
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource12, GPIO_AF_TIM4);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource13, GPIO_AF_TIM4);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource14, GPIO_AF_TIM4);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource15, GPIO_AF_TIM4);
    
    /* 设置PIN脚 */
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_Init(GPIOD, &GPIO_InitStruct);
}

完整的代码

#include "defines.h"
#include "stm32f4xx.h"
#include "stm32f4xx_rcc.h"
#include "stm32f4xx_gpio.h"
#include "stm32f4xx_tim.h"

TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
TIM_OCInitTypeDef  TIM_OCInitStructure;
u16 TimerPeriod = 0;
u16 Channel1Pulse = 0, Channel2Pulse = 0, Channel3Pulse = 0, Channel4Pulse = 0;

void DecreasePuls() {
  Channel1Pulse = (Channel1Pulse <= 10)? TimerPeriod : Channel1Pulse - 10;
  Channel2Pulse = (Channel2Pulse <= 10)? TimerPeriod : Channel2Pulse - 10;
  Channel3Pulse = (Channel3Pulse <= 10)? TimerPeriod : Channel3Pulse - 10;
  Channel4Pulse = (Channel4Pulse <= 10)? TimerPeriod : Channel4Pulse - 10;
}

void TIM_Config(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;

  /* GPIOA, GPIOB Clocks enable */
  RCC_AHB1PeriphClockCmd( RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB , ENABLE);
  
  /* GPIOA Configuration: Channel 1, 2, 3, 4 as alternate function push-pull */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP ;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
   
  GPIO_PinAFConfig(GPIOA, GPIO_PinSource0, GPIO_AF_TIM2);
  GPIO_PinAFConfig(GPIOA, GPIO_PinSource1, GPIO_AF_TIM2);
  GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_TIM2);
  GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_TIM2);
}

int main(void)
{
  Systick_Init();
  USART1_Init();

  TIM_Config();

  /* Compute the value to be set in ARR register to generate signal frequency at 17.57 Khz */
  TimerPeriod = (SystemCoreClock / 17570 ) - 1;
  /* Compute CCR1 value to generate a duty cycle at 100% for channel 1 and 1N */
  Channel1Pulse = (u16) (((u32) 10 * (TimerPeriod - 1)) / 10);
  /* Compute CCR2 value to generate a duty cycle at 30%  for channel 2 and 2N */
  Channel2Pulse = (u16) (((u32) 300 * (TimerPeriod - 1)) / 1000);
  /* Compute CCR3 value to generate a duty cycle at 20%  for channel 3 and 3N */
  Channel3Pulse = (u16) (((u32) 20 * (TimerPeriod - 1)) / 100);
  /* Compute CCR4 value to generate a duty cycle at 10%  for channel 4 */
  Channel4Pulse = (u16) (((u32) 100 * (TimerPeriod- 1)) / 1000);

  /* TIM2 clock enable */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
  
  /* Time Base configuration */
  TIM_TimeBaseStructure.TIM_Prescaler = 0;
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
  TIM_TimeBaseStructure.TIM_Period = TimerPeriod;
  TIM_TimeBaseStructure.TIM_ClockDivision = 0;
  //TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
  TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

  /* Channel 1, 2,3 and 4 Configuration in PWM mode */
  TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // PWM: trigger from valid -> invalid
  // TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable;
  TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
  //TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High;
  // Specifies the TIM Output Compare pin state during Idle state, ## valid only for TIM1 and TIM8 ##
  //TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set;
  //TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCIdleState_Reset;

  //Specifies the TIM Output Compare state
  TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;

  TIM_OCInitStructure.TIM_Pulse = Channel1Pulse;
  TIM_OC1Init(TIM2, &TIM_OCInitStructure);
  // TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);

  TIM_OCInitStructure.TIM_Pulse = Channel2Pulse;
  TIM_OC2Init(TIM2, &TIM_OCInitStructure);
  // TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable);

  TIM_OCInitStructure.TIM_Pulse = Channel3Pulse;
  TIM_OC3Init(TIM2, &TIM_OCInitStructure);
  // Enables or disables the TIMx peripheral Preload register on CCR1
  // TIM_OC3PreloadConfig(TIM2, TIM_OCPreload_Enable);

  TIM_OCInitStructure.TIM_Pulse = Channel4Pulse;
  TIM_OC4Init(TIM2, &TIM_OCInitStructure);
  // TIM_OC4PreloadConfig(TIM2, TIM_OCPreload_Enable);

  // TIM_ARRPreloadConfig(TIM2, ENABLE);

  /* TIM2 counter enable */
  TIM_Cmd(TIM2, ENABLE);

  /* TIM2 Main Output Enable */
  TIM_CtrlPWMOutputs(TIM2, ENABLE); // Enables or disables the TIM peripheral Main Outputs. 

  while (1)
  {
    printf("TP:%d, CH1:%d, CH2:%d, CH3:%d, CH4:%d
", TimerPeriod, Channel1Pulse, Channel2Pulse, Channel3Pulse, Channel4Pulse);
    DecreasePuls();
    TIM_SetCompare1(TIM2, Channel1Pulse);
    TIM_SetCompare2(TIM2, Channel2Pulse);
    TIM_SetCompare3(TIM2, Channel3Pulse);
    TIM_SetCompare4(TIM2, Channel4Pulse);
    Systick_Delay_ms(100);
  }

代码中的几个函数的说明

TIM_OC4PreloadConfig

TIM_OC4PreloadConfig(TIM2, TIM_OCPreload_Enable);

使能TIM2 在 CCR4 上的预装载寄存器, 即 TIM2_CCR4 的预装载值在更新事件到来时才能被传送至当前寄存器中. 就是设置 CCR4 中的预装载值何时被传送到当前的CNT寄存器中, 设置为ENABLE, 表示仅当更新事件到来的时候才装载, 追踪寄存器的设置可知, 原来设置的是CCMR1的OC2PE, 其实还有一种方式是立即装载. OC1PE:输出比较1预装载使能(Output compare 1 preload enable)位3

  • 0:禁止TIMx_CCR1寄存器的预装载功能, 可随时写入TIMx_CCR1寄存器, 并且新写入的数值立即起作用
  • 1:开启TIMx_CCR1寄存器的预装载功能, 读写操作仅对预装载寄存器操作, TIMx_CCR1的预装载值在更新事件到来时被传送至当前寄存器中

TIM_ARRPreloadConfig

TIM_ARRPreloadConfig的作用, 是允许或禁止在定时器工作时向ARR的缓冲器中写入新值, 以便在更新事件发生时载入覆盖以前的值. 如果在初始化的时候设置了ARR的值TIM_TimeBaseStructure.TIM_Period=2000;, 后来也没更改(没有编写中断服务函数或者在中断服务函数中没有给ARR缓冲器重新写入新值), 那么设置为DISABLE 和ENABLE都没有影响, 这个方法可以不写.

参考

原文地址:https://www.cnblogs.com/milton/p/15028413.html