友情提示:380元/半年,儿童学编程,就上码丁实验室。
我是潘,曾经是个工程师。这是“Arduino 公开课” 系列的入门教程。上一课介绍了霍尔传感器的测速原理,现在将继续通过外部中断,来读取旋转编码器的信息。同时我们利用两个程序去分析中断的效率。有任何疑问请在评论区提出,我会逐一回答。
编码器外形很像电位器,但它的输出的是两路脉冲信号,通过程序对比来判断顺时针、逆时针以及增减量是多少。人机交互中,编码器可以精确控制输出,每一个脉冲信号代表一个调整信号,而且它的旋转没有圈数限制,使用灵活、不容易磨损。
常见的旋转编码器,每圈有16步到1024步等多种型号,配合旋转编码器上的按键,可以复位到初始状态,即从0开始计数。现在我们使用的是一圈20步机械编码器,其抖动较大,程序中需要去抖处理,但日常使用足够了,而在高精度场合可以使用光栅型的。
旋转编码器模块有5个引脚,分别是VCC, GND, Switch(中轴复位按键)、编码端A、编码端B(有些产品会将A、B端标记为CLK、DT,完全等效)。
这类旋转编码器模块一般在 A、B 两端内置了上拉电阻,而单个元件的则需要自己接上拉电阻。
我们不深究期内部原理,只要知道,机械旋转编码器有类似齿轮缺口与感应的机制(光学型为光栅感应),旋转时,A、B两路脉冲将输出不同的讯号,正转与逆转得到不同的一串讯号:
来源:Arduino 官方网站
程序设计逻辑是:
当 B 信号输出低电平,A 输出高电平,则为顺时针,反之为逆时针;或者:
当 B 信号输出高电平,A 输出高电平,则为逆时针,反之为顺时针。逻辑图如下:
看出规律吗?当A、B信号反转时,顺时针;当A、B信号一样时,逆时针。
接线方式很简单,只要将模块 Vcc、GND、A、B 分别接电源和D2、D3,(如果是单个元件,A、B 分别接 10K 拉电阻),通过下面程序测试一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
/*
作者:Ardui.Co
效果:旋转编码器每部增减1
版本:1.0
更新时间:2017年2月21日
*/
constbyteinterruptPinA=2;
constbyteinterruptPinB=3;
unsignedlongtime=0;//临时时间变量,用于防抖
longcount=0;//计数值
longnum=0;
voidsetup()
{
Serial.begin(9600);
pinMode(interruptPinA,INPUT);
pinMode(interruptPinB,INPUT);
attachInterrupt(digitalPinToInterrupt(interruptPinA),Addition,LOW); //调用中断Addition
attachInterrupt(digitalPinToInterrupt(interruptPinB),Subtraction,LOW);
time=millis();//时间初值
}
voidloop()
{
while(num!=count)//每次调整只输出1次,避免loop反复输出结果
{
num=count;
Serial.println(num);
}
}
voidAddition()
{
if((millis()-time)>5)//防抖
count++;
time=millis();
}
voidSubtraction()
{
if((millis()-time)>5) //防抖
count–;
time=millis();
}
|
打开串口监视器旋转编码器,可以看到数字按每步±1变化。对于机械编码器,我们加入了 5ms 防抖延时。另外,A、B两个端口是可以互换的,但互换后旋转方向会反转。
这个程序看似很完美,不过,占用了两个宝贵的中断端口,尤其是Uno,就没办法做其他中断处理了。实际上,完全可以用其他数字端口来操作,但我一般不建议这样做:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
/*
作者:Ardui.Co
效果:旋转编码器每部增减1,不占用中断端口
版本:1.0
更新时间:2017年2月21日
*/
intencoderPos;
constintencoderPinA=4;
constintencoderPinB=5;
booleanencoderALast; // 记录前一次A端口的状态
voidsetup()
{
pinMode(encoderPinA,INPUT);
pinMode(encoderPinB,INPUT);
digitalWrite(encoderPinA,HIGH);
digitalWrite(encoderPinB,HIGH);
Serial.begin(9600);
}
voidloop()
{
booleanencoderA=digitalRead(encoderPinA);
if((encoderALast==HIGH)&&(encoderA==LOW))//第一个条件的作用是延时
{
if(digitalRead(encoderPinB)==LOW)
{
encoderPos–;
}
else// 相当于encoderPinB == HIGH
{
encoderPos++;
}
Serial.println(encoderPos);
}
encoderALast=encoderA;
}
|
encoderALast 作用是延时,确保每一步只增减 1。 如果 A 处于LOW状态,说明刚结束一个循环。第二个循环中,if 判断先不执行,同时由于 digitalRead(encoderPinA) 永远能读出 HIGH (上拉电阻),所以 encoderALast 又会被赋予 HIGH。直到第三个循环就可以执行了下一个步进了。
简单来讲,encoderALast 确保每隔一个 loop() 才执行一个步进,由于 loop 时间很短,所以旋转时不会感到延时的存在,但又能防止一个步进促发多次增减。
回到刚才的问题,为什么可以通过数字端口实现,我们却要用宝贵的中断端口呢?看看官方教程解释:
Using interrupts to read a rotary encoder is a perfect job for interrupts because the interrupt service routine (a function) can be short and quick, because it doesn’t need to do much.
“中断很适合旋转编码器,因为中断过程短且块,这过程中无需要做太多其他事情。”
具体含义是,中断能够释放CPU的资源。对于Arduino来说,每次loop()、对每个数字端口的扫描、监测都会消耗非常有限的CPU资源。如果不采用中断,CPU要不断扫两个数字端口,判断描编码器是否有输入,效率极为低下。
而采用中断,CPU可以不管输入状态,而进行其他任务,只有当旋转编码器发出中断请求时才响应。所以,在第一个程序中,如果不需要在屏幕上输出结果,loop() 完全可以置空,或者加入其他更重要的内容。
外部中断就介绍到这里,以后我们会经常用到。后面我们会深入 Arduino 内部,介绍内部中断的原理和使用。