通用定时器_测量PWM方波的周期和占空比案例

引言

前面介绍了触发信号、从模式以及PWM输入的相关基础知识,接下来我们就开始实战利用一下,来实现对输入的PWM方波的周期和占空比的测量。大家如果对触发信号、从模式这些还不太熟悉,可参见前面的博客如下:

通用定时器_触发信号、从模式和PWM输入-CSDN博客https://blog.csdn.net/2301_79475128/article/details/156339564?spm=1001.2014.3001.5501


一、相关寄存器介绍

我们先介绍一下利用从模式来实现时需要涉及的相关寄存器,前面说过,要实现占空比测量实际上利用的仍时输入捕获模式,只不过那是一个特例------PWM输入,经过前面介绍,我们可以知道实际上就是利用了两个定时器通道+从模式触发实现,因此这里涉及到的寄存器主要是基础的定时器配置相关的+输入捕获相关的+从模式触发相关的寄存器。

考虑到前面通用定时器学习已经多次叙述了基础的配置,因此这里不过多赘述,可参考下面输入捕获介绍的文章:

通用定时器_输入捕获介绍及案例实操-CSDN博客https://blog.csdn.net/2301_79475128/article/details/152608991 因此这里我主要介绍的是PWM输入需要增加配置的寄存器相关内容。

1.1 TIMx_CCMR1寄存器

这个寄存器在输入捕获内容介绍时其实说过,不过因为本次需要配置新的通道的输入,因此就再介绍一下。回顾前面说的PWM输入的方式,也就是使用通道1输入PWM波,然后经过滤波和边沿检测得到两路信号,分别给到通道1的IC1、一路交叉给到通道2的IC2去,因此这里我们需要增加TI1向IC2的映射关系,也就是配置CCMR1寄存器的CCxS

其中CC1S用于配置通道1的输入输出和信号映射关系,CC2S用于配置通道2的输入输出和信号映射关系:

参考代码如下:

cpp 复制代码
// CH1通道配置为输入,并IC1映射到TI1上:CCMR1_CC1S=01
TIM4->CCMR1 &= ~TIM_CCMR1_CC1S_1;
TIM4->CCMR1 |= TIM_CCMR1_CC1S_0;

// CH2通道配置为输入,IC2映射到TI1上: CCMR1_CC2S=10
TIM4->CCMR1 |= TIM_CCMR1_CC2S_1;
TIM4->CCMR1 &= ~TIM_CCMR1_CC2S_0;

1.2 TIMx_SMCR寄存器

这个寄存器前面介绍从模式基础知识的时候已经提到过,也就是从模式控制寄存器,用于配置从模式相关内容。

本次主要配置的是触发信号选择以及从模式选择的内容。其中TS用于配置触发信号的选择,SMS用于配置从模式的选择:

根据前面对基础知识和实现占空比测量思路的描述,我们选择的是通道1的TI1FP1信号作为触发信号,然后选择复位模式作为从模式即可。参考代码如下:

cpp 复制代码
// 必须配置从模式控制器为复位模式 SMS=100, 触发输入信号为:TI1FP1 TS=101
TIM4->SMCR |= TIM_SMCR_TS_2;
TIM4->SMCR &= ~TIM_SMCR_TS_1;
TIM4->SMCR |= TIM_SMCR_TS_0;

TIM4->SMCR |= TIM_SMCR_SMS_2;
TIM4->SMCR &= ~(TIM_SMCR_SMS_1 | TIM_SMCR_SMS_0);

二、测量周期占空比案例

2.1 需求分析

用一个定时器的2个通道同时测量周期和占空比

2.2 硬件设计

与前面输入捕获的案例相同,只是现在多一个占空比的测量,硬件连接上没有区别:

