概述

在前面的实验中,已经使用 TIM1 生成 PWM 输出,并通过修改 PSC、ARR 和 CCR 实现了 PWM 频率和占空比控制。本篇文章继续在此基础上,使用 TIM15 输入捕获功能 对 TIM1 输出的 PWM 信号进行测量,从而实现频率和占空比检测。

需要样片的可以加群申请:925643491 / 615061293 。

视频教学

https://www.bilibili.com/video/BV1J6T56oEKy/

样品申请

https://www.wjx.top/vm/OhcKxJk.aspx#

源码下载

https://download.csdn.net/download/qq_24312945/93058804

硬件准备

首先需要准备一个开发板,这里我准备的是自己绘制的开发板,需要的可以进行申请。
主控为STM32C542CCT6
在这里插入图片描述

参考程序

https://github.com/CoreMaker-lab/STM32C542_SENSOR

https://gitee.com/CoreMaker/STM32C542_SENSOR

串口配置

查看原理图,PA9和PA10设置为开发板的串口。
在这里插入图片描述
在这里插入图片描述

  1. 在左侧 Peripherals 中选择 Connectivity → USART1
  2. Mode 选择 Async,表示配置为异步串口模式
  3. Function used by the component 显示为 UART,说明 USART1 在异步模式下使用 UART HAL 驱动
  4. 串口参数配置为:115200 波特率、8 位数据位、无校验、1 位停止位、收发模式

在这里插入图片描述

  1. GPIO Tx:USART1_TX 选择 PA9
  2. GPIO Rx:USART1_RX 选择 PA10
  3. PA9 / PA10 均配置为 Alternate 复用功能模式
  4. Pull 选择 No pull-up and no pull-down,Output type 选择 Push pull,Speed 选择 Low

在这里插入图片描述

TIM1基础参数

使用PA7去捕获PA5的PWM信号。

在这里插入图片描述

先确认 TIM15 的输入时钟频率,如果时钟频率判断错误,最终计算出来的 PWM 频率也会不准确。这里 TIM15 Clock = 144 MHz

在这里插入图片描述

在 STM32CubeMX2 中进入 Timers → TIM15 配置页面,并使能 TIM15 外设。
本文没有直接在 STM32CubeMX2 中固定 Prescaler 和 Counter period,而是在代码中进行配置。这样后续修改测量精度和测量范围时更加方便。

在这里插入图片描述
完成 TIM15 基础参数配置后,接下来需要配置 TIM15_CH1 输入捕获通道。本文使用 TIM15_CH1 捕获 TIM1 输出的 PWM 信号,并通过捕获到的上升沿和下降沿计算频率和占空比。
其中,Channel direction 选择 Input,表示 TIM15_CH1 作为输入捕获通道使用。Input capture source 选择 Input capture direct mode,表示捕获信号直接来自 TIM15_CH1 对应的输入引脚。本文中 TIM15_CH1 映射到 PA7,因此外部 PWM 信号需要输入到 PA7 引脚。
Prescaler 选择 ÷1,表示每一个有效捕获边沿都会触发捕获事件,不对输入捕获事件进行分频。这样可以保证每个 PWM 边沿都能够被 TIM15 捕获。
Filter 选择 No filter。本实验中 TIM1 输出 PWM 后直接通过杜邦线连接到 TIM15_CH1,信号相对干净,因此不需要额外滤波。如果后续测量外部噪声较大的信号,可以根据实际情况开启输入滤波。
本实验最关键的配置是:

Polarity:Both edges

这表示 TIM15_CH1 会同时捕获输入 PWM 的上升沿和下降沿。这样只需要一个输入捕获通道,就可以同时测量 PWM 的周期和高电平时间。

在这里插入图片描述

测量逻辑如下:

上升沿 -> 保存周期起点
下降沿 -> 保存高电平结束点
下一次上升沿 -> 计算完整周期

其中:

上升沿到下一次上升沿的时间 = PWM 周期 period
上升沿到下降沿的时间 = 高电平时间 high

因此可以得到:

频率 = TIM15_COUNTER_HZ / period

占空比 = high / period × 100%

在代码中,TIM15_CH1 配置为 Both edges 后,每次捕获中断都会进入 HAL_TIM_InputCaptureCallback()。为了区分当前捕获的是上升沿还是下降沿,程序会读取 PA7 当前电平:

