一、脉冲计数器

1介绍

脉冲计数器模块用于对输入脉冲的上升沿或下降沿进行计数。每个脉冲计数器单元均有一个带符号的 16-bit 计数寄存器以及两个通道,通过配置可以加减计数器。每个通道均有一个脉冲输入信号以及一个能够用于控制输
入信号的控制信号。输入信号可以打开或关闭滤波功能

2 功能描述

脉冲计数器每个单元有两个通道:ch0和ch1。这两个功能相似。每个通道均
有一个输入信号和一个控制输入信号,都能连接到芯片引脚。上升沿和下降沿中的计数工作模式可以分别进行增加、不增不减或者减少计数值的配置行为。对控制信号而言,通过配置硬件可以更改上升沿和下降沿的工作
模式,包括:反转、禁止和保持。该计数器本身是一个带符号的 16-bit 加减计数器。它的值可以由软件直接读取,硬件通过将该值与一组比较器进行比较,可以产生中断。

二、定时器

1 定时器的介绍

ESP32 内置 4 个 64-bit 通用定时器。每个定时器包含一个 16-bit 预分频器和一个 64-bit 可自动重新加载向上/向下计数器。
ESP32 的定时器分为 2 组,每组 2 个。TIMGn_Tx 的 n 代表组别,x 代表定时器编号。具有16-bit预分频、64-bit时基计数、警报产生、MWDT(主系统看门狗)、中断等多种功能

2 定时器特性

  • 可配置向上/向下时基计数器
  • 暂停和恢复时基计数器
  • 电平触发中断和边沿触发中断
  • 软件控制的即时重新加载

三、实验介绍

1 脉冲计数器的配置

定时器获取时间、PCNT进行脉冲计数、LED等随着电机旋转速度变化
在这里插入图片描述
这里为脉冲计数器的实验基础知识,详细配置如下:

  1. 初始化PCNT
static void pcnt_example_init(int unit)
{
    //准备PCNT单元
    pcnt_config_t pcnt_config = {
        //设定pct输入信号,控制gpio
        .pulse_gpio_num = PCNT_INPUT_SIG_IO,
        .ctrl_gpio_num = PCNT_INPUT_CTRL_IO,
        .channel = PCNT_CHANNEL_0,
        .unit = unit,
        //在脉冲输入的正/负边缘做什么?
        .pos_mode = PCNT_COUNT_INC, // 计算正面的边缘
        .neg_mode = PCNT_COUNT_DIS, // 保持计数器值在负边
        //控制输入低或高时该怎么办?
        .lctrl_mode = PCNT_MODE_REVERSE, // 如果计数方向低,则反向计数
        .hctrl_mode = PCNT_MODE_KEEP,    // 如果高,保持主计数器模式
        // 最大最小值的限制
        .counter_h_lim = PCNT_H_LIM_VAL,
        .counter_l_lim = PCNT_L_LIM_VAL,
    };
  1. 设置阈值:
    pcnt_set_event_value(unit, PCNT_EVT_THRES_1, PCNT_THRESH1_VAL);
    pcnt_event_enable(unit, PCNT_EVT_THRES_1);

    pcnt_set_event_value(unit, PCNT_EVT_THRES_0, PCNT_THRESH0_VAL);
    pcnt_event_enable(unit, PCNT_EVT_THRES_0);

PCNT_EVT_THRES_1 为正转计数;PCNT_EVT_THRES_0 为反转计数;

PCNT_THRESH1_VAL和PCNT_THRESH0_VAL在宏定义时分别设置为11和-11(上图已经解释);

  1. 执行事件并记录:
while (1)
    {
        //等待事件信息从PCNT的中断处理程序传递,一旦收到,解码事件类型并打印在串行监视器上
        res = xQueueReceive(pcnt_evt_queue, &evt, DELAY_BASE_MS / portTICK_PERIOD_MS);
        if (res == pdTRUE)
        {
            pcnt_get_counter_value(pcnt_unit, &count);
            // ESP_LOGI(TAG, "Event PCNT unit[%d]; cnt: %d", evt.unit, count);
            if (evt.status & PCNT_EVT_THRES_1)
            {
            	//每执行一次,LED灯的状态翻转一次,实现与电机一起改变状态
                static bool state = true;
                gpio_set_level(LED_R, state);
                state = !state;
                //记录圈数
                i++;
                //除46是因为我的电机减速比是1:46
                f = i / 46.0;
                printf("cylinder number : %f\n", f);
            }

            if (evt.status & PCNT_EVT_THRES_0)
            {
                ESP_LOGI(TAG, "THRES0 EVT");
            }
        }
    }

到这里,脉冲计数已经基本完成,现在需要配置定时器,规定时间内测出多少圈,从而得出转速。

2. 定时器的配置

定时器可设置为一次性调用定时器,这样可以实现在规定时间内测出电机的圈数,从而得到电机的速度,定时器配置如下:

    //创建一个一次性定时器
    const esp_timer_create_args_t periodic_timer_args = {
        .callback = &periodic_timer_callback, //只回调一次
        //定时器名称
        .name = "periodic"};
    esp_timer_handle_t periodic_timer;
    /* 计时器已经创建,但还没有运行 */
    ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));
    //创建定时器句柄,设置定时器周期为60秒,启动定时器
    ESP_ERROR_CHECK(esp_timer_start_once(periodic_timer, 60 * 1000 * 1000));