通用定时器_输入捕获介绍及案例实操-CSDN博客https://blog.csdn.net/2301_79475128/article/details/152608991 然后使用的单片机为STM32F103ZET6,使用TIM4和TIM5两个定时器,TIM5用于生成PWM波,TIM4用于测量生成的PWM波的周期和占空比。其中,TIM5使用的引脚为PB6(TIM5_CH2),TIM4使用的引脚为PA1(TIM4_CH2)。

2.3 软件设计

2.3.1 实现逻辑分析

经过前面的介绍,其实软件部分的思路已经很清楚了,这里再以图示形式整理一下:

(1)PWM方波生成

1、TIM5基础配置(PA1引脚配置、预分频系数、自动重装载值、计数方向)

2、输出比较配置(CCR占空比初始值、输出通道输出、输出比较模式、通道极性)

3、TIM5对应通道使能

4、启动TIM5定时器(使能TIM5)

(2)PWM测量

1、TIM4基础配置(PB6,与上同理)

2、PWM输入-输入捕获配置

2.1 通道选择(通道1)

2.2 输入滤波选择(按需选择)

2.3 边沿检测(通道1检测上升沿、通道2检测下降沿)

2.4 模式与信号映射(通道1输入、直通映射TI1映射至IC1;通道2输入、TI1映射至IC2)

2.5 预分频(通道12均按需选择,默认不分频)

2.6 触发信号选择(使用TI1FP1作为触发输入)

2.7 从模式选择(使用复位模式作为从模式)

3、TIM4对应通道使能(CH1 CH2)

4、TIM4定时器启动(使能定时器)

(3)周期频率测量

捕获1寄存器记录的值即周期,倒数即频率,单位换算即可

(4)占空比测量

捕获2寄存器记录的值即占空比,单位换算即可

2.3.2 程序实现(寄存器方式)

根据上面的思路,实现起来就很简单了,秩序对照手册查看相关寄存器并进行配置即可。

2.3.2.1 TIM5初始化

笔者这里选择7200分频,也就是100us计数一次,然后重装载值给99,也就是计数100次,因此生成的PWM方波理论上周期为10ms,然后默认给的CCR为30,也就是占空比为30%,参考代码如下:

cpp 复制代码
void TIM5_Init(void)
{
    // 1. 开启时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

    // 2. 配置GPIO工作模式 PA1:复用推挽输出模式 MODE-11 CNF-10
    GPIOA->CRL |= GPIO_CRL_MODE1;
    GPIOA->CRL |= GPIO_CRL_CNF1_1;
    GPIOA->CRL &= ~GPIO_CRL_CNF1_0;

    // 3. 设置定时器
    // 3.1 设置预分频值 72-1
    TIM5->PSC = 7200 - 1;

    // 3.2 设置重装载值 每10ms溢出一次 99
    TIM5->ARR = 100 - 1;

    // 3.3 设置计数方向
    TIM5->CR1 &= ~TIM_CR1_DIR;

    // 3.4 设置CCR2
    TIM5->CCR2 = 30;

    // 3.5 配置通道模式为输出 CC2S-00
    TIM5->CCMR1 &= ~TIM_CCMR1_CC2S;

    // 3.6 设置pwm模式1 OC2M-110
    TIM5->CCMR1 |= TIM_CCMR1_OC2M_2;
    TIM5->CCMR1 |= TIM_CCMR1_OC2M_1;
    TIM5->CCMR1 &= ~TIM_CCMR1_OC2M_0;

    // 3.7 设置通道极性 低电平有效
    TIM5->CCER |= TIM_CCER_CC2P;

    // 3.8 使能通道2
    TIM5->CCER |= TIM_CCER_CC2E;
}
2.3.2.2 定时器启停

笔者借鉴HAL中对定时器的启停控制方法,将开启和停止单独封装为函数。

cpp 复制代码
// 开启定时器
void TIM5_Start(void)
{
    TIM5->CR1 |= TIM_CR1_CEN;
}

