前言:在前一篇《STM32WL开发之易智联LORA评估板上按键KEY的配置与应用》中已经基于易智联的LoRa评估板LM401-Pro-Kit介绍了在其Demo例程上如何实现按键KEY控制LED闪烁的功能,本文将介绍如何基于LoRa评估板和Demo例程实现定时器的配置与应用。
基础知识:STM32的外设与总线
STM32是意法半导体公司(ST)设计推出的一款以ARM Cortex-M 为内核的32位单片机。所以可以看出STM32的内核是ARM公司的,但ARM公司只设计内核不生产芯片,它主要做的事情就是将有关内核的技术授权给各半导体厂商生产使用例如ST、TI,大多数厂商都是以这个内核为基础去设计自己芯片上外设如SRAM、ROM、FLASH、USART、GPIO等,然后把这些外设集成到一个硅片上,就组成了我们现在所使用的芯片。所以STM32 芯片内部也大致的分为两部分:内核+片上外设。
芯片内部架构如下图所示:
说到这里我们知道芯片内部内核和外设分别由两个公司设计的,那他们要做到协同高效的工作,就必须有沟通的桥梁,这个桥梁就是总线。一般的计算机有五大组成部分运算器、控制器、存储器、输入设备、输出设备,它们之间的通信靠的就是总线。单片机就是一个小型的计算机,所以他内部的连接通信也是靠总线。
单片机的外设一般分为内部外设和外部外设,内部外设就是集成到芯片内部的,外部外设是指单片机扩展的功能。单片机内部的外设一般包括:串口控制模块,SPI模块,I2C模块,A/D模块,PWM模块,CAN模块,EEPROM,比较器模块等等,它们都集成在单片机内部,有相对应的内部控制寄存器,可通过单片机指令直接控制。外设指的是单片机外部的外围功能模块,比如键盘控制芯片,液晶,A/D转换芯片等等。外设可通过单片机的I/O,SPI,I2C等总线控制。
STM32芯片内部有多达11条总线,其中与外设相关的主要是AHB总线和APB总线。AHB总线是一种高性能系统总线,主要用于高性能模块(如CPU、DMA和DSP等)之间的连接。APB是外设总线,主要用于低带宽的周边外设之间的连接,又分为APB1和APB2总线。APB1上面连接的是低速外设,包括电源接口、备份接口、CAN、USB、I2C1、I2C2、UART2、UART3等,APB2上面连接的是高速外设包括 UART1、SPI1、Timer1、ADC1、ADC2、所有普通IO口(PA~PE)、第二功能 IO 口等。APB总线最终连接到AHB总线上。
STM32外设的操作步骤
STM32的外设都在AHB、APB1、APB2总线上,其操作步骤大致如下:
1、必须对设备进行使能。
2、设置外设的时钟
3、操作外设
具体操作包括:关联GPIO与外设、配置GPIO与外设、DMA或中断的配置与使能等。
Timer定时器
单片机中的Timer很多时候完整的表达是 定时/计数器 ,某种意义上来说Timer最基本的功能就仅仅只是计数而已,定时这个功能也只是在计数这个基础功能之上实现的。
定时器基本功能的流程如下:
时钟系统
STM32的时钟系统是由振荡器(信号源)、定时唤醒器、分频器等组成的电路。常用的信号源有晶体振荡器和RC振荡器。就像人需要有心跳和脉搏一样,时钟是嵌入式系统的脉搏,处理器内核在时钟驱动下完成指令执行,状态变换等动作;外设部件在时钟的驱动下完成各种工作,比如串口数据的发送、A/D转换、定时器计数等。
STM32本身非常复杂,外设非常多,对于同一个电路,时钟越快功耗越大,同时抗电磁干扰能力也会越弱,而且也并不是所有的外设都需要系统时钟这么高的频率,比如看门狗以及RTC只需要几十k的时钟即可,所以对于较复杂的MCU一般采用多时钟源的方法来解决这个问题。
STM32WL的时钟系统框图如下:
如上图所示,STM32WL中,有6重要的时钟源,为HSI、HSE、LSI、LSE、MSI、PLL,注意STM32WL相比STM32Fx等系列芯片,在PLL这里精简去了PLLISAI1和PLLSAI2。
从时钟频率来分可以分为高速时钟源和低速时钟源;从时钟频率来分可以分为高速时钟源和低速时钟源;从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源。
LSI 是低速内部时钟,RC 振荡器,频率为 32kHz 左右。供独立看门狗、 RTC 和 LCD使用。
LSE 是低速外部时钟,接频率为 32.768kHz 的石英晶体。这个主要是 RTC 的时钟源。
HSE 是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz-48MHz。我们的开发板接的是 8MHz 的晶振。 HSE 也可以直接做为系统时钟或者 PLL 输入。
HSI 是高速内部时钟,RC 振荡器, 频率为 16MHz。可以直接作为系统时钟或者用作PLL 输入。
MSI 时钟信号由内部 RC 振荡器产生。其频率范围可通过时钟控制寄存器(RCC_CR)中的 MSIRANGE[3:0]位进行调整。
PLL 为锁相环倍频输出。STM32WL只有一个PLL时钟源, PLL可由 HSE、 HIS 或者 MSI 提供时钟信号,并具有三个不同的输出时钟:
1) 输出PLLRCLK,用于生成高速的系统时钟(SYSCLK,最高48MHz);
2)输出PLLQCLK,可为SPI、RNG等提供时钟源;
3)输出PLLPCLK,可为ADC等提供时钟源。
PLL时钟图如下:
从图中可以看出,主 PLL 的时钟源要先经过一个分频系数为 M 的分频器,然后经过倍频系数为 N 的倍频器,出来之后还需要经过分频系数为 R(输出 PLLR 时钟)、或者 P(输出 PLLP时钟)、或者 Q(输出 PLLQ 时钟),最后才生成最终的主 PLL 时钟。
定时器时钟源
定时器要实现计数必须有个时钟源,基本定时器时钟只能来自内部时钟,高级控制定时器和通用定时器还可以选择外部时钟源或者直接来自其他定时器等待模式。我们可以通过 RCC 专用时钟配置寄存器(RCC_DCKCFGR)的 TIMPRE位设置所有定时器的时钟频率,我们一般设置该位为默认值 0,这时的最大定时器时钟为 48MHz,即基本定时器的内部时钟(CK_INT)频率为 48MHz。定时器时钟源一般来说都会采用内部时钟,高级定时器挂载在APB1总线,基本和通用定时器都是挂载在APB2总线下的,不过最后经过一系列倍频什么的都是48Mhz,计数器的最终的频率还需要经过PSC预分频计算才能得到。
如上面的通用定时器框图,经过时钟源后就进入了PSC预分频器,就是CK_CNT,CK_CNT用来驱动计数器计数。 PSC 是16 位的预分频器,可以对定时器时钟进行 1~65536 之间的任何一个数进行分频,具体计算方式为:CK_CNT=48Mhz/(PSC+1)
再之后就进入计数器CNT,CNT也是16位的计数器,最大计数值为65535,当计数值达到自动重装载寄存器的时候就产生更新事件,并重新进行计数。当然自动重装载寄存器的值也是我们设置的,自动重装载寄存器ARR也是一个16位的寄存器,当计数值达到这个值的时候,就会产生更新事件,比如中断事件,触发其他外设的事件,或者复位计数器的事件。所以最终定时时间为:(PSC+1)x(ARR+1)/ 48000000 秒
计数器
基本定时器计数过程主要涉及到三个寄存器内容,分别是计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR),这三个寄存器都是 16 位有效数字,即可设置值为 0至 65535。
定时器参数设置
先看一下STM32WL中的定时器初始化的数据结构,如下:
/**
* @brief TIM Time base Configuration Structure definition
*/
typedef struct
{
uint32_t Prescaler; // 预分频器
uint32_t CounterMode; // 计数模式
uint32_t Period; // 定时器周期
uint32_t ClockDivision; // 时钟分频
uint32_t RepetitionCounter; // 重复计算器
uint32_t AutoReloadPreload; // 预装载
} TIM_Base_InitTypeDef;
下面看一下定时器设置中的这几个主要参数:
Prescaler 预分频系数
定时器预分频器设置,时钟源经该预分频器才是定时器时钟,它设定TIMx_PSC 寄存器的值。可设置范围为 0 至 65535,实现 1至 65536 分频。为啥要搞一个预分频器,那是因为系统时钟频率太快了,比如STM32WL的时钟频率是48MHZ,这一般的定时器可顶不住这么快的速度,所以分频一下,让主频分给定时器的时钟频率少一点,仅此而已。
输入给计数器的信号频率 = 输入到预分频器的信号频率 / (预分频系数 + 1)
该值为0相当于对输入信号1分频,也就是不分频;该值为1相当于对输入信号2分频,依此类推;在STM32系列中该值常见取值范围为0~65535;
CounterMode 计数模式
计数模式常见的就是 Up(向上计数模式),这个模式下计数器初始值为0,计数到下面的 Period+1 算作一个周期;其它可选值 Down(和UP反一反),Up/down(第一个周期是UP、第二个周期是Down,反复进行);
Period 计数周期
定时器周期,实际就是设定自动重载寄存器的值。以 Up模式 为例,在此模式下计数器从0开始计数,每一个信号计数值+1, 当计数Period次之后计数器值为Period,当再有一个信号进入后就算计数满一个周期 ,可以触发溢出中断或是其它动作;或者举个更通俗的例子,举个例子,你要往桶里面放水,水满了之后把他倒掉。那水满需要多少水呢?就给他设定一个值,滴水滴100000滴才满,拿去倒掉。倒掉之后,在重新设置滴10000滴,满了再倒掉…
在STM32系列中该值常见取值范围为0~65535;
AutoReloadPreload 预装载
计数器再计满一个周期之后会自动重新计数,也就是默认会连续运行。这连续运行过程中如果你修改了Period,那么根据当前状态的不同有可能发生超出预料的过程。如果使能了AutoReloadPreload,那么你对Period的修改将会在完成当前计数周期后才更新;
Timer作为定时使用时信号来源通常使用内部时钟,当我们确定Timer的时钟信号频率后根据此设定实现定时某一时间周期所需要的参数了,这里主要涉及Prescaler和Period两个参数。Prescaler是对输入定时器的时钟信号进行分频,Period为一个周期中的计数值。
TIM_ClockDivision 时钟分频
设置定时器时钟 CK_INT 频率与数字滤波器采样时钟频率分频比,基本定时器没有此功能,不用设置。
TIM_RepetitionCounter 重复计数器
重复计数器,属于高级控制寄存器专用寄存器位,利用它可以非常容易控制输出 PWM 的个数。这里不用设置.
定时器周期计算
定时事件生成时间主要由 TIMx_PSC 和 TIMx_ARR两个寄存器值决定,这个也就是定时器的周期。比如我们需要一个 1s周期的定时器,具体这两个寄存器值该如何设置?
假设,我们先设置 TIMx_ARR寄存器值为 9999,即当 TIMx_CNT从 0开始计算,刚好等于 9999时生成事件,总共计数 10000次,那么如果此时时钟源周期为 100us即可得到刚好 1s的定时周期。
接下来问题就是设置 TIMx_PSC寄存器值使得 CK_CNT 输出为 100us 周期(10000Hz)的时钟。预分频器的输入时钟 CK_PSC为 90MHz,所以设置预分频器值为(9000-1)即可满足。
根据上面的内容Timer每计数一次的时间为 1秒 ÷ (时钟频率 ÷ (Prescaler + 1)) ,定时器计数满一个周期的时间为 计数一次时间 × (Period + 1) 秒 。所以定时时间计算公式如下: 定时时间 = (Prescaler + 1) × (Period + 1) ÷ 时钟频率 单位:秒
STM32单片机中有很多个Timer,通常TIM6和TIM7称为基础定时器、TIM1和TIM8称为高级定时器、其余的被称为通用定时器。基础定时器基本上只有定时功能;通用定时器在定时基础上还支持外部输入捕获、比较、PWM输出等功能;高级定时器在通用定时器的基础上只要增加了用于电机控制等功能。
STM32WL有1个高级定时器TIM1,3个通用定时器TIM2/TIM16/TIM17和三个低功耗定时器LPTIM1/LPTIM2/LPTIM3.
定时器设置的一般步骤
设置通用定时器,并产生相应中断,主要分为以下几个步骤(以TIM2为例)
功能描述
此例将实现配置TIM2定时器外设以生成一秒时基的中断,并进行闪灯指示。
定时器配置分析
首先给定时器分频。在此示例中,系统主时钟是48MHz, TIM2输入时钟(TIM2CLK)设置为APB1时钟(PCLK1),由于APB1预分频器等于1。
TIM2CLK=PCLK1
PCLK1=HCLK
=>TIM2CLK=HCLK=SystemCoreClock
假设把TIM2的计数器时钟设置为10KHZ,预分频器Prescaler的计算如下:
预分频器Prescaler =(TIM2CLK/TIM2计数器时钟)-1
预分频器Prescaler =(SystemCoreClock/10KHz)-1 = 48000000/10000 – 1 = 4799
对于STM32WLxx设备,SystemCoreClock设置为48MHz。
其次,设置计数器的重载值,这个是跟具体要定时多长时间有关的。由于10KHz就是一秒钟10000次,而实际是想要一秒一次的1Hz计数速率,那么只能把TIM2ARR寄存器值也即是计数周期Period设置为10000-1,也就是说计数器计数10000-1次后产生溢出中断。
更新速率 =TIM2计数器时钟/(周期Period + 1)=1赫兹,
所以TIM2会每1秒产生一个中断
设计概要
当计数器值达到自动重新加载寄存器值时,TIM更新产生中断, 此时程序控制引脚PB5所连接的LED1指示灯状态被切换。 由于是两秒完成一次亮和灭,所以LED1在以下频率闪烁:0.5Hz。如果出现错误的情况,PB3所连接的LED3导通。
实现代码
1、系统时钟配置
首先看一下系统的时钟配置,通过SystemClock_Config函数实现:
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure LSE Drive Capability
* 对LSE的外部晶振驱动能力的一个配置,若不起振等异常情况请调整这里,不使用外部时钟则忽略这里
*/
//HAL_PWR_EnableBkUpAccess();
//__HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
/** Configure the main internal regulator output voltage
* 配置内部稳压器的输出电压
*/
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); // 配置调压器的输出电压级别,级别数值越小工作频率越高
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_MSI; // 选择MSI作为时钟源
RCC_OscInitStruct.MSIState = RCC_MSI_ON; // 打开
RCC_OscInitStruct.MSICalibrationValue = RCC_MSICALIBRATION_DEFAULT; // MSI校准调整值,选择默认(可不写)
RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_11; // 时钟频率选择 48MHz
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE; // 不使用PLL锁相环
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) // 选择时钟源,设置其时钟频率,并等待时钟源切换成功
{
Error_Handler();
}
/** Configure the SYSCLKSource, HCLK, PCLK1 and PCLK2 clocks dividers
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK3|RCC_CLOCKTYPE_HCLK
|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1
|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_MSI; // 选择系统时钟源为MSI
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // 设置AHB总线时钟HCLK的分频系数为1
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // 设置APB1总线时钟PCLK1的分频系数为1
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // 设置APB2总线时钟PCLK2的分频系数为1
RCC_ClkInitStruct.AHBCLK3Divider = RCC_SYSCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) // 配置系统时钟以及系统时钟SYSCLK、AHB总线时钟HCLK、APB1总线时钟PCLK1、APB2总线时钟PCLK2的分频频率
{
Error_Handler();
}
}
2、通用定时器TIM2初始化
下面是定时器TIM2的初始化代码,在MX_TIM2_Init函数中实现:
static void MX_TIM2_Init(void)
{
/* USER CODE BEGIN TIM2_Init 0 */
/* USER CODE END TIM2_Init 0 */
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
/* USER CODE BEGIN TIM2_Init 1 */
/* USER CODE END TIM2_Init 1 */
htim2.Instance = TIM2; // 配置TIM2定时器
htim2.Init.Prescaler = PRESCALER_VALUE; // 设置用来作为TIM2时钟频率除数的预分频值,根据预设的10Khz的计数频率,那么这里就应该是 4799
htim2.Init.CounterMode = TIM_COUNTERMODE_UP; // 计数器采用向上计数的计数模式
htim2.Init.Period = PERIOD_VALUE; // 设置在下一个更新事件装入活动的自动重装载寄存器周期的值,因为想要1s的计数频率,所以根据10KHz的设计频率,计数周期就要是9999
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 设置时钟分割:TDTS = Tck_tim
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; // 自动重装载使能中关闭寄存器的缓冲器开关,使寄存器无缓冲器,这样若要改变Period的值时会在下个计数周期立即生效,而不用等下下个
if (HAL_TIM_Base_Init(&htim2) != HAL_OK) // 根据TIM_TimeBaseInitStruct中指定的参数初始化TIM2的时间基数单位
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; // TIM2定时器采用内部时钟源
if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK) // 配置TIM2的时钟源
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK) // 配置定时器的主输出模式
{
Error_Handler();
}
/* USER CODE BEGIN TIM2_Init 2 */
/* USER CODE END TIM2_Init 2 */
}
定时器的MSP初始化函数,这个时在HAL_TIM_Base_Init函数中被调用的:
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base)
{
if(htim_base->Instance==TIM2)
{
/* USER CODE BEGIN TIM2_MspInit 0 */
/* USER CODE END TIM2_MspInit 0 */
/* Peripheral clock enable */
__HAL_RCC_TIM2_CLK_ENABLE(); // 使能TIM2的时钟
/* TIM2 interrupt Init */
HAL_NVIC_SetPriority(TIM2_IRQn, 3, 0); // 配置TIM2定时器中断的抢先优先级为3,响应优先级为0
HAL_NVIC_EnableIRQ(TIM2_IRQn); // 启用TIM2定时器的中断
/* USER CODE BEGIN TIM2_MspInit 1 */
/* USER CODE END TIM2_MspInit 1 */
}
}
3、定时器溢出中断处理
定时器TIM2的溢出中断处理函数:
void TIM2_IRQHandler(void)
{
/* USER CODE BEGIN TIM2_IRQn 0 */
/* USER CODE END TIM2_IRQn 0 */
HAL_TIM_IRQHandler(&htim2); // TIM2定时器中断处理
/* USER CODE BEGIN TIM2_IRQn 1 */
/* USER CODE END TIM2_IRQn 1 */
}
注意, HAL_TIM_IRQHandler 函数中会调用 HAL_TIM_PeriodElapsedCallback 函数,这个回调函数的实现是需要重新定义的,可以把中断发生后的闪灯操作放入其中。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) // TIM2定时器中断触发后的回调函数
{
BSP_LED_Toggle(LED_BLUE); // TIM2定时器中断到来是执行闪灯操作
}
当系统在配置或运行中出现错误时,会调用Error_Handler函数,修改该函数,把亮红灯的处理添加进去:
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
BSP_LED_On(LED_RED); // 系统运行中出现错误亮红灯
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
4、主函数
最后来看一下主函数的实现:
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 */
/* 通用定时器TIM2初始化 */
MX_TIM2_Init();
/* 初始化LED灯 */
BSP_LED_Init(LED_BLUE); // 作定时器计数溢出中断发生时闪灯用
BSP_LED_Init(LED_RED); // 作系统错误时指示用
/* USER CODE BEGIN 2 */
/*## 以中断模式开启定时器 ####################*/
/* Start Channel1 */
if (HAL_TIM_Base_Start_IT(&htim2) != HAL_OK)
{
/* Starting Error */
Error_Handler();
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
至此,完成了定时器计数功能的主要代码,完整工程代码可以从文末所附地址下载。
测试验证
编译代码后下载到LM401-Pro-Kit评估板上,可以看到蓝灯闪烁,如下图:
通过示波器查看LED灯的端口PB5的电平输出,如下图:
由上图中的测量结果可知一个周期是2秒,高低电平各持续1秒,定时器时间满足设计预期。
基础知识:PWM介绍
1. PWM概述
PWM,英文名Pulse Width Modulation,是脉冲宽度调制缩写,它是通过对一系列脉冲的宽度进行调制,等效出所需要的波形(包含形状以及幅值),对模拟信号电平进行数字编码,也就是说通过调节占空比的变化来调节信号、能量等的变化。占空比就是指在一个周期内,信号处于高电平的时间占据整个信号周期的百分比,例如方波的占空比就是50%。PWM的功能有很多种,比如控制呼吸灯、控制直流电机或者舵机等驱动原件等等,是单片机的一个十分重要的功能。
第一个PWM波,周期为10ms,高电平的时间为4ms,所以占空比为40%,同理第二个PWM波为60%,第三个为80%。
PWM的频率:PWM的频率的整个周期的倒数,所以说上图PWM的周期为1/0.01,也就是100HZ。改变PWM的频率是通过改变整个的周期实现的。所以通过改变高低电平总共的时间、改变高电平占总周期的比例就可以实现任意频率、任意占空比的PWM波。
PWM的用途和优点:可以用在比如控制呼吸灯、电机调速、功率调制、PID调节、通信等等,配置简单、抗干扰能力强,从处理器到被控系统信号都是数字形式的,无需进行数模转换。并且让信号保持为数字形式可将噪声影响降到最小,噪声只有在强到足以将逻辑1改变为逻辑0或将逻辑0改变为逻辑1时,也才能对数字信号产生影响,这是PWM用于通信的主要原因。
2、STM32的管脚复用
STM32没有专门的PWM引脚,所以使用IO口的复用模式。比如若要使用STM32WL的TIM1高级定时器进行PWM的输出,那么先在Datasheet数据手册中的Alternatefunction mapping框图中查找TIM1各通道分别复用的GPIO为PA8、PA9、PA10、PA11,如下图:
3、STM32输出PWM原理
PWM脉冲宽度调制模式可以生成一个信号,该信号频率由TIMx_ARR 寄存器值决定,其占空比则由TIMx_CCRx 寄存器值决定。
从上图可以看出,当CCR寄存器和CNT计数器数值一样时,会产生动作(改变通道对应的GPIO输出电平)。由于CNT溢出时,重载值由TIMx_ARR寄存器值决定的。所以说TIMx_ARR寄存器值决定周期,而TIMx_CCRx寄存器值决定CNT溢出时,经过多久会产生动作(改变通道对应的GPIO输出电平),也就是决定了占空比。
那么在STM32单片机中,可以使用定时器的输出比较功能来产生PWM波,也即是说PWM模式可以产生一个由TIMx_ARR寄存器确定频率、由TIMx_CCRx寄存器确定占空比的信号。以向上计数为例,重载值为ARR,比较值为CRRx,则其框图如下图所示:
看上图,横坐标是时间变量,纵坐标是CNT计数值,CNT计数值随着时间的推进会不断经历从0到ARR,清零复位再到ARR的这一过程。这之中还有一个数值是CCRx即比较值,通过比较值和输出配置可以使之输出高低电平逻辑,这样就产生了PWM波形。
所以,在0-t1段,定时器计数器TIMx_CNT值小于CCRx值,输出低电平。t1-t2段,定时器计数器TIMx_CNT值大于CCRx值,输出高电平。当TIMx_CNT值达到ARR时,定时器溢出,重新向上计数...循环此过程至此一个PWM周期完成。
上图更加形象的说明了,信号频率由TIMx_ARR 寄存器值决定。占空比则由TIMx_CCRx 寄存器值决定。通过调节ARR的值可以调节PWM的周期,调节CCRx的值大小可以调节PWM占空比。
我们以通道1为例,详细讲解PWM的工作过程,如下图所示:
从最左边进入的是时钟源,由内部时钟(CNT)或者外部触发时钟(ETRF)输入,进入输入模式控制器,通过OCMR1寄存器的OC1M[2:0]位来配置PWM模式,之后进入一个选择器,由CCER寄存器的CC1P位来设置输出极性,最后由CCER寄存器的CC1E位来使能输出,然后通过OC1来输出PWM波。
CCR1: 捕获比较(值)寄存器(还有CCR2、CCR3、CCR4): 该寄存器用于设置比较值。
CCMR1: 捕获比较模式寄存器,其中的OC1M[2:0]位,对于PWM方式下,用于设置PWM模式1或者PWM模式2,这两种模式的区别可以简单理解为:PWM模式1的情况下,当前值小于比较值为有效电平;PWM模式2的情况下,当前值大于比较值为有效电平。
CCxS[1:0]位用于定义通道的方向。
CCER: 捕获比较使能寄存器。其中的CC1P位用于设置输入/捕获1输出极性。0:高电平有效,1:低电平有效。
CCER: 捕获比较使能寄存器。其中的CC1E位用于输入/捕获1输出使能。0:关闭,1:打开。
功能描述
配置外设定时器TIM1,输出PWM波形
定时器配置分析
首先,为定时器TIM1分频。对于STM32WLxx设备,SystemCoreClock设置为48MHz。在本例中,TIM1输入时钟(TIM1CLK)设置为APB1时钟(PCLK1), 由于APB1预分频器等于1。
TIM1CLK=PCLK1
PCLK1=HCLK
=>TIM1CLK=HCLK=SystemCoreClock
本例假设我们需要为TIM1计数器分得1MHz的时钟频率,那么为了获得这1MHz的TIM1计数器时钟,预分频器Prescaler的计算如下:
预分频器Prescaler =(TIM1CLK/TIM1计数器时钟)-1
预分频器Prescaler =((SystemCoreClock)/1MHz)-1
其次,设置计数器的重载值。本例假设我们是需要25KHz的更新速率,或者说输出时钟,而定时器TIM1分得的频率是1MHz,也即是1秒钟1000000次,那么为了得到25KHZ的TIM1输出时钟,那么就需要 1000000/25000=40, 也就是说计数器累计到40就要产生一个溢出,所以周期period( ARR)的值计算如下:
ARR=(TIM1计数器时钟/TIM1输出时钟)-1 = 39
设计概要
本例中我们要求达到如下的占空比:
TIM1_1占空比=(TIM1_CCR1/TIM1_ARR+1)*100=50%
TIM1_2占空比=(TIM1_CCR2/TIM1_ARR+1)*100=37.5%
TIM1_3占空比=(TIM1_CCR3/TIM1_ARR+1)*100=25%
TIM1_4占空比=(TIM1_CCR4/TIM1_ARR+1)*100=12.5%
注意:如果要使用HAL_Delay(),则必须小心,此函数提供精确的延迟(以毫秒为单位)
基于在系统ISR中递增的变量. 这意味着如果从调用HAL_Delay()一个外围ISR进程,那么系统中断必须有更高的优先级(数字上更低)比外围中断。 否则调用者ISR进程将被阻塞。要更改时钟的中断优先级,您必须使用HAL_NVIC_SetPriority()函数。
实现代码
1、系统时钟配置
首先看一下系统时钟的配置,在函数中:
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
* 配置内部稳压器的输出电压
*/
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); // 配置调压器的输出电压级别,级别数值越小工作频率越高
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_MSI; // 选择MSI作为时钟源
RCC_OscInitStruct.MSIState = RCC_MSI_ON; // 打开
RCC_OscInitStruct.MSICalibrationValue = RCC_MSICALIBRATION_DEFAULT; // MSI校准调整值,选择默认(可不写)
RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_11; // 时钟频率选择 48MHz
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE; // 不使用PLL锁相环
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) // 选择时钟源,设置其时钟频率,并等待时钟源切换成功
{
Error_Handler();
}
/** Configure the SYSCLKSource, HCLK, PCLK1 and PCLK2 clocks dividers
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK3|RCC_CLOCKTYPE_HCLK
|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1
|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_MSI; // 选择系统时钟源为MSI
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // 设置AHB总线时钟HCLK的分频系数为1
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // 设置APB1总线时钟PCLK1的分频系数为1
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // 设置APB2总线时钟PCLK2的分频系数为1
RCC_ClkInitStruct.AHBCLK3Divider = RCC_SYSCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) // 配置系统时钟以及系统时钟SYSCLK、AHB总线时钟HCLK、APB1总线时钟PCLK1、APB2总线时钟PCLK2的分频频率
{
Error_Handler();
}
}
2、高级定时器TIM1初始化
下面对高级定时器TIM1进行初始化,在函数MX_TIM1_Init如中实现,如下:
static void MX_TIM1_Init(void)
{
/* USER CODE BEGIN TIM1_Init 0 */
/* USER CODE END TIM1_Init 0 */
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_OC_InitTypeDef sConfigOC = {0};
TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};
/* USER CODE BEGIN TIM1_Init 1 */
/* USER CODE END TIM1_Init 1 */
htim1.Instance = TIM1; // 配置TIM1定时器
htim1.Init.Prescaler = PRESCALER_VALUE; // 设置用来作为TIM1时钟频率除数的预分频值,根据预设的1MHz的计数频率,那么这里就应该是 47
htim1.Init.CounterMode = TIM_COUNTERMODE_UP; // 计数器采用向上计数的计数模式
htim1.Init.Period = PERIOD_VALUE; // 设置在下一个更新事件装入活动的自动重装载寄存器周期的值,因为想要25KHz的计数频率,所以用定时器分得的1MHz除以25KHz再减一,计数周期就要是39
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 设置时钟分割/分频:TDTS = Tck_tim
htim1.Init.RepetitionCounter = 0; // 重复计数器,属于高级控制寄存器专用寄存器位,利用它可以非常容易控制输出 PWM 的个数。该值等于0表示不重复计数,计数器达到Period的值即溢出重新开始,该值若是1则达到Period后无中断或电平反转等溢出更新动作,而是再重复计数一次才动作,同理值为2是重复两次
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; // 自动重装载使能中关闭寄存器的缓冲器开关,使寄存器无缓冲器,这样若要改变Period的值时会在下个计数周期立即生效,而不用等下下个,这对PWM很适合
if (HAL_TIM_PWM_Init(&htim1) != HAL_OK) // PWM初始化
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterOutputTrigger2 = TIM_TRGO2_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
sConfigOC.OCMode = TIM_OCMODE_PWM1; // PWM模式采用PWM1, 此模式下,如果是向上计数,那么当计时器值CNT小于比较器设定的值CCR时则TIMx输出脚此时输出的电平为有效电平,结合OCPolarity配置为高极性可得出此情况下CNTCRRx为无效电平,电平值是低电平。
sConfigOC.Pulse = PULSE1_VALUE; // 脉冲值,本例中就是高电平持续的定时器时钟周期数。所以PWM占空比只跟计数器周期Period和脉冲值Pulse有关。比如Peirod=1000,Pluse=200就表示一个周期1000个定时器时钟周期,有效高电平占200个,占空比20%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; // 输出高极性,说明当电平位有效时是高电平。反之,如果设置的是低极性,那么当电平位有效时就是低电平。结合上面的向上计数方式和PWM1模式设置可知,此例输出的OCx波形为凹形,因为向上计数肯定是从0开始的,开始时CNT必然小于CCR,电平有效,而又设置高电平有效故PWM波形开始时必是高电平
sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH; // OCxN输出高极性,此种情况下时凸形,与上面相反
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1) != HAL_OK) // PWM的通道1配置(通道1的脉冲值是PULSE1_VALUE,决定占空比)
{
Error_Handler();
}
sConfigOC.Pulse = PULSE2_VALUE; // 通道2的脉冲值,用以设置通道2的占空比
if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_2) != HAL_OK) // PWM的通道2配置
{
Error_Handler();
}
sConfigOC.Pulse = PULSE3_VALUE; // 通道3的脉冲值,用以设置通道3的占空比
if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_3) != HAL_OK) // PWM的通道3配置
{
Error_Handler();
}
sConfigOC.Pulse = PULSE4_VALUE; // 通道4的脉冲值,用以设置通道4的占空比
if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_4) != HAL_OK) // PWM的通道4配置
{
Error_Handler();
}
sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE;
sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
sBreakDeadTimeConfig.DeadTime = 0;
sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;
sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
sBreakDeadTimeConfig.BreakFilter = 0;
sBreakDeadTimeConfig.BreakAFMode = TIM_BREAK_AFMODE_INPUT;
sBreakDeadTimeConfig.Break2State = TIM_BREAK2_DISABLE;
sBreakDeadTimeConfig.Break2Polarity = TIM_BREAK2POLARITY_HIGH;
sBreakDeadTimeConfig.Break2Filter = 0;
sBreakDeadTimeConfig.Break2AFMode = TIM_BREAK_AFMODE_INPUT;
sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;
if (HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sBreakDeadTimeConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN TIM1_Init 2 */
/* USER CODE END TIM1_Init 2 */
HAL_TIM_MspPostInit(&htim1); // 该函数中是四个通道到GPIO引脚的配置
}
在上面的MX_TIM1_Init函数中的HAL_TIM_PWM_Init函数会调用到HAL_TIM_PWM_MspInit函数,这个函数是一个重定义函数,主要是开启TIM1的时钟,如下:
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef* htim_pwm)
{
if(htim_pwm->Instance==TIM1)
{
/* USER CODE BEGIN TIM1_MspInit 0 */
/* USER CODE END TIM1_MspInit 0 */
/* Peripheral clock enable */
__HAL_RCC_TIM1_CLK_ENABLE(); // 开启外设TIM1的时钟
/* USER CODE BEGIN TIM1_MspInit 1 */
/* USER CODE END TIM1_MspInit 1 */
}
}
在上面MX_TIM1_Init函数中也会调用到HAL_TIM_MspPostInit函数,这个HAL_TIM_MspPostInit函数主要是对思路PWM的输出引脚进行初始化配置,如下:
void HAL_TIM_MspPostInit(TIM_HandleTypeDef* htim)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(htim->Instance==TIM1)
{
/* USER CODE BEGIN TIM1_MspPostInit 0 */
/* USER CODE END TIM1_MspPostInit 0 */
__HAL_RCC_GPIOA_CLK_ENABLE();
/**TIM1 GPIO Configuration
PA11 ------> TIM1_CH4
PA10 ------> TIM1_CH3
PA9 ------> TIM1_CH2
PA8 ------> TIM1_CH1
*/
GPIO_InitStruct.Pin = GPIO_PIN_11|GPIO_PIN_10|GPIO_PIN_9|GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF1_TIM1; // TIM1复用
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USER CODE BEGIN TIM1_MspPostInit 1 */
/* USER CODE END TIM1_MspPostInit 1 */
}
}
3、主函数
最后,这个工程的主函数如下:
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 */
/* GPIO初始化 */
MX_GPIO_Init();
/* 高级定时器TIM1初始化 */
MX_TIM1_Init();
/* 初始化LED灯 */
BSP_LED_Init(LED_RED); // 作系统错误时指示用
/* USER CODE BEGIN 2 */
/*## 开始生成PWM波形 ####################*/
/* Start Channel1 */
if (HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1) != HAL_OK) // 开启定时器TIM1的通道1的PWM信号输出
{
/* PWM Generation Error */
Error_Handler();
}
/* Start channel 2 */
if (HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2) != HAL_OK)
{
/* PWM Generation Error */
Error_Handler();
}
/* Start channel 3 */
if (HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3) != HAL_OK)
{
/* PWM generation Error */
Error_Handler();
}
/* Start channel 4 */
if (HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4) != HAL_OK)
{
/* PWM generation Error */
Error_Handler();
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
测试验证
编译下载到LM401-Pro-Kit的评估板上之后,用示波器接到开发板上的PA9接口,查看一下通道2输出的PWM波形,如下:
由上图可以看到占空比37.5%,是满足设计预期的。
工程代码下载地址
定时器应用一之基本定时器工程代码:
链接: https://pan.baidu.com/s/1KSIIRc9aqOUcTrNYCAtzRQ
提取码: ruij
定时器应用二之定时器PWM输出工程代码:
链接: https://pan.baidu.com/s/1bWnNWza6-2Kf0XXcRG44eg
提取码: 7972