旋转编码器:如何在 Arduino 上使用 Keys KY-040 编码器

旋转编码器是一种输入设备,您可以在任一方向连续旋转。当您转动设备时,它会生成数字脉冲,以使用两个相控输出信号显示旋转方向。这两个输出还指示单个位置运动,因此您可以在控制面板中使用它们来增加或减少参数。

注意:由于开关弹跳,旋转编码器会产生极其嘈杂的输出振荡,本页上的信息为您提供了 两种消除这种噪声的技术。第一种是简单的过滤方法,第二种方法使用表解码从低质量设备中获得非常好的输出。

下面用于演示的编码器类型也称为增量旋转编码器,因为它产生指示单步变化的脉冲。其他类型生成绝对输出,即为编码器的特定位置生成相同的输出编号(4 位或更多位,具体取决于所需的精度),您可以在机器人应用中使用这些输出。

本教程的目的是提供一个简单的旋转编码器实现的 arduino 示例。

旋转编码器允许您轻松地增加或减少单个值的参数。

除了生成方向信息和阶跃变化脉冲外,该设备还具有物理反馈机制,让您在从一个位置移动到下一个位置时感觉到。这些点被称为棘爪,在 360° 旋转范围内的位置范围为 12 到 24 个。对于此处使用的设备,有 20 个棘爪。

与电位器不同,旋转编码器没有终点挡板,因此您可以使用它来不断增加或减少参数(一旦由微控制器解码),并且无需将控制位置设置回起点(没有)。

他们还经常在轴中内置一个按钮开关,这对于菜单选择等很有用。

您可以将它们用于许多应用程序,包括:

  • 音量控制

  • 照度控制

  • 参数控制,例如速度、高度、温度等过程的参数控制。

  • 菜单选择(按钮在这里很有用)。

由于输出是数字信号,您可以使用微控制器对其进行处理,并以您想要的任何方式使用结果,即更改表示系统参数的变量的值。

事实上,旋转编码器看起来很简单,但在这些小型设备(~11mm x ~13mm)内部却发生了很多事情。

这是发生了什么:

  • 两个输出提供正交编码信号。

  • 物理位置反馈和缓冲块(称为棘爪)- 对于该设备,有 20 个棘爪。

  • 主轴按钮(按下开关)。

当您转动控制旋钮时,您会感觉到每个“制动”位置都停止了,因此您知道何时将设备转动了一个位置。这提供了细粒度的物理反馈,允许精确的参数改变。这与在没有物理反馈的情况下使用电位器设置音量等非常不同。

正交相移编码

这种技术性很强的编码方法实际上非常简单。这意味着两个信号彼此偏移四分之一周期(或相移 90?)。生成的信号是灰色编码的,这也意味着没有两个信号边缘对齐,即信号输出不会同时改变状态。

格雷编码可用于机电设备生成明确的信号。例如,如果输出是二进制编码的,那么在转换点(由于信号路径中的小延迟),您可能会解码一个完全错误的值,即在转换点可能会生成任何代码。

这可能是一个问题,尤其是在仅使用组合逻辑作为解码器的情况下。格雷码阻止了这种情况的发生(尽管它不会阻止开关弹跳)。

下图分别显示引脚 A 和 B (CLK) 和 (DT) 上的旋转编码器波形输出。

旋转编码器:如何在 Arduino 上使用 Keys KY-040 编码器_第1张图片

*[来源 PEC11L 数据表]*

注意:上图中的 D 表示止动位置的位置。事实上,这是输出没有接地的地方,因此它们被分线板上的 10k 电阻拉高。

旋转编码器内部

下图显示了旋转编码器的内部工作原理。三个连接8A、8B和8C中的每一个都由向下推基板的弹簧臂形成。

共有三个信号,一个连接到金属基板(接地),另外两个在交替的基板图案上移动。因此,当设备旋转时,输出会短接到地,然后当触点位于基板间隙中时,输出会悬空(未连接)。

请注意弹簧臂触点如何物理偏移四分之一周期(由物理基板定义) - 下图中的触点 8B 和 8C - 这就是正交编码输出的生成方式。

旋转编码器:如何在 Arduino 上使用 Keys KY-040 编码器_第2张图片

*资料来源:过期专利(现在在公共领域)。*

注意:触点 8A、8B、8C 是在触点基板上弹跳和弹跳的弹簧,导致输出信号在高电平和低电平之间弹跳,即开关弹跳。

旋转编码器的类型

接触增量式旋转编码器

这是本页演示中使用的设备类型。在每个定位位置产生两个正交信号,指示单个位置变化并显示旋转方向。

这种特殊的设备具有相当长的旋转寿命 - 100k 旋转(参见数据表) - 但由于存在物理接触,设备最终会磨损。在 Bourns目录中,其他物理设备的最大旋转范围为 15k 到 200k。

光学编码器