// 关闭定时器
void TIM5_Close(void)
{
    TIM5->CR1 &= ~TIM_CR1_CEN;
}
2.3.2.3 占空比设置

实际上就是比较值CCR2,这里封装成函数,方便修改。

cpp 复制代码
// 设置pwm占空比
void TIM5_SetDutyCycle(uint8_t dutyCycle)
{
    TIM5->CCR2 = dutyCycle;
}
2.3.2.4 TIM4初始化

TIM4用于测量PWM波,因此其频率主要影响的是可测量PWM频率的范围,首先重装载值直接拉满65535,其次预分频这里给71,也就是72分频得1MHz ,对于测量100Hz的PWM波绰绰有余。需要注意的是,由于是测量外部输入的PWM波,因此对应的定时器通道引脚应设置为浮空输入模式。

cpp 复制代码
void TIM4_Init(void)
{
    // 1. 开启时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;

    // 2. 配置GPIO工作模式 浮空输入 MODE-00 CNF-01
    GPIOB->CRL &= ~GPIO_CRL_MODE6;
    GPIOB->CRL |= GPIO_CRL_CNF6_0;
    GPIOB->CRL &= ~GPIO_CRL_CNF6_1;

    // 设置定时器
    // 3. 时基单元
    // 3.1 设置预分频值 72 1MHz
    TIM4->PSC = 72 - 1;

    // 3.2 设置重装载值 最大,避免溢出 65535
    TIM4->ARR = 65536 - 1;

    // 3.3 设置计数方向
    TIM4->CR1 &= ~TIM_CR1_DIR;

    // 4. 配置通道
    // 4.1 选择通道1
    TIM4->CR2 &= ~TIM_CR2_TI1S;

    // 4.2 设置通道1滤波
    TIM4->CCMR1 &= ~TIM_CCMR1_IC1F;

    // 4.3 设置极性 上升沿
    TIM4->CCER &= ~TIM_CCER_CC1P;

    // 通道2:下降沿
    TIM4->CCER |= TIM_CCER_CC2P;

    // 4.4 配置通道1为输入模式,直接映射IT1 CC1S-01
    TIM4->CCMR1 &= ~TIM_CCMR1_CC1S_1;
    TIM4->CCMR1 |= TIM_CCMR1_CC1S_0;

    // 通道2:IT1映射到IC1上 CC2S - 10
    TIM4->CCMR1 |= TIM_CCMR1_CC2S_1;
    TIM4->CCMR1 &= ~TIM_CCMR1_CC2S_0;

    // 4.5 设置预分频 触发上升沿直接捕获一次IC1PSC-00
    TIM4->CCMR1 &= ~TIM_CCMR1_IC1PSC;

    // 通道2:预分频
    TIM4->CCMR1 &= ~TIM_CCMR1_IC2PSC;

    // 4.6 设置触发模式 TS - 101
    TIM4->SMCR |= TIM_SMCR_TS_2;
    TIM4->SMCR &= ~TIM_SMCR_TS_1;
    TIM4->SMCR |= TIM_SMCR_TS_0;

    // 4.7 设置模式:复位模式 SMS - 100
    TIM4->SMCR |= TIM_SMCR_SMS_2;
    TIM4->SMCR &= ~TIM_SMCR_SMS_1;
    TIM4->SMCR &= ~TIM_SMCR_SMS_0;

    // 4.8 使能通道1
    TIM4->CCER |= TIM_CCER_CC1E;

    // 通道2:使能
    TIM4->CCER |= TIM_CCER_CC2E;
}
2.3.2.5 TIM4定时器启停

同样也是定时器停止和启动单独封装。

cpp 复制代码
// 开启定时器
void TIM4_Start(void)
{
    TIM4->CR1 |= TIM_CR1_CEN;
}

// 关闭定时器
void TIM4_Close(void)
{
    TIM4->CR1 &= ~TIM_CR1_CEN;
}
2.3.2.6 周期占空比测量

