Using Teensy for big MIDI control surface

ghostdzog1

Active member
Hello
I am interested in using teensy boards in my MIDI controller projects.

I need loads of inputs for internal interrupts, and when I say loads I mean loads (300)
I was wondering if it is possible to network several teensy boards together (so as to
allow the connection of many components that need internal interupts)
and then designate one of the teensy boards as master, the others as slaves
and then connect to a computer via a USB connector?

I am not a teensy expert, but was wondering how feasible it would be to
achieve this type of scenario.
Thanks
 
What are you interfacing to that needs so many interrupt inputs?
Hello
Thanks for replying to my post.

To answer your question, I want to use high definition endless rotary encoders and precision really is essential for this particular purpose.
I have tried rotary encoders connected using digital pins and using external interrupts
I found the precision is superior using the interrupts.

If you take a look at a pro (large and small) mixing consoles / desks, or perhaps the Euphonix system 5 control surface
that will give you some idea of what I would like to achieve

although perhaps a cut down version, even 24 channel version would be something.
 
You can daisy chain 8 MCP23017s off one i2c bus, giving 128 (interrupt capable) digital pins.

That would support 64 encoders (if you're not using switches).

And the Teensy 4 has 3 I2C busses, so you can support 192 encoders without having to do anything crazy.

You will also have plenty of other digital pins still available for more (especially with a T4.1).
 
You can daisy chain 8 MCP23017s off one i2c bus, giving 128 (interrupt capable) digital pins.

That would support 64 encoders (if you're not using switches).

And the Teensy 4 has 3 I2C busses, so you can support 192 encoders without having to do anything crazy.

You will also have plenty of other digital pins still available for more (especially with a T4.1).


Hi Kalikak
thanks very much for your advice
I will have to look into this, as it sounds like a solution
great
 
I want to use high definition endless rotary encoders

Perhaps a link to the datasheet for the parts in question? - better than trying to guess what "high definition"
might mean to you, 1000 PPR, 10000 PPR, or even more?

How many encoders are likely to be changing simultaneously too - that could be a thing to consider.
 
Perhaps a link to the datasheet for the parts in question? - better than trying to guess what "high definition"
might mean to you, 1000 PPR, 10000 PPR, or even more?

How many encoders are likely to be changing simultaneously too - that could be a thing to consider.

Hello Mark T
Thank you for replying to my thread.

I would ideally like to use interrupts instead of normal digital pins, as in practice, I have tried both, and the interrupt method gives
a really smooth precise performance compared with the normal method, which gives a fairly poor performance.

Presently using the encoders to control various features and functions in Cubase 12.
The digital pin method basically unusable.

So, if all it means is to add a couple of extra chips that gives an expansion
then why not

From a sound engineering / music producers perspective, people generally want to go for best performance in such areas.
Also, from a pro engineering perspective. if the encoders were used on a large format mixing console. It isn't that uncommon
in pro studios, for more than one engineer to be accessing several controls at the same time.

take sliding potentiometers for example
it isn't uncommon for one person to attenuate several sliding potentiometers at the same time, either literally or when included as a group
some of the more fancy large format mixing consoles allow for the user to incorporate several into a group, which then can be operated using one fader

when it comes to rotary encoders, there could be several all being twisted at the same time, even on a small format mixing console

anyway, if the cost of implementing a couple of expansion chips is only $20
then why not use interrupts if it gives a superior performance

The euphonix system 5, when released, used to cost an arm and a leg (in the tens of thousands, probably closer to $100,000)
so, $20 to include interrupts seems reasonable for a budget version of something similar but cut down
 
come to think about it, when using so many encoders (150+)
using interrupts is likely the most efficient with regards to memory usage, as the encoders only send data when their settings change
compared to the teensy having to constantly check the settings, imagine how memory intensive it would be using the latter method
if you have 150 plus encoders that need to be checked constantly

yep. i am sold on the interrupt method. more efficient.
 
Two questions:- How many Counts per revolution are you looking for? and what type of encoders?

For what I'm doing with Midi, most parameters are in the range 0-127 so 96 CPR covers the range with about 1.25 turns. 48 CPR - boring!
 
Two questions:- How many Counts per revolution are you looking for? and what type of encoders?

For what I'm doing with Midi, most parameters are in the range 0-127 so 96 CPR covers the range with about 1.25 turns. 48 CPR - boring!

I just know from practice, using Cubase 12. Both methods still output MIDI data
the second using interrupts gives a smooth easy to control performance

the first gives a really jumpy performance with the encoders that I currently am using
especially if using multiplex's or shift registers to connect the amount I want.

If the interrupt method is more data efficient than the digital pin method and I can implement the mcp23017s easy enough, then why not.

Perhaps you should try the interrupt method and compare.
Even if using Arduino Uno's, you have 2 interrupts which can be used to connect 1 rotary encoder, while connecting the same type of encoder using your normal method.

Just try and see.
 
Am using interrupts. Did need to tweak EncoderTool library to generate additional address bits for 74HCT138 for up to eight pairs of 4067s however uses more pins than the MCP23017 method. Either way is still a multiplexer.

As for the (mechanical) encoder itself, a batch of 50 had some duds, like 20% so is a good idea to run them in and test before comitting them to solder. Wise to design hardware so are easily replaceable.
 
Some thoughts:

- memory is a non-issue for this application
- interrupts are more efficient and more elegant to implement than polling (what you are calling the digital pin method, but both methods are digital)
- if you are seeing such a big difference though, check your code. I reckon a T4 can comfortably poll > 100 encoders in under a millisecond.
- with MCP23017s you can read all 16 bits with one call
- 4067s are analog switches, not an advantage for encoders, but necessary if you want to multiplex pots. However you have to deal with noise, settling time etc, so they can be more complex to deal with.

In the past I made a control surface for a Blofeld that had maybe 80 controls - a mixture of pots, encoders and switches. I used a T4 and 3 MCP23017s and 2 CD74HC4067s. All worked well and easily.
 
Some thoughts:

- memory is a non-issue for this application
- interrupts are more efficient and more elegant to implement than polling (what you are calling the digital pin method, but both methods are digital)
- if you are seeing such a big difference though, check your code. I reckon a T4 can comfortably poll > 100 encoders in under a millisecond.
- with MCP23017s you can read all 16 bits with one call
- 4067s are analog switches, not an advantage for encoders, but necessary if you want to multiplex pots. However you have to deal with noise, settling time etc, so they can be more complex to deal with.

In the past I made a control surface for a Blofeld that had maybe 80 controls - a mixture of pots, encoders and switches. I used a T4 and 3 MCP23017s and 2 CD74HC4067s. All worked well and easily.


Thanks for your advice and I can understand your perspective and opinion. it all sounds very interesting
but to be honest, when i say 150 rotary encoders, i mean the min is 150, and ideally a great deal more.

I don't understand why you have a problem with me using this latter form of methodology, as after all, what difference does it matter to any one else in the universe. I just see it as more efficient

as if you take a moment to consider, 150 plus encoders constantly polling is far less effiecient than one or two encoders sending interrupt messages every now and then

that's how i see it basically, interrupts logically sound miles more efficient long term than using constant polling
elegant or not
 
I don't understand why you have a problem with me using this latter form of methodology

I don't understand why you would think I do - I explicitly stated that interrupts are better and more elegant.

But from your comments it seems like you are new to this, so I tried to address some aspects that are relevant and understanding them should help you make a better solution.

But don't worry - I won't disturb you anymore.
 
Discussions about encoder readout strategies seem to always end up like windows vs. linux discussions :)

I'm the author of the EncoderTool library which, to avoid those discussions, supports both strategies :). As MatrixRat already mentioned, it also supports a few multiplexing methods out of the box (see https://github.com/luni64/EncoderTool/tree/master/extras) and can easily be adapted to support others like a MCP23017. I'd probably go for the SPI version which is faster.

To read out 150+ encoders I'd spend a dedicated T4.0 to do the readout. The additional cost will be marginal compared to the cost of the encoders, boards and mechanics. I personally would start with implementing a polled algorithm since it is more robust, but if you prefer, you can of course implement the readout interrupt based.The library doesn't really care. In any case the processor will be quite busy with this. But, since neither NXP nor PJRC will refund any money for not used processor cycles I wouldn't care. The EncoderTool also implements a callback mechanism which triggers on value changes and might come in handy for this amount of encoders.

@MatrixRat: Do you have anything online of your project? If you don't mind I'd like to add a link as usage example to it. Your extension to more than one 4067 might also be a nice example...
 
Last edited:
plus one for the SPI version: MCP23s17. also has two interrupts for each chip (two ports), and like I2C can be chained to eight in length. obviously nowhere near 150 encoders, I am running sixteen rotary encoders in polling mode on a single chain, and no glitches. the interrupt method was proving a little tricky to nail down.
 
plus one for the SPI version: MCP23s17. also has two interrupts for each chip (two ports), and like I2C can be chained to eight in length. obviously nowhere near 150 encoders, I am running sixteen rotary encoders in polling mode on a single chain, and no glitches. the interrupt method was proving a little tricky to nail down.

thanks to everyone for their advice
 
I don't understand why you would think I do - I explicitly stated that interrupts are better and more elegant.

But from your comments it seems like you are new to this, so I tried to address some aspects that are relevant and understanding them should help you make a better solution.

But don't worry - I won't disturb you anymore.

Hello Kalilikak
thanks for your reply, please don't be offended by my previous reply. I suffer from Asperger Syndrome, which does affect the way I communicate
I understand that at times I can come across as abrupt and offensive when I really do not mean to be.
I am just trying to work out the best solution for the problem I have at hand and am very grateful for any advice from you and others who are experienced in
this area, and in return, I would be happy to help people who help me in areas that I may have more experience in.
Thanks again for your advice
 
I'm using the Sparkfun 4067 breakouts and the actual chip is marked HP4067 which i think translates to 74HC4067. I see nothing preventing one doing the job of my HC138 so theoretically at least that's 256 encoders.

At the end of the day, it doesn't matter what method, just that you get the results you want and at my age, not really fussed about elegance.

@luni. My 32 enc 4067 version has been tested on a T4.1, used series R from T4 outputs to HCT245. Think it a good idea to add support for 23017.

My ears must have been burning earlier because I got out the board and camera. Will put something together.
 
Think it a good idea to add support for 23017.

Yes, I just looked through my stock of bought but never used parts and found a couple of 23S017. Starting to rain here. So, extending the EncoderTool might be a nice weekend project :)
 
