Incremental rotary encoders (Part 8)

Part 1234567, 8, 9, 10

Recent works on human interfaces lead me to improve PlainENC library which is dedicated to the drive of rotary encoders. PlainENC features three particular new aspects:

  • Rotary encoder switches trigger interrupts
  • The sensing pattern is changed in order to improve the reliability of count up and down
  • Any type of encoder can be used: one or two detent (click) per pulse

As before, the encoder can be connected to any pin belonging to one single port. The only change in the initialization function deals with the encoder type: one or two clicks per pulse

void InitializeENC(uint8_t swA, uint8_t swB, uint8_t swPb, volatile uint8_t *port, uint8_t detentsPerPulse);

The detentsPerPulse argument can take any of the following values

#define ENC_ONE_DET_PER_PULSE 0x01
#define ENC_TWO_DET_PER_PULSE 0x02

As the detection of pulses requires multiple operations, the code inside the  interrupt service is pretty long,  so as to say, longer than usually expected. So that this code starts with few line which disable the interrupts while computing the encoder pattern.

	/* Record register content and disable interrupt */
	uint8_t PCICRRegContent = PCICR;
	PCICR = 0x00;

and restores interrupt on completion of the computing code

	/* Resume interrupt */
	PCICR = PCICRRegContent;

The use of both types of encoders (one or two clicks per pulse) lead me to adopt a pattern recognition. How does this work?

Each switch (typically named A and B) is given a weight (1 and 2). So that the sum of the values that they represent can be written in this way, assuming that the sum is computed when a change of state has occurred while turning the knob of the encoder clockwise

    0  2   3   1    0    2   3   1    0
	________	  ________
       |        |        |        |
2  ____|        |________|        |________
             ________	       ________
            |        |        |        |
1  _________|        |________|        |_>__

 

Recording the switching pattern consists in recording the sequence of sums as computed before. This switching pattern results in:

    0  2   3   1    0    2   3   1    0
	________	  ________
       |        |        |        |
2  ____|        |________|        |________
             ________	       ________
            |        |        |        |
1  _________|        |________|        |___
       nc  11   13   4   2   11   13   4

Here are some more explanations:

On the first rise of the signal from the switch which weight is 2, we record the sum which is 2. On the next change of state (rise of the signal from the switch which weight is 1), we firstly shift the previous sum of two bits (in other words, we << the sum two times) and we add (using the “or” function) the new sum of weights which is 3.  The sequence expressed in binary and decimal can also be written:

  • Initialize variable: grand total = b0 (d0)
  • First sum of weight: grand total = b10  (d2)
  • Switch 2 bits: grand total = b1000 (d8)
  • Add sum of weight: grand total = b1011 (d11)

The same principle applies while turning the knob counter-clockwise:

       0   1   3    2   0    1   3    2   1
	        ________	  ________
               |        |        |        |
2  ____________|        |________|        |_______
            ________	      ________
           |        |        |        |
1  ________|        |________|        |___________
          nc   7   14   8    1   7   14

You surely noticed that each combination results in a unique grand total, in other words each switching pattern is unique. We use this property for sensing cw and ccw rotations, as well as actual switch states. This results in the following lines of code for encoders having two detents per pulse.

if ((_bufferedEncoderStates == 0x04) || (_bufferedEncoderStates == 0x0B)) {
	stepValue = 1;
} else if ((_bufferedEncoderStates == 0x08) || (_bufferedEncoderStates == 0x07)) {
	stepValue = -1;
}

The same principle applies again for encoders having one detent per pulse. The only change is the number of events which are captured. The grand total is now made of the records of 4 consecutive changes of switch states.