PEC11L 的最大 RPM 为 60RPM,而该目录中的光学编码器具有 1000 万转的使用寿命,并且可以在 3000rpm 下运行 - 这些是您可以用于高速机械测量的类型,但请参阅下面的磁性编码器,其具有更高生命当然还有更高的成本!

磁性编码器

为了获得更高的旋转寿命,磁编码器提供了最佳选择(因为设备内部没有物理接触),唯一会磨损的部件是轴承。它们提供 1 亿转的旋转寿命!

这些设备有 4 种不同的口味:

  1. 增量正交(与此处使用的 PEC11L 相同)。

  2. 方向/步长编码器 - 提供更好的分辨率(每转最多 512 个脉冲)。

  3. 绝对编码器 - 允许编码器的绝对位置检测(1024 个代码定义位置)。

  4. PWM 编码器 - 产生 1us 至 1024us 宽度的 PWM 输出 - 声称的优势是抗噪性和更快的数据采集。

测量:使用旋转增量编码器

以下示例涵盖以下测量:

  • 速度

  • 对数变化

速度

您可能希望测量速度以用作代码中的参数,例如,如果您更快地转动车轮然后执行不同的操作,例如以不同的速率更改参数。

对数

这是一种测量旋转速度的参数调整,如果发现是恒定的,则周期性地增加参数。这对于具有大范围控制的设备非常有用,例如可以输出 1 到 10MHz 频率的 DDS(直接数字合成)。您真的不想坐在那里将旋钮转动 1Hz 周期以达到 10MHz!

解码方法

有多种方法可以对旋转编码器输出进行解码:

  • 轮询

  • 中断

在 KY-040 上有两个标记为 DT 和 CLK 的信号,分别表示 CLOCK 和 DATA。如果您查看这些信号的时序图,显然将 CLOCK 用作时钟并在时钟的上升沿读取 DATA 输入。然而,这忽略了信号在各处反弹的事实。

如果您使用 CLK 信号作为中断,您将陷入严重的麻烦,因为输入的随机弹跳将一直触发中断(而不是在您想要读取数据信号的时间),因此您将获得不正确的数据。

存在使用状态机对格雷编码信号进行解码的轮询方法,从而忽略反弹信号,即忽略错误状态。这些非常复杂,有时会不同步。

我使用这些设备的方式是结合少量的平滑电容器和简单的数字旋转开关去抖算法。(见下面的代码)。这提供了易于理解的代码(也很小的代码大小),并且可以准确地获得单个定位器位置信息以及准确的方向旋转信息。

但是,有时您可能有一个质量很差的编码器,并且需要更多的努力来解码,在这种情况下,您需要在这里查看更复杂的鲁棒解码器代码。

设备解码技术

您可以使用许多巧妙的方法来解码涉及复杂状态机和灰色解码算法的输出。有些使用中断,大多数使用轮询。将输出连接到中断引脚的问题在于,您无法控制可能遇到的反弹,并且处理器可能被中断太多而无法执行任何有用的工作(甚至可能挂起),并且会从无论如何输入数据。

警告:由于设备的内部结构(使用在基板连接上弹跳的物理接触弹簧),旋转编码器的噪音非常大。这使得准确解码设备输出变得极其困难。

但是请参阅我的新技术 - 上面的最后一个代码示例代码。

开关弹跳发生是因为触点是在基板触点上弹跳和弹跳的弹簧 - 即使数据表表明您每转一圈可以获得的开关弹跳时间最长为 10 毫秒 (Bourns PEC11L)。

旋转编码器:如何在 Arduino 上使用 Keys KY-040 编码器_第3张图片

*[来源 PEC11L 数据表]*

您可以看到标记为 A 和 B 的信号可以更改为 CLK(时钟)和 DT(数据),如果顺时针旋转,时钟信号 (A) 上的上升沿将在 DT (B) 上产生逻辑低电平,并且如果逆时针转动,则为逻辑高(当以相反方向转动时,下降沿变为上升沿!)。

电容平滑

添加一个巨大的平滑电容器(和电阻器见下图,并用 470nF 代替 0.01uF 作为“太大”电容器的一个例子 - 这是一些人建议的)来停止反弹会阻止反弹,但也会减慢输入信号电平到它将通过微控制器的未定义逻辑输入电平(低于最高阈值 V IH和高于下阈值 V IL)的点。在这个输入区域中,该输入上的噪声可能(并且经常发生!)触发输入高或低会导致振荡,即产生更多的反弹信号而根本没有解决问题。

您可以通过使用 74HC14 等施密特触发器设备来创建正确的快速边沿信号来解决此问题,但您可能会过多地改变时序以获得有用的输出信号。

RC 对和数字滤波器

我发现的一种方法是使用一个小的平滑电容电阻对和一个数字去抖滤波器。这允许准确识别各个制动位置(控制轴的缓慢转动被准确解码)。在较快的旋转中,代码会丢失,但旋转编码器的真正意义在于允许准确的单个定位(和方向)检测。您不需要知道快速旋转的确切制动停止 - 您只需要知道用户想要更快地增加参数。

数字去抖滤波器