Yes, I just looked through my stock of bought but never used parts and found a couple of 23S017. Starting to rain here. So, extending the EncoderTool might be a nice weekend project :)

Cool. I think there would be a fan base for such endeavours, perhaps even something you could get people to donate money to you in order to do so.
In a platform such as patreon.

I will order a couple of teensys and a few mcp23017s, already have a few multiplexes.
Will need to go through encoders to try and find the ones that i think are best for the purpose I need them for.
and buy some for the prototyping stage, write / test code before moving on to creating pcb's and building the end result.

If i get stumped, I may even see if i can pay someone to help complete this ambitious project.
 
Here an example how to use a MCP23S17 to read out encoders with the EncoderTool (working example here: https://github.com/luni64/EncoderTool/tree/master/examples/2_multiplexing/multiplexed_MCP23S17). If you want to use any kind of multiplexer all you need to do is to subclass the EncPlex base class and call its update(A,B) function whenever you have got new A/B values for one of the encoders. In this example the updates happen in a tick() function which you need to call as often as possible (loop() or better yield() is a good place to do so)

file: EncPlex23S17.h
Code:
#pragma once

#include "Adafruit_MCP23X17.h"  //  //https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library,  a bit slow, I'd look for a faster one
#include "EncoderTool.h"

namespace EncoderTool
{
    class EncPlex23S17 : public EncPlexBase  // The base class will take care of the bookkeeping and decoding 
    {
     public:
        inline EncPlex23S17(unsigned EncoderCount);  

        inline void begin(CountMode mode);
        inline void tick(); // call as often as possible

     protected:
        Adafruit_MCP23X17 mcp21S17;
        bool isSetup = false;
    };

    //================================================================================
    // INLINE IMPLEMENTATION

    EncPlex23S17::EncPlex23S17(unsigned encoderCount) // nothing to do but telling the base class the number of encoders it shall generate
        : EncPlexBase(encoderCount)
    {}

    void EncPlex23S17::begin(CountMode mode = CountMode::quarter)
    {
        EncPlexBase::begin(mode);                   // setup the base class
        mcp21S17.begin_SPI(10);                     // setup the Adafruit MCP21S17 interface: SPI, CS on pin 10
        for (unsigned i = 0; i < encoderCount; i++) // configure all needed pins as input. We start with A_0/B_0 up to the required number of pin pairs
        {
            mcp21S17.pinMode(i, INPUT_PULLUP);
            mcp21S17.pinMode(i + 8, INPUT_PULLUP);
        }
        isSetup = true;
    }

    void EncPlex23S17::tick() // call this as often as possible
    {
        if (isSetup)  // tick might be called from a timer or yield before begin was called -> Prevent accessing the muliplexer before it is setup
        {
            uint16_t data = mcp21S17.readGPIOAB();       // read the data from the 23S17 multiplexer
            for (unsigned i = 0; i < encoderCount; i++)  // for all configured encoders
            {                                            // extract the A/B
                unsigned A = (data & 1 << i) != 0;       //
                unsigned B = (data & 1 << (i + 8)) != 0; //
                int delta = encoders[i].update(A, B);    // the base class will take care of the decoding

                if (delta != 0 && callback != nullptr) callback(i, encoders[i].getValue(), delta); // if something changed, invoke the callback
            }
        }
    }
} // namespace EncoderTool

And here how to use it. The example uses a callback to print changes of the encoder. You can also directly access the encoders using the [] operator of the EncPlex23S17 class. E.g. encoders[3].getValue() would return the current value of the encoder attached to A_3 B_3 inputs of the 23S17 (See here https://github.com/luni64/EncoderTool/blob/master/Documentation/Reference.md#multiplexed-encoders for more information on using mulitplexed encoders)


Code:
/************************************************************
 *
 * Use a MCP23S17 multiplexer to read out up to 8 attached encoders
 * The A/B pins of the encoders go to the A/B inputs of the MCP
 *
 ************************************************************/

#include "EncPlex23S17.h"
#include "EncoderTool.h"

using namespace EncoderTool;

EncPlex23S17 encoder(8); // use all 8 (A/B) inputs of the 23S17 to connect encoders

// this will be called whenever one of the connected encoders changes
void onChange(byte ch, int value, int delta)
{
    Serial.printf("Encoder #: %d, value: %3d, delta: %2d\n", ch, value, delta);
}

void setup()
{
    digitalWriteFast(LED_BUILTIN, OUTPUT);

    encoder.begin();
    encoder.attachCallback(onChange); // attach a common callback for all encoders
}

void loop()
{
    encoder.tick();
}

To extend the code to more than one 23S17 you'd simply extend the tick function to loop over the used chips. The example uses the Adafruit_MCP23X17 library to read out the multiplexer. Looks like this library is a bit slow (takes ~30µs to read out the 16 pins). If you want to read out 150+ encoders with this chip, using a faster library might be a good idea.

I used this breakout:
https://www.amazon.de/-/en/MCP23017...efix=port+expander+mcp23s17+spi,aps,71&sr=8-1

Hope that helps...
 
Last edited:
Here an example how to use a MCP23S17 to read out encoders with the EncoderTool (working example here: https://github.com/luni64/EncoderTool/tree/master/examples/2_multiplexing/multiplexed_MCP23S17). If you want to use any kind of multiplexer all you need to do is to subclass the EncPlex base class and call its update(A,B) function whenever you have got new A/B values for one of the encoders. In this example the updates happen in a tick() function which you need to call as often as possible (loop() or better yield() is a good place to do so)

file: EncPlex23S17.h
Code:
#pragma once

#include "Adafruit_MCP23X17.h"  //  //https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library,  a bit slow, I'd look for a faster one
#include "EncoderTool.h"

namespace EncoderTool
{
    class EncPlex23S17 : public EncPlexBase  // The base class will take care of the bookkeeping and decoding 
    {
     public:
        inline EncPlex23S17(unsigned EncoderCount);  

        inline void begin(CountMode mode);
        inline void tick(); // call as often as possible

     protected:
        Adafruit_MCP23X17 mcp21S17;
        bool isSetup = false;
    };

    //================================================================================
    // INLINE IMPLEMENTATION

    EncPlex23S17::EncPlex23S17(unsigned encoderCount) // nothing to do but telling the base class the number of encoders it shall generate
        : EncPlexBase(encoderCount)
    {}

    void EncPlex23S17::begin(CountMode mode = CountMode::quarter)
    {
        EncPlexBase::begin(mode);                   // setup the base class
        mcp21S17.begin_SPI(10);                     // setup the Adafruit MCP21S17 interface: SPI, CS on pin 10
        for (unsigned i = 0; i < encoderCount; i++) // configure all needed pins as input. We start with A_0/B_0 up to the required number of pin pairs
        {
            mcp21S17.pinMode(i, INPUT_PULLUP);
            mcp21S17.pinMode(i + 8, INPUT_PULLUP);
        }
        isSetup = true;
    }

    void EncPlex23S17::tick() // call this as often as possible
    {
        if (isSetup)  // tick might be called from a timer or yield before begin was called -> Prevent accessing the muliplexer before it is setup
        {
            uint16_t data = mcp21S17.readGPIOAB();       // read the data from the 23S17 multiplexer
            for (unsigned i = 0; i < encoderCount; i++)  // for all configured encoders
            {                                            // extract the A/B
                unsigned A = (data & 1 << i) != 0;       //
                unsigned B = (data & 1 << (i + 8)) != 0; //
                int delta = encoders[i].update(A, B);    // the base class will take care of the decoding

                if (delta != 0 && callback != nullptr) callback(i, encoders[i].getValue(), delta); // if something changed, invoke the callback
            }
        }
    }
} // namespace EncoderTool

And here how to use it. The example uses a callback to print changes of the encoder. You can also directly access the encoders using the [] operator of the EncPlex23S17 class. E.g. encoders[3].getValue() would return the current value of the encoder attached to A_3 B_3 inputs of the 23S17 (See here https://github.com/luni64/EncoderTool/blob/master/Documentation/Reference.md#multiplexed-encoders for more information on using mulitplexed encoders)


Code:
/************************************************************
 *
 * Use a MCP23S17 multiplexer to read out up to 8 attached encoders
 * The A/B pins of the encoders go to the A/B inputs of the MCP
 *
 ************************************************************/

#include "EncPlex23S17.h"
#include "EncoderTool.h"

using namespace EncoderTool;

EncPlex23S17 encoder(8); // use all 8 (A/B) inputs of the 23S17 to connect encoders

// this will be called whenever one of the connected encoders changes
void onChange(byte ch, int value, int delta)
{
    Serial.printf("Encoder #: %d, value: %3d, delta: %2d\n", ch, value, delta);
}

void setup()
{
    digitalWriteFast(LED_BUILTIN, OUTPUT);

    encoder.begin();
    encoder.attachCallback(onChange); // attach a common callback for all encoders
}

void loop()
{
    encoder.tick();
}

To extend the code to more than one 23S17 you'd simply extend the tick function to loop over the used chips. The example uses the Adafruit_MCP23X17 library to read out the multiplexer. Looks like this library is a bit slow (takes ~30µs to read out the 16 pins). If you want to read out 150+ encoders with this chip, using a faster library might be a good idea.

I used this breakout:
https://www.amazon.de/-/en/MCP23017...efix=port+expander+mcp23s17+spi,aps,71&sr=8-1

Hope that helps...
Hello Luni
Thanks so much for posting such helpful / useful information.
I am presently in the learning phase of the project, learning how the tech works and how to implement it. So, very good timing.
I already own a handful of multiplex's, but do not own any 23017s, so may buy a few, just to try out and test. See what type of performance from them both.
If I can get the mux's to work to a high enough level of performance, perhaps i will stay with it.
Although think it would be worth trying out the mcp23017s, just to see.

Yes, many thanks for your help on this project
j
 
Back
Top