根据前面逻辑分析,我们知道周期是由通道1的捕获寄存器捕获计数值而来,所以直接获取TIM4的CCR1,然后进行单位换算即可。

cpp 复制代码
// 读取pwm的周期,单位us,返回为ms
double TIM4_GetDutyCycle(void)
{
    return TIM4->CCR1 / 1000.0;
}

// 读取频率
double TIM4_GetFreq(void)
{
    return 1000000.0 / TIM4->CCR1;
}

然后占空比也就是有效电平宽度,根据前面分析我们利用的通道2捕获了高电平宽度的时间,因此占空比就和TIM4的CCR2有关,当然这里可能需要考虑一下PWM波的有效电平,如果是高电平那么直接就是CCR2的值与周期值之比,反之需要100-才是,同时这里也需要先进行单位换算。

cpp 复制代码
// 获取占空比
double TIM4_GetPwmDuty(void)
{
    return 1.0 - TIM4->CCR2 * 1.0 / TIM4->CCR1;
}
2.3.2.7 主程序实现

最后写一下main程序即可。笔者逻辑就是初始化后1s打印一次测量数据。

cpp 复制代码
/*
 * @Description:
 * @version:
 * @Author: BreezeJuvenile
 * @Date: 2025-03-01 20:49:32
 * @LastEditors: BreezeJuvenile
 * @LastEditTime: 2025-03-06 19:38:00
 */
#include "usart.h"
#include "tim5.h"
#include "tim4.h"
#include "Delay.h"

int main(void)
{
	// 1. 初始化
	USART_Init();
	TIM5_Init();
	TIM4_Init();

	printf("Hello, World!\n");

	// 2. 开启定时器
	TIM5_Start();
	TIM4_Start();

	// 死循环保持状态
	while (1)
	{
		// 打印周期频率数据
		printf("T = %.2f ms, f = %.2f Hz, d = %.2f %%\n", TIM4_GetDutyCycle(), TIM4_GetFreq(), 100 * TIM4_GetPwmDuty());

		// 延时1s
		Delay_ms(1000);
	}
}
2.3.2.8 测试效果

对代码进行编译和烧录后得到下图效果。

同时,我们可以借助逻辑分析仪看看波形,检查是否一致。

很明显是一致的,因此说明程序没有什么问题。

2.3.3 程序实现(HAL库方式)

接下来,我们使用HAL库方式实现一下。利用HAL库实现就能省略大量工作,直接图形化配置,然后简单在主函数写几句即可。

1、新建HAL库工程

进入STM32CubeMX中,选择MCU型号后打开。

2、进行图形化配置

2.1 系统核心中的调试器配置

2.2 时钟配置

2.3 串口1配置

用于打印测量数据,简单设置为异步即可。后面注意keil勾选micro Lib,然后重定向printf就好了。

2.4 TIM5配置

这里是用于生成PWM波的,配置如下

这里我配置的高电平为有效电平,同时比较值为50,即占空比50%。

这里稍微检查一下引脚是不是PA1

2.4 TIM4配置

TIM4是用于测量生成的PWM,所以相对多一些,配置如下

这里同样检查一下引脚是不是PB6,如果不是的话可能是HAL选择的重映射引脚,只需自己修改一下(先重置原本引脚状态,然后选择PB6引脚作为TIM4的通道1功能),不过此时修改后需要重新配置上述内容,可能稍显麻烦。

2.5 工程管理配置

这里就是对该工程命名和结构的设置了,主要注意工程路径选择、工具链设置等,配置如下

2.6 生成代码

最后点击生成代码即可。

3、配置keil相关内容

图形化配置完成后,将工程用keil打开,进入魔术棒配置一下相关内容。

首先这个是串口重定向printf准备的。

然后这是调试器相关设置。

最后不要忘记保存确认了。

4、在VSCode中完善程序逻辑

