ESP32在Arduino固件下的脉冲计数器(PCNT)

ESP32的脉冲计数器(PCNT)

PCNT 用于统计输入信号的上升沿和/或下降沿的数量。ESP32-S3 集成了多个脉冲计数单元,1 每个单元都是包含多个通道的独立计数器。通道可独立配置为统计上升沿或下降沿数量的递增计数器或递减计数器。

PCNT 通道可检测边沿信号及电平信号。对于比较简单的应用,检测边沿信号就足够了。PCNT 通道可检测上升沿信号、下降沿信号,同时也能设置为递增计数,递减计数,或停止计数。电平信号就是所谓的 控制信号,可用来控制边沿信号的计数模式。通过设置电平信号与边沿信号的检测模式,PCNT单元可用作正交解码器
每个PCNT单元还包含一个滤波器,用于滤除线路毛刺。

PCNT 模块通常用于:

  • 对一段时间内的脉冲计数,进而计算得到周期信号的频率;
  • 对正交信号进行解码,进而获得速度和方向信息。

本文主要介绍在Arduino固件中如何配置和使用PCNT模块去读编码盘的计数增量。方便我们后面进行电机速度环的控制。

Arduino固件中关于PCNT的头文件存放在driver/pcnt.h

使用PCNT的大致流程为:

  • 配置PCNT单元的配置文件结构体
  • 初始化PCNT单元
  • 启动PCNT单元
  • 获取计数结果(或者是回调函数)

关于pcnt.h中部分代码的讲解

在pcnt.h中定义了PCNT单元的枚举类型

typedef enum {
    PCNT_UNIT_0, /*!< PCNT unit 0 */
    PCNT_UNIT_1, /*!< PCNT unit 1 */
    PCNT_UNIT_2, /*!< PCNT unit 2 */
    PCNT_UNIT_3, /*!< PCNT unit 3 */
#if SOC_PCNT_UNITS_PER_GROUP > 4
    PCNT_UNIT_4, /*!< PCNT unit 4 */
    PCNT_UNIT_5, /*!< PCNT unit 5 */
    PCNT_UNIT_6, /*!< PCNT unit 6 */
    PCNT_UNIT_7, /*!< PCNT unit 7 */
#endif
    PCNT_UNIT_MAX,
} pcnt_unit_t;

这个枚举类型展示了PCNT单元的数量。对于ESP32、ESP32-S2和ESP32-S3,SOC_PCNT_UNITS_PER_GROUP宏的值分别为8, 4, 4。而ESP32C3没有PCNT单元。

因此对于ESP32-S3, 我们只能使用PCNT_UNIT_0PCNT_UNIT_3这4个PCNT单元。


pcnt.h中还有关于PCNT通道的枚举类型,如下

typedef enum {
    PCNT_CHANNEL_0, /*!< PCNT channel 0 */
    PCNT_CHANNEL_1, /*!< PCNT channel 1 */
    PCNT_CHANNEL_MAX,
} pcnt_channel_t;

每个PCNT单元最多可以使用2个通道。


在pcnt.h中,PCNT通道的配置结构体PCNT_CHANNEL_CONFIG_T定义如下

/**
 * @brief Pulse Counter configuration for a single channel
    [翻译] 用于单个通道的脉冲计数器配置
 */
typedef struct {
    int pulse_gpio_num;          /*!< Pulse input GPIO number, if you want to use GPIO16, enter pulse_gpio_num = 16, a negative value will be ignored */
    int ctrl_gpio_num;           /*!< Control signal input GPIO number, a negative value will be ignored */
    pcnt_ctrl_mode_t lctrl_mode; /*!< PCNT low control mode */
    pcnt_ctrl_mode_t hctrl_mode; /*!< PCNT high control mode */
    pcnt_count_mode_t pos_mode;  /*!< PCNT positive edge count mode */
    pcnt_count_mode_t neg_mode;  /*!< PCNT negative edge count mode */
    int16_t counter_h_lim;       /*!< Maximum counter value */
    int16_t counter_l_lim;       /*!< Minimum counter value */
    pcnt_unit_t unit;            /*!< PCNT unit number */
    pcnt_channel_t channel;      /*!< the PCNT channel */
} pcnt_config_t;

讲解一下里面各个成员的意义:

  • 成员1 -> int pulse_gpio_num:指定用于接收脉冲信号的GPIO引脚编号。负值将被忽略。

  • 成员2 -> int ctrl_gpio_num:指定用于控制脉冲计数的GPIO引脚编号。如果不需要控制信号,则设置为负值。

    为什么需要控制信号引脚呢?
    在许多应用中,只需要一个引脚的脉冲输入即可完成基本的功能。然而,在某些高级应用中,可能需要额外的控制信号来改变计数的行为。

  • 成员3 -> pcnt_ctrl_mode_t lctrl_mode:定义当控制信号处于低电平时的行为模式。ta可以是下面三种模式之一

  • 成员4 -> pcnt_ctrl_mode_t hctrl_mode:定义当控制信号处于高电平时的行为模式。ta可以是下面三种模式之一

