前言:pcnt是esp32的高速脉冲计数模式,用于计数编码器等波形,其可以通过硬件执行减少软件资源消耗。通常需要俩个io口来控制。
本次使用包含pcnt计数及其中断,简述了pcnt及其使用方法,介绍了自定义边沿和双通道计数(对标stm32的编码器模式)。
目录
1.认识pcnt
1.1简述
这里重点是 CH0 CH1 Unit0
uint代表计数单元 这里一共有四个单元(这里是esp32s3其他看手册)。
每个单元有俩个通道ch0 ch1都可以用来计数。
每个通道控制需要俩个gpio口,即input pulse引脚,control引脚。这里先不用管具体含义后面会叙述怎么用。
1.2特性
计数范围是16位,可以设置滤波(pcnt_set_filter_value())最大计数速率为40mhz
2.使用配置(重点)
先看这张图只看ch0 其他都不看
看这里
这就是前面ch0接的俩个引脚 pulse引脚和ctrl引脚(名字而已对于编码器来说不重要)
filter是滤波器也不管后面有api直接设置和开启
重点看inc_dec里面有四个寄存器来控制 知道就行后面用结构体会来详细说明
(1)pcnt_ch0_neg_mode_un :用来控制下降沿的计数模式 (接在pulse引脚上)
(2)pcnt_ch0_pos_mode_un:用来控制上升沿的计数模式(接在pulse引脚上)
(3)pcnt_ch0_hctrl_mode_un:用来控制高电平的计数模式是否反转或者不计数(接在ctrl引脚上)
(4)pcnt_ch0_lctrl_mode_un:用来控制低电平的计数模式是否反转或者不计数(接在ctrl引脚上)
我们用俩个引脚来控制计数的模式其中第一个为pulse引脚 esp32会在pulse引脚产生上升沿和下降沿时产生中断,通过检测ctrl引脚的高低电平来判断是增加计数还是减少计数。
2.1再来看编码器原理
我们定义正旋计数+1反向-1.
这里我们设置A相为pulse引脚只用于检测上升沿和下降沿。
B相为ctrl引脚只用于检测高低电平。
正旋计数时A相比B相超前90°,A相上升沿时B为低电平,A相下降沿时B为高电平。
简称:A上B低 A下B高 为正旋 (后面要用的)
反旋计数时B相比A相超前90°,A相上升沿时B为高电平,A相下降沿时B为低电平。
简称:A上B高 A下B低 为反旋(后面要用的)
这样就可以分出正反转了 下一步我们来配置。
2.2结构体配置:
主要通过该结构体来配置 看不懂不要紧跟我一起来配就行
#include <driver/pcnt.h>
#include "encoder.h"
#include "esp_err.h"
#include "soc/pcnt_reg.h"
#include "soc/soc.h"
#include "arduino.h"
typedef struct {
int pulse_gpio_num; //pulse引脚号
int ctrl_gpio_num; //ctrl引脚号
pcnt_ctrl_mode_t lctrl_mode; //设置ctrl引脚为低电平时的计数方式
pcnt_ctrl_mode_t hctrl_mode; //设置ctrl引脚为高电平时的计数方式
pcnt_count_mode_t pos_mode; //设置上升沿时的计数方式
pcnt_count_mode_t neg_mode; //设置为下降沿时的计数方式
int16_t counter_h_lim; //最大计数0-65535
int16_t counter_l_lim; //最小计数0-65535
pcnt_unit_t unit; //计数单元选择 uint0-uint4
pcnt_channel_t channel; //ch0还是ch1的通道
} pcnt_config_t;
(1)首先创建一个该结构体
pcnt_config_t pcnt_config_0;
(2)引出该成员
pcnt_config_0.pulse_gpio_num =; // 旋钮 A 相 (CLK)
pcnt_config_0.ctrl_gpio_num = ; // 旋钮 B 相 (DT)
pcnt_config_0.channel = ;
pcnt_config_0.unit = ;
pcnt_config_0.pos_mode = ;
pcnt_config_0.neg_mode = ;
pcnt_config_0.lctrl_mode = ;
pcnt_config_0.hctrl_mode = ;
pcnt_config_0.counter_h_lim = 32767; // 计数上限
pcnt_config_0.counter_l_lim = -32767; // 计数下限
(3)配置成员
-------pulse_gpio_num:
pulse引脚对应A或者B相任意一相引脚这里我是A相。
-------ctrl_gpio_num:
ctrl引脚同上。
-------channel:
PCNT_CHANNEL_0对应同一个uint下的ch0通道(见1.1简述)
PCNT_CHANNEL_1对应同一个uint下的ch1通道
-------unit:
PCNT_UNIT_0, 选择uint0-4(见1.1简述)
PCNT_UNIT_1,
PCNT_UNIT_2,
PCNT_UNIT_3,
-------pos_mode:
上升沿计数方式这里我是A相
PCNT_COUNT_DIS 不计数
PCNT_COUNT_INC 上升计数即计数+1
PCNT_COUNT_DEC 下降计数即计数-1
-------neg_mode:
下降沿计数方式这里我是A相
同上
-------lctrl_mode:
低电平是否改变计数模式
PCNT_MODE_KEEP 不改变
PCNT_MODE_REVERSE 反转(上升变下降)
PCNT_MODE_DISABLE 不计数计数不加不减
-------hctrl_mode:
高电平是否改变计数模式
同上
(4)例子:
还是来看这张图
A上B低 A下B高 为正旋我们计数+1
A上B高 A下B低 为反旋我们计数-1
先看(A上B低 A下B高 为正旋我们计数+1):
这里我们要分辨正反计数就很明了了,pos_mode =PCNT_COUNT_INC 设置上升沿计数为增加计数计数+1遇到上升沿就会+1(设置为减少也行后面会说为什么先默认增加计数)。
然后从左往右看 第一个A相上升时我们看到B为低电平说明我们上升沿计数上升不改变其计数的模式,所以lctrl_mode=PCNT_MODE_KEEP 让A保持其计数模式即计数+1
然后往右继续走,第一个A的下降沿时B为高电平 我们设置neg_mode = PCNT_COUNT_DEC下降沿计数-1,但是这显然不是我们要的 我们要正旋时一直+1这里显然不是我们要的。这里计数模式改成减少计数了,我们要让他增加就得切换他的计数模式即hctrl_mode=PCNT_MODE_REVERSE
让B引脚在高电平时切换计数模式从-1的计数模式->变成+1的计数模式。这样正旋我们一直计数+1了。
再来看A上B高 A下B低 为反旋我们计数-1:
同理我们已经设置过了pos_mode 和neg_mode 分别为+1计数和-1计数也设置了hctrl_mode为反转计数模式使得遇到B为高电平时neg_mode 从-1计数反转为+1计数(低电平未设置)。
看反旋的图,A上升沿B为高电平要让计数-1,前面已经设置过了A为上升沿时计数+1的模式,且B为高电平时反转计数模式,这里就会刚好使得反转时A为上升沿时从-1变成+1的计数模式。很巧对吧。在A第一个下降沿时B为低电平,显然我们A下降沿时为-1的计数模式所以保持计数模式所以我们设置lctrl_mode = PCNT_MODE_KEEP使得保持-1计数。
为什么要一个+1一个-1:因为只在A上计数如果不设置+1-1就会无法判断方向,大家自行试着都设置+1试试这样会在反向时出现问题,交换上下边沿+1或者-1只是交换了极性而已。
我们总结一下
关于A引脚需要设置其上下沿的计数模式即+1还是-1或者不计数遇到上下沿就会改变编码器的计数值。(我们计数是通过A引脚的边沿来计数的)
关于B引脚是用来控制A引脚的计数方式是否需要反转A引脚的计数方式。即从+1变成-1或者反之再或者不变。(B只改变A的计数模式通过改变计数模式来判断正反旋向)
贴上配置代码
pcnt_config_t pcnt_config_0;
pcnt_config_0.pulse_gpio_num = ENCODER_PIN_A; // 旋钮 A 相 (CLK)
pcnt_config_0.ctrl_gpio_num = ENCODER_PIN_B; // 旋钮 B 相 (DT)
pcnt_config_0.channel = PCNT_CHANNEL_0;
pcnt_config_0.unit = PCNT_UNIT_0;
pcnt_config_0.pos_mode = PCNT_COUNT_INC;
pcnt_config_0.neg_mode = PCNT_COUNT_DEC;
pcnt_config_0.lctrl_mode = PCNT_MODE_KEEP;
pcnt_config_0.hctrl_mode = PCNT_MODE_REVERSE;
pcnt_config_0.counter_h_lim = 32767; // 计数上限
pcnt_config_0.counter_l_lim = -32767; // 计数下限
(5)初始化
pcnt_unit_config(&pcnt_config_0); // 初始化 PCNT
pcnt_set_filter_value(PCNT_UNIT_0, 1000); // 设置滤波时间大小0-1023 这里不多说需要的看手册
pcnt_filter_enable(PCNT_UNIT_0); // 启用滤波
(6)开始计数
清除计数器并启动
pcnt_counter_pause(PCNT_UNIT_0);
pcnt_counter_clear(PCNT_UNIT_0);
pcnt_counter_resume(PCNT_UNIT_0);
5-6自己看一下就好注意一定要有 pcnt_counter_clear(PCNT_UNIT_0);不然计数的
(7)中断设置
pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_THRES_1);
pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_THRES_0);//开始事件中断
pcnt_set_event_value(PCNT_UNIT_0, PCNT_EVT_THRES_0, 100);//设置事件中断的阈值
pcnt_set_event_value(PCNT_UNIT_0, PCNT_EVT_THRES_1, -100);
pcnt_isr_service_install(0);//设置优先级
pcnt_isr_handler_add(PCNT_UNIT_0, encoder_callback, NULL);//设置中断回调函数encoder_callback
中断类型有这么几种事件
PCNT_EVT_THRES_1 计数达到设定值1时中断 通过pcnt_set_event_value设置
PCNT_EVT_THRES_0 计数达到设定值2时中断通过pcnt_set_event_value设置
PCNT_EVT_L_LIM = 1 计数达到最大设定值中断(结构体设置里面的最大计数)
PCNT_EVT_H_LIM = 1计数达到最小设定值中断
PCNT_EVT_ZERO = 1 计数值达到0时中断
(8)中断回调处理
void encoder_callback(void* arg) {
uint32_t status;
pcnt_get_counter_value(PCNT_UNIT_0,&encoder_value);
pcnt_get_event_status(PCNT_UNIT_0,&status);
Serial.printf("Encoder Value: %d, Status: 0x%x\n", encoder_value, status);
if(status&PCNT_EVT_THRES_0){
msg_count=1;
pcnt_counter_clear(PCNT_UNIT_0); // 手动清除计数器值
WRITE_PERI_REG(PCNT_INT_CLR_REG, PCNT_CNT_THR_EVENT_U0_INT_CLR);
}else if (status&PCNT_EVT_THRES_1)
{
pcnt_counter_clear(PCNT_UNIT_0); // 手动清除计数器值
msg_count=2;
WRITE_PERI_REG(PCNT_INT_CLR_REG, PCNT_CNT_THR_EVENT_U0_INT_CLR);
}
}
pcnt_get_event_status()获取(7)中的中断事件来判断是什么中断
这里我偷懒了一下用arduino的固件来做,方便串口打印(懒得写串口了),中断我们要判断是哪个unit的中的那个中断。用pcnt_get_event_status()获取中断标志位的值通过&的方式来判断是哪一个中断产生了并清除中断标志位。我这里通过直接写入寄存器清除(因为arduino固件的pcnt里面没有api来清除我直接操作寄存器了WRITE_PERI_REG(PCNT_INT_CLR_REG, PCNT_CNT_THR_EVENT_U0_INT_CLR);看下俩张图就知道怎么清除了还是很简单的)当然也可以用指针操作 知道地址直接置1就行了很简单!idf用户有结构体可以访问清除不多说。
(8)主函数
void setup() {
Serial.begin(9600);
encoder_init();
}
void loop() {
if(msg_count==1){
Serial.printf("msg_count=1,encoer_value=%d\n",encoder_value);
msg_count=0;
}else if (msg_count==2)
{
Serial.printf("msg_count=2,encoer_value=%d\n",encoder_value);
msg_count=0;
}
pcnt_get_counter_value(PCNT_UNIT_0,&encoder_value);/获取计数值
Serial.printf("encoder_value:%d\n",encoder_value);
delay(1000);
}
(9)效果
达到100或者-100时产生中断
3.其他计数模式对标stm32 单/双边沿计数(扩展)
我们知道stm32的编码器模式可以双边沿计数单边沿计数
esp32如何实现单/双边沿呢?
3.1单边沿(自定义边沿计数)
很简单我们设置A相计数的时候设置上或者下边沿不计数就好了.
3.2双边沿
uint作为一个计数单元会计数来自ch1和ch2的俩个通道的计数。但是通过前面我们知道计数只能是A作为pulse引脚来计数或者B作为pulse引脚来计数另一个作为正反转控制引脚。
所以我们只需要将A和B引脚互换原来接在ch0上的A作为pulse引脚计数改为B作为pulse引脚计数并接到ch1上面这样同样俩个引脚A在ch0上计数B在ch1上计数即可。看具体代码就知道了
pcnt_config_t pcnt_config_0;
pcnt_config_0.pulse_gpio_num = ENCODER_PIN_A; // 旋钮 A 相 (CLK)
pcnt_config_0.ctrl_gpio_num = ENCODER_PIN_B; // 旋钮 B 相 (DT)
pcnt_config_0.channel = PCNT_CHANNEL_0;
pcnt_config_0.unit = PCNT_UNIT_0;
pcnt_config_0.pos_mode = PCNT_COUNT_INC;
pcnt_config_0.neg_mode = PCNT_COUNT_DEC;
pcnt_config_0.lctrl_mode = PCNT_MODE_KEEP;
pcnt_config_0.hctrl_mode = PCNT_MODE_REVERSE;
pcnt_config_0.counter_h_lim = 32767; // 计数上限
pcnt_config_0.counter_l_lim = -32767; // 计数下限
pcnt_config_t pcnt_config_1;
pcnt_config_1.pulse_gpio_num = ENCODER_PIN_B; // 旋钮 A 相 (CLK)
pcnt_config_1.ctrl_gpio_num = ENCODER_PIN_A; // 旋钮 B 相 (DT)
pcnt_config_1.channel = PCNT_CHANNEL_1;
pcnt_config_1.unit = PCNT_UNIT_0;
pcnt_config_1.pos_mode = PCNT_COUNT_INC;
pcnt_config_1.neg_mode = PCNT_COUNT_DEC;
pcnt_config_1.lctrl_mode =PCNT_MODE_REVERSE;
pcnt_config_1.hctrl_mode = PCNT_MODE_KEEP;
pcnt_config_1.counter_h_lim = 32767; // 计数上限
pcnt_config_1.counter_l_lim = -32767; // 计数下限
pcnt_unit_config(&pcnt_config_0); // 初始化 PCNT
pcnt_unit_config(&pcnt_config_1); // 初始化 PCNT
我们将AB引脚互换重新配置了一个结构图 pcnt_config_1 因为
A上B低 A下B高 为正旋我们计数+1
A上B高 A下B低 为反旋我们计数-1
对于B来说是相反的即
B上A低 B下A高 为正旋我们计数-1
B上A高 B下A低 为反旋我们计数+1
所以我们需要改变ctrl引脚在pcnt_config_1 交换(跟pcnt_config_0是反过来的)
pcnt_config_1.lctrl_mode =PCNT_MODE_REVERSE;
pcnt_config_1.hctrl_mode = PCNT_MODE_KEEP;
配置即可
(别问为什么不改pcnt_config_1.pos_mode = PCNT_COUNT_INC; pcnt_config_1.neg_mode = PCNT_COUNT_DEC;你可以自己照着2中的分析一下就明白了)
完整配置代码(直接修改可用)
#include <driver/pcnt.h>
#include "encoder.h"
#include "esp_err.h"
#include "soc/pcnt_reg.h"
#include "soc/soc.h"
#include "arduino.h"
int16_t encoder_value = 0;
int msg_count = 0;
#define ENCODER_PIN_A 4
#define ENCODER_PIN_B 5
#define PCNT_CNT_THR_EVENT_0_INT_CLR (*((volatile uint32_t*)0x3FF40000))
void encoder_init() {
pcnt_config_t pcnt_config_0;
pcnt_config_0.pulse_gpio_num = ENCODER_PIN_A; // 旋钮 A 相 (CLK)
pcnt_config_0.ctrl_gpio_num = ENCODER_PIN_B; // 旋钮 B 相 (DT)
pcnt_config_0.channel = PCNT_CHANNEL_0;
pcnt_config_0.unit = PCNT_UNIT_0;
pcnt_config_0.pos_mode = PCNT_COUNT_INC;
pcnt_config_0.neg_mode = PCNT_COUNT_DEC;
pcnt_config_0.lctrl_mode = PCNT_MODE_KEEP;
pcnt_config_0.hctrl_mode = PCNT_MODE_REVERSE;
pcnt_config_0.counter_h_lim = 32767; // 计数上限
pcnt_config_0.counter_l_lim = -32767; // 计数下限
//双通道计数时添加下列
pcnt_config_t pcnt_config_1;
pcnt_config_1.pulse_gpio_num = ENCODER_PIN_B; // 旋钮 A 相 (CLK)
pcnt_config_1.ctrl_gpio_num = ENCODER_PIN_A; // 旋钮 B 相 (DT)
pcnt_config_1.channel = PCNT_CHANNEL_1;
pcnt_config_1.unit = PCNT_UNIT_0;
pcnt_config_1.pos_mode = PCNT_COUNT_INC;
pcnt_config_1.neg_mode = PCNT_COUNT_DEC;
pcnt_config_1.lctrl_mode =PCNT_MODE_REVERSE;
pcnt_config_1.hctrl_mode = PCNT_MODE_KEEP;
pcnt_config_1.counter_h_lim = 32767; // 计数上限
pcnt_config_1.counter_l_lim = -32767; // 计数下限
pcnt_unit_config(&pcnt_config_1); // 初始化 PCNT
pcnt_unit_config(&pcnt_config_0); // 初始化 PCNT
pcnt_set_filter_value(PCNT_UNIT_0, 1000); // 设置滤波窗口大小
pcnt_filter_enable(PCNT_UNIT_0); // 启用滤波
// 2. 清除计数器并启动
pcnt_counter_pause(PCNT_UNIT_0);
pcnt_counter_clear(PCNT_UNIT_0);
pcnt_counter_resume(PCNT_UNIT_0);
// 3. 注册中断
pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_THRES_1);
pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_THRES_0);
pcnt_set_event_value(PCNT_UNIT_0, PCNT_EVT_THRES_0, 100);
pcnt_set_event_value(PCNT_UNIT_0, PCNT_EVT_THRES_1, -100);
pcnt_isr_service_install(0);
pcnt_isr_handler_add(PCNT_UNIT_0, encoder_callback, NULL);
}
void encoder_callback(void* arg) {
uint32_t status;
pcnt_get_counter_value(PCNT_UNIT_0,&encoder_value);
pcnt_get_event_status(PCNT_UNIT_0,&status);
Serial.printf("Encoder Value: %d, Status: 0x%x\n", encoder_value, status);
if(status&PCNT_EVT_THRES_0){
msg_count=1;
pcnt_counter_clear(PCNT_UNIT_0); // 手动清除计数器值
WRITE_PERI_REG(PCNT_INT_CLR_REG, PCNT_CNT_THR_EVENT_U0_INT_CLR);
}else if (status&PCNT_EVT_THRES_1)
{
pcnt_counter_clear(PCNT_UNIT_0); // 手动清除计数器值
msg_count=2;
WRITE_PERI_REG(PCNT_INT_CLR_REG, PCNT_CNT_THR_EVENT_U0_INT_CLR);
}
}