pin_level = (GPIOA->IDR & GPIO_IDR_ID7);

如果 PA7 当前为高电平,说明刚刚发生的是上升沿;如果 PA7 当前为低电平,说明刚刚发生的是下降沿。通过这种方式,程序就可以在一个输入捕获通道上同时完成频率和占空比测量。

在这里插入图片描述

完成 TIM15_CH1 输入捕获通道配置后,还需要确认 TIM15 的高级功能配置。本文只是使用 TIM15_CH1 对外部 PWM 信号进行输入捕获测量,因此大部分高级功能保持默认即可。
其中,Update event generation 保持 Enabled。这表示 TIM15 允许产生更新事件。更新事件通常与计数器溢出、重新装载等动作有关。本文虽然主要使用输入捕获功能,但保持更新事件使能即可,不需要额外修改。
Update event source 选择 Regular,表示使用常规更新事件来源。对于本文的输入捕获测频实验来说,这里保持默认配置即可。

完成 TIM15_CH1 输入捕获通道配置后,还需要检查 TIM15 对应的 GPIO 和中断配置。本文使用 TIM15_CH1 捕获 TIM1 输出的 PWM 信号,因此需要确认 TIM15_CH1 已经正确映射到 PA7,并且 TIM15 中断已经使能。
因为本文使用的是输入捕获中断方式,当 PA7 检测到 PWM 的上升沿或下降沿时,TIM15 会触发输入捕获事件,并进入 HAL_TIM_InputCaptureCallback() 回调函数。
如果这里没有开启 TIM15 的 Global interrupt,即使 TIM15_CH1 配置正确,也无法进入输入捕获回调函数,程序就无法计算频率和占空比。
本文中 TIM15_CH1 配置为 Both edges,因此上升沿和下降沿都会触发输入捕获中断。程序在回调函数中读取捕获值,并通过 PA7 当前电平判断本次捕获的是上升沿还是下降沿,最终计算 PWM 的周期、高电平时间、频率和占空比。

在这里插入图片描述

设置工程编码

  1. 在 Project Explorer 中选中当前工程
  2. 点击菜单栏 Project
  3. 选择 Properties,进入工程属性设置

在这里插入图片描述

  1. 在工程属性中选择 Resource
  2. Text file encoding 选择 Other
  3. 编码格式输入 GBK
  4. 点击 Apply and Close 保存设置

在这里插入图片描述

初始配置

初始配置修改如下状态。

/**
  * brief:  The application entry point.
  * retval: none but we specify int to comply with C99 standard
  */
int main(void)
{
  /** System Init: this code placed in targets folder initializes your system.
    * It calls the initialization (and sets the initial configuration) of the peripherals.
    * You can use STM32CubeMX to generate and call this code or not in this project.
    * It also contains the HAL initialization and the initial clock configuration.
    */
  if (mx_system_init() != SYSTEM_OK)
  {
    return (-1);
  }
  else
  {
    /*
      * You can start your application code here
      */
	    /*
	     * TIM1_CH1 -> PB8
	     * TIM1_CH3 -> PA5
	     *
	     * 当前配置:
	     * TIM1_CLK = 144 MHz
	     * ARR = 9999
	     */

	    /*
	     * 设置 PWM 频率为 1Hz
	     *
	     * PWM频率 = 144MHz / ((PSC + 1) * (ARR + 1))
	     * ARR = 9999
	     * 当 freq = 1Hz 时,PSC = 14399
	     */
	    if (TIM1_SetFrequency(1) != HAL_OK)
	    {
	        printf("TIM1_SetFrequency failed\r\n");
	    }

	    /*
	     * 设置多通道占空比
	     *
	     * CH1 = 25%
	     * CH3 = 75%
	     */
	    if (TIM1_SetChannelDuty(HAL_TIM_CHANNEL_1, TIM1_PWM_ARR, 25) != HAL_OK)
	    {
	        printf("TIM1 CH1 duty set failed\r\n");
	    }

	    if (TIM1_SetChannelDuty(HAL_TIM_CHANNEL_3, TIM1_PWM_ARR, 75) != HAL_OK)
	    {
	        printf("TIM1 CH3 duty set failed\r\n");
	    }

	    /*
	     * 启动 TIM1 CH1 和 CH3 PWM 输出
	     */
	    if (TIM1_PWM_Start_CH1_CH3() != HAL_OK)
	    {
	        printf("TIM1 PWM start failed\r\n");
	    }

        printf("TIM1 PWM: 1000Hz, CH1=25%%, CH3=75%%\r\n");

        /*
         * 设置 TIM1 PWM 频率为 1000Hz
         *
         * TIM1_CLK = 144MHz
         * ARR = 999
         * PSC = 143
         *
         * PWM频率 = 144MHz / ((143 + 1) * (999 + 1))
         *         = 1000Hz
         */
        TIM1_SetFrequency(1000);

        /*
         * CH1 输出 25% 占空比
         * CH3 输出 75% 占空比
         */
        TIM1_SetChannelDuty(HAL_TIM_CHANNEL_1, TIM1_PWM_ARR, 25);
        TIM1_SetChannelDuty(HAL_TIM_CHANNEL_3, TIM1_PWM_ARR, 75);

	    HAL_Delay(5000);
	    while (1)
	    {

	    }
	  }
} /* end main */

