一、PWM简介
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调试。是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。广泛应用在从测量、通信到功率控制与变换的许多领域中。
例如上图中,图b)是微处理输出的数字信号,实际上他接到电机等功率设备上时,效果相当于图a)。这就是PWM调制。例如输出占空比为50%,频率为10Hz的脉冲,高电平为3.3V.则其输出的模拟效果相当于输出一个1.65V的高电平。脉冲调制有两个重要的参数,第一个就是输出频率,频率越高,则模拟的效果越好。第二个就是占空比。占空比就是改变输出模拟效果的电压大小。占空比越大则模拟出的电压越大。
二、定时器简介
STM32F1 系列中,除了互联型的产品,共有 8
个定时器,分为基本定时器,通用定时器和高级定时器。
基本定时器 TIM6
和 TIM7
是一个 16 位的只能向上计数的定时器,只能定时,没有外部 IO。
通用定时器 TIM2/3/4/5
是一个 16 位的可以向上/下计数的定时器,可以定时,可以输出比较,可以输入捕捉,每个定时器有四个外部 IO。
高级定时器 TIM1/8
是一个 16 位的可以向上/下计数的定时器,可以定时,可以输出比较,可以输入捕捉,还可以有三相电机互补输出信号,每个定时器有 8 个外部 IO。
STM32 的定时器除了 TIM6
和 TIM7
(基本定时器)。其他的定时器都可以用来产生 PWM 输出。其中高级定时器 TIM1
和 TIM8
可以同时产生多达 7
路的 PWM 输出。而通用定时器 TIM2/3/4/5
也能同时产生多达 4
路的 PWM 输出。这样,STM32 最多可以同时产生 30 路 PWM 输出。
每个定时器有四个通道,每一个通道都有一个捕获比较寄存器,将寄存器值和计数器值比较,通过比较结果输出高低电平,便可以实现脉冲宽度调制模式(PWM信号)。
三、新建工程
1. 打开 STM32CubeMX 软件,点击“新建工程”
2. 选择 MCU 和封装
3. 配置时钟
RCC 设置,选择 HSE(外部高速时钟) 为 Crystal/Ceramic Resonator(晶振/陶瓷谐振器)
选择 Clock Configuration,配置系统时钟 SYSCLK 为 72MHz
修改 HCLK 的值为 72 后,输入回车,软件会自动修改所有配置
4. 配置调试模式
非常重要的一步,否则会造成第一次烧录程序后续无法识别调试器
SYS 设置,选择 Debug 为 Serial Wire
四、TIM3通用定时器
4.1 选择定时器
由于 LED 灯所在引脚为 PB1
,在右边图中找到 LED 灯对应引脚,选择 TIM3_CH4
4.2 参数配置
在 Timers
中选择 TIM3
设置,指定时钟源为 Internal Clock
内部时钟,通道4选择 PWM Generation CH4
PWM输出。
在 Parameter Settings
进行具体参数配置。
- Counter setting
- Prescaler(时钟预分频数):1024-1
- Counter Mode(计数模式):Up(向上计数模式)
- Counter Period(自动重装载值):200-1
- Internal Clock Division(CKD)(时钟分频因子):No Division(不分频)
- auto-reload preload(自动重装载):Enable(使能)
- TRGO Output (TRGO) Parameters
TRGO:在定时器的定时时间到达的时候输出一个信号(如:定时器更新产生TRGO信号来触发ADC的同步转换)
- Master/Slave Mode(MSM bit):不使能
- Trigger Event Selection:Reset(UG bit from TIMx_EGR)
- PWM Generation Channel 4
- Mode(定时模式):PWM mode 1
设置定时器计数器与比较值相等时输出引脚的状态
- Pulse(计数比较值):0
这里建议设置为0,在中断中改变比较寄存器的值
- Output compare preload(输出比较预加载):Enable(使能)
作用和 auto-reload preload 类似
- Fast Mode(脉冲快速模式):Disable(不使能)
与我们配置无关不使能
- CH Polarity(输出极性):Low
当定时器计数值小于 CCR1_Val 时,输出低电平
4.2 配置GPIO
在 GPIO Settings
配置速度为高速。
4.3 配置NVIC
使能定时器中断
4.4 生成代码
输入项目名和项目路径
选择应用的 IDE 开发环境 MDK-ARM V5
每个外设生成独立的
’.c/.h’
文件
不勾:所有初始化代码都生成在 main.c
勾选:初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。
点击 GENERATE CODE 生成代码
4.5 添加全局变量
在 main.c
头部添加 PWM 表
/* Private variables ---------------------------------------------------------*/
TIM_HandleTypeDef htim3;
/* USER CODE BEGIN PV */
uint16_t indexWave[] = {
1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4,
4, 5, 5, 6, 7, 8, 9, 10, 11, 13,
15, 17, 19, 22, 25, 28, 32, 36,
41, 47, 53, 61, 69, 79, 89, 102,
116, 131, 149, 170, 193, 219, 250,
284, 323, 367, 417, 474, 539, 613,
697, 792, 901, 1024, 1024, 901, 792,
697, 613, 539, 474, 417, 367, 323,
284, 250, 219, 193, 170, 149, 131,
116, 102, 89, 79, 69, 61, 53, 47, 41,
36, 32, 28, 25, 22, 19, 17, 15, 13,
11, 10, 9, 8, 7, 6, 5, 5, 4, 4, 3, 3,
2, 2, 2, 2, 1, 1, 1, 1
};
uint16_t POINT_NUM = sizeof(indexWave)/sizeof(indexWave[0]);
/* USER CODE END PV */
并在 stm32f1xx_it.c
中声明
/* External variables --------------------------------------------------------*/
extern TIM_HandleTypeDef htim3;
/* USER CODE BEGIN EV */
extern uint16_t indexWave[];
extern uint16_t POINT_NUM; /*PWM表中的点数*/
/* USER CODE END EV */
4.6 修改中断回调函数
打开 stm32f1xx_it.c
中断服务函数文件,找到 TIM3 中断的服务函数 TIM3_IRQHandler()
中断服务函数里面就调用了定时器中断处理函数 HAL_TIM_IRQHandler()
打开 stm32f1xx_hal_tim.c
文件,找到定时器中断处理函数原型 HAL_TIM_IRQHandler()
,其主要作用就是判断是哪个定时器产生哪种事件中断,清除中断标识位,然后调用中断回调函数 HAL_TIM_PeriodElapsedCallback()
。
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_GPIO_EXTI_Callback could be implemented in the user file
*/
这个函数不应该被改变,如果需要使用回调函数,请重新在用户文件中实现该函数。
HAL_TIM_PeriodElapsedCallback()
按照官方提示我们应该再次定义该函数,__weak
是一个弱化标识,带有这个的函数就是一个弱化函数,就是你可以在其他地方写一个名称和参数都一模一样的函数,编译器就会忽略这一个函数,而去执行你写的那个函数;而 UNUSED(htim)
,这就是一个防报错的定义,当传进来的定时器号没有做任何处理的时候,编译器也不会报出警告。其实我们在开发的时候已经不需要去理会中断服务函数了,只需要找到这个中断回调函数并将其重写即可而这个回调函数还有一点非常便利的地方这里没有体现出来,就是当同时有多个中断使能的时候,STM32CubeMX会自动地将几个中断的服务函数规整到一起并调用一个回调函数,也就是无论几个中断,我们只需要重写一个回调函并判断传进来的定时器号即可。
接下来我们就在 stm32f1xx_it.c
这个文件的最下面添加 HAL_TIM_PeriodElapsedCallback()
/* USER CODE BEGIN 1 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint8_t pwm_index = 1; /* 用于PWM查表 */
static uint8_t period_cnt = 0; /* 用于计算周期数 */
period_cnt++;
/* 若输出的周期数大于20,输出下一种脉冲宽的PWM波 */
if(period_cnt >= 20)
{
/* 根据PWM表修改定时器的比较寄存器值 */
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_4, indexWave[pwm_index]);
/* 标志PWM表的下一个元素 */
pwm_index++;
/* 若PWM脉冲表已经输出完成一遍,重置PWM查表标志 */
if( pwm_index >= POINT_NUM)
{
pwm_index=0;
}
/* 重置周期计数标志 */
period_cnt=0;
}
}
/* USER CODE END 1 */
其中 pwm_index 比较容易理解,它用于指示当前要使用 PWM 表中的哪个元素,从而 在“BRE_TIMx->BRE_CCRx = indexWave[pwm_index];‖语句中可以给 CCRx 赋予正确的数值,而且当 PWM 表中的数据都使用一遍时,pwm_index 将重新指向 PWM 表的开头,开始下一次呼吸循环。在本实验的单次呼吸循环中,每个 PWM 表元素都会使用 20 次,代码中利用 period_cnt 变量指示当前使用的次数,当 period_cnt> period_class 时(即period_cnt>10 时),pwm_index 才会指向下一个元素。每个 PWM 表元素使用多次,主要是为了在 TIMPeriod、PWM 表的点数、TIM_Prescaler 都固定的情况下,通过调整每个元素的重复次数可以调整整个拟合波形的周期。如把代码中的比较值 period_class 改为 200,每个 PWM 表遍历一次的时间就变为原来配置的 10 倍,其拟合的呼吸周期也就相应地改变了。图 40-7 说明了 period =3 和 period=1 时输出的 PWM 波形。
4.7 添加定时器启动函数
现在进入 main 函数并在 while 循环前加入开启定时器函数 HAL_TIM_Base_Start_IT()
和 PWM 开启函数 HAL_TIM_PWM_Start()
,这里所传入的 htim3 就是刚刚定时器初始化后的结构体。
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM3_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim3);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
4.8 HAL库与标准库代码比较
STM32CubeMX 使用 HAL 库生成的代码:
/**
* @brief TIM3 Initialization Function
* @param None
* @retval None
*/
static void MX_TIM3_Init(void)
{
/* USER CODE BEGIN TIM3_Init 0 */
/* USER CODE END TIM3_Init 0 */
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_OC_InitTypeDef sConfigOC = {0};
/* USER CODE BEGIN TIM3_Init 1 */
/* USER CODE END TIM3_Init 1 */
htim3.Instance = TIM3;
htim3.Init.Prescaler = 1023;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 199;
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_Init(&htim3) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 0;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_4) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN TIM3_Init 2 */
/* USER CODE END TIM3_Init 2 */
HAL_TIM_MspPostInit(&htim3);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint8_t pwm_index = 1; /* 用于PWM查表 */
static uint8_t period_cnt = 0; /* 用于计算周期数 */
period_cnt++;
/* 若输出的周期数大于20,输出下一种脉冲宽的PWM波 */
if(period_cnt >= 20)
{
/* 根据PWM表修改定时器的比较寄存器值 */
__HAL_TIM_SET_COMPARE(htim,TIM_CHANNEL_4,indexWave[pwm_index]);
/* 标志PWM表的下一个元素 */
pwm_index++;
/* 若PWM脉冲表已经输出完成一遍,重置PWM查表标志 */
if( pwm_index >= POINT_NUM)
{
pwm_index=0;
}
/* 重置周期计数标志 */
period_cnt=0;
}
}
HAL_TIM_Base_Start_IT(&htim3);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);
使用 STM32 标准库的代码:
/**
* @brief 配置TIM复用输出PWM时用到的I/O
* @param 无
* @retval 无
*/
static void TIMx_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* clock enable */
RCC_APB2PeriphClockCmd(BRE_TIM_GPIO_CLK, ENABLE);
BRE_TIM_GPIO_APBxClock_FUN ( BRE_TIM_GPIO_CLK, ENABLE );
BRE_GPIO_REMAP_FUN();
/* 配置呼吸灯用到的引脚 */
GPIO_InitStructure.GPIO_Pin = BRE_TIM_LED_PIN ;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init( BRE_TIM_LED_PORT, &GPIO_InitStructure );
}
/**
* @brief 配置嵌套向量中断控制器NVIC
* @param 无
* @retval 无
*/
static void NVIC_Config_PWM(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* Configure one bit for preemption priority */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
/* 配置TIM3_IRQ中断为中断源 */
NVIC_InitStructure.NVIC_IRQChannel = BRE_TIMx_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
/**
* @brief 配置TIM输出的PWM信号的模式,如周期、极性
* @param 无
* @retval 无
*/
static void TIMx_Mode_Config(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
/* 设置TIM3CLK 时钟 */
BRE_TIM_APBxClock_FUN ( BRE_TIM_CLK, ENABLE );
/* 基本定时器配置 ,配合PWM表点数、中断服务函数中的period_cnt循环次数设置*/
/* 设置使得整个呼吸过程为3秒左右即可达到很好的效果 */
//要求:
//TIM_Period:与PWM表中数值范围一致
//TIM_Prescaler:越小越好,可减轻闪烁现象
//PERIOD_CLASS:中断服务函数中控制单个点循环的次数,调整它可控制拟合曲线的周期
//POINT_NUM:PWM表的元素,它是PWM拟合曲线的采样点数
/*************本实验中的配置***************/
/***********************************************
#python计算脚本 count.py
#PWM点数
POINT_NUM = 110
#周期倍数
PERIOD_CLASS = 10
#定时器定时周期
TIMER_TIM_Period = 2**10
#定时器分频
TIMER_TIM_Prescaler = 200
#STM32系统时钟频率和周期
f_pclk = 72000000
t_pclk = 1/f_pclk
#定时器update事件周期
t_timer = t_pclk*TIMER_TIM_Prescaler*TIMER_TIM_Period
#每个PWM点的时间
T_Point = t_timer * PERIOD_CLASS
#整个呼吸周期
T_Up_Down_Cycle = T_Point * POINT_NUM
print ("呼吸周期:",T_Up_Down_Cycle)
#运行结果:
呼吸周期:3.12888
************************************************************/
/* 基本定时器配置 */
TIM_TimeBaseStructure.TIM_Period = (1024-1);; //当定时器从0计数到 TIM_Period+1 ,为一个定时周期
TIM_TimeBaseStructure.TIM_Prescaler = (200-1); //设置预分频
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1 ; //设置时钟分频系数:不分频(这里用不到)
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseInit(BRE_TIMx, &TIM_TimeBaseStructure);
/* PWM模式配置 */
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //配置为PWM模式1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //使能输出
TIM_OCInitStructure.TIM_Pulse = 0; //设置初始PWM脉冲宽度为0
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //当定时器计数值小于CCR1_Val时为低电平
BRE_TIM_OCxInit ( BRE_TIMx, &TIM_OCInitStructure ); //使能通道
BRE_TIM_OCxPreloadConfig ( BRE_TIMx, TIM_OCPreload_Enable ); //使能预装载
TIM_ARRPreloadConfig(BRE_TIMx, ENABLE); //使能TIM重载寄存器ARR
/* TIM3 enable counter */
TIM_Cmd(BRE_TIMx, ENABLE); //使能定时器
TIM_ITConfig(BRE_TIMx, TIM_IT_Update, ENABLE); //使能update中断
NVIC_Config_PWM(); //配置中断优先级
}
void BRE_TIMx_IRQHandler(void)
{
static uint16_t pwm_index = 0; //用于PWM查表
static uint16_t period_cnt = 0; //用于计算周期数
if (TIM_GetITStatus(BRE_TIMx, TIM_IT_Update) != RESET) //TIM_IT_Update
{
period_cnt++;
BRE_TIMx->BRE_CCRx = indexWave[pwm_index]; //根据PWM表修改定时器的比较寄存器值
//每个PWM表中的每个元素使用period_class次
if(period_cnt > period_class)
{
pwm_index++; //标志PWM表指向下一个元素
//若PWM表已到达结尾,重新指向表头
if( pwm_index >= POINT_NUM)
{
pwm_index=0;
}
period_cnt=0; //重置周期计数标志
}
else
{
}
TIM_ClearITPendingBit (BRE_TIMx, TIM_IT_Update); //必须要清除中断标志位
}
}
MX_TIM3_Init();
对应 TIMx_GPIO_Config();NVIC_Config_PWM();TIMx_Mode_Config();
HAL_TIM_PWM_Init(&htim3)
对应 TIM_TimeBaseInit(BRE_TIMx, &TIM_TimeBaseStructure)
HAL_TIM_Base_Start_IT(&htim3);HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);
对应 TIM_Cmd(BRE_TIMx, ENABLE);TIM_ITConfig(BRE_TIMx, TIM_IT_Update, ENABLE);
五、注意事项
用户代码要加在 USER CODE BEGIN N
和 USER CODE END N
之间,否则下次使用 STM32CubeMX 重新生成代码后,会被删除。
• 由 Leung 写于 2021 年 2 月 3 日
• 参考:STM32CubeMX系列教程4:PWM
《嵌入式-STM32开发指南》第二部分 基础篇 - 第5章 PWM(HAL库)
STM32CubeMX实战教程(五)——通用定时器(PWM输出)
【STM32】HAL库 STM32CubeMX教程七---PWM输出(呼吸灯)