介绍
旋转或编码器是一个角度測量装置. 他用作精确測量电机的旋转角度或者用来控制控制轮子(能够无限旋转,而电位器只能旋转到特定位置)。其中有一些还安装了一个能够在轴上按的button,就像音乐播放器的控制button。Some of them are also equipped with a pushbutton when you press on the axis (like the ones used for navigation on many music controllers). 它们的精度多种多样。有每圈16步到1024步的各种。价格也从2到200欧元不等。
我写了一个小样例去读旋转编码器,而且使将读数通过RS232显示。我们非常容易实现当编码器每走一步更新一下计数,而且将它通过串口显示在电脑上(通过串口监视器)。
这个程序在ALPS STEC12E08编码器(每圈有24步)上执行良好。可是我觉得当它使用在一个有更高精度的编码器上时有可能就会失效或者当电机旋转非常快,或者你拓展这个程序以适应多个编码器。
请先试试他吧。
我在Arduino distribution(AVRLib的一部分)的encoder.h中学会了如何操作编码器。谢谢作者:Pascal Stang,感谢他对每个函数友好而具体的解释。例如以下:
Example 1
/* Read Quadrature Encoder
* Connect Encoder to Pins encoder0PinA, encoder0PinB, and +5V.
*
* Sketch by max wolf / www.meso.net
* v. 0.1 - very basic functions - mw 20061220
*
*/
int val;
int encoder0PinA = 3;
int encoder0PinB = 4;
int encoder0Pos = 0;
int encoder0PinALast = LOW;
int n = LOW;
void setup() {
pinMode (encoder0PinA,INPUT);
pinMode (encoder0PinB,INPUT);
Serial.begin (9600);
}
void loop() {
n = digitalRead(encoder0PinA);
if ((encoder0PinALast == LOW) && (n == HIGH)) {//上升沿
if (digitalRead(encoder0PinB) == LOW) {
encoder0Pos--;
} else {
encoder0Pos++;
}
Serial.print (encoder0Pos);
Serial.print ("/");
}
encoder0PinALast = n;
}
要注意的几点: encoder0Pos
会一直记数,那也就意味着假设电机一直向一个方向进行旋转。那么串口消息会变的非常长(最多6个字符),这种话就会画很多其它的时间去转换。你须要保证当encoder0Pos
溢出的时候。在你的PC端不会发生bugs-假设它的值大于INT的最大值(32,767)时,它会突变为-32,768!反之亦然。
改进建议: 仅当上位机须要读数的时候,将计数相加。即只计数发送周期之间的数据。当然,假设你在loop()中加入很多其它的代码,或者使用更高精度的编码器,就有可能丢失某一步(少计数)。更好的解决的方法是使用中断(当检測到信号的突变时)。我上面提到库文件就是这么去做的。可是如今(2006-12) 无法再Arduino环境下编译,或者我不知道怎么去做…… 。
更深的解释, 包含编码器时序图
我对于编码器的时序的原理不怎么确定,可是还是将这些内容加入到这里吧. Paul Badger
下图。是 编码器A & B两通道的时序图。
以下的描写叙述会更好的解释编码器是怎么执行的。当代码检測到A通道一个上升沿的时候。他会接着去检查B通道是高电平或者是低电平。由于方向不同,接着它会将计数添加或者降低。为了检測到波形。编码器必须旋转。
上面代码的优势是,他只检測了关于编码器的一种可能性(共4种各自是检測A(上升沿,下降沿)(B+。B-),检測B(上升沿,下降沿)(A+,A-))。为了方便解释,无论红色还是绿色虚线标注的变换,都是依据编码器的旋转方向变化而变化的。
中断的样例
使用一个中断的样例
以下是使用中断的代码. 当Arduino检測到A通道有变化(上升沿或下降沿), 它立马跳转到 “doEncoder” 函数, 中断函数会在上升沿和下降沿都会被调用,所以每个一步都会被计两次。
我不想使用还有一个中断去检查B通道的2个变换 ( 上图紫色和青色线标注处),可是即使调用了,也不会非常麻烦.
使用中断去操作旋转编码器比較不错。由于中断响应时间非常快,由于它不用操作非常多任务。
I used the encoder as a “mode selector” on a synthesizer made solely from an Arduino chip(译者
作者可能是说他将这个旋转编码器用作一个模式选择用途,就是如同button形式的东西). 这是一个比較任意的程序。由于用户对单片机丢失一些脉冲并不在意. 中断方法比較重要的应用是在伺服电机或者机器人的轮子上,在这些应用中,单片机不能丢失不论什么一个脉冲。不然运动的精度就无法保证了。
要注意的还有一点是: I used the Arduino’s pullup resistors to “steer” the inputs high when they were not engaged by the encoder. Hence the encoder common pin is connected to ground. (译者
作者使用Arduino内部上拉电阻使输入端的常态是高电平,因此编码器的公共端是连接到地上)上面的程序没有提到的一点是:输入端须要串联上拉电阻(10K欧就好),由于编码器的公共端连接是+5V.
/* read a rotary encoder with interrupts
Encoder hooked up with common to GROUND,
encoder0PinA to pin 2, encoder0PinB to pin 4 (or pin 3 see below)
it doesn't matter which encoder pin you use for A or B
uses Arduino pullups on A & B channel outputs
turning on the pullups saves having to hook up resistors
to the A & B channel outputs
*/
#define encoder0PinA 2
#define encoder0PinB 4
volatile unsigned int encoder0Pos = 0;
void setup() {
pinMode(encoder0PinA, INPUT);
digitalWrite(encoder0PinA, HIGH); // turn on pullup resistor
pinMode(encoder0PinB, INPUT);
digitalWrite(encoder0PinB, HIGH); // turn on pullup resistor
attachInterrupt(0, doEncoder, CHANGE); // encoder pin on interrupt 0 - pin 2
Serial.begin (9600);
Serial.println("start"); // a personal quirk
}
void loop(){
// do some stuff here - the joy of interrupts is that they take care of themselves
}
void doEncoder() {
/* If pinA and pinB are both high or both low, it is spinning
* forward. If they're different, it's going backward.
*
* For more information on speeding up this process, see
* [Reference/PortManipulation], specifically the PIND register.
*/
if (digitalRead(encoder0PinA) == digitalRead(encoder0PinB)) {
encoder0Pos++;
} else {
encoder0Pos--;
}
Serial.println (encoder0Pos, DEC);
}
/* See this expanded function to get a better understanding of the
* meanings of the four possible (pinA, pinB) value pairs:
*/
void doEncoder_Expanded(){
if (digitalRead(encoder0PinA) == HIGH) { // found a low-to-high on channel A
if (digitalRead(encoder0PinB) == LOW) { // check channel B to see which way
// encoder is turning
encoder0Pos = encoder0Pos - 1; // CCW
}
else {
encoder0Pos = encoder0Pos + 1; // CW
}
}
else // found a high-to-low on channel A
{
if (digitalRead(encoder0PinB) == LOW) { // check channel B to see which way
// encoder is turning
encoder0Pos = encoder0Pos + 1; // CW
}
else {
encoder0Pos = encoder0Pos - 1; // CCW
}
}
Serial.println (encoder0Pos, DEC); // debug - remember to comment out
// before final program run
// you don't want serial slowing down your program if not needed
}
/* to read the other two transitions - just use another attachInterrupt()
in the setup and duplicate the doEncoder function into say,
doEncoderA and doEncoderB.
You also need to move the other encoder wire over to pin 3 (interrupt 1).
*/
BY:dskv 注意!!!
在中断程序中使用Serial.Print要特别注意,大多数情况下它会失败, 可是有时它会成功, 这是个程序比較糟糕的bugs. 以下连接是解释文档:
“https://groups.google.com/a/arduino.cc/forum/#!topic/developers/HKzEcN6gikM”
“”
“http://forum.jeelabs.net/node/1188.html”
中断例程 (编码器中断主线程). 使用两个中断端口
读编码器,使用2个中断 pin 2 & pin3
注意:以下的程序使用两个中断来使用编码器的最高精度(就像步进电机的细分. 上文的程序使用1个中断. 它只读取一半精度,通过检測EncoderPin A的位置,可是它省一个中断程序。
#define encoder0PinA 2
#define encoder0PinB 3
volatile unsigned int encoder0Pos = 0;
void setup() {
pinMode(encoder0PinA, INPUT);
pinMode(encoder0PinB, INPUT);
// encoder pin on interrupt 0 (pin 2)
attachInterrupt(0, doEncoderA, CHANGE);
// encoder pin on interrupt 1 (pin 3)
attachInterrupt(1, doEncoderB, CHANGE);
Serial.begin (9600);
}
void loop(){ //Do stuff here }
void doEncoderA(){
// look for a low-to-high on channel A
if (digitalRead(encoder0PinA) == HIGH) {
// check channel B to see which way encoder is turning
if (digitalRead(encoder0PinB) == LOW) {
encoder0Pos = encoder0Pos + 1; // CW
}
else {
encoder0Pos = encoder0Pos - 1; // CCW
}
}
else // must be a high-to-low edge on channel A
{
// check channel B to see which way encoder is turning
if (digitalRead(encoder0PinB) == HIGH) {
encoder0Pos = encoder0Pos + 1; // CW
}
else {
encoder0Pos = encoder0Pos - 1; // CCW
}
}
Serial.println (encoder0Pos, DEC);
// use for debugging - remember to comment out
}
void doEncoderB(){
// look for a low-to-high on channel B
if (digitalRead(encoder0PinB) == HIGH) {
// check channel A to see which way encoder is turning
if (digitalRead(encoder0PinA) == HIGH) {
encoder0Pos = encoder0Pos + 1; // CW
}
else {
encoder0Pos = encoder0Pos - 1; // CCW
}
}
// Look for a high-to-low on channel B
else {
// check channel B to see which way encoder is turning
if (digitalRead(encoder0PinA) == LOW) {
encoder0Pos = encoder0Pos + 1; // CW
}
else {
encoder0Pos = encoder0Pos - 1; // CCW
}
}
}
中端样例(编码器中断主线程). 用一个中断端口, 将代码包装进C++类.
类包装 by mikkoh [01/2010]
将上面的样例包装 (一个中断的例程) 进类而且降低一点doEncoder 函数代码的体积(希望代码对你们还是可读的). 在这个类的文档中一个样例.
#ifndef __ENCODER_H__
#define __ENCODER_H__
#include "WProgram.h"
class Encoder {
/*
wraps encoder setup and update functions in a class
!!! NOTE : user must call the encoders update method from an
interrupt function himself! i.e. user must attach an interrupt to the
encoder pin A and call the encoder update method from within the
interrupt
uses Arduino pullups on A & B channel outputs
turning on the pullups saves having to hook up resistors
to the A & B channel outputs
// ------------------------------------------------------------------------------------------------
// Example usage :
// ------------------------------------------------------------------------------------------------
#include "Encoder.h"
Encoder encoder(2, 4);
void setup() {
attachInterrupt(0, doEncoder, CHANGE);
Serial.begin (115200);
Serial.println("start");
}
void loop(){
// do some stuff here - the joy of interrupts is that they take care of themselves
}
void doEncoder(){
encoder.update();
Serial.println( encoder.getPosition() );
}
// ------------------------------------------------------------------------------------------------
// Example usage end
// ------------------------------------------------------------------------------------------------
*/
public:
// constructor : sets pins as inputs and turns on pullup resistors
Encoder( int8_t PinA, int8_t PinB) : pin_a ( PinA), pin_b( PinB ) {
// set pin a and b to be input
pinMode(pin_a, INPUT);
pinMode(pin_b, INPUT);
// and turn on pullup resistors
digitalWrite(pin_a, HIGH);
digitalWrite(pin_b, HIGH);
};
// call this from your interrupt function
void update () {
if (digitalRead(pin_a)) digitalRead(pin_b) ? position++ : position--;
else digitalRead(pin_b) ?
position-- : position++;
};
// returns current position
long int getPosition () { return position; };
// set the position value
void setPosition ( const long int p) { position = p; };
private:
long int position;
int8_t pin_a;
int8_t pin_b;
};
#endif // __ENCODER_H__
(译者
上面的代码使用long int
类型,关于Arduino 数据类型Arduino 数据类型)
中断样例(编码器中断主线程),使用两个中断
使用两个外部中断,只计算一个方向的脉冲.
注意: 虽然代码感觉比較有效率, 可是由于使用了digitalRead()这个库函数,依据 Pin I/O performance(译者地址
Arduino 端口性能解说)上说的它会比直接读端口要慢50倍。
最优效率的旋转编码器计数 by m3tr0g33k
Paul Badger 的工作和原博客非常有启示性而且非常实用,在看代码之前,请理解他们是怎么说的(希望你能看明确)。
My project is a data loger where three analogue inputs are sampled each time a rotary encoder pulse steps clockwise. On an Arduino, time is of the essence to get this data sampled and saved somewhere (I have not included the ‘saving somewhere’ part of this project yet.) (译者
大体意思是当旋转编码器旋转一周将有3个输入量出现,可是在Arduino中处理器资源是有限的)为了节约一些处理器资源。我对终端系统做了略微的改动,去维持在中断循环外的一对布尔声明。
我的想法是改变一个A或B的布尔变量。当在A或B口收到一个有效的跳变沿. 当你因A端口中断,而且是有效的。你将A_set=true. 然后检測B_set是不是false.假设是, 那么 A 比 B 的相位靠前,这表明是顺时针 (计数++)。同理,当你收到有效的因端口B中断, 你会改变set B_set=true. 然后你检查 A_set 是否是 false. 假设是, 那么 B 比A的相位靠前 , 那么意味着是逆时针旋转 (计数–)。
此代码与曾经的代码不同的地方是:当A或B发生中断的时候(中断标志是CHANGE),检測A和B口的状态,假设A或B是0则设置A_set或B_set为false,其它的工作就不须要了。降低了中断的占用时间(由于曾经在一次中断须要读取A和B口的状态。而这个只须要读取一个口的状态)。
无论如何,在代码中有足够的解释,代码例如以下:
#define encoder0PinA 2
#define encoder0PinB 3
volatile unsigned int encoder0Pos = 0;
unsigned int tmp_Pos = 1;
unsigned int valx;
unsigned int valy;
unsigned int valz;
boolean A_set;
boolean B_set;
void setup() {
pinMode(encoder0PinA, INPUT);
pinMode(encoder0PinB, INPUT);
// encoder pin on interrupt 0 (pin 2)
attachInterrupt(0, doEncoderA, CHANGE);
// encoder pin on interrupt 1 (pin 3)
attachInterrupt(1, doEncoderB, CHANGE);
Serial.begin (9600);
}
void loop(){
//Check each second for change in position
if (tmp_Pos != encoder0Pos) {
Serial.print("Index:"); Serial.print(encoder0Pos, DEC); Serial.print(", Values: ");
Serial.print(valx, DEC); Serial.print(", ");
Serial.print(valy, DEC); Serial.print(", ");
Serial.print(valz, DEC); Serial.println();
tmp_Pos = encoder0Pos;
}
delay(1000);
}
// Interrupt on A changing state
void doEncoderA(){
// Low to High transition?
if (digitalRead(encoder0PinA) == HIGH) {
A_set = true;
if (!B_set) {
encoder0Pos = encoder0Pos + 1;
valx=analogRead(0);
valy=analogRead(1);
valz=analogRead(2);
}
}
// High-to-low transition?
if (digitalRead(encoder0PinA) == LOW) {
A_set = false;
}
}
// Interrupt on B changing state
void doEncoderB(){
// Low-to-high transition?
if (digitalRead(encoder0PinB) == HIGH) {
B_set = true;
if (!A_set) {
encoder0Pos = encoder0Pos - 1;
}
}
// High-to-low transition?
if (digitalRead(encoder0PinB) == LOW) {
B_set = false;
}
}
围绕中断代码的其它部分不过为了展示它是怎么工作的。就像我说的,我只希望演示电机正传(在我的电车样例中就是向前走)。当检測到电机是反转的时候,我不过更新那个计数变量。在 loop{} 循环体中,我们每一秒显示一次当前编码器的位置和对应引脚读数数据。可是仅当编码器位置发生改变的时候才去更新,假设位置没有发生变化,则就不更新。
你能够试试一秒钟你能转你的编码器多少圈。我的记录是300步或者是1.5圈每秒。
这个代码有一个逻辑问题,假设你频繁改变方向,那么你或许会想知道在每步的中间位置是否改变了方向,而且在那个位置,你的计数參数是不发生改变的。这就是半步滞后现象。
当然,在绝大多数情况下,这并不能观察得到或者是重要的,可是思考一下这个问题是比較重要的。
希望这个执行速度的提升能够帮助一些人!m3tr0g33k
中断样例(编码器中断主线程),使用两个中断。简化上面代码的中断响应函数
简化中断程序
依照A_set == B_set
来推断滞后或提前。你能够简化相当一部分中断程序的代码。
中断程序变为:
// Interrupt on A changing state
void doEncoderA(){
// Test transition
A_set = digitalRead(encoderPinA) == HIGH;
// and adjust counter + if A leads B
encoderPos += (A_set != B_set) ? +1 : -1;
}
// Interrupt on B changing state
void doEncoderB(){
// Test transition
B_set = digitalRead(encoderPinB) == HIGH;
// and adjust counter + if B follows A
encoderPos += (A_set == B_set) ?
+1 : -1;
}
其基本原理为:当当前的引脚的改变的状态和还有一个引脚的状态一致,那么,这个引脚就比还有一个引脚滞后。假设状态不一致,那么当前引脚就超前。
终于结果:两行代码便构成了中断程序。
全部代码为:
enum PinAssignments {
encoderPinA = 2,
encoderPinB = 3,
clearButton = 8
};
volatile unsigned int encoderPos = 0;
unsigned int lastReportedPos = 1;
boolean A_set = false;
boolean B_set = false;
void setup() {
pinMode(encoderPinA, INPUT);
pinMode(encoderPinB, INPUT);
pinMode(clearButton, INPUT);
digitalWrite(encoderPinA, HIGH); // turn on pullup resistor
digitalWrite(encoderPinB, HIGH); // turn on pullup resistor
digitalWrite(clearButton, HIGH);
// encoder pin on interrupt 0 (pin 2)
attachInterrupt(0, doEncoderA, CHANGE);
// encoder pin on interrupt 1 (pin 3)
attachInterrupt(1, doEncoderB, CHANGE);
Serial.begin(9600);
}
void loop(){
if (lastReportedPos != encoderPos) {
Serial.print("Index:");
Serial.print(encoderPos, DEC);
Serial.println();
lastReportedPos = encoderPos;
}
if (digitalRead(clearButton) == LOW) {
encoderPos = 0;
}
}
// Interrupt on A changing state
void doEncoderA(){
// Test transition
A_set = digitalRead(encoderPinA) == HIGH;
// and adjust counter + if A leads B
encoderPos += (A_set != B_set) ?
+1 : -1;
}
// Interrupt on B changing state
void doEncoderB(){
// Test transition
B_set = digitalRead(encoderPinB) == HIGH;
// and adjust counter + if B follows A
encoderPos += (A_set == B_set) ?
+1 : -1;
}
其它关于编码器的链接:
a good explanation of grey codes and absolute encoders
this code did work better for me than most others, with good explanation
中断样例(编码器中断主线程),使用两个外部中断。初始化读后,就不再读引脚的状态
更快读编码器:仅使用中断
我也在应用中使用到了读旋转编码器。经过多次尝试,我非常高兴快速大家有一个新的方式处理他们。依据前任的建议,我在AMT旋转编码器上使用了它而且执行真的非常好。不幸的是。其它的方法失败了,由于计数太快了。
为了避免读Arduino引脚的,我想或许有更快的方法。通过只使用中断。以下是它的代码:
//PIN's definition
#define encoder0PinA 2
#define encoder0PinB 3
volatile int encoder0Pos = 0;
volatile boolean PastA = 0;
volatile boolean PastB = 0;
void setup()
{
pinMode(encoder0PinA, INPUT);
//turn on pullup resistor
//digitalWrite(encoder0PinA, HIGH); //ONLY FOR SOME ENCODER(MAGNETIC)!!!!
pinMode(encoder0PinB, INPUT);
//turn on pullup resistor
//digitalWrite(encoder0PinB, HIGH); //ONLY FOR SOME ENCODER(MAGNETIC)!!!!
PastA = (boolean)digitalRead(encoder0PinA); //initial value of channel A;
PastB = (boolean)digitalRead(encoder0PinB); //and channel B
//To speed up even more, you may define manually the ISRs
// encoder A channel on interrupt 0 (arduino's pin 2)
attachInterrupt(0, doEncoderA, RISING);
// encoder B channel pin on interrupt 1 (arduino's pin 3)
attachInterrupt(1, doEncoderB, CHANGE);
}
void loop()
{
//your staff....ENJOY! :D
}
//you may easily modify the code get quadrature..
//..but be sure this whouldn't let Arduino back!
void doEncoderA()
{
PastB ? encoder0Pos--: encoder0Pos++;
}
void doEncoderB()
{
PastB = !PastB;
}
希望能帮助到大家! by carnevaledaniele [04/2010]
旋转编码器库,在loop()
中使用。没有演示样例代码
以下是完整的旋转编码器操作库代码(译者
以下的代码没有使用中断。将占用较多的处理器资源):
#include <inttypes.h>
#include "HardwareSerial.h"
// 12 Step Rotary Encoder with Click //
// http://www.sparkfun.com/products/9117 //
#define EncoderPinA 20 // Rotary Encoder Left Pin //
#define EncoderPinB 19 // Rotary Encoder Right Pin //
#define EncoderPinP 21 // Rotary Encoder Click //
// ======================================================================================= //
class Encoder
{
public:
Encoder()
{
pinMode(EncoderPinA, INPUT);
digitalWrite(EncoderPinA, HIGH);
pinMode(EncoderPinB, INPUT);
digitalWrite(EncoderPinB, HIGH);
pinMode(EncoderPinP, INPUT);
digitalWrite(EncoderPinP, HIGH);
Position = 0;
Position2 = 0;
Max = 127;
Min = 0;
clickMultiply = 10;
}
void Tick(void)
{
Position2 = (digitalRead(EncoderPinB) * 2) + digitalRead(EncoderPinA);;
if (Position2 != Position)
{
isFwd = ((Position == 0) && (Position2 == 1)) || ((Position == 1) && (Position2 == 3)) ||
((Position == 3) && (Position2 == 2)) || ((Position == 2) && (Position2 == 0));
if (!digitalRead(EncoderPinP)) { if (isFwd) Pos += clickMultiply; else Pos -= clickMultiply; }
else { if (isFwd) Pos++; else Pos--; }
if (Pos < Min) Pos = Min;
if (Pos > Max) Pos = Max;
}
Position = Position2;
}
int getPos(void)
{
return (Pos/4);
}
void setMinMax(int _Min, int _Max)
{
Min = _Min*4;
Max = _Max*4;
if (Pos < Min) Pos = Min;
if (Pos > Max) Pos = Max;
}
void setClickMultiply(int _clickMultiply)
{
clickMultiply = _clickMultiply;
}
private:
int clickMultiply;
int Max;
int Min;
int Pos;
int Position;
int Position2;
int isFwd;
};
中断样例(编码器中断主线程)–>这个没看
使用两个外部中断端口,主要依据Circuits@home代码,不防抖动。 注意: Serial.print()
函数会占用几毫秒。所以此代码中的中断程序执行时间会相对照较长,当将这些声明去掉的时候,那么就会提升程序的性能。
(由于其中断程序占用足够长的时间。那么信号抖动甚至一些信号将会被丢失)。-ED
代码:
/*
RotaryInterrupts - a port-read and interrupt based rotary encoder sketch
Created by Joshua Layne (w15p), January 4, 2011.
based largely on: http://www.circuitsathome.com/mcu/reading-rotary-encoder-on-arduino
Released into the public domain.
*/
#define ENC_A 2
#define ENC_B 3
#define ENC_PORT PIND
uint8_t bitShift = 2; // change to suit your pins (offset from 0,1 per port)
// Note: You need to choose pins that have Interrupt capability.
int counter;
boolean ticToc;
void setup()
{
pinMode(ENC_A, INPUT);
digitalWrite(ENC_A, HIGH);
pinMode(ENC_B, INPUT);
digitalWrite(ENC_B, HIGH);
Serial.begin (115200);
Serial.println("Start");
counter = 0;
ticToc = false;
// Attach ISR to both interrupts
attachInterrupt(0, read_encoder, CHANGE);
attachInterrupt(1, read_encoder, CHANGE);
}
void loop()
{
// do some stuff here - the joy of interrupts is that they take care of themselves
}
void read_encoder()
{
int8_t enc_states[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};
static uint8_t encoderState = 0;
static uint8_t stateIndex = 0;
static uint8_t filteredPort = 0;
uint8_t filter = 0x03; // base filter: 0b00000011
filter <<= bitShift;
Serial.print("raw port value: ");
Serial.println(ENC_PORT, BIN);
Serial.print("filter bitmask: ");
Serial.println(filter, BIN);
filteredPort = ENC_PORT & filter;
Serial.print("filtered port state: ");
Serial.println(filteredPort, BIN);
Serial.print("old encoder state: ");
Serial.println(encoderState, BIN);
encoderState &= filter; // filter out everything except the rotary encoder pins
Serial.print("filtered old encoder state: ");
Serial.println(encoderState, BIN);
encoderState <<= 2; // shift existing value two bits to the left
Serial.print("filtered and shifted (<<2) old encoder state: ");
Serial.println(encoderState, BIN);
encoderState |= filteredPort; // add filteredport value
Serial.print("old encoder state + port state: ");
Serial.println(encoderState, BIN);
stateIndex = encoderState >> bitShift;
Serial.print("encoder state index: ");
Serial.println(stateIndex, DEC);
if (ticToc) {
Serial.print("counter tic: ");
Serial.println(enc_states[stateIndex], DEC);
counter += enc_states[stateIndex];
Serial.print("counter: ");
Serial.println(counter, DEC);
}
ticToc = !ticToc;
Serial.println("----------");
}
中断样例(旋转编码器中断主线程)
使用1个中断,这样就丢失了一半的精度,作者说速度比較快可是却用了digitalRead()
函数而不是直接去操作引脚。
(具体请看: http://jeelabs.org/2010/01/06/pin-io-performance/))<< 2011/06 >>
通过尝试全部的样例,我觉得我的代码对于SaprkFun的旋转编码器(SaprkFunRotary encoder)操作地比較快了。可是请注意:须要在编码器的中间引脚和B引脚之间串连一个电容(译者
就是在输出口和GND之间串联一个电容)。
程序:
#include <LiquidCrystal.h>
LiquidCrystal lcd(12,11,5,4,8,7);
#define encoder0PinA 2
#define encoder0PinB 3
volatile int encoder0Pos = 0;
volatile boolean PastB = 0;
volatile boolean update = false;
void setup()
{
lcd.begin(16,2);
lcd.print("Pos:");
pinMode(encoder0PinA, INPUT);
//turn on pullup resistor
digitalWrite(encoder0PinA, HIGH);
pinMode(encoder0PinB, INPUT);
//turn on pullup resistor
digitalWrite(encoder0PinB, HIGH);
attachInterrupt(1, doEncoderB, FALLING);
}
void loop()
{
if (update){
update = false;
PastB? encoder0Pos++:encoder0Pos--;
lcd.setCursor(7,0);
lcd.print(" ");
lcd.setCursor(7,0);
lcd.print(encoder0Pos,DEC);
}
}
void doEncoderB()
{
PastB=(boolean)digitalRead(encoder0PinA);
update = true;
}
去抖动电路
去抖动电路1
7.8.2011 deif
通过很多尝试,我建立了一个电路消除大多数的抖动。
以下是我最简形式的代码:
ISR(INT0_vect){
int a;
a = PIND & 0x0c;
if ((a == 0x0c) || (a == 0)){
encoderCount++;
}
else {
encoderCount--;
}
}
void setupPinInterrupt(){
EICRA = 0x01; //int0 on pinchange
EIMSK = 0x01; //enable interrupt 0
EIFR = 0; //clear flags
}
去抖动电路2
2011-12-11 by _bse_
和上拉电阻一起使用(还是加入电容)
中断样例(旋转编码器中断主进程)。使用两个外部中断
注意:在中断程序中,使用digitalRead()
而不是直接操作端口。
使用XOR(异或)的方法。
通过异或的方法,将当前B口的状态和前一次端口A的状态进行比較,得出是应该降低还是添加计数(即正反转)。较少的代码,编码器的全精度而且达到了Arduino最大採集速率。
使用愉快!
15 August 2011 by Bruno Chaparro
#define encoder0PinA 2
#define encoder0PinB 3
volatile unsigned int encoder0Pos = 0;
unsigned int tmp = 0;
unsigned int Aold = 0;
unsigned int Bnew = 0;
void setup() {
pinMode(encoder0PinA, INPUT);
pinMode(encoder0PinB, INPUT);
// encoder pin on interrupt 0 (pin 2)
attachInterrupt(0, doEncoderA, CHANGE);
// encoder pin on interrupt 1 (pin 3)
attachInterrupt(1, doEncoderB, CHANGE);
// set up the Serial Connection
Serial.begin (115200);
}
void loop(){
//Check each changes in position
if (tmp != encoder0Pos) {
Serial.println(encoder0Pos, DEC);
tmp = encoder0Pos;
}
delay(500);
}
// Interrupt on A changing state
void doEncoderA(){
Bnew^Aold ? encoder0Pos++:encoder0Pos--;
Aold=digitalRead(encoder0PinA);
}
// Interrupt on B changing state
void doEncoderB(){
Bnew=digitalRead(encoder0PinB);
Bnew^Aold ? encoder0Pos++:encoder0Pos--;
}
我不知道上面代码是怎么执行的(就是XOR方法)(译者
事实上没啥神奇的就是将是否相等变成使用异或的方法进行实现,可是我不知道这样是否会对代码的执行速度带来提升。),可是当我将代码和PinChangeIntlibrary
(这样我能够使用全部的I/O口作为中断口)进行联合使用时,我在的小车上执行了两个增量编码器而且执行良好。
I am using a Solarbotics SB Freeduino board with an ATMEGA 328P.(译者
没使用过这个板子,不清楚)。
我希望我对上面的XOR代码是怎么执行了解很多其它。我不得写点什么在其它时间。可是这个代码非常棒。
谢谢你!
中断样例使用上面的“XOR 方法”
使用XOR方法驱动一个LCD和一个LED
使用上面Bruno Chaparro所提到XOR方法。我将一个驱动LCD bargraph和LED的样例和它结合到了一起。
29 December 2011 Mark Amos
// increments/decrements a counter based on the movement of a rotary encoder and
// displays the results on an LCD in digital and bargraph form.
// rotary encoder is a 5 pin. Pins, left to right:
// Encoder pin B - connect to D2 (interrupt 0)
// +5 VDC
// Encoder pin A - connect to D3 (interrupt 1)
// NC
// Ground
// Pin D5 is used to light up an LED connected to ground with a 1K resistor
// using PWM with brightness proportional to the encoder position.
#include <LiquidCrystal.h>
#define encoder0PinA 3
#define encoder0PinB 2
#define analogOutPin 5
LiquidCrystal lcd(13,12,11,10,9,8,7);
volatile unsigned int encoder0Pos = 0;
unsigned int tmp = 0;
unsigned int Aold = 0;
unsigned int Bnew = 0;
void setup() {
pinMode(encoder0PinA, INPUT);
pinMode(encoder0PinB, INPUT);
lcd.begin(20,2);
lcd.clear();
// encoder A on interrupt 1 (pin 3)
attachInterrupt(1, doEncoderA, CHANGE);
// encoder B on interrupt 0 (pin 2)
attachInterrupt(0, doEncoderB, CHANGE);
// set up the Serial Connection
Serial.begin (115200);
Serial.println("Starting");
}
void loop(){
//if position has changed, display it on serial and bargraph
if (tmp != encoder0Pos) {
tmp = encoder0Pos;
Serial.println(tmp, DEC);
lcd.setCursor(0,0);
lcd.print(tmp);
lcd.print(" ");
lcd.setCursor(0,1);
//scale the range of the LCD bargraph from 0-1023 to 0-20 by dividing by 50
for (int loopCnt = 0; loopCnt < (tmp / 50) +1 ; loopCnt++) {
lcd.write(1);
}
lcd.print(" ");
//scale the encorer0Pos from 0 - 1023 to the range of the PWM (0 - 255) by dividing by 4.
analogWrite(analogOutPin, tmp / 4);
}
}
// Interrupt on A changing state
void doEncoderA(){
// if Bnew = Aold, increment, otherwise decrement
Bnew^Aold ? encoder0Pos++:encoder0Pos--;
Aold=digitalRead(encoder0PinA);
// check for underflow (< 0)
if (bitRead(encoder0Pos, 15) == 1) encoder0Pos = 0;
// check for overflow (> 1023)
if (bitRead(encoder0Pos, 10) == 1) encoder0Pos = 1023;
constrain(encoder0Pos, 0, 1023);
}
// Interrupt on B changing state
void doEncoderB(){
Bnew=digitalRead(encoder0PinB);
// if Bnew = Aold, increment, otherwise decrement
Bnew^Aold ? encoder0Pos++:encoder0Pos--;
// check for underflow (< 0)
if (bitRead(encoder0Pos, 15) == 1) encoder0Pos = 0;
// check for overflow (> 1023)
if (bitRead(encoder0Pos, 10) == 1) encoder0Pos = 1023;
}
中断库(旋转编码器中断主线程)
使用PinChangeInt
库来利用ATmega328p的全部的引脚作为中断口。by GreyGnome
AdaEncoder
库适用于2个基本端口的增量旋转编码器。就像以下链接中所看到的:
https://www.adafruit.com/products/377
http://www.sparkfun.com/products/9117
通过介绍我们知道:
以下翻译是2016年9月6日15:03:10后翻译。所以有可能翻译没有曾经那么准确了。
(时间太长了)-译者
这个库适用于2端口编码器(一个A口一个B口还有一个公共口C口)。
这个库不会通知每个状态的改变,而是仅当编码器从一个定位点变化到还有一个的时候才会通知用户(这个没有看懂)。 It does not indicate every state change, rather, it reports only when the decoder is turned from one detent position to the next. 它是通过中断触发的而且设计的尽量反应快和好用。 The interrupt routine is lightweight, and the programmer is then able to read the direction the encoder turned at their leisure (within reason; what’s important is that the library is reasonably forgiving). The library is designed to be easy to use (it bears repeating :-) ) and it is reasonably immune to switch bounce.
See the project page at: http://code.google.com/p/adaencoder/
See a speed discussion at: http://code.google.com/p/adaencoder/wiki/Speed
See the PinChangeInt library project page at: http://code.google.com/p/arduino-pinchangeint/
Here’s an example with two encoders connected. Encoder a is connected to pins 2 and 3, b is connected to 5 and 6:
#include <PinChangeInt.h> // necessary otherwise we get undefined reference errors.
#include <AdaEncoder.h>
#define a_PINA 2
#define a_PINB 3
#define b_PINA 5
#define b_PINB 6
int8_t clicks=0;
char id=0;
void setup()
{
Serial.begin(115200);
AdaEncoder::addEncoder('a', a_PINA, a_PINB);
AdaEncoder::addEncoder('b', b_PINA, b_PINB);
}
void loop()
{
encoder *thisEncoder;
thisEncoder=AdaEncoder::genie(&clicks, &id);
if (thisEncoder != NULL) {
Serial.print(id); Serial.print(':');
if (clicks > 0) {
Serial.println(" CW");
}
if (clicks < 0) {
Serial.println(" CCW");
}
}
}
另外一个中断库(绝对能用)(旋转编码器中断主线程而且防抖动–>完美!
。)
by rafbuff
(译者
这个有点吹了。他在中断程序中使用delay(1)
,这也就是说计数的周期应该远大于1ms。可是别人也有小于1ms的啊!!
)
我尝试了上面大多数的程序,可是发现它们的计数不是那么的可靠,大多数不能防抖动。同一时候我尝试了很多技巧性的功能,如中断的使用和提高效率的办法,当要求精确计数的时候。我发现以下的这个执行最好。
/* interrupt routine for Rotary Encoders
tested with Noble RE0124PVB 17.7FINB-24 http://www.nobleusa.com/pdf/xre.pdf - available at pollin.de
and a few others, seems pretty universal
The average rotary encoder has three pins, seen from front: A C B
Clockwise rotation A(on)->B(on)->A(off)->B(off)
CounterCW rotation B(on)->A(on)->B(off)->A(off)
and may be a push switch with another two pins, pulled low at pin 8 in this case
[email protected] 20120107
*/
// usually the rotary encoders three pins have the ground pin in the middle
enum PinAssignments {
encoderPinA = 2, // rigth
encoderPinB = 3, // left
clearButton = 8 // another two pins
};
volatile unsigned int encoderPos = 0; // a counter for the dial
unsigned int lastReportedPos = 1; // change management
static boolean rotating=false; // debounce management
// interrupt service routine vars
boolean A_set = false;
boolean B_set = false;
void setup() {
pinMode(encoderPinA, INPUT);
pinMode(encoderPinB, INPUT);
pinMode(clearButton, INPUT);
// turn on pullup resistors
digitalWrite(encoderPinA, HIGH);
digitalWrite(encoderPinB, HIGH);
digitalWrite(clearButton, HIGH);
// encoder pin on interrupt 0 (pin 2)
attachInterrupt(0, doEncoderA, CHANGE);
// encoder pin on interrupt 1 (pin 3)
attachInterrupt(1, doEncoderB, CHANGE);
Serial.begin(9600); // output
}
// main loop, work is done by interrupt service routines, this one only prints stuff
void loop() {
rotating = true; // reset the debouncer
if (lastReportedPos != encoderPos) {
Serial.print("Index:");
Serial.println(encoderPos, DEC);
lastReportedPos = encoderPos;
}
if (digitalRead(clearButton) == LOW ) {
encoderPos = 0;
}
}
// Interrupt on A changing state
void doEncoderA(){
// debounce
if ( rotating ) delay (1); // wait a little until the bouncing is done
// Test transition, did things really change?
if( digitalRead(encoderPinA) != A_set ) { // debounce once more
A_set = !A_set;
// adjust counter + if A leads B
if ( A_set && !B_set )
encoderPos += 1;
rotating = false; // no more debouncing until loop() hits again
}
}
// Interrupt on B changing state, same as A above
void doEncoderB(){
if ( rotating ) delay (1);
if( digitalRead(encoderPinB) != B_set ) {
B_set = !B_set;
// adjust counter - 1 if B leads A
if( B_set && !A_set )
encoderPos -= 1;
rotating = false;
}
}
loop() Example, and the Encoder interrupts the processor.
Uses a single External Interrupt pin and also requires loop() for debouncing.
Software Debounce
By jonfraz 09/11
First post from me also, hope it’s useful.
I’ve been playing around with a pretty cheap mechanical encoder and found bouncing was a big issue when I experimented with the other code above.Ideally I would debounce it with hardware, but I’m a noob and lack the knowledge/components. However, thanks to Hifiduino I managed to get my encoder working pretty well with a simple bit of software debouncing.The code uses an interrupt to detect any signal change from the encoder, but then waits 2 milliseconds before calculating the encoder position:
*/ Software Debouncing - Mechanical Rotary Encoder */
#define encoder0PinA 2
#define encoder0PinB 4
volatile unsigned int encoder0Pos = 0;
static boolean rotating=false;
void setup() {
pinMode(encoder0PinA, INPUT);
digitalWrite(encoder0PinA, HIGH);
pinMode(encoder0PinB, INPUT);
digitalWrite(encoder0PinB, HIGH);
attachInterrupt(0, rotEncoder, CHANGE);
Serial.begin (9600);
}
void rotEncoder(){
rotating=true;
// If a signal change (noise or otherwise) is detected
// in the rotary encoder, the flag is set to true
}
void loop() {
while(rotating) {
delay(2);
// When signal changes we wait 2 milliseconds for it to
// stabilise before reading (increase this value if there
// still bounce issues)
if (digitalRead(encoder0PinA) == digitalRead(encoder0PinB)) {
encoder0Pos++;
}
else {
encoder0Pos--;
}
rotating=false; // Reset the flag back to false
Serial.println(encoder0Pos);
}
}
Int0 & Int1 example using bitRead() with debounce handling and true Rotary Encoder pulse tracking
J.Carter(of Earth)
This tiny amount of code is focused on keeping the interrupts fast and totally responsible for encoder position
// 'threshold' is the De-bounce Adjustment factor for the Rotary Encoder.
//
// The threshold value I'm using limits it to 100 half pulses a second
//
// My encoder has 12 pulses per 360deg rotation and the specs say
// it is rated at a maximum of 100rpm.
//
// This threshold will permit my encoder to reach 250rpm so if it was connected
// to a motor instead of a manually operated knob I
// might possibly need to adjust it to 25000. However, this threshold
// value is working perfectly for my situation
//
volatile unsigned long threshold = 10000;
// 'rotaryHalfSteps' is the counter of half-steps. The actual
// number of steps will be equal to rotaryHalfSteps / 2
//
volatile long rotaryHalfSteps = 0;
// Working variables for the interrupt routines
//
volatile unsigned long int0time = 0;
volatile unsigned long int1time = 0;
volatile uint8_t int0signal = 0;
volatile uint8_t int1signal = 0;
volatile uint8_t int0history = 0;
volatile uint8_t int1history = 0;
void int0()
{
if ( micros() - int0time < threshold )
return;
int0history = int0signal;
int0signal = bitRead(PIND,2);
if ( int0history==int0signal )
return;
int0time = micros();
if ( int0signal == int1signal )
rotaryHalfSteps++;
else
rotaryHalfSteps--;
}
void int1()
{
if ( micros() - int1time < threshold )
return;
int1history = int1signal;
int1signal = bitRead(PIND,3);
if ( int1history==int1signal )
return;
int1time = micros();
}
void setup()
{
digitalWrite(2, HIGH);
digitalWrite(3, HIGH);
attachInterrupt(0, int0, CHANGE);
attachInterrupt(1, int1, CHANGE);
}
void loop()
{
long actualRotaryTicks = (rotaryHalfSteps / 2);
}
Others
On Interrupts
by GreyGnome
The ATmega328p has two different kinds of interrupts: “external”, and “pin change”. There are only two external interrupt pins, INT0 and INT1, and they are mapped to Arduino pins 2 and 3. These interrupts can be set to trigger on RISING or FALLING signal edges, or on low level. Most of the sketches and libraries given on this page use one or two of the External Interrupt pins. The interrupt is theoretically very quick because you can set the hardware to tell you how you want the interrupt to trigger. Each pin can have a separate interrupt routine associated with it.
On the other hand the pin change interrupts can be enabled on any or all of theATmega328p’s signal pins. They are triggered equally on RISING or FALLING signal edges, so it is up to the interrupt code to determine what happened (did the signal rise, or fall?
) and handle it properly. Furthermore, the pin change interrupts are grouped into 3 “port”s on the MCU, so there are only 3 interrupt vectors (subroutines) for the entire body of 19 pins. This makes the job of resolving the action on a single interrupt even more complicated. The interrupt routine should be fast, but complication is the enemy of speed.
Interrupts disrupt the normal flow of the program. This can be a bit of a curse, although the compiler adds some code for you in order to take away much of the pain of the interrupt. The benefit is that, unlike polling in loop(), you have a better chance of getting all the transitions from your device.
What if, for example, the Arduino is talking to an I2C device via the Wire library?
You may not realize that the I2C library has a busy wait portion in it, which means that your 16MHz processor is busy waiting on a 100khz serial communication. That communication is actually being performed in hardware, so there’s no reason that the processor must wait, but that’s how the library was designed. So if you use polling in loop() to check on your rotary encoder, you may miss a quick transition if your code is waiting for a communication to end.
This is not an issue with an interrupt, because your rotary encoder will trigger the interrupt routine even if the Wire library is busy in a while() loop. For this reason, you should be careful using code that polls a switch in loop() on more complicated projects, and understand how long each section of your code will take in worse case scenarios.
On Speed
by GreyGnome
If your application is speed-critical, know that digitalRead() and digitalWrite() are relatively slow. See http://jeelabs.org/2010/01/06/pin-io-performance/.
From that link:
Let me just conclude with: there are order-of-magnitude performance implications, depending on how you do things. As long as you keep that in mind, you’ll be fine.
To get a closer look at the implications, I actually measured the speed.
Speed Tests
From http://arduino-pinchangeint.googlecode.com/files/PinChangeInt%20Speed%20Test-1.3.pdf Tested External Interrupts against Pin Change Interrupt, to look at the relative speeds vs. the External Interrupts, and also to better judge the performance hit of using digitalRead() and digitalWrite().
1 | 2 | External | 1420 | 14.20 |
2 | 2 | Pin Change | 2967 | 29.67 |
Next, we turned on the LED Pin using direct port manipulation, then turned it on using digitalWrite() under Arduino 022 and Arduino 1.0. This test is enabled by #define’ing the COMPAREWRITES compiler directive in the source code. If DIRECTPORT is #define’ed the LED pin is turned on and off using direct port manipulation. If #undef’ed, digitalWrite() is used to manipulate the LED pin.
1 | 2 | External | 022 | Direct Port Manipulation | 1603 | 16.03 |
2 | 2 | External | 022 | digitalWrite() | 2232 | 22.32 |
3 | 2 | External | 1.0 | digitalWrite() | 2245 | 22.45 |
Now read the LED Pin using direct port manipulation, then read it using digitalWrite() under Arduino 022 and Arduino 1.0. This test is enabled by #define’ing the COMPAREREADS compiler directive in the source code. If DIRECTPORT is #define’ed the LED pin is read using direct port manipulation. If #undef’ed, digitalREAD() is used to manipulate the LED pin.
1 | 2 | External | 022 | Direct Port Manipulation | 1559 | 15.59 |
2 | 2 | External | 022 | digitalRead() | 2188 | 21.88 |
3 | 2 | External | 1.0 | digitalRead() | 2189 | 21.89 |
PLUS:这是第二次使用MarkDown进行博客的编辑。先比較上一次。速度上有所提升,可是感觉比較不好的是,代码的渲染和CSDN那个传统编辑器比太low。另外正文的字体略微小了一点。不知道能否够更改。
文章结尾是MarkDown的demo,如今先不删了。以后还要仿照着用
关于翻译这篇文章:由于译者(xuanyuanlei1020)的水平有限,有些地方还没有找到比較好的翻译或有点地方还没翻译,我也是用到的时候再去翻译的,也就是说不一定什么时候翻译完,请谅解。另外本文各类应用的程序较多,看完本文也是有较大的启示,可是也耗费了大量的时间。希望对大家有所帮助。
关于文章的解释:带译者
的部分是本人(xuanyuanlei1020)的理解,其它的都是翻译的原话
巨人正在奔跑!
。
- Markdown和扩展Markdown简洁的语法
- 代码块高亮
- 图片链接和图片上传
- LaTex数学公式
- UML序列图和流程图
- 离线写博客
- 导入导出Markdown文件
- 丰富的快捷键
快捷键
- 加粗
Ctrl + B
- 斜体
Ctrl + I
- 引用
Ctrl + Q
- 插入链接
Ctrl + L
- 插入代码
Ctrl + K
- 插入图片
Ctrl + G
- 提升标题
Ctrl + H
- 有序列表
Ctrl + O
- 无序列表
Ctrl + U
- 横线
Ctrl + R
- 撤销
Ctrl + Z
- 重做
Ctrl + Y
Markdown及扩展
使用简单的符号标识不同的标题,将某些文字标记为粗体或者斜体。创建一个链接等,具体语法參考帮助?。
本编辑器支持 Markdown Extra , 扩展了非常多好用的功能。具体请參考Github.
表格
Markdown Extra 表格语法:
Computer | $1600 |
Phone | $12 |
Pipe | $1 |
能够使用冒号来定义对齐方式:
Computer | 1600 元 | 5 |
Phone | 12 元 | 12 |
Pipe | 1 元 | 234 |
定义列表
- Markdown Extra 定义列表语法:
- 项目1
- 项目2
- 定义 A
- 定义 B
- 项目3
- 定义 C
定义 D
代码块
代码块语法遵循标准markdown代码,比如:
@requires_authorization
def somefunc(param1='', param2=0):
'''A docstring'''
if param1 > param2: # interesting
print 'Greater'
return (param2 - param1 + 1) or None
class SomeClass:
pass
>>> message = '''interpreter
... prompt'''
脚注
生成一个脚注1.
文件夹
用 [TOC]
来生成文件夹:
数学公式
使用MathJax渲染LaTex 数学公式,详见math.stackexchange.com.
- 行内公式,数学公式为:Γ(n)=(n−1)!∀n∈N。
- 块级公式:
很多其它LaTex语法请參考 这儿.
UML 图:
能够渲染序列图:
或者流程图:
离线写博客
即使用户在没有网络的情况下,也能够通过本编辑器离线写博客(直接在曾经使用过的浏览器中输入write.blog.csdn.net/mdeditor就可以。
Markdown编辑器使用浏览器离线存储将内容保存在本地。
用户写博客的过程中。内容实时保存在浏览器缓存中。在用户关闭浏览器或者其它异常情况下。内容不会丢失。用户再次打开浏览器时,会显示上次用户正在编辑的没有发表的内容。
博客发表后,本地缓存将被删除。
用户能够选择 把正在写的博客保存到server草稿箱。即使换浏览器或者清除缓存,内容也不会丢失。
浏览器兼容
- 眼下,本编辑器对Chrome浏览器支持最为完整。建议大家使用较新版本号的Chrome。
- IE9以下不支持
- IE9,10,11存在以下问题
- 不支持离线功能
- IE9不支持文件导入导出
- IE10不支持拖拽文件导入
- 这里是 脚注 的 内容. ↩