引言
前面介绍了触发信号、从模式以及PWM输入的相关基础知识,接下来我们就开始实战利用一下,来实现对输入的PWM方波的周期和占空比的测量。大家如果对触发信号、从模式这些还不太熟悉,可参见前面的博客如下:
一、相关寄存器介绍
我们先介绍一下利用从模式来实现时需要涉及的相关寄存器,前面说过,要实现占空比测量实际上利用的仍时输入捕获模式,只不过那是一个特例------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波形的周期、频率和占空比,实测结果与逻辑分析仪验证一致。
以上便是本次文章的所有内容,欢迎各位朋友在评论区讨论,本人也是一名初学小白,愿大家共同努力,一起进步吧!
鉴于笔者能力有限,难免出现一些纰漏和不足,望大家在评论区批评指正,谢谢!