STM32C562开发(5)----定时器配置输入捕获测量频率

STM32C562开发.5--定时器配置输入捕获测量频率

概述

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

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

视频教学

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

样品申请

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

源码下载

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

硬件准备

首先需要准备一个开发板,这里我准备的是自己绘制的开发板,需要的可以进行申请。

主控为STM32C562CET6

参考程序

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

https://gitee.com/CoreMaker/STM32C562_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,信号相对干净,因此不需要额外滤波。如果后续测量外部噪声较大的信号,可以根据实际情况开启输入滤波。

本实验最关键的配置是:

c 复制代码
Polarity:Both edges

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

测量逻辑如下:

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

其中:

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

因此可以得到:

c 复制代码
频率 = TIM15_COUNTER_HZ / period

占空比 = high / period × 100%

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

c 复制代码
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 保存设置

初始配置

初始配置修改如下状态。

c 复制代码
/**
  * 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 中添加头文件

c 复制代码
#include "mx_usart1.h"
#include <string.h>

printf 重定向

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

c 复制代码
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 次测量后打印一次结果

c 复制代码
#define TIM15_CLK_HZ 144000000UL 
#define TIM15_COUNTER_HZ 1000000UL 
#define TIM15_PRINT_INTERVAL 50U

为了让 TIM15 计数频率变成 1 MHz,需要设置 Prescaler:

c 复制代码
#define TIM15_PSC ((TIM15_CLK_HZ / TIM15_COUNTER_HZ) - 1U) 
#define TIM15_ARR 0xFFFFU

计算过程如下:

c 复制代码
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,也就是:

c 复制代码
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:测量次数计数

c 复制代码
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() 函数

c 复制代码
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 计数器清零
c 复制代码
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 计数器
c 复制代码
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 当前为低电平,说明刚刚发生的是下降沿

c 复制代码
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:

c 复制代码
/*
 *
 * 硬件连接:
 * 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 是否置位。

c 复制代码
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);
    }
}

这里先关闭中断,是为了防止主循环读取变量时,中断刚好更新这些变量,导致读取到一半新数据、一半旧数据。

读取完成后重新打开中断,再通过串口打印:

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

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

c 复制代码
TIM15 measured: freq=1000 Hz, duty=25.0%, period=1000, high=250

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

c 复制代码
TIM15 measured: freq=1000 Hz, duty=75.0%, period=1000, high=750

演示结果

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