数字滤波器由一个 16 位整数变量组成,您可以将输入引脚的当前状态转换为该变量:

状态=(状态<<1)| 数字读取(CLK_PIN) | 0xe000;

这是一个非常紧凑的过滤器 - 每次循环都有一个新的位左移(在位 0)。带有 0xe000 的“或”动作定义了迭代次数,即前 3 位被阻止,其余的作为有用的输入。这个想法是您测试状态 0xf000,只有在有 1 0000 0000 0000 个输入的序列时才会发生这种情况,这意味着信号在循环中已经稳定了 12 次迭代,即没有反弹。

Arduino 旋转编码器数据表

KY-040 中使用的旋转编码器看起来像 Bourns PEC11L 设备 - 您可以从下面的链接下载该旋转编码器数据表。分线板所做的只是添加两个 10k 上拉电阻(R2 和 R3),而开关上拉的空间留空。

下载PEC11L 数据表。

Arduino 旋转编码器软件设置:

使用的IDE 版本:1.6.4 使用的板:Arduino Uno R3

旋转编码器硬件设置

使用设备:KY-040(分线板)

其他元件 10k 电阻和 10nF 电容——仅用于时钟信号,连接方式如下图:

旋转编码器:如何在 Arduino 上使用 Keys KY-040 编码器_第4张图片

*[来源 PEC11L 数据表]*

注意:10k 和 10n 是分线板额外的(A 和 B 在板上有 10k 上拉)。仅将它们添加到时钟信号 (A)。

示例旋转编码器代码:

这是一个arduino ky-040 旋转编码器示例,向您展示如何通过消除开关弹跳来解码 20 转编码器。使用数字滤波器技术。此处讨论此数字滤波器的操作。

注意:编码器的质量会影响输出信号(我的一个跳过代码,而质量更高的一个不会!)。

#define CLK_PIN  2
#define DATA_PIN 7
#define YLED A1
​

void setup() {
   pinMode(CLK_PIN,INPUT);
   pinMode(DATA_PIN,INPUT);
   pinMode(YLED,OUTPUT);
​
   Serial.begin(9600);
   Serial.println("Rotary Encoder KY-040");
}
​

void loop() {
static uint16_t state=0,counter=0;
​
    delayMicroseconds(100); // Simulate doing somehing else as well.
​
    state=(state<<1) | digitalRead(CLK_PIN) | 0xe000;
​
    if (state==0xf000){
       state=0x0000;
       if(digitalRead(DATA_PIN))
         counter++;
       else
         counter--;
       Serial.println(counter);
    }
}

驯服嘈杂的旋转编码器

由于开关弹跳,keyes-040 编码器可能会非常嘈杂,您可能需要使用更强大的解码方式 - 我有一个表现相当好的和一个非常嘈杂的。

下面的例子使用了一种表格解码方法,它比前面的例子需要更多的代码,但能够读取旋转编码器,而根本不需要任何去抖电容。(但是请检查这是否适用于您自己的硬件以确保这一点)。

它的工作方式是将解码器的输出编码为二进制数。为此,您可以将 CLK 定义为 LSB 二进制数字,将 DATA 定义为 MSB 二进制数字。

然后观察输出可以占据的有效状态,即下图中虚线所示的状态。

因为输出是正交的,并且因为这导致格雷码输出,所以没有输出与另一个同时改变状态。这意味着只有两个输出中的一个会在任何转换边缘反弹。这意味着弹跳信号很容易被忽略,因为弹跳通常会产生无效的编码器状态。

旋转编码器:如何在 Arduino 上使用 Keys KY-040 编码器_第5张图片

如果您查看上图,您可以看到有四种状态(11、10、00、01)。除此之外,只有 8 种方法可以从一种状态移动到下一种状态,包括倒退(逆时针)。

对于顺时针运动,您只能执行以下操作:

(11 > 10)、(10 > 00)、(00 > 01) 和 (01 >11)

同样,只有以下编码器输出转换对逆时针旋转有效:

(01 > 00)、(00 > 10)、(10 > 11) 和 (11 > 01)

你可以在这里找到其他的旋转解码方法(包括这个)。

表格方法背后的想法是您存储先前的状态和当前状态并将它们设置为二进制代码。通过这种方式,表格直接编码了有效输出的转换 - 该技术背后的主要目的是丢弃由开关弹跳引起的无效输出。

有效代码输出

所以对于上面的顺时针方向,有四个有效输出(其中 2 个 MSBits 是前一个状态,2 个 LSBits 是当前状态):

1110

1000

0001

0111

只有这些是有效的状态。理论上只有这些应该由旋转编码器输出,但实际上开关弹跳会产生其他代码。

对于相反的方向(逆时针),以下代码有效:

0100

0010

1011

1101

为了允许微控制器检查有效代码并忽略无效代码,需要一个表格(使用 4 位 PSNS - 上一个状态下一个状态 - 代码作为输入):