这里就已经配置定时器为60秒的一次性定时器,配置好后只需要调用并执行定时器内的事件:

static void periodic_timer_callback(void *arg)
{
    int64_t time_since_boot = esp_timer_get_time();
        printf("vQueueDelete pcnt_evt_queue\n");
        vQueueDelete(pcnt_evt_queue);
        pcnt_isr_handler_remove(PCNT_UNIT_0);
}

当这里调用一次性定时器时,会删除脉冲计数器的事件、中断,从而让计数器不再计数,精确的测出电机速度。

四、实例代码

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/ledc.h"
#include "driver/pcnt.h"
#include "esp_attr.h"
#include "esp_log.h"
#include "sdkconfig.h"
#include "driver/gpio.h"
#include "esp_timer.h"

static const char *TAG = "example";

#define PCNT_H_LIM_VAL 11
#define PCNT_L_LIM_VAL -40
#define PCNT_THRESH1_VAL 10
#define PCNT_THRESH0_VAL -10
#define PCNT_INPUT_SIG_IO 4  // 脉冲输入GPIO
#define PCNT_INPUT_CTRL_IO 5 // 控制GPIO HIGH=计数向上,LOW=计数向下
#define LED_R 2

xQueueHandle pcnt_evt_queue; // 处理脉冲计数器事件的队列

//从PCNT传递事件的示例结构,主程序的中断处理程序。
typedef struct
{
    int unit;        //触发中断的PCNT装置
    uint32_t status; // 关于导致中断的事件类型的信息
} pcnt_evt_t;

//解码PCNT的部门发出的中断信号并将此消息与时间类型一起传递给队列的主程序
static void IRAM_ATTR pcnt_example_intr_handler(void *arg)
{
    int pcnt_unit = (int)arg;
    pcnt_evt_t evt;
    evt.unit = pcnt_unit;
    //保存导致中断的PCNT事件类型将它传递给主程序
    pcnt_get_event_status(pcnt_unit, &evt.status);
    xQueueSendFromISR(pcnt_evt_queue, &evt, NULL);
}

void LED_Init(void)
{
    gpio_pad_select_gpio(LED_R);                 // 选择GPIO口
    gpio_set_direction(LED_R, GPIO_MODE_OUTPUT); // GPIO作为输出
    gpio_set_level(LED_R, 0);                    // 默认低电平
}

/* 初始化PCNT函数:
 *  配置和初始化PCNT
 *  设置输入过滤器
 *  设置计数器时间观看
 */