typedef enum {
    PCNT_CHANNEL_LEVEL_ACTION_KEEP,  /*!< 保持当前的计数模式不变 */
    PCNT_CHANNEL_LEVEL_ACTION_INVERSE, /*!< 反转当前的计数模式  */
    PCNT_CHANNEL_LEVEL_ACTION_HOLD,    /*!< 保持当前的计数值不变,停止计数 */
} pcnt_channel_level_action_t;
typedef pcnt_channel_level_action_t pcnt_ctrl_mode_t;

pcnt_ctrl_mode_t 成员详解

  • PCNT_CHANNEL_LEVEL_ACTION_KEEP
    • 描述:保持当前的计数模式不变。- 作用:保持当前的计数模式不变。
    • 示例:如果当前计数模式是增加(PCNT_COUNT_INC),则继续增加;如果是减少(PCNT_COUNT_DEC),则继续减少。
    • 应用场景:当你希望在控制信号变化时,脉冲计数器的行为不发生变化。
  • PCNT_CHANNEL_LEVEL_ACTION_INVERSE
    • 作用:反转当前的计数模式。
    • 示例:如果当前计数模式是增加(PCNT_COUNT_INC),则变为减少(PCNT_COUNT_DEC);如果是减少(PCNT_COUNT_DEC),则变为增加(PCNT_COUNT_INC)。
    • 应用场景:适用于需要根据控制信号的变化来切换计数方向的应用,例如双向旋转编码器。
  • PCNT_CHANNEL_LEVEL_ACTION_HOLD
    • 作用:保持当前的计数值不变,停止计数。
    • 示例:无论脉冲信号如何变化,计数值都不会改变。
    • 应用场景:适用于需要暂停计数的应用,例如在某个特定条件满足时暂停计数。
  • 成员5 -> pcnt_count_mode_t pos_mode
    • 作用:定义正向边缘(上升沿)触发时的行为模式。
    • 参数类型:pcnt_count_mode_t
    • 参数选项:
      1. PCNT_COUNT_INC:增加计数值。
      2. PCNT_COUNT_DEC:减少计数值。
      3. PCNT_COUNT_DIS:禁止计数。
      4. PCNT_COUNT_H_LIM:达到上限时停止计数。
      5. PCNT_COUNT_L_LIM:达到下限时停止计数。
    • 示例:pos_mode = PCNT_COUNT_INC 表示在检测到正向边缘时增加计数值。
  • 成员6 -> pcnt_count_mode_t pos_mode
    • 作用:定义负向边缘(下降沿)触发时的行为模式。
    • 参数类型:pcnt_count_mode_t
    • 参数选项:
      1. PCNT_COUNT_INC:增加计数值。
      2. PCNT_COUNT_DEC:减少计数值。
      3. PCNT_COUNT_DIS:禁止计数。
      4. PCNT_COUNT_H_LIM:达到上限时停止计数。
      5. PCNT_COUNT_L_LIM:达到下限时停止计数。
    • 示例:neg_mode = PCNT_COUNT_DIS 表示在检测到负向边缘时不改变计数值。

pcnt_count_mode_t定义如下:

typedef enum {
    PCNT_CHANNEL_EDGE_ACTION_HOLD,     /*!< Hold current count value */
    PCNT_CHANNEL_EDGE_ACTION_INCREASE, /*!< Increase count value */
    PCNT_CHANNEL_EDGE_ACTION_DECREASE, /*!< Decrease count value */
} pcnt_channel_edge_action_t;
typedef pcnt_channel_edge_action_t pcnt_count_mode_t;
  • 成员7 -> int16_t counter_h_lim
    • 作用:定义计数器的最大值限制。
    • 参数范围:int16_t类型的整数,通常设置为正值。
    • 示例:counter_h_lim = INT16_MAX 表示最大计数值为32767。
  • 成员8 -> int16_t counter_l_lim
    • 作用:定义计数器的最小值限制。
    • 参数范围:int16_t类型的整数,通常设置为负值。
    • 示例:counter_l_lim = INT16_MIN 表示最小计数值为-32768。
  • 成员9 -> pcnt_unit_t unit
    • 作用:指定使用的PCNT单元编号。
    • 参数选项:(前面在介绍的PCNT单元熟练地时候已经说过参数了)
      1. PCNT_UNIT_0:使用PCNT单元0。
      2. PCNT_UNIT_1:使用PCNT单元1。
      3. ......
    • 示例:unit = PCNT_UNIT_0 表示使用PCNT单元0。
  • 成员10 -> pcnt_channel_t channel
    • 作用:指定使用的PCNT通道编号。
    • 参数选项:
    • PCNT_CHANNEL_0:使用PCNT通道0。
    • PCNT_CHANNEL_1:使用PCNT通道1。
    • 示例:channel = PCNT_CHANNEL_0 表示使用PCNT通道0。