PSNS(上一个状态,下一个状态) 有效代码 方向
0000 X X
0001 有效的 连续波
0010 有效的 逆时针
0011 X X
0100 有效的 逆时针
0101 X X
0110 X X
0111 有效的 连续波
1000 有效的 连续波
1001 X X
1010 X X
1011 有效的 逆时针
1100 X X
1101 有效的 逆时针
1110 有效的 连续波
1111 X X

将其编码到 C 表中并将 CW 替换为 1 并将 CCW 替换为 -1 并且无效为 0 会导致以下结果:

rot_enc_table[]= {0,1,-1,0,-1,0,0,1,1,0,0,-1,0,-1,1,0};

您可以在网络上的其他地方找到使用此方法的代码(我可能已将 -1 交换为 1 - 只是交换 SIG A 和 SIG B 并不重要),但该方法将任何 CW 或 CCW 有效输出作为真正的转换等等对于“止动”到“止动”运动,返回四个 CW 或四个 CCW 状态(止动位置如下图所示)。问题是弹跳可能会导致 a 状态向后更改,直到开关稳定并再次前进。

旋转编码器:如何在 Arduino 上使用 Keys KY-040 编码器_第6张图片

改进的表解码方法

通过使用以下代码,您可以看到每个定位器之间生成的输出。代码在找到 7 或 0xB 时仅生成一个换行符。这些是执行止动到止动旋转时生成的最后代码。

旋转编码器质量测试程序

使用以下程序查看您的编码器的好坏程度(观察下面的典型结果)。

#define CLK 2
#define DATA 7
#define BUTTON A5
#define YLED A2
​
void setup() {
  pinMode(CLK, INPUT);
  pinMode(CLK, INPUT_PULLUP);
  pinMode(DATA, INPUT);
  pinMode(DATA, INPUT_PULLUP);
  pinMode(BUTTON, INPUT);
  pinMode(BUTTON, INPUT_PULLUP);
  pinMode(YLED,OUTPUT);
​
  Serial.begin (115200);
  Serial.println("KY-040 Quality test:");
}
​
static uint8_t prevNextCode = 0;
​
void loop() {
uint32_t pwas=0;
​
   if( read_rotary() ) {
​
      Serial.print(prevNextCode&0xf,HEX);Serial.print(" ");
​
      if ( (prevNextCode&0x0f)==0x0b) Serial.println("eleven ");
      if ( (prevNextCode&0x0f)==0x07) Serial.println("seven ");
   }
​
   if (digitalRead(BUTTON)==0) {
​
      delay(10);
      if (digitalRead(BUTTON)==0) {
          Serial.println("Next Detent");
          while(digitalRead(BUTTON)==0);
      }
   }
}
​
// A vald CW or CCW move returns 1, invalid returns 0.
int8_t read_rotary() {
  static int8_t rot_enc_table[] = {0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0};
​
  prevNextCode <<= 2;
  if (digitalRead(DATA)) prevNextCode |= 0x02;
  if (digitalRead(CLK)) prevNextCode |= 0x01;
  prevNextCode &= 0x0f;
​
  return ( rot_enc_table[( prevNextCode & 0x0f )]);
}

使用上面的程序,我按下旋转编码器按钮以生成文本“Next Detent”,然后将编码器转到下一个定位位置。这使您可以查看在一次位置更改期间生成的所有代码。

您可以看到,一些旋转导致了很多代码,但这些代码只能返回一个状态,然后才能返回到正确的状态。更重要的是,您可以看到最后 2 个代码始终与完整旋转序列的最后 2 个半字节匹配:D42B 和 E817。

对于“坏”的旋转编码器,生成了以下输出:

劣质旋转编码器
​
KY-040 品质测试:
D 4 2 8 2 B 十一
下一个止动
D 4 2 8 2 B 十一
下一个止动
D 4 1 4 2 B 十一
下一个止动
E 8 2 8 2 8 2 8 1 4 1 7 七
D 7 七
下一个止动
EB十一
E 8 2 8 2 8 1 7 七
下一个止动
E 8 2 8 2 8 2 8 1 4 1 7 七
下一个止动
EB十一
E 8 2 8 2 8 1 4 1 4 1 7 七
下一个止动
E 8 1 4 1 4 1 4 1 4 1 4 1 7 七
下一个止动
E 8 1 4 1 7 七
下一个止动
E 8 1 7 七
下一个止动
EB十一
EB十一
EB十一
EB十一
EB十一
EB十一
EB十一
E 8 2 8 2 8 1 4 1 7 七
下一个止动

对于优质编码器,生成以下输出:

旋转编码器测试质量更好的编码器。
​
KY-040 品质测试:
E 8 1 7 七
下一个止动
E 8 1 7 七
下一个止动
E 8 1 7 七
下一个止动
E 8 1 7 七
下一个止动
E 8 1 7 七
EB十一
下一个止动
EB十一
EB十一
EB十一
EB十一
EB十一
EB十一
EB十一
EB十一
EB十一
E 8 1 7 七
下一个止动
D 4 2 B 十一
D 7 七
D 7 七
下一个止动
D 4 2 B 十一
下一个止动
D 4 2 B 十一
下一个止动