添加头文件

在 main.c 中添加头文件

#include "mx_usart1.h"
#include <string.h>

在这里插入图片描述

printf 重定向

为了让 printf() 输出到 USART1,需要重写 _write() 函数。GCC 工程中,printf() 底层会调用 _write() 输出字符,因此只需要在 _write() 中调用 HAL_UART_Transmit(),就可以把 printf() 的内容通过串口发送出去。

int _write(int file, char *ptr, int len)
{
    hal_uart_handle_t *huart1 = mx_usart1_uart_gethandle();

    if (huart1 != NULL)
    {
        HAL_UART_Transmit(huart1, ptr, len, 1000);
    }

    return len;
}

在这里插入图片描述

TIM15 计数频率配置

TIM15_CLK_HZ:TIM15 输入时钟频率,这里为 144 MHz
TIM15_COUNTER_HZ:希望 TIM15 的计数频率为 1 MHz
TIM15_PRINT_INTERVAL:每累计 50 次测量后打印一次结果

#define TIM15_CLK_HZ 144000000UL 
#define TIM15_COUNTER_HZ 1000000UL 
#define TIM15_PRINT_INTERVAL 50U

在这里插入图片描述
为了让 TIM15 计数频率变成 1 MHz,需要设置 Prescaler:

#define TIM15_PSC ((TIM15_CLK_HZ / TIM15_COUNTER_HZ) - 1U) 
#define TIM15_ARR 0xFFFFU

计算过程如下:

TIM15 计数频率 = TIM15_CLK_HZ / (PSC + 1)

TIM15_CLK_HZ = 144 MHz
TIM15_COUNTER_HZ = 1 MHz

PSC = 144,000,000 / 1,000,000 - 1
    = 143

因此,当 TIM15_PSC = 143 时,TIM15 的计数频率为 1 MHz,也就是:

1 个计数 = 1 us

这样在测量 PWM 信号时非常方便。例如输入 PWM 为 1000 Hz,则周期为 1 ms,也就是 1000 us,理论上 TIM15 两次上升沿之间的捕获差值约为 1000。

在这里插入图片描述

捕获变量说明

代码中定义了一组变量,用于保存上升沿、下降沿和测量结果这些变量的作用如下:
tim15_rise_last:上一次上升沿捕获值
tim15_rise_now:当前上升沿捕获值
tim15_fall_now:当前下降沿捕获值
tim15_period_count:PWM 周期计数值
tim15_high_count:PWM 高电平计数值
tim15_print_freq:准备打印的频率
tim15_print_duty_x10:准备打印的占空比,放大 10 倍
tim15_print_period:准备打印的周期计数值
tim15_print_high:准备打印的高电平计数值
tim15_print_flag:打印标志位
tim15_rise_valid:上升沿捕获有效标志
tim15_fall_valid:下降沿捕获有效标志
tim15_measure_count:测量次数计数

