概述
定时器的输出通道作为 PWM 驱动是 MCU 的常用功能。
PY32F003 有一个高级定时器 TIM1 和一个通用定时器 TIM3,这两个定时器都可以驱动4个输出通道。现在我们就利用 TIM1 的某一个通道实现可控占空比的 PWM 输出。
原理简介
看数据手册,简单摘录如下。
关于 TIM1 的描述,厂家的《Reference_manual v1.1.pdf》中很详细了,内容很多,这里不再详述了。不说废话,上代码吧。
实现代码
使用 SystemClock_Config 函数选择 HSE
/********************************************************************************************************
**函数信息 :void SystemClock_Config(void)
**功能描述 :系统时钟配置
**输入参数 :
**输出参数 :
** 备注 :
********************************************************************************************************/
HAL_StatusTypeDef SystemClock_Config(void)
{
HAL_StatusTypeDef conf_res= HAL_OK;
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE |
RCC_OSCILLATORTYPE_HSI |
RCC_OSCILLATORTYPE_LSI; // 配置时钟源HSE/HSI/LSE/LSI
RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 开启HSI
RCC_OscInitStruct.HSIDiv = RCC_HSI_DIV1; // 不分频
//RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_8MHz; // 配置HSI输出时钟为8MHz
//RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_4MHz; // 配置HSI输出时钟为4MHz
//RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_16MHz; // 配置HSI输出时钟为16MHz
//RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_22p12MHz; // 配置HSI输出时钟为22.12MHz
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_24MHz; // 配置HSI输出时钟为24MHz
RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 开启 HSE
RCC_OscInitStruct.HSEFreq = RCC_HSE_16_32MHz; // HSE工作频率范围16M~32M
RCC_OscInitStruct.LSIState = RCC_LSI_OFF; // 关闭 LSI
conf_res = HAL_RCC_OscConfig(&RCC_OscInitStruct); // 初始化RCC振荡器
if (conf_res != HAL_OK)
return conf_res;
//初始化CPU,AHB,APB总线时钟
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK |
RCC_CLOCKTYPE_SYSCLK |
RCC_CLOCKTYPE_PCLK1; // RCC系统时钟类型
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSE; // SYSCLK的源选择为HSE
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // APH时钟不分频
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB时钟不分频
conf_res = HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_1); // 初始化RCC系统时钟
// (FLASH_LATENCY_0=24M以下;
// FLASH_LATENCY_1=48M)
if (conf_res != HAL_OK)
return conf_res;
return HAL_OK;
}
和先前开发笔记中的相同,选用 HSE 作为 TIM 子系统的时钟。重要的语句是
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSE;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
这三句,设置TIM 子系统的总线频率和 HSE 相同(24MHz)。
SystemClock_Config 函数存放在 Application/User 组的 app.c 文件中。
main.h 声明 TIM1_PWM 相关的几个函数
/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __MAIN_H
#define __MAIN_H
#ifdef __cplusplus
extern "C"
{
#endif
/* Includes ------------------------------------------------------------------*/
#include "py32f0xx_hal.h"
#include "py32f003xx_Start_Kit.h"
#include <stdbool.h>
/* Exported functions prototypes ---------------------------------------------*/
HAL_StatusTypeDef SystemClock_Config(void);
HAL_StatusTypeDef GPIO_Config(void);
HAL_StatusTypeDef USART_Config(void);
HAL_StatusTypeDef DBG_UART_Start(void);
HAL_StatusTypeDef TIM16_Config(void);
HAL_StatusTypeDef TIM16_Start(void);
/**
* ------------------------------------------------------
* Added for TIM1 and PWM output.
* Marked by HardCoder_LLuo@2023-12-07
*/
HAL_StatusTypeDef TIM1_PWM_Config(void);
HAL_StatusTypeDef TIM1_PWM_Start(uint32_t duty);
HAL_StatusTypeDef TIM1_PWM_Stop(void);
void TIM1_PWM_Output(const uint8_t duty_percent);
/* ------------------------------------------------------
*/
void Debug_Info(const char* msg);
......
......
在 app_pwm.c 中编写代码
在 Application/User 组加入 app_pwm.c 文件,编写在 main.h 中声明的函数。
/**
******************************************************************************
* @file app_pwm.c
* @brief Application level PWM codes.
******************************************************************************
* @attention
*
* Copyright (c) 2023 CuteModem Intelligence.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
#include "main.h"
#define PWM_PERIOD 1200
TIM_HandleTypeDef htim1;
TIM_OC_InitTypeDef cfg_tim1;
HAL_StatusTypeDef TIM1_PWM_Config(void)
{
HAL_StatusTypeDef cfg_res = HAL_OK;
htim1.Instance = TIM1; // 选择TIM1
htim1.Init.Period = PWM_PERIOD - 1; // 自动重装载值,和Prescaler可确定PWM的频率
htim1.Init.Prescaler = 1 - 1; // 预分频为1-1
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 时钟不分频
htim1.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
htim1.Init.RepetitionCounter = 1 - 1; // 不重复计数
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; // 自动重装载寄存器无缓冲
cfg_res = HAL_TIM_Base_Init(&htim1); // 初始化
if (cfg_res != HAL_OK)
{
return cfg_res;
}
cfg_tim1.OCMode = TIM_OCMODE_PWM1; // 输出配置为模式1
cfg_tim1.OCPolarity = TIM_OCPOLARITY_HIGH; // OC通道输出高电平有效
cfg_tim1.OCFastMode = TIM_OCFAST_DISABLE; // 输出快速使能关闭
cfg_tim1.OCNPolarity = TIM_OCNPOLARITY_HIGH; // OCN通道输出高电平有效
cfg_tim1.OCNIdleState = TIM_OCNIDLESTATE_RESET; // 空闲状态OC1N输出低电平
cfg_tim1.OCIdleState = TIM_OCIDLESTATE_RESET; // 空闲状态OC1输出低电平
cfg_tim1.Pulse = 0; // CC1值为0,相当于复位PWM输出
cfg_res = HAL_TIM_OC_ConfigChannel(&htim1, &cfg_tim1, TIM_CHANNEL_1); // OC1配置通道
if (cfg_res != HAL_OK) return cfg_res;
cfg_res = HAL_TIM_OC_Start(&htim1, TIM_CHANNEL_1); // OC1通道开始输出
if ( cfg_res != HAL_OK) return cfg_res;
return cfg_res;
}
HAL_StatusTypeDef TIM1_PWM_Start(uint32_t duty)
{
HAL_StatusTypeDef cfg_res = HAL_OK;
cfg_tim1.Pulse = duty - 1; // CC1值为duty,PWM平均=duty/period
cfg_res = HAL_TIM_OC_ConfigChannel(&htim1, &cfg_tim1, TIM_CHANNEL_1); // OC1配置通道
if (cfg_res != HAL_OK) return cfg_res;
cfg_res = HAL_TIM_OC_Start(&htim1, TIM_CHANNEL_1); // OC1通道开始输出
if ( cfg_res != HAL_OK) return cfg_res;
return cfg_res;
}
HAL_StatusTypeDef TIM1_PWM_Stop(void)
{
return HAL_TIM_OC_Stop(&htim1, TIM_CHANNEL_1);
}
void TIM1_PWM_Output(const uint8_t duty_percent)
{
uint8_t tmp_duty = 0;
uint32_t duty = 0;
tmp_duty = duty_percent;
if(duty_percent > 100) tmp_duty = 100;
duty = (uint32_t)(tmp_duty * PWM_PERIOD / 100.0F + 0.5F) + 1;
TIM1_PWM_Start(duty);
}
在 py32_f0xx_hal_msp.c 中增加对复用管脚的初始化
/**
******************************************************************************
* @file py32f0xx_hal_msp.c
* @author MCU Application Team
* @Version V1.0.0
* @Date 2020-10-19
* @brief This file provides code for the MSP Initialization
* and de-Initialization codes.
******************************************************************************
*/
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/**
* Initializes the Global MSP.
*/
void HAL_MspInit(void)
{
BSP_LED_Init(LED_GREEN);
BSP_PB_Init(BUTTON_USER,BUTTON_MODE_GPIO);
}
/**
* -----------------------------------------------------------------------
* @name : void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
* @brief : 功能描述 :初始化TIM相关MSP
* @param : [in] htim, TIM handler pointer
* @retval : void
* @remark :
* -----------------------------------------------------------------------
*/
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM16)
{
__HAL_RCC_TIM16_CLK_ENABLE(); //使能TIM16时钟
HAL_NVIC_SetPriority(TIM16_IRQn, 0, 8); //设置中断优先级
HAL_NVIC_EnableIRQ(TIM16_IRQn); //使能TIM1中断
return;
}
if(htim->Instance == TIM1)
{
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_TIM1_CLK_ENABLE(); // TIM1 时钟使能
__HAL_RCC_GPIOA_CLK_ENABLE(); // GPIOA 时钟使能
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用
GPIO_InitStruct.Pull = GPIO_PULLDOWN; // 下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速
GPIO_InitStruct.Pin = GPIO_PIN_3; // PA3 初始化成 TIM1_CH1
GPIO_InitStruct.Alternate = GPIO_AF13_TIM1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}
在 py32_f0xx_hal_it.c 中增加对中断服务程序的重定向
extern TIM_HandleTypeDef htim16;
extern TIM_HandleTypeDef htim1;
...
...
// Override. For TIM1_PWM service
void TIM1_BRK_UP_TRG_COM_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim1);
}
...
...
void TIM16_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim16);
}
在 main.c 中添加对这些功能的调用
/**
* -------------------------------------------------------------------------
* @file : int main(void)
* @brief : main函数
* @param : 无
* @retval : 无限循环,无返回值
* @remark :
* -------------------------------------------------------------------------
*/
int main(void)
{
HAL_Init(); // systick初始化
SystemClock_Config(); // 配置系统时钟
if(USART_Config() != HAL_OK) Error_Handler();
printf("[SYS_INIT] Debug port initilaized.\r\n");
if(GPIO_Config() != HAL_OK) Error_Handler();
printf("[SYS_INIT] Board LED initilaized.\r\n");
if(TIM16_Config() != HAL_OK) Error_Handler();
printf("[SYS_INIT] Timer initialized.\r\n");
if (TIM16_Start() != HAL_OK) Error_Handler();
printf("[SYS_INIT] Timer started.\r\n");
/**
* Added for TIM1 PWM output
* By HardCoder_LLuo@2023-12-07
*/
if(TIM1_PWM_Config() != HAL_OK) Error_Handler();
printf("[SYS_INIT] PWM initialized.\r\n");
printf("\r\n+---------------------------------------+"
"\r\n| PY32F003 MCU is ready. |"
"\r\n+---------------------------------------+"
"\r\n");
if (DBG_UART_Start() != HAL_OK) Error_Handler();
TIM1_PWM_Output(0);
// while(BSP_PB_GetState(BUTTON_USER) != 0){}
// TIM1_PWM_Output(20);
while(BSP_PB_GetState(BUTTON_USER) != 0){}
TIM1_PWM_Output(50);
//
// while(BSP_PB_GetState(BUTTON_USER) != 0){}
// TIM1_PWM_Output(60);
//
// while(BSP_PB_GetState(BUTTON_USER) != 0){}
// TIM1_PWM_Output(80);
//
// while(BSP_PB_GetState(BUTTON_USER) != 0){}
// TIM1_PWM_Output(100);
while (1)
{
/**
* For testing GPIO output
* 2023-11-24
* Hard coder Luoyuan
*/
#if(0)
// Toggle LED3 in TIM16 IT service procedure instead.
HAL_Delay(1000);
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
#endif
}
}
代码新增部分实现对 TIM1_PWM 的初始化,然后将 TIM1 的 CHANNEL_1 拉低,等待按压一下开发板上的按钮,设置不同的 PWM 占空比,在示波器上观察这个输出通道的波形。
先前开发的功能仍然在使用,设置不同的中断优先级,避免中断之间的冲突。
实验结果
- 启动后,在 XCOM 的显示如下图。
- 启动信息显示正常,连续发送字符串,MCU 回显正常。
- 板载的 LED 以 1Hz 的频率在翻转,说明原有的 TIM16 中断运行正常。
- 查看按钮按下后,测量 GND 和 PA3 的波形如下,按顺序分别是占空比为 20%, 50%, 60%, 80% 和 100% 时的输出波形。
总结
利用 PY32F003 实现PWM输出的步骤:
- 选择时钟
- 设置端口复用
- 初始化高级定时器 TIM1
- 为 TIM1 的 OCx(x取值0,1,2,3) 通道设定占空比的值
- 启动(重启)这个通道
注意事项
- 在 HAL_TIM_Base_MspInit 函数中,定义输出通道对应的 IO 管脚时,应根据外部硬件电路是否有上拉或者下拉来设置 GPIO_InitStructure.Pull 属性的值。在这里的实验当中,PWM输出的初始状态是全低电平(PWM关断的状态),并且这个 IO 管脚外部没有上拉电阻,那么这个 GPIO_InitStructure.Pull 属性就应该设置成 PULLDOWN,如果设置成了 PULLUP(默认值)就会在 PWM 通道进行初始化前产生一个高电平脉冲。在示波器上观察,这个高电平脉冲的宽度大约有 50us,如果是驱动一个灵敏的外部设备,例如一盏 LED 灯的话,足以让这盏灯突然闪亮一下了(有网友取名为“零点爆闪”现象,如果做到产品中,那是很 LOW 的哦)。相反,如果外部有上拉电阻,并且是使用 PWM2 模式输出的话,就应该将其初始化成 PULLUP。
- 想要使 PWM 输出变成零,设置 PWM CCx 输出为0 和 PWM_PERIOD,都可以。这是因为比较器比较 0 和比较 PWM_PERIOD,两者之差一拍,就都能产生 Elapsed 的结果。
- 和通用定时器相同,TIM1 的Period 和 Prescaler 决定了 PWM 波形的频率。零输出和满输出时,PWM 频率体现不出来。
- TIM1 可以同时驱动 4 个通道的 PWM 输出,这里只列出了一个。
谬误之处,欢迎指正。