Here is a full picture of the code which is run inside the interrupt service:

	static uint8_t _lastEncoderState = 0xFF;
	static uint8_t _bufferedEncoderStates = 0x00;
	static uint32_t _lastTime;
	/* Record register content and disable interrupt */
	uint8_t PCICRRegContent = PCICR;
	PCICR = 0x00;
	/* Read state of switch A & B */
	uint8_t stateSwA = ((PIN(*_encPort) >> _swA) & 0x01);
	uint8_t stateSwB = ((PIN(*_encPort) >> _swB) & 0x01);
	/* Compute encoder state */
	uint8_t encoderState = (stateSwA | (stateSwB << 1));
	if (encoderState != _lastEncoderState) {
		_bufferedEncoderStates <<= 2; /* Shift previous state (2 bits) */
		_bufferedEncoderStates |= encoderState; /* Add current state */
		int16_t stepValue = 0;
		if (_detentsPerPulse == 1) {
			_bufferedEncoderStates &= 0x3F; /* Mask states of interest */
			if (_bufferedEncoderStates == 0x34) {
				stepValue = 1;
			} else if (_bufferedEncoderStates == 0x38) {
				stepValue = -1;
			}
		} else {
			_bufferedEncoderStates &= 0x0F; /* Mask states of interest */
			if ((_bufferedEncoderStates == 0x04) || (_bufferedEncoderStates == 0x0B)) {
				stepValue = 1;
			} else if ((_bufferedEncoderStates == 0x08) || (_bufferedEncoderStates == 0x07)) {
				stepValue = -1;
			}
		}
		if (stepValue != 0) {
			uint32_t _now = millis();
			uint32_t elapsedTime = (_now - _lastTime);
			if (elapsedTime < _encBoostCutOff) {
				stepValue *= (1 + (((_encBoostFactor - 1) * (_encBoostCutOff - elapsedTime)) / _encBoostCutOff));
			}
			_lastTime = _now;
		}
		_encCounts += stepValue;
		if (_encCounts < _encMinCounts) {
			_encCounts = _encMinCounts;
		} else if (_encCounts > _encMaxCounts) {
			_encCounts = _encMaxCounts;
		}
		_lastEncoderState = encoderState;
	}
	/* Resume interrupt */
	PCICR = PCICRRegContent;

 

One of the advantages of this method is the high rejection level of false signals, mainly generated by switch bounces. After multiple tests on various encoders, I managed to run all of them without filtering components, which are recommend by some vendors. The library still contains some fancy and specific functions such as the booster function which takes into account the rotational speed of the knob for amplifying the counter steps.

Next post on same subject

 

9 Comments

  1. LeandroM says:

    Mr Didier, me again… I’m really beginning to feel clingy so please tell me straight out if you are going to share with me the libraries… I can understand any kind of apprehension you might have, so just a something in any direction because I’ve been waiting for your plainENC to re-write the whole code of my guitar…
    About my question on talking some more about the minimal of human interface, did it get you to any points?
    good day

    • Didier says:

      Summer time ! Thanks for your patience. I will get to you soon.
      You did not suggest a simple application which would feature an encoder and a 16×2 LCD. Any hint ?

  2. LeandroM says:

    With the approach you describe on the human interface post, I would think about a list you develop as large you need with a “up-a-level” option always at the bottom, and with 3 basic levels, as I think a simple app wont need more. So, it is basically what you describe, but I would add double-click availability on each bottom-option to allow to change the values of the on screen selected option, so you turn clockwise or counter without leaving the option and you can either change the way the option behaves, or the values of the variable the option relates to. I work with the RGB encoders, so I recommend also a color feedback allowing you to know in what mode you are, as you are standing on the option of the menus. That’s it.
    About the other matter, the library is shared through the blog itself with a password?, or do you send it through e-mail?
    greets!

  3. Didier says:

    I spent quite some time lately on reviewing and improving some MicroXXX type applications. MicroBLINK (Really simple blink control), MicroHTR (heater control, featuring PID, PWM control) and MicroCLK (improved version of the already published clock application) benefit from these works. I plan to publish posts on this matter along the next weeks. These publication have been given the “box” suffix”, as they can now be boxed in an enclosure for autonomous operations.
    You can use the libraries that I send as much as you want without password as long as you comply to the related licence (mostly GNU GPL v3) as mentioned in the source code files.

  4. LeandroM says:

    Oh, yes, it is Ok, I really aint profiting from this work , and if I were, I’d really like to share any of it.
    But sorry, you havent send me any library, and I have sent two requests from leamucho@gmail.com.
    What else do I have to do?

    • Didier says:

      I will release the libraries as soon as the post will be published.

      • LeandroM says:

        ummm, but the PlainENC is already available? I think by now, I only need that one… thanks

        greetings!

        • Didier says:

          The thing is that I updated this library which is now PlainENCi (“i” for interrupted). On the other hand, I always check the code that I send away, including the compatibility of “Plain” libraries. In the case of this new series of post, up to six libraries may coexist in the same application (e.g. MicroHTRbox). That’s quite some work, and I take advantage of the preparation or the posts to carefully test my code. It’s my way of seeing things. Sorry, I do not know an alternative to the job well done 😉

          • LeandroM says:

            Ohhhhh “caramba”!, I really didnt understand… ok, I guess Iam right now not of any help, but if I can assist you, please tell me 😉

Leave a Reply

You must be logged in to post a comment.