您可以看到两者之间存在很大差异,第一个生成的代码输出要多得多(由于开关弹跳)。指示单个止动到止动运动的实际代码是 E817 和 D42B,它们是“有效”prevstate、nextstate 编码的相同值(在上面关于有效二进制代码的讨论中显示)。

您可以看到当开关在棘爪之间时有很多弹跳,但在到达末端时没有。所有代码输出都以正确的代码 E 或 D 开始,然后反弹很多,然后以最后两个代码结束。

改进的表解码代码的操作

下面的代码查找最后两个状态以指示有效的旋转代码输出( 0x2b 和 0x17 )。这会产生双重处理去抖动——第一个是“有效”输出,第二个是“有效旋转”。这非常有效,甚至允许旋转编码器返回到其原始位置(顺时针旋转 20 个位置,然后逆时针旋转 20 个位置)而不会丢失代码 - 即使对于噪音非常大的旋转编码器也是如此。

您也许可以使用完整的 16 位十六进制代码来获取噪音较小的代码(或高质量的代码)。您的结果可能会有所不同。

请注意,这是直接连接到编码器 - 没有去抖动电阻器或电容器(只有 10k 电阻器在分线板上拉起)。

改进表解码的代码

// Robust Rotary encoder reading
//
// Copyright John Main - best-microcontroller-projects.com
//
#define CLK 2
#define DATA 7

void setup() {
  pinMode(CLK, INPUT);
  pinMode(CLK, INPUT_PULLUP);
  pinMode(DATA, INPUT);
  pinMode(DATA, INPUT_PULLUP);
  Serial.begin (115200);
  Serial.println("KY-040 Start:");
}

static uint8_t prevNextCode = 0;
static uint16_t store=0;

void loop() {
static int8_t c,val;

   if( val=read_rotary() ) {
      c +=val;
      Serial.print(c);Serial.print(" ");

      if ( prevNextCode==0x0b) {
         Serial.print("eleven ");
         Serial.println(store,HEX);
      }

      if ( prevNextCode==0x07) {
         Serial.print("seven ");
         Serial.println(store,HEX);
      }
   }
}

// A vald CW or  CCW move returns 1, invalid returns 0.
int8_t read_rotary() {
  static int8_t rot_enc_table[] = {0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0};

  prevNextCode <<= 2;
  if (digitalRead(DATA)) prevNextCode |= 0x02;
  if (digitalRead(CLK)) prevNextCode |= 0x01;
  prevNextCode &= 0x0f;

   // If valid then store as 16 bit data.
   if  (rot_enc_table[prevNextCode] ) {
      store <<= 4;
      store |= prevNextCode;
      //if (store==0xd42b) return 1;
      //if (store==0xe817) return -1;
      if ((store&0xff)==0x2b) return -1;
      if ((store&0xff)==0x17) return 1;
   }
   return 0;
}

 TO:苏贺桥

最近没有研究,你看下这个程序能否满足你要求。

#include 

// Rotary encoder pins
#define PIN_A 32
#define PIN_B 4
#define PUSH_BTN 16

// A turn counter for the rotary encoder (negative = anti-clockwise)
int rotationCounter = 200;

// Flag from interrupt routine (moved=true)
volatile bool rotaryEncoder = false;

// Interrupt routine just sets a flag when rotation is detected
void IRAM_ATTR rotary()
{
    rotaryEncoder = true;
}

// Rotary encoder has moved (interrupt tells us) but what happened?
// See https://www.pinteric.com/rotary.html
int8_t checkRotaryEncoder()
{
    // Reset the flag that brought us here (from ISR)
    rotaryEncoder = false;

    static uint8_t lrmem = 3;
    static int lrsum = 0;
    static int8_t TRANS[] = {0, -1, 1, 14, 1, 0, 14, -1, -1, 14, 0, 1, 14, 1, -1, 0};

    // Read BOTH pin states to deterimine validity of rotation (ie not just switch bounce)
    int8_t l = digitalRead(PIN_A);
    int8_t r = digitalRead(PIN_B);

    // Move previous value 2 bits to the left and add in our new values
    lrmem = ((lrmem & 0x03) << 2) + 2 * l + r;

    // Convert the bit pattern to a movement indicator (14 = impossible, ie switch bounce)
    lrsum += TRANS[lrmem];

    /* encoder not in the neutral (detent) state */
    if (lrsum % 4 != 0)
    {
        return 0;
    }

    /* encoder in the neutral state - clockwise rotation*/
    if (lrsum == 4)
    {
        lrsum = 0;
        return 1;
    }

    /* encoder in the neutral state - anti-clockwise rotation*/
    if (lrsum == -4)
    {
        lrsum = 0;
        return -1;
    }

    // An impossible rotation has been detected - ignore the movement
    lrsum = 0;
    return 0;
}

