Arduino Series: Working With An Optical Encoder

The Goal

I have an old White 1602 knitting machine that uses a light scanner to produce patterns in the knit fabric. The bed of the knitting machine syncs up with the controller via two obsolete rotary encoders and the stitch patterns are produced as a sequence of pulses causes specific needles to be selected.

The first problem is that the light scanner has a lot of mechanical parts that have deteriorated with age. Parts are no longer available.

The second problem is that the width of the pattern is constrained by the width of the mylar that feeds into the light scanner to product the pattern.

The third problem is that while the light scanner does its job well when it’s functioning, all of its capabilities could be performed more efficiently and accurately by a computer.

My goal is to completely replace the light scanner with newer technology. This post illustrates a prototype for how I might use an optical coder to track the position of the knitting carriage as well as when it changes direction.

Equipment

Arduino Mega 2560 R2
US Digital Optical Encoder S1-1250-I
4 male-to-female jumpers
Electrical tape

About The Encoder

While obsolete, the S1-1250-I encoder is a very capable piece of hardware, but much more expensive than what’s available on today’s market. I used it because I already had one, but the information presented in this post should work with any rotary quadrature encoder. I’ll most likely replace the US Digital with a SparkFun’s COM-11102 1024 P/R Quadrature Encoder I have on order.

About The Approach

There are basically two ways to interface with the encoder: polling and interrupts. A little project I’m playing with will require a considerable amount of accuracy, so I chose to use interrupts as polling might result in missed pulses.

 Wiring

The encoder has 3 outputs: channel A, channel B and index. We’re not going to use index, so we need to make 4 connections — one for each of the two channels, one for power and one for ground. The encoder has raw wires so we need to add pins in order to attach it to the Arduino.

  1. Make sure the Arduino is powered off.
  2. Strip 1/4″ – 3/8″ of insulation from the encoder’s leads for power, ground, channel A and channel B.
  3. Insert the end of each wire into the female end of a jumper and secure with electrical tape.
  4. Connect the power lead to the 5V power pin.
  5. Connect the ground lead to one of the Arduino’s ground pins.
  6. Connect the channel A lead to digital pin 20. This pin is one of the 6 Arduino pins that support interrupts. The other pins with interrupts are 2, 3, 18, 19 and 21.
  7. Connect the channel B lead to digital pin 17.

The Code

/****************************************************************************************

Author:    Brenda A Bell
Permalink: https://www.brendaabell.com/2014/02/arduino-series-working-with-an-optical-encoder/

****************************************************************************************/

#define ENCODER0PINA         20      // this pin needs to support interrupts
#define ENCODER0PINB         17      // no interrupt required
#define CPR                  1250    // encoder cycles per revolution
#define CLOCKWISE            1       // direction constant
#define COUNTER_CLOCKWISE    2       // direction constant

// variables modified by interrupt handler must be declared as volatile
volatile long encoder0Position = 0;
volatile long interruptsReceived = 0;

// track direction: 0 = counter-clockwise; 1 = clockwise
short currentDirection = CLOCKWISE;

// track last position so we know whether it's worth printing new output
long previousPosition = 0;

void setup()
{

  // inputs
  pinMode(ENCODER0PINA, INPUT);
  pinMode(ENCODER0PINB, INPUT);

  // interrupts
  attachInterrupt(3, onInterrupt, RISING);

  // enable diagnostic output
  Serial.begin (9600);
  Serial.println("\n\n\n");
  Serial.println("Ready.");
}

void loop()
{
  // only display position info if has changed
  if (encoder0Position != previousPosition )
  {
    Serial.print(encoder0Position, DEC);
    Serial.print("\t");
    Serial.print(currentDirection == CLOCKWISE ? "clockwise" : "counter-clockwise");
    Serial.print("\t");
    Serial.println(interruptsReceived, DEC);
    previousPosition = encoder0Position;
  }
}

// interrupt function needs to do as little as possible
void onInterrupt()
{
  // read both inputs
  int a = digitalRead(ENCODER0PINA);
  int b = digitalRead(ENCODER0PINB);

  if (a == b )
  {
    // b is leading a (counter-clockwise)
    encoder0Position--;
    currentDirection = COUNTER_CLOCKWISE;
  }
  else
  {
    // a is leading b (clockwise)
    encoder0Position++;
    currentDirection = CLOCKWISE;
  }

  // track 0 to 1249
  encoder0Position = encoder0Position % CPR;

  // track the number of interrupts
  interruptsReceived++;
}

How It Works

