文章目录[隐藏]
旋转编码器是一种输入设备,您可以在任一方向连续旋转。当您转动设备时,它会生成数字脉冲,以使用两个相控输出信号显示旋转方向。这两个输出还指示单个位置运动,因此您可以在控制面板中使用它们来增加或减少参数。
注意:由于开关弹跳,旋转编码器会产生极其嘈杂的输出振荡,本页上的信息为您提供了 两种消除这种噪声的技术。第一种是简单的过滤方法,第二种方法使用表解码从低质量设备中获得非常好的输出。
下面用于演示的编码器类型也称为增量旋转编码器,因为它产生指示单步变化的脉冲。其他类型生成绝对输出,即为编码器的特定位置生成相同的输出编号(4 位或更多位,具体取决于所需的精度),您可以在机器人应用中使用这些输出。
本教程的目的是提供一个简单的旋转编码器实现的 arduino 示例。
旋转编码器允许您轻松地增加或减少单个值的参数。
除了生成方向信息和阶跃变化脉冲外,该设备还具有物理反馈机制,让您在从一个位置移动到下一个位置时感觉到。这些点被称为棘爪,在 360° 旋转范围内的位置范围为 12 到 24 个。对于此处使用的设备,有 20 个棘爪。
与电位器不同,旋转编码器没有终点挡板,因此您可以使用它来不断增加或减少参数(一旦由微控制器解码),并且无需将控制位置设置回起点(没有)。
他们还经常在轴中内置一个按钮开关,这对于菜单选择等很有用。
您可以将它们用于许多应用程序,包括:
-
音量控制
-
照度控制
-
参数控制,例如速度、高度、温度等过程的参数控制。
-
菜单选择(按钮在这里很有用)。
由于输出是数字信号,您可以使用微控制器对其进行处理,并以您想要的任何方式使用结果,即更改表示系统参数的变量的值。
事实上,旋转编码器看起来很简单,但在这些小型设备(~11mm x ~13mm)内部却发生了很多事情。
这是发生了什么:
-
两个输出提供正交编码信号。
-
物理位置反馈和缓冲块(称为棘爪)- 对于该设备,有 20 个棘爪。
-
主轴按钮(按下开关)。
当您转动控制旋钮时,您会感觉到每个“制动”位置都停止了,因此您知道何时将设备转动了一个位置。这提供了细粒度的物理反馈,允许精确的参数改变。这与在没有物理反馈的情况下使用电位器设置音量等非常不同。
正交相移编码
这种技术性很强的编码方法实际上非常简单。这意味着两个信号彼此偏移四分之一周期(或相移 90?)。生成的信号是灰色编码的,这也意味着没有两个信号边缘对齐,即信号输出不会同时改变状态。
格雷编码可用于机电设备生成明确的信号。例如,如果输出是二进制编码的,那么在转换点(由于信号路径中的小延迟),您可能会解码一个完全错误的值,即在转换点可能会生成任何代码。
这可能是一个问题,尤其是在仅使用组合逻辑作为解码器的情况下。格雷码阻止了这种情况的发生(尽管它不会阻止开关弹跳)。
下图分别显示引脚 A 和 B (CLK) 和 (DT) 上的旋转编码器波形输出。
*[来源 PEC11L 数据表]*
注意:上图中的 D 表示止动位置的位置。事实上,这是输出没有接地的地方,因此它们被分线板上的 10k 电阻拉高。
旋转编码器内部
下图显示了旋转编码器的内部工作原理。三个连接8A、8B和8C中的每一个都由向下推基板的弹簧臂形成。
共有三个信号,一个连接到金属基板(接地),另外两个在交替的基板图案上移动。因此,当设备旋转时,输出会短接到地,然后当触点位于基板间隙中时,输出会悬空(未连接)。
请注意弹簧臂触点如何物理偏移四分之一周期(由物理基板定义) - 下图中的触点 8B 和 8C - 这就是正交编码输出的生成方式。
*资料来源:过期专利(现在在公共领域)。*
注意:触点 8A、8B、8C 是在触点基板上弹跳和弹跳的弹簧,导致输出信号在高电平和低电平之间弹跳,即开关弹跳。
旋转编码器的类型
接触增量式旋转编码器
这是本页演示中使用的设备类型。在每个定位位置产生两个正交信号,指示单个位置变化并显示旋转方向。
这种特殊的设备具有相当长的旋转寿命 - 100k 旋转(参见数据表) - 但由于存在物理接触,设备最终会磨损。在 Bourns目录中,其他物理设备的最大旋转范围为 15k 到 200k。
光学编码器
PEC11L 的最大 RPM 为 60RPM,而该目录中的光学编码器具有 1000 万转的使用寿命,并且可以在 3000rpm 下运行 - 这些是您可以用于高速机械测量的类型,但请参阅下面的磁性编码器,其具有更高生命当然还有更高的成本!
磁性编码器
为了获得更高的旋转寿命,磁编码器提供了最佳选择(因为设备内部没有物理接触),唯一会磨损的部件是轴承。它们提供 1 亿转的旋转寿命!
这些设备有 4 种不同的口味:
-
增量正交(与此处使用的 PEC11L 相同)。
-
方向/步长编码器 - 提供更好的分辨率(每转最多 512 个脉冲)。
-
绝对编码器 - 允许编码器的绝对位置检测(1024 个代码定义位置)。
-
PWM 编码器 - 产生 1us 至 1024us 宽度的 PWM 输出 - 声称的优势是抗噪性和更快的数据采集。
测量:使用旋转增量编码器
以下示例涵盖以下测量:
-
速度
-
对数变化
速度
您可能希望测量速度以用作代码中的参数,例如,如果您更快地转动车轮然后执行不同的操作,例如以不同的速率更改参数。
对数
这是一种测量旋转速度的参数调整,如果发现是恒定的,则周期性地增加参数。这对于具有大范围控制的设备非常有用,例如可以输出 1 到 10MHz 频率的 DDS(直接数字合成)。您真的不想坐在那里将旋钮转动 1Hz 周期以达到 10MHz!
解码方法
有多种方法可以对旋转编码器输出进行解码:
-
轮询
-
中断
在 KY-040 上有两个标记为 DT 和 CLK 的信号,分别表示 CLOCK 和 DATA。如果您查看这些信号的时序图,显然将 CLOCK 用作时钟并在时钟的上升沿读取 DATA 输入。然而,这忽略了信号在各处反弹的事实。
如果您使用 CLK 信号作为中断,您将陷入严重的麻烦,因为输入的随机弹跳将一直触发中断(而不是在您想要读取数据信号的时间),因此您将获得不正确的数据。
存在使用状态机对格雷编码信号进行解码的轮询方法,从而忽略反弹信号,即忽略错误状态。这些非常复杂,有时会不同步。
我使用这些设备的方式是结合少量的平滑电容器和简单的数字旋转开关去抖算法。(见下面的代码)。这提供了易于理解的代码(也很小的代码大小),并且可以准确地获得单个定位器位置信息以及准确的方向旋转信息。
但是,有时您可能有一个质量很差的编码器,并且需要更多的努力来解码,在这种情况下,您需要在这里查看更复杂的鲁棒解码器代码。
设备解码技术
您可以使用许多巧妙的方法来解码涉及复杂状态机和灰色解码算法的输出。有些使用中断,大多数使用轮询。将输出连接到中断引脚的问题在于,您无法控制可能遇到的反弹,并且处理器可能被中断太多而无法执行任何有用的工作(甚至可能挂起),并且会从无论如何输入数据。
警告:由于设备的内部结构(使用在基板连接上弹跳的物理接触弹簧),旋转编码器的噪音非常大。这使得准确解码设备输出变得极其困难。
但是请参阅我的新技术 - 上面的最后一个代码示例代码。
开关弹跳发生是因为触点是在基板触点上弹跳和弹跳的弹簧 - 即使数据表表明您每转一圈可以获得的开关弹跳时间最长为 10 毫秒 (Bourns PEC11L)。
*[来源 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 电容——仅用于时钟信号,连接方式如下图:
*[来源 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 二进制数字。
然后观察输出可以占据的有效状态,即下图中虚线所示的状态。
因为输出是正交的,并且因为这导致格雷码输出,所以没有输出与另一个同时改变状态。这意味着只有两个输出中的一个会在任何转换边缘反弹。这意味着弹跳信号很容易被忽略,因为弹跳通常会产生无效的编码器状态。
如果您查看上图,您可以看到有四种状态(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 状态向后更改,直到开关稳定并再次前进。
改进的表解码方法
通过使用以下代码,您可以看到每个定位器之间生成的输出。代码在找到 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;
}
版权声明:本文为CSDN博主「armcsdn」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/armcsdn/article/details/122781836
暂无评论