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
如果需要在运行时修改, 你可以:
- 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}'
- 通过调用
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\r\n", 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\r\n", 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都没有影响, 这个方法可以不写.
参考
- 使用std periph库实现的,包含代码讲解, 用的是TIM4, 可以和官方标准库里的例子(TIM3)对比着看 https://stm32f4-discovery.net/2014/05/stm32f4-stm32f429-discovery-pwm-tutorial/
- 原理和实现机制, HAL库 https://deepbluembedded.com/stm32-pwm-example-timer-pwm-mode-tutorial/
- 使用std periph库实现的,包含代码讲解 https://thecodeprogram.com/stm32f4-timers-and-pwm-generation-with-std-periph
- 使用L293控制马达转速和方向, 用一个按键切换方向, 用三个按键输出不同的PWM占空比 https://www.engineersgarage.com/dc-motor-control-with-stm32-microcontroller/