Lines 8 – 12 define a few useful constants to make the code more readable. What they do should be obvious from the comments.

Lines 15 – 16 define global variables that will be modified by the interrupt handler.

Line 19 & 22 define other global variables we’ll use inside the Arduino loop.

The setup() function on line 24 configures our channel A and channel B pins for input, attaches an interrupt handler to channel A’s pin and configures the serial port so we can see some diagnostic output. Note that we’re going to interrupt on a rising state change so we know that the state of channel A will always be high when our interrupt is triggered. Using a rising or falling interrupt means:

  • We always know the state of A without having to perform a read: A is always high in a rising interrupt and always low in a falling interrupt.
  • Since we always know the starting state of A, we only have to test the state of B to determine direction and track the current position.

The Arduino loop() function on line 40 does nothing more than print some diagnostic information about what we’re reading from the encoder. To avoid chatter, the loop is tracking current values against previous values to we don’t print information we’ve already seen.

The interrupt handler on line 55 does all the heavy lifting:

  • When the encoder is moving in one direction, the pulse from channel A is leading the pulse from channel B. When the encoder is moving in the other direction, the pulses are reversed.
  • When the state of A and B are equal, B must be leading A, so the encoder is turning counter-clockwise. Otherwise, A is leading B, so the encoder is turning clockwise. Remember when we configured our interrupt to fire on rising? The state of channel A will always be high, so we only need to check the state of channel B to determine direction.
  • By comparing A to B instead of hard-coded constants, we can change the interrupt between rising and falling without breaking the interrupt handler.

The code on line 75 keeps the counter within the range 0 to 1249. This would allow us to compute angle or synchronize the position of the encoder to some other device.

The code on line 78 is an extra bit of diagnostic info we can use to track how many times our interrupt has fired.

Further Discussion

It’s much easier to understand how the interrupt handler works if you understand what’s happening when you turn the encoder shaft and reverse direction.

When you turn the encoder’s  shaft clockwise, A is leading B. This results in 4 distinct transitions that are repeated over and over as long as the shaft continues rotating in the same direction.

AB
HIGHLOW
HIGHHIGH
LOWHIGH
LOWLOW

What’s important is this:

  • The inputs are latched, meaning that when we read B’s value from A’s interrupt handler the value we get is B’s state as it existed at the time the interrupt handler was fired. 
  • The handler is fired when A goes high.
  • When the shaft is turning clockwise, the handler is fired between the first two transitions —before B goes high — so we know the shaft is rotating clockwise when A is high and B is low.

If the shaft is turning clockwise and you stop turning, A remains high and B remains low.

If the shaft then starts turning counter-clockwise, B is leading A. This means that B has to go high before A’s interrupt fires again. Therefore, when both A and B are high, the shaft must be turning counter-clockwise.

Some makers may be inclined to use interrupts on both A and B. Unless you have an application where you absolutely must perform some action between A and B going high in both directions, the second interrupt is completely unnecessary. Interrupts are an expensive, limited resource so it’s wise to only use them when you need them.

References

http://playground.arduino.cc/Main/RotaryEncoders#Example1

Posted in articles, knitting, technology and tagged , , , , , , .