void setup()
{
    Serial.begin(115200);

    // The module already has pullup resistors on board
    pinMode(PIN_A, INPUT);
    pinMode(PIN_B, INPUT);

    // But not for the push switch
    pinMode(PUSH_BTN, INPUT_PULLUP);

    // We need to monitor both pins, rising and falling for all states
    attachInterrupt(digitalPinToInterrupt(PIN_A), rotary, CHANGE);
    attachInterrupt(digitalPinToInterrupt(PIN_B), rotary, CHANGE);
    Serial.println("Setup completed");
}

void loop()
{
    // Has rotary encoder moved?
    if (rotaryEncoder)
    {
        // Get the movement (if valid)
        int8_t rotationValue = checkRotaryEncoder();

        // If valid movement, do something
        if (rotationValue != 0)
        {
            rotationCounter += rotationValue * 5;
            Serial.print(rotationValue < 1 ? "L" :  "R");
            Serial.println(rotationCounter);
        }
    }

    if (digitalRead(PUSH_BTN) == LOW)
    {
        rotationCounter = 0;
        Serial.print("X");
        Serial.println(rotationCounter);
 
        // Wait until button released (demo only! Blocking call!)
        while (digitalRead(PUSH_BTN) == LOW)
        {
            delay(100);
        }
    }

 又一种代码

/*
 * Demonstrate reading a rotary encoder
 *
 * Author: John Fredine
 * Original Date: 1/8/2019
 */

#include 

//******************************************************************************
// User adjustable parameters
// Rotary encoder pin connections
// BTN_ENA leads BTN_ENB w/ clockwise rotation
const int BTN_ENA = 8, BTN_ENB = 9;

// number of "stopping points" in a full revolution of the encoder
const int num_detents_per_revolution = 20;

// number of samples with constant signal value to be considered "debounced"
const int debounce_stable_count = 4;

// time between interrupts for monitoring signal values
// resolution of the timer is 4us so make this >= 4 and a multiple of 4
#define ISR_INTERVAL_us 256

//******************************************************************************
// calculated parameters which should not need user adjustment

// assume a rising and folling edge of each button happens between detents
const int num_edges_per_revolution = 2 * num_detents_per_revolution;

// Number of timer ticks between interrupts.  Each tick is 4us
#define OCR0A_INCR (ISR_INTERVAL_us / 4)

// current position of the encoder
volatile int pos;

/*
 * TIMER0_COMPA_vect
 * ISR triggered by timer0 where inputs are read and debounced.
 * In order to get interrupts at a rate greater than 1 ms (default for timer0),
 * but not require an additional timer and not disturb the timer0 overflow rate
 * (because that is used by default libraries), it makes use of the timer
 * compare interrupt and adjusts the comparison value within the ISR.
 */

ISR(TIMER0_COMPA_vect) {
    static int last_a = 1;
    static int curr_a = 1;
    static int curr_a_stable_count;

    static int initial_b;

    // set the time of the next interrupt
    OCR0A = (OCR0A + OCR0A_INCR) & 0xff;

    // get value of B the first time we detect a change on A
    // we assume B is stable by the time A begins to change
    int val = digitalRead(BTN_ENA);
    if ((curr_a_stable_count == 0) && (val != last_a)) {
        initial_b = digitalRead(BTN_ENB);
    }

    // debounce A
    if (val != curr_a) {
        curr_a = val;
        curr_a_stable_count = 1;
    } else {
        if (curr_a_stable_count > 0) {
            curr_a_stable_count++;
        }
    }

    // if values are stable, adjust rotary encoder position
    // B low on rising edge of A indicates clockwise motion while
    // B high on rising edge of A indicates counter clockwise motion.
    // B low on falling edge on A indicates counter clockwise motion while
    // B high on falling edge of A indicates clockwise motion
    if (curr_a_stable_count == debounce_stable_count) {
        if (last_a != curr_a) {
            if (curr_a == initial_b) {
                pos--;
                if (pos < 0) {
                    pos += num_edges_per_revolution;
                }
            } else {
                pos++;
                if (pos >= num_edges_per_revolution) {
                    pos -= num_edges_per_revolution;
                }
            }
        }

        last_a = curr_a;
        curr_a_stable_count = 0;
    }
}


void setup() {
    Serial.begin(9600);

    // Add a pullup to the buttons
    pinMode(BTN_ENA, INPUT_PULLUP);
    pinMode(BTN_ENB, INPUT_PULLUP);

    // set up a recuring timer interrupt
    cli();  // disable interrupts

    // Turn off PWM modes of timer0 because such modes prevent the
    // immediate re-assignment of compare values to OCR0A
    TCCR0A &= ~((1 << WGM01) | (1 << WGM00));
    TCCR0B &= ~(1 << WGM02);

    OCR0A = 1;              // Set the timer0 compare value.
                            // The ISR will adjust this value to to potentially
                            // produce multiple interrupts during the normal
                            // 0 -> FF count sequence which takes 1024us
    TIMSK0 |= _BV(OCIE0A);  // Enable the timer compare interrupt

    sei();  // enable interrupts
}


void loop() {
    static int last_pos;

    if (pos != last_pos) {
        Serial.print("Pos: ");
        Serial.println(pos);
        last_pos = pos;
    }
}

还可以参考如下文章:

Arduino: Using a rotary encoder
 

Arduino: Using a rotary encoder

Posted on 2011/04/17 by rt

As explained in this post, rotary encoders are notoriously unreliable. Well, the cheap ones are. Why bother?

But wait, could they be used at all? Most of the problems people complain about have to do with bouncing. But is bouncing as important if someone is turning a rotary encoder manually to increment a counter? Also, debouncing often slows down the response speed of the Arduino code enough to miss a few clicks. Is missing a few clicks important?

What I’m trying to implement is a simple way of incrementing or decrementing a counter to choose a stored patch value in a pedal board. Practically, this is done by manually turning a knob attached to a rotary encoder. Software will read the encoder and increment (or decrement) the value of a counter associated with a particular patch, or memory value.

The rotary encoder that I’m using came with a knob:

旋转编码器:如何在 Arduino 上使用 Keys KY-040 编码器_第7张图片

As you can see, the knob is huge! The speed at which one can twist a knob between one’s thumb and index finger is linked directly to the size of the knob, within limits. The bigger the knob, the slower the turning speed. This knob’s diameter is 25 mm (1 inch). The maximum speed that I can achieve is about one revolution per second, in two steps. The clicking speed is then about 40 clicks per second, or 25 ms per click. (I’m guessing the designers knew this)

Let’s see how that affects the debouncing that we might want to do. If we have 25 ms per click, we have to be pretty reasonable about the delay that we think is necessary waiting for the switch to stop bouncing. Actually, even this assumption is wrong. Referring to the previous post, each pin goes through a 0 to 1 then 1 to 0 transition between two clicks. Effectively, our debouncing has to happen twice per click per pin to be able to register every state change. Again, if we have 25 ms per click, there is not much time left to do anything! What to do? As you will see in my solutions, I’m only interested in a transition of one of the pins from 0 to 1. This means that I only have to debounce once per click.

The maximum speed that I could impart on that knob is one revolution per second. I can see myself reaching that speed if I want to jump between two values of my counter as fast as possible. So what if I miss a few clicks? The numbers would be going up fast enough that I couldn’t even see the missing steps (I tried). In practice, as I get close to the chosen value, I would slow down enough to be able to read each number and mentally process its value. As slow as 1/4 of a second per click, or 250 ms! Plenty of time to do a little debouncing!!

After much testing, reading and breadboarding, I chose to explore 3 methods that work well for what I’m trying to achieve.

Before presenting the solutions, I have to state that I rely on one assumption. With a rotary encoder, a transition on pin1 (A above) from 0 to 1 while pin2 (B above)= 1 means that a counter is increased by one. Otherwise, the counter decreases by 1. This is based on the graphic above (thanks to sagsaw). It is in line with the testing done in this post.  In Arduino code, this translates to:

if (pin1 == HIGH)
  if (pin2 == HIGH)
    counter++;
    else
    counter--;

In fact, what I want to catch is the moment when pin1 becomes HIGH. There are a few programming techniques available to achieve this.

Method 1: Using the Arduino Bounce Library

#include 

// This code increments or decrements a counter based on
// the status of a rotaty encoder

#define pin1 2
#define pin2 3
#define LED 13
int counter = 0;

// Instantiate a Bounce object with a 5 millisecond debounce time
// Only pin1 needs to be debounced. It is assumed that pin2
// will be stable when reading pin1
Bounce bouncer1 = Bounce( pin1,5 ); 

void setup() {
  pinMode(pin1,INPUT);
  pinMode(pin2,INPUT);
  pinMode(LED,OUTPUT);
  Serial.begin(9600);
}

void loop() {
 // Update the debouncer
  bouncer1.update ( );

 // Turn on or off the LED and
 // increment or decrement the counter
 if ( bouncer1.risingEdge()) {
   if (digitalRead(pin2)){
     digitalWrite(LED, HIGH );
     counter++;
     Serial.println(counter);
   }else{
     counter--;
     Serial.println(counter);
   }
 } else {
    digitalWrite(LED, LOW );
 }

}

This sketch is based on the sample code of the new Arduino “Bounce” library, that is replacing the old “Debounce” library. I’m reading the rising edge value on pin1. I’m only interested in a pin1 transition from 0 to 1 (LOW to HIGH). The LED and the Serial commands are for testing only.

I am able to overflow this code if I remove the knob from the rotary encoder and twist the shaft as fast as I can, snapping it between my thumb and index finger. What happens then is that the counter might increase or decrease by one, randomly, until the rotational speed slows down a bit. If you read the counter values in the Arduino IDE, you can spot the artifact. But driving a real life counter, like a 3 digit LED display, you will not be aware of the incrementation skips.

This code will start misbehaving as the Arduino code gets more complex. If I add a delay in the main loop, say delay(100), the encoder readings becomes unreliable.With this in mind, I decided to try a version with an interrupt handler.

Method 2: External Interrupt Handler

The Arduino can accept external interrupts on some of its pins. The goal is again to catch the rising or falling edge on pin1.

/*  Digital Pin 2 accepts external interrupts. Pin1 of a rotary encoder
    is attached to DigitalPin2. An interrupt routine will be called
    when pin1 changes state, including noise.
    This will be made more efficient with hardware debouncing.
    */
int pin1 = 2;
int pin2 = 3;
int counter;
boolean goingUp = false;
boolean goingDown = false;
void setup()
{
  counter = 0;
  //Serial prints for debugging and testing
  Serial.begin(9600);

/* Setup encoder pins as inputs */
    pinMode(pin1, INPUT); // Pin 2
    pinMode(pin2, INPUT); // Pin 4 

// encoder pin on interrupt 0 (pin 2)
  attachInterrupt(0, decoder, FALLING);

}

void loop()
{
//using while statement to stay in the loop for continuous
//interrupts
while(goingUp==1) // CW motion in the rotary encoder
{
goingUp=0; // Reset the flag
counter ++;
Serial.println(counter);
}

while(goingDown==1) // CCW motion in rotary encoder
{
goingDown=0; // clear the flag
counter --;
Serial.println(counter);
}
}

void decoder()
//very short interrupt routine 
//Remember that the routine is only called when pin1
//changes state, so it's the value of pin2 that we're
//interrested in here
{
if (digitalRead(pin1) == digitalRead(pin2))
{
goingUp = 1; //if encoder channels are the same, direction is CW
}
else
{
goingDown = 1; //if they are not the same, direction is CCW
}
}

This sketch works well because the interrupt routine is very short. I works even better if a bit of hardware debouncing is forced on the rotary encoder. With two 0.1 uF capacitors soldered to the encoder pins, the number of calls to the interrupt routine is dramatically reduced. This is important because too many calls to the interrupt routine will rob computing cycles from the main routine, negating the effects of using interrupts to save computing power. It has been recommended to never connect a bouncy switch directly to a controllers interrupt pins.

Method 3: Using a Timer Interrupt

/* 
 * Example on how to configure the periodical execution of a user 
 * defined function (Interrupt service routine) using Timer2. This 
 * example will run the function every 1ms.  
 */

#include  

/* Timer2 reload value, globally available */
unsigned int tcnt2;  

int pin1 = 2;
int pin2 = 3;

// Instantiate a Bounce object with a 5 millisecond debounce time
// Only pin1 needs to be debounced. It is assumed that pin2
// will be stable when reading pin1
Bounce bouncer1 = Bounce( pin1,5 );

/* Setup phase: configure and enable timer2 overflow interrupt */
void setup() {  

  pinMode(pin1, INPUT);
  pinMode(pin2, INPUT);
  Serial.begin(9600); 

   /* First disable the timer overflow interrupt while we're configuring */
  TIMSK2 &= ~(1<Serial.println("CW");
      else
      Serial.println("CCW");
    }
}  

void loop() {
  delay(100);
//  Serial.println(millis());
}

I am still working on this version. Ideally, I would like to remove the call to the Bounce Library. But the timer interrupt subroutine is still extremely fast (tested at 12usec). The results are also unaffected by the length of the main loop. I tested it with a delay of 100 ms and the rotary switch was still decoded precisely.

Another version of the Timer function could be:

ISR(TIMER2_OVF_vect) {  
  /* Reload the timer */
  TCNT2 = tcnt2;  

  state=(state<<1) | !digitalRead(pin1) | 0xe000;
//  Serial.println(state,HEX);
  if (state==0xf000){
    state=0x0000;
    if(digitalRead(pin2))
      counter++;
    else
      counter--;
  }
}

This will increase or decrease the counter if 12 consecutive reads agree with our condition, but my switch seems to be too unreliable to constantly produce interesting results. I would save 4 usec using this routine.

__________________________________

So, the Timer Interrupt method is the best one. Studying the code of the new Arduino Bounce library (and testing with micros() ) confirmed that the library doesn’t rely on delay() for debouncing, effectively adding very little overhead to method 1 or 3.

This solution will be perfect for my initial version of the Rotary Encoder Arduino sub-project.

If you have read this so far, please keep in mind that your results may vary. I AM ASSUMING  that pin2 is stable when I accept the debounced status on pin1. Otherwise, the switch is totally unreliable.

Also, if your application needs 100% confidence that the encoder will never miss a state change, or produce false positive, just buy a better encoder. I definitely would not trust my little encoder to drive a robot or a high precision machine. Take a look at this page and you will realise that my little encoder, at $1, really is at the bottom end of the quality scale. It was definitely made for manual operation, as a volume control or, like I’m doing here, to increase and decrease a counter.

你可能感兴趣的:(Arduino,arduino)