如果希望检查一下自动生成的相关初始化代码,比如定时器的,可以自行查看

接着我们来完善一下相关代码。

4.1 串口重定向

这部分是为打印测量数据准备,主要是在usart.c中重写一下fputc函数即可,注意引入stdio.h头文件。

cpp 复制代码
// 重写fputc()函数
int fputc(int ch, FILE * file)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
  return ch;
}

4.2 测量周期和占空比函数实现

这里和前面寄存器实现其实一样的,区分在于这里是直接调用HAL库函数,关于库函数我怎么知道,那可以网上搜一下或者其他办法,记得就行。

我们在tim.c中对应位置进行自定义实现一下:

cpp 复制代码
/* USER CODE BEGIN 1 */

// 获取pwm周期
double TIM4_GetPWMCycle(void)
{
  return __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1) / 1000.0;
}

// 获取PWM频率
double TIM4_GetPWMFreq(void)
{
  return 1000000.0 / __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1); 
}

// 获取pwm占空比
double TIM4_GetPWMDuty(void)
{
  return __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_2) * 1.0 / __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1);
}

/* USER CODE END 1 */

当然,不要忘记在头文件声明这三个自定义的函数。

4.3 主函数初始化和数据打印

最后,补充主函数逻辑即可。由于HAL库将定时器启停单独封装函数,因此首先在main函数的while循环之前需要启动定时器相应的通道:

cpp 复制代码
  /* USER CODE BEGIN 2 */

  // 开启时钟
  HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);
  HAL_TIM_IC_Start(&htim4, TIM_CHANNEL_1);
  HAL_TIM_IC_Start(&htim4, TIM_CHANNEL_2);

  printf("Hello, World!\n");

  /* USER CODE END 2 */

这里加了个打印提示,各位随意。

然后while循环每秒打印测量数据即可。

至此基于HAL库的程序也写完了,接着进行烧录测试看看效果。

5、测试效果

很显然测量没有问题。


三、总结

本文详细介绍了利用STM32定时器测量PWM波形周期和占空比的方法。通过配置TIM5生成PWM波形,TIM4作为输入捕获测量波形参数。文章分别从寄存器配置和HAL库实现两种方式展开:寄存器方式详细说明了CCMR1、SMCR等关键寄存器的配置,并给出了完整的代码实现;HAL库方式则通过STM32CubeMX图形化配置简化开发流程。两种方法最终都能准确测量PWM波形的周期、频率和占空比,实测结果与逻辑分析仪验证一致。


以上便是本次文章的所有内容,欢迎各位朋友在评论区讨论,本人也是一名初学小白,愿大家共同努力,一起进步吧!

鉴于笔者能力有限,难免出现一些纰漏和不足,望大家在评论区批评指正,谢谢!

相关推荐
进阶的猪2 小时前
stm32f407 RCC时钟配置
stm32·单片机·嵌入式硬件
周末不下雨2 小时前
发明专利学习记录
学习
亚里随笔2 小时前
偏离主路径:RLVR在参数空间中的非主方向学习机制
人工智能·深度学习·学习
ArrebolJiuZhou3 小时前
02arm指令集(一)——LDR,MOV,STR的使用
linux·网络·单片机
我命由我123453 小时前
Photoshop - Photoshop 工具栏(46)渐变工具
经验分享·笔记·学习·ui·职场和发展·学习方法·photoshop
丝斯20113 小时前
AI学习笔记整理(38)——自然语言处理的‌基于深度学习的语言模型
人工智能·学习·自然语言处理
~光~~3 小时前
【记录——内核模块加载到内核】基于鲁班猫4 rk3588s
c++·学习·rk3588s
diegoXie4 小时前
【R】tidyr::pivot_longer / pivot_wider 学习笔记
笔记·学习·r语言
小刘爱玩单片机4 小时前
【stm32简单外设篇】- 三色LED
c语言·stm32·单片机·嵌入式硬件