Sunday 7 December 2014

Continual Interruptions

This is the story of doing the job properly: tracking the rotation of rotary encoders using an interrupt-based approach. The story leads to updated versions of the m0xpd VFO code for AD9850 and Si5351 systems.

I have explained how a desire for simplicity kept the early designs published on this blog based on a simple "polled" rotary encoder architecture. In truth, there was a little more to it than just a "desire for simplicity"...



The Kanga / m0xpd DDS shield has a user-configurable pin assignment for the DDS module pins. However, it is biased towards the use of pins D2:D5 - these are the easiest pin allocations to make, as you see in the photo above. This will use up the pins D2 and D3 on the Arduino UNO which are the only two which support hardware interrupts. Accordingly, the systems which used the DDS shield tended to occupy pins D2 and D3, making interrupts inaccessible.

Ever since the appearance of the Dual DDS scheme and, specifically, its embodiment in the R(F)duino, I have intentionally freed up pins 2 and 3 for the Rotary encoder in my own builds, with the intention of - one day - moving to an interrupt-driven interface.



That day has arrived.

Its arrival was ushered in by two additional factors. First was the appearance of the Si5351, which sits on the I2C interface and completely frees up all the digital pins originally squandered on the DDS module. Second - and most important - was inspiration from Tom Hall, ak2b's beautiful receiver, which I saw on Tom's You Tube video. Correspondence with Tom finally persuaded me I HAD to act...

The problem with the original "polling" interface is one of visibility - the encoder is largely invisible to the code.

It is as if the encoder is obscured behind a brick wall, glimpsed only for an instant within the course of each execution of the main program loop, through a gap in the wall...


The code executing in the continuous "loop" function passes this "gap" in the wall only once per cycle and has to look for a change in the encoder's state since the last time the device was polled. It is seen that there is a rather precarious dependence upon the time it takes for the other tasks which the code has to perform in the one pass around the loop (and, hence the sample rate) and the speed of rotation. This explains my recent preoccupation with capacitors on the encoder output!

The code associated with this polling approach is not elegant, dealing - as it must - with the consequences of the two possible changes in observed state...


By contrast, the interrupt approach allows a complete brick wall between loop( ) and the encoder, but establishes a second process - the Interrupt Service Routine ('ISR')- to count the pulses from the encoder.

The ISR has 20:20 vision of the encoder.



In my implementation, I've arranged for the Interrupt Service Routine to maintain a variable, "Turns", which counts the number of "clicks" from a start time until reset. Clockwise rotation adds to the count, whilst counter-clockwise subtracts...


In the new code, the main program loop (which, in the user-friendly world of the Arduino, is called "loop( )") has vision of the count variable, "Turns", AND can reset it back to zero.

The frequency adjustment routine, equivalent to the one above, now becomes...


Notice that if more than one "Turn" event occurs in one pass through the loop( ) it is is now handled correctly, rather than just counted as one (or missed altogether). Accordingly, the frequency adjustment (etc) is very much smoother.

The new interface to the rotary encoder depends upon an Arduino library, Rotary, which can be downloaded from Przemek, sq9nje's Github repository.

I have used the new interrupt handling approach to produce a revised version of the Dual DDS VFO code and the Si5351 VFO Code, both of which can be found here. Reports, please, from anybody who tries them (I've tested "personal" versions of these new interrupt-driven programs with great results in my own rigs, including the Breadboard BITX and the Multi-band rig).

The same methods can be used by readers who wish to modify any of the previous m0xpd systems (such as the Occam's projects or the Kanga VFO systems) but this will require re-allocation of pins 2 and 3 which presently are assigned to the DDS - a simple enough task.

...-.- de m0xpd

3 comments:

  1. Thank you for beating me to it. I have been thinking seriously about ging to interupts, but now you have done all the hard work and I can relax!

    After a quick look at the start of the code to see what I would have to do to merge your work with mine, I would like to suggest a small mod:
    // Rotary Encoder...
    const int RotEncAPin = 2;
    const int RotEncBPin = 3;
    const int RotEncSwPin =4;
    // Pushbuttons...
    const int modeSw1 = 5;
    const int modeSw3 = 6;

    This allows a single 6-cond cable between the encoder and the Arduino, 5 cond into a single plug, with a short pig-tail to a gnd pin.

    More when I get a chance to work with the code. Time for some XMAS shopping right now.
    Fred LaPlante, WA1DLZ

    ReplyDelete
  2. Now we're talkig!!! Tuning is just great with the interupts. No more 'chatter' in the digits display as you tune. Slow tuning was always annoying and sometimes went backward - no more, tuning now follows my intent.

    Now if I can just find how I lost the frequency digit cursor!

    Thanks for continuing to enhance a great product.
    Fred LaPlante, WA1DLZ

    ReplyDelete
  3. Nobody seemed to know where the 'Rotary' library came from but after looking in the .cpp file I found that it came from Ben Buxton, who described its intricacies on this page:
    http://www.buxtronix.net/2011/10/rotary-encoders-done-properly.html
    Ben implies that it works well with polling. I haven't tried this yet. Polling might be the preferred method on Raspberry Pi's and Beaglebone Blacks.

    Tom, ak2b

    ReplyDelete