static volatile uint32_t tim15_rise_last = 0; 
static volatile uint32_t tim15_rise_now = 0; 
static volatile uint32_t tim15_fall_now = 0; 
static volatile uint32_t tim15_period_count = 0; 
static volatile uint32_t tim15_high_count = 0; 
static volatile uint32_t tim15_print_freq = 0; 
static volatile uint32_t tim15_print_duty_x10 = 0; 
static volatile uint32_t tim15_print_period = 0; 
static volatile uint32_t tim15_print_high = 0; 
static volatile uint8_t tim15_print_flag = 0; 
static volatile uint8_t tim15_rise_valid = 0; 
static volatile uint8_t tim15_fall_valid = 0; 
static volatile uint32_t tim15_measure_count = 0;

在这里插入图片描述

处理计数器溢出

由于 TIM15 是 16 位计数器,Counter period 设置为 0xFFFF,计数器从 0 计数到 65535 后会重新回到 0。
因此,两次捕获值相减时需要考虑溢出情况。代码中封装了 TIM15_GetDiff() 函数

static uint32_t TIM15_GetDiff(uint32_t start, uint32_t end)
{
    if (end >= start)
    {
        return end - start;
    }
    else
    {
        return (TIM15_ARR + 1U - start) + end;
    }
}

在这里插入图片描述

本文没有在 STM32CubeMX2 中固定 TIM15 的 Prescaler 和 Counter period,而是在代码中通过 HAL2 函数动态配置 。
这个函数主要完成三件事:

  1. 使用 HAL_TIM_SetPrescaler() 设置 TIM15_PSC
  2. 使用 HAL_TIM_SetPeriod() 设置 TIM15_ARR
  3. 使用 HAL_TIM_SetCounter() 将 TIM15 计数器清零
static hal_status_t TIM15_IC_SetPSC_ARR(uint32_t psc, uint32_t arr)
{
    hal_tim_handle_t* htim15 = mx_tim15_gethandle();

    if (htim15 == NULL)
    {
        return HAL_ERROR;
    }

    if (HAL_TIM_SetPrescaler(htim15, psc) != HAL_OK)
    {
        return HAL_ERROR;
    }

    if (HAL_TIM_SetPeriod(htim15, arr) != HAL_OK)
    {
        return HAL_ERROR;
    }

    if (HAL_TIM_SetCounter(htim15, 0) != HAL_OK)
    {
        return HAL_ERROR;
    }

    return HAL_OK;
}

在这里插入图片描述

启动输入捕获中断

完成 TIM15 基础参数配置后,需要启动 TIM15_CH1 输入捕获中断 。

  1. HAL_TIM_IC_StartChannel_IT():启动 TIM15_CH1 输入捕获中断
  2. HAL_TIM_Start():启动 TIM15 计数器
static hal_status_t TIM15_IC_Start(void)
{
    hal_tim_handle_t* htim15 = mx_tim15_gethandle();

    if (htim15 == NULL)
    {
        return HAL_ERROR;
    }

    /*
     * 鍚姩 TIM15_CH1 杈撳叆鎹曡幏涓柇
     */
    if (HAL_TIM_IC_StartChannel_IT(htim15, HAL_TIM_CHANNEL_1) != HAL_OK)
    {
        return HAL_ERROR;
    }

    /*
     * 鍚姩 TIM15 璁℃暟鍣�
     */
    if (HAL_TIM_Start(htim15) != HAL_OK)
    {
        return HAL_ERROR;
    }

    return HAL_OK;
}

在这里插入图片描述

输入捕获回调函数

TIM15_CH1 在 STM32CubeMX2 中配置为 Both edges,也就是上升沿和下降沿都会触发输入捕获中断。
由于 TIM15_CH1 配置为 Both edges,所以同一个回调函数既会响应上升沿,也会响应下降沿。为了判断当前捕获的是上升沿还是下降沿,代码中读取 PA7 当前电平:
pin_level = (GPIOA->IDR & GPIO_IDR_ID7);
判断逻辑如下:
如果 PA7 当前为高电平,说明刚刚发生的是上升沿
如果 PA7 当前为低电平,说明刚刚发生的是下降沿