pcnt_config_t 结构体讲解完之后,你应该已经会配置PCNT了

初始化PCNT单元

这一块就比较简单了,先展示一下函数声明:

/**
 * @brief Configure Pulse Counter unit
 * @note 调用此函数时,会自动禁用三个事件:PCNT_EVT_L_LIM、PCNT_EVT_H_LIM 和 PCNT_EVT_ZERO。如果你需要这些事件,请在配置完成后重新启用它们。
 *
 * @param pcnt_config Pointer of Pulse Counter unit configure parameter
 *
 * @return
 *     - ESP_OK Success
 *     - ESP_ERR_INVALID_STATE pcnt driver already initialized
 *     - ESP_ERR_INVALID_ARG Parameter error
 */
esp_err_t pcnt_unit_config(const pcnt_config_t *pcnt_config);

假设你已经配置好了一个pcnt_config,那直接调用pcnt_unit_config(&pcnt_config);就可以安装PCNT了

启动PCNT单元

先贴出函数声明

/**
 * @brief Resume counting for PCNT counter
 *
 * @param pcnt_unit PCNT unit number, select from pcnt_unit_t
 *
 * @return
 *     - ESP_OK Success
 *     - ESP_ERR_INVALID_STATE pcnt driver has not been initialized
 *     - ESP_ERR_INVALID_ARG Parameter error
 */
esp_err_t pcnt_counter_resume(pcnt_unit_t pcnt_unit);

这个函数的功能其实是恢复指定 PCNT 单元的计数功能。但是当我们初始化完PCNT后,技术功能默认是暂停的,因此我们需要调用这个函数来恢复计数功能。

关于PCNT单元的滤波和计数值清零

这是设置PCNT滤波时间的函数:

/**
 * @brief Set PCNT filter value
 *
 * @param unit PCNT unit number
 * @param filter_val PCNT signal filter value, counter in APB_CLK cycles.
 *        Any pulses lasting shorter than this will be ignored when the filter is enabled.
 *        @note
 *        filter_val is a 10-bit value, so the maximum filter_val should be limited to 1023.
 *
 * @return
 *     - ESP_OK Success
 *     - ESP_ERR_INVALID_STATE pcnt driver has not been initialized
 *     - ESP_ERR_INVALID_ARG Parameter error
 */
esp_err_t pcnt_set_filter_value(pcnt_unit_t unit, uint16_t filter_val);

设置好滤波后调用函数pcnt_filter_enable(pcnt_unit_t unit);使能滤波就可以了

滤波器清零的函数如下
pcnt_counter_clear(cnt_unit_t unit);

示例代码

#include <Arduino.h>
#include <driver/pcnt.h>

void setup()
{
    pcnt_config_t pcnt_config = {0};
    pcnt_config.pulse_gpio_num = 15;      // 脉冲脚 CLK
    pcnt_config.ctrl_gpio_num = 16;       // 方向脚 DT
    pcnt_config.unit = PCNT_UNIT_0;
    pcnt_config.channel = PCNT_CHANNEL_0;
    pcnt_config.pos_mode = PCNT_COUNT_INC;
    pcnt_config.neg_mode = PCNT_COUNT_DIS;
    pcnt_config.lctrl_mode = PCNT_MODE_REVERSE;
    pcnt_config.hctrl_mode = PCNT_MODE_KEEP;
    pcnt_config.counter_h_lim = 32767;  // 最大计数值
    pcnt_config.counter_l_lim = -32767; // 最小计数值
    pcnt_unit_config(&pcnt_config);
    pcnt_set_filter_value(PCNT_UNIT_0, 1023); // 设定滤波时间
    pcnt_filter_enable(PCNT_UNIT_0);
    pcnt_counter_clear(PCNT_UNIT_0);
    pcnt_counter_resume(PCNT_UNIT_0);
    Serial.begin(115200);
}

int16_t count = 0;
void loop() // 不展示使用定时器了,简单写个读取100ms内的计数值
{
    delay(100);
    pcnt_get_counter_value(PCNT_UNIT_0, &count);
    pcnt_counter_clear(PCNT_UNIT_0);
    Serial.print("count:");
    Serial.println(count);
}
posted @ 2025-07-12 13:09  ouyanglingle  阅读(526)  评论(0)    收藏  举报