31 Comments

    • Maybe. I really can’t say because I don’t know anything about the encoders on printers. I suppose it really depends on what you’re trying to do and how old the technology is. For instance, the project where I’m using this is replacing an ancient rotary encoder that uses photosensors to detect position. In order to get my project to work, I have to completely eliminate the photosensors which means I also have to eliminate a bunch of other stuff or find a way to hack in a new interface.

    • I don’t think so, but I may be misunderstanding what you’re trying to do. In this post, my optical encoder is an input device. I.e., something mechanically rotates the shaft on the encoder, the encoder pulses a pin on the Arduino, and the Arduino uses the pulses to determine the degree of rotation. It sounds like you’re trying to use that encoder as an output device. It’s possible that’s the way it works, but don’t have any experience there. Sorry.

    • Actually, I just read some more of the specs on that encoder and I’m pretty sure it works similar to mine, except mine is single-ended. So we’re back to my first question which is what you’re trying to do. An Arduino doesn’t “drive” the encoder… it’s the other way around.

      • I apologize for my interpretation.

        I have a 23.5V DC pancake motor integrated with the US Digital E5 Optical Kit Encoder.

        My aim is to use the US Digital E5 Optical Kit Encoder as an input device where it will send signals to the Arduino. The Arduino will then indicate the degree of rotation of the DC pancake motor. The motor will be running at 6 RPM and will only be rotating in clockwise direction.

        Kind regards,

        Nayan

        • Ah. Now I understand. So if your E5 works the same as my encoder (and it does as best as I can tell), you should be able to use what I did stripping out all the directional stuff. The critical part is in the interrupt function. If the shaft on your encoder is only turning clockwise, you should never hit the a==b logic because A will always be leading B in your case. I only care about that bit because the mechanical device that’s driving my encoder reverses direction.

          To compute the position, you’ll need to replace my 1249 with whatever the resolution of your encoder is (mine is 1250).

          I think if I were in your position, I’d just take this code, modify the pins where necessary and try to get it to run without futzing with it too much. Then you can figure out where you can start ripping things out to optimize it.

          • How to do you work out the resolution of the encoder?

            As I am using the Arduino Uno, which pin of the Arduino Uno do I assign B to? I know that I have to assign A to an interrupt pin.

            Also, I am only planning to use the pins A, B, 5VDC and GND of my encoder, similar to what you have done. This means I will leave out the other pins such as Index, Index-, A-, B- and the other 5VDC and GND (There are two pins of 5VDC and GND), should this be a problem?

            Kind regards,

            Nayan

          • You can assign B to any available digital pin.

            The resolution should be stated in the encoder specs. Another look at that page seems to indicate it’s variable (32-4000 cycles per revolution/128 to 20000 pulses per revolution). Maybe there’s some sort of switch on that encoder that lets you set the resolution?

            You should be good to go with just connecting the four pins you mention. However, if you can’t locate the pulses per revolution, I think you can use the index to figure it out. Connect the index pin to another interrupt and define another long to serve as a counter. The first time the index interrupt function fires, set the counter to 0. In the A interrupt function, increment the counter by 1. When the index interrupt fires a second time, the counter should hold the resolution (# pulses per revolution). I’ve never done this, but it should work if I’m understanding things correctly.

            Edit:

            I just looked at the ordering information. You need to specify the CPR (cycles per revolution). 32 CPR = 128 PPR (pulses per revolution).

          • I set the pins and specified the CPR and the code uploads onto the Arduino but I do not get a reading from the encoder on the serial monitor when the shaft of the motor is rotating.

            I have assigned A to pin 3 (Interrupt pin), B to pin 7 (Digital pin), 5VDC to the 5V pin of the Arduino (Power pin), GND to GND pin of the Arduino (Power pin) and CPR to 32.

            The encoder’s leads for A, B, 5VDC, GND are connected to a 15-way d-type connector which in turn, is connected to the Arduino using wires that have been stripped.

            Kind regards,

            Nayan

          • That all sounds right, but did you change the first parameter of the attachInterrupt call to 1?

            I.e., on my Mega, I’m using interrupt 3 on pin 20. The UNO only has two interrupts: 0 on pin 2 and 1 on pin 3.

          • I get a reading from the encoder every time the shaft of the motor has turned 360 degrees.

            Is it possible to get a precise reading from the encoder for every movement the shaft makes up to 360 degrees?

            Kind regards,

            Nayan

          • Make sure you have the right wires connected to the right pins. It sounds like you connected the index to your interrupt instead of channel A.

          • I had the index pin connected to the interrupt instead of channel A, incorrect wiring.

            I changed the CPR to 1000 because this is the number that appears on the serial monitor when the shaft of the motor has turned 360 degrees clockwise.

            Is there a way where a can get a precise reading from the encoder to show on the serial monitor? For example, when the shaft of the motor has turned 200 degrees, the serial monitor shows 200 degrees aswell.

            Kind regards,

            Nayan

          • Glad to hear you got it working.

            At this point, it’s basically a math question. The counter should increment by 1 for each tick of precision — from 0 to 1000. 1000 ticks = 360 degrees, so you simply need to convert the number of ticks to degrees. Something like this should work:

            (counter / CPR) * 360

            For counter = 555, the result would be 199.8.

            You should also add logic that constrains the counter values. Start it at 0, keep incrementing by 1… when it reaches 1000, set it back to 0.

          • I am struggling to implement this into the code as I have inadequate experience in Arduino coding.

            Do I have to define the counter? Also, where do I implement the arithmetic?

            Kind regards,

            Nayan

          • Sorry for the late response, but work has been crazy and I just now saw this.

            So if you’ve implemented the code the way I have it, encoder0Position will contain the encoder’s current position in terms of the resolution. Assuming you set CPR to 1000, that means that the value of encoder0Position will always be a number between 0 and 999 inclusive immediately after the following line of code executes:


            encoder0Position = encoder0Position % CPR

            If I’m understanding you correctly, at that point you’d like to display the degrees in the serial output, right?

            If you insert this code after that line, it should do what you want:


            int degrees = encoder0Position / CPR * 360;
            Serial.print(degrees, DEC);

            This is going to display 0 after a complete revolution. If you want the output to print 360 instead of 0, add the new code before that line.

            If you do it this way, it will generate a lot of output. It’s an interesting exercise because you’ll be able to see the precision of the interrupts first-hand.

            If you only want to see the output when there’s a noticeable change, replace the line in loop() that says Serial.print(encoder0Position, DEC); with what I wrote above.

            As I said earlier… at this point it’s just math. You’re simply converting the absolute encoder position to its position in terms of angle in a 360 degree circle.

  1. Brenda
    I really enjoyed this article. I have a question. I teach a lab that I would like to use an Arduino to measure the postion/speed of the shaft of a DC motor. I have used POTs in the past but they bottom out (and break..ugh). I would like to use an encoder and your description seems good. US Digital has something (E4T series) or the one you suggest. Since I need about 10 of them cost is an issue. Do you have any guidance. The RPM could be as high as 800RPM but likely for the class it should be much lower.
    thanks

    • Glad you enjoyed it.

      You might be able to get away with something like this wheel encoder or this rotary encoder if you can figure out a reliable way to couple your motor to the knob. The one I used is a bit pricey because I really needed quadrature. Obviously, you’d have to check the specs to make sure it’ll accommodate the throughput you’re looking for… but the Arduino code would be similar.

      If it were me, I’d probably hit eBay to see if I could get better encoders for a discounted price. There’s not much that can go wrong with these little components so I think the risk of getting a dud would be pretty low.

  2. Great work. I’m a newbie, can you provide a wire diagram picture I’m not clear how the encoder color connect to Arduino.

    Thank you for any assistance

    • I’m not sure what you’re asking. Can you be more specific? How you connect the encoder to the Arduino depends on which encoder you’re using. The documentation that comes with your encoder should document the wiring for channel A, channel B, index, power and ground.

  3. Dear Brenda,

    I have almost the same encoder as you. A US Digital S1-512-B.
    Sadly i can not get anything out of your code 🙁 even though it looked so promising.
    I changed the CPR to 512 and the encoder pins to 2 and 4 to match my Arduino Uno.
    Pin 2 supports interrupts so this should work. But all i get in the serial monitor is “ready” once and that’s it.

    I am looking for a way to get a 1 on CW rotation and a 65 on CCW rotation to the computer.
    If you have an idea how i could get higher numbers on faster rotation that would be even better but i would be happy with 1 and 65 already.
    I am new to the world of micro controllers and would really appreciate some help.
    Thanks and best regards!
    Nina

    • Sorry for the late response…

      Looking at the doc, it should just work. However, take a look at the Arduino reference for attachInterrupt(). It’s possible you’re attaching to the wrong interrupt. Try using digitalPinToInterrupt(pin) to get the right interrupt number for your pin… then use that for the first arg to attachInterrupt().

      Sorry I can’t be of more help.

  4. Brenda,
    I came across several Lin Electric stepper motors (24 VDC) which have US Digital rotary encoders mounted on them. I wanted to use the encoder to measure the rotation of the shaft, much like you did in your article. I want to incorporate a program from SparkFun, which was included in the “Big Easy Hookup Guide” to rotate the stepper motor.The stepper motor is working fine with this program, but I’ve been unable to incorporate your program into this SparkFun one. I’m a novice at Arduino and I don’t know what to change in your program. I don’t think it should be very difficult, but I can’t get the program to compile with my modifications. I know you have much more important things to worry about, but I would appreciate any information you might have available to point me in the right direction. Thank you for your help!

    • Sorry for the late response… I don’t have any experience with stepper motors, so it would be impossible for me to answer your question without seeing the documentation on your device. If you can point me to a link, there’s a slim chance I *might* be able to figure it out if you still need help.

  5. Hi there,

    I’m a bit confused, is the second rotary encoder that is contained within the pattern box connected to the PCB not of any use? Is it not possible to just directly connect the arduino to the COP420 pin that connects to the LED and Reader?

    • Sorry for the late response. Work got in the way and I’m terribly behind on things.

      It’s been a while since I had the pattern box open, but I’m reasonably sure the encoder in the box is managing the position of the belt — not the needle bed. It *might* be possible to use the PCB in the box, but the ideal solution would completely eliminate all of the box components given their age and current failure rate.

Leave a Reply

Your email address will not be published. Required fields are marked *