void HAL_TIM_InputCaptureCallback(hal_tim_handle_t* htim, hal_tim_channel_t channel)
{
    uint32_t capture_value;
    uint32_t period;
    uint32_t high;
    uint32_t freq;
    uint32_t duty_x10;
    uint32_t pin_level;

    if ((htim != mx_tim15_gethandle()) || (channel != HAL_TIM_CHANNEL_1))
    {
        return;
    }

    capture_value = HAL_TIM_IC_ReadChannelCapturedValue(htim, HAL_TIM_CHANNEL_1);

    /*
     * 读取 PA7 当前电平:
     * 如果当前为高电平,说明刚刚发生的是上升沿;
     * 如果当前为低电平,说明刚刚发生的是下降沿。
     */
    pin_level = (GPIOA->IDR & GPIO_IDR_ID7);

    if (pin_level != 0U)
    {
        /*
         * 上升沿
         */
        if (tim15_rise_valid == 0U)
        {
            tim15_rise_last = capture_value;
            tim15_rise_valid = 1U;
            tim15_fall_valid = 0U;
        }
        else
        {
            tim15_rise_now = capture_value;

            period = TIM15_GetDiff(tim15_rise_last, tim15_rise_now);

            if ((period != 0U) && (tim15_fall_valid != 0U))
            {
                high = TIM15_GetDiff(tim15_rise_last, tim15_fall_now);

                if (high <= period)
                {
                    freq = TIM15_COUNTER_HZ / period;

                    /*
                     * duty_x10 放大 10 倍,方便打印一位小数
                     * 例如 253 表示 25.3%
                     */
                    duty_x10 = (high * 1000U) / period;

                    tim15_period_count = period;
                    tim15_high_count = high;

                    tim15_measure_count++;

                    if (tim15_measure_count >= TIM15_PRINT_INTERVAL)
                    {
                        tim15_measure_count = 0;

                        tim15_print_freq = freq;
                        tim15_print_duty_x10 = duty_x10;
                        tim15_print_period = period;
                        tim15_print_high = high;
                        tim15_print_flag = 1;
                    }
                }
            }

            tim15_rise_last = tim15_rise_now;
            tim15_fall_valid = 0U;
        }
    }
    else
    {
        /*
         * 下降沿
         */
        if (tim15_rise_valid != 0U)
        {
            tim15_fall_now = capture_value;
            tim15_fall_valid = 1U;
        }
    }
}

在这里插入图片描述

增加 TIM15 初始化和启动

你的 main() 里,在启动 TIM1 PWM 后面增加 TIM15:

/*
 *
 * 硬件连接:
 * PA5 接 PA7
 */
if (TIM15_IC_SetPSC_ARR(TIM15_PSC, TIM15_ARR) != HAL_OK)
{
    printf("TIM15 IC set PSC/ARR failed\r\n");
}

if (TIM15_IC_Start() != HAL_OK)
{
    printf("TIM15 IC start failed\r\n");
}

在这里插入图片描述

主循环

在 while(1) 中,程序判断 tim15_print_flag 是否置位。

while (1)
{


    if (tim15_print_flag)
    {
        uint32_t freq;
        uint32_t duty_x10;
        uint32_t period;
        uint32_t high;

        __disable_irq();

        tim15_print_flag = 0;
        freq = tim15_print_freq;
        duty_x10 = tim15_print_duty_x10;
        period = tim15_print_period;
        high = tim15_print_high;

        __enable_irq();

        printf("TIM15 measured: freq=%lu Hz, duty=%lu.%lu%%, period=%lu, high=%lu\r\n",
               freq,
               duty_x10 / 10U,
               duty_x10 % 10U,
               period,
               high);
    }
}

在这里插入图片描述
这里先关闭中断,是为了防止主循环读取变量时,中断刚好更新这些变量,导致读取到一半新数据、一半旧数据。
读取完成后重新打开中断,再通过串口打印:

freq:测量到的 PWM 频率
duty:测量到的 PWM 占空比
period:一个周期对应的计数值
high:高电平对应的计数值

如果输入信号为 1000 Hz、25% 占空比,理论打印结果接近:

TIM15 measured: freq=1000 Hz, duty=25.0%, period=1000, high=250

如果输入信号为 1000 Hz、75% 占空比,理论打印结果接近:

TIM15 measured: freq=1000 Hz, duty=75.0%, period=1000, high=750

演示结果

检测到的频率逻辑分析仪和串口打印基本上都是1025Hz,正占空比为75%。

在这里插入图片描述

在这里插入图片描述

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