static void pcnt_example_init(int unit)
{
    //准备PCNT单元
    pcnt_config_t pcnt_config = {
        //设定pct输入信号,控制gpio
        .pulse_gpio_num = PCNT_INPUT_SIG_IO,
        .ctrl_gpio_num = PCNT_INPUT_CTRL_IO,
        .channel = PCNT_CHANNEL_0,
        .unit = unit,
        //在脉冲输入的正/负边缘做什么?
        .pos_mode = PCNT_COUNT_INC, // 计算正面的边缘
        .neg_mode = PCNT_COUNT_DIS, // 保持计数器值在负边
        //控制输入低或高时该怎么办?
        .lctrl_mode = PCNT_MODE_REVERSE, // 如果计数方向低,则反向计数
        .hctrl_mode = PCNT_MODE_KEEP,    // 如果高,保持主计数器模式
        // 最大最小值的限制
        .counter_h_lim = PCNT_H_LIM_VAL,
        .counter_l_lim = PCNT_L_LIM_VAL,
    };
    //初始化PCNT单元
    pcnt_unit_config(&pcnt_config);

    //配置并启用输入过滤器
    pcnt_set_filter_value(unit, 100);
    pcnt_filter_enable(unit);

    //设置阈值0和1,并启用事件监视
    pcnt_set_event_value(unit, PCNT_EVT_THRES_1, PCNT_THRESH1_VAL);
    pcnt_event_enable(unit, PCNT_EVT_THRES_1);

    pcnt_set_event_value(unit, PCNT_EVT_THRES_0, PCNT_THRESH0_VAL);
    pcnt_event_enable(unit, PCNT_EVT_THRES_0);

    //初始化PCNT的计数器
    pcnt_counter_pause(unit);
    pcnt_counter_clear(unit);

    //安装中断服务并添加isr回调处理器
    pcnt_isr_service_install(0);
    pcnt_isr_handler_add(unit, pcnt_example_intr_handler, (void *)unit);

    //一切都设置好了,现在去计数
    pcnt_counter_resume(unit);
}
static void periodic_timer_callback(void *arg);
#define DELAY_BASE_MS 1000
void app_main(void)
{

    //创建一个一次性定时器
    const esp_timer_create_args_t periodic_timer_args = {
        .callback = &periodic_timer_callback, //只回调一次回调
        //定时器名称
        .name = "periodic"};
    esp_timer_handle_t periodic_timer;
    /* 计时器已经创建,但还没有运行 */
    ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));
    //创建定时器句柄,设置定时器周期为2秒,启动定时器
    ESP_ERROR_CHECK(esp_timer_start_once(periodic_timer, 10 * 1000 * 1000));

    LED_Init();
    int pcnt_unit = PCNT_UNIT_0;
    //初始化LEDC产生采样脉冲信号
    int i = 0;
    double f;
    //初始化PCNT事件队列和PCNT函数
    pcnt_evt_queue = xQueueCreate(10, sizeof(pcnt_evt_t));
    pcnt_example_init(pcnt_unit);

    int16_t count = 0;
    pcnt_evt_t evt;
    portBASE_TYPE res;
    while (1)
    {
        //等待事件信息从PCNT的中断处理程序传递,一旦收到,解码事件类型并打印在串行监视器上
        res = xQueueReceive(pcnt_evt_queue, &evt, DELAY_BASE_MS / portTICK_PERIOD_MS);
        if (res == pdTRUE)
        {
            pcnt_get_counter_value(pcnt_unit, &count);
            // ESP_LOGI(TAG, "Event PCNT unit[%d]; cnt: %d", evt.unit, count);
            if (evt.status & PCNT_EVT_THRES_1)
            {
                static bool state = true;
                gpio_set_level(LED_R, state);
                state = !state;
                i++;
                // f=i/10.0;
                f = i / 46.0;
                printf("cylinder number : %f\n", f);
            }

            if (evt.status & PCNT_EVT_THRES_0)
            {
                ESP_LOGI(TAG, "THRES0 EVT");
            }
        }
    }
}
static int j = 0;
static void periodic_timer_callback(void *arg)
{
    int64_t time_since_boot = esp_timer_get_time();
    j++;
    printf("j = %d\n", j);
    if (j >= 1)
    {
        printf("vQueueDelete pcnt_evt_queue\n");
        vQueueDelete(pcnt_evt_queue);
        pcnt_isr_handler_remove(PCNT_UNIT_0);
    }
}

五、总结

在这里插入图片描述

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