New Teensy 4.1 DIY Synthesizer

Can I ask about the screen do you feel it’s a much option and advise it going forward for others?

Also the encoder work you’ve done is that part of a common library or just custom code? I liked the acceleration option very nice.
 
The 3.5-inch Touch Display is a Waveshare ST7796S. It's high quality and has a built-in frame with a protective glass cover. This makes it very suitable for installation in devices.

Screenshot 2026-03-09 185742.png


Link : https://www.amazon.de/Waveshare-3-5...pcontext&ref_=fplfs&psc=1&smid=A3U321I9X7C9XA

The encoder query is handled by an MSP23017 chip and Adafruit_MCP23X17.h Lib.
I'm using the Teensy 4.1's I2C bus for control. I wrote a custom function to process the encoder data.

Link : https://forum.pjrc.com/index.php?threads/new-teensy-4-1-diy-synthesizer.63255/post-366769
 
The 3.5-inch Touch Display is a Waveshare ST7796S. It's high quality and has a built-in frame with a protective glass cover. This makes it very suitable for installation in devices.

View attachment 39121

Link : https://www.amazon.de/Waveshare-3-5...pcontext&ref_=fplfs&psc=1&smid=A3U321I9X7C9XA

The encoder query is handled by an MSP23017 chip and Adafruit_MCP23X17.h Lib.
I'm using the Teensy 4.1's I2C bus for control. I wrote a custom function to process the encoder data.

Link : https://forum.pjrc.com/index.php?threads/new-teensy-4-1-diy-synthesizer.63255/post-366769
Thanks for that. I have a few displays I bought from various sites currently doesn’t look to bad on those but I do like the mounting options on those.

I did have encoders in the smaller version of my code so will revert back to look in to what I used and update where needed.

Thanks for posting
 
The trick with my encoder query is that I trigger a hardware interrupt on the Teensy4.1 with every change and then read the 16-bit GPIO data register from the MCP23017 only once. From this, I can extract the encoder number and direction of movement. This reduces access to the slow I2C bus (400 kHz).

C:
#include <Arduino.h>
#include <Adafruit_MCP23X17.h>

// Encoders variable
volatile uint16_t current_GPIO1 = 0;
uint16_t old_GPIO1 = 0;

// Encoder values  for 10x Menu Pages with 7x Encoder
int16_t Enc_Pos[10][7] = {
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0}};

// U10 MCP23017 Encoder Pin
#define enc1_A 256      // GPB0 
#define enc1_B 512      // GPB1
#define enc2_A 1024     // GPB2
#define enc2_B 2048     // GPB3
#define enc3_A 4096     // GPB4
#define enc3_B 8192     // GPB5
#define enc4_A 16384    // GPB6
#define enc4_B 32768    // GPA7
#define enc5_A 128      // GPA0
#define enc5_B 64       // GPA1
#define enc6_A 32       // GPA2
#define enc6_B 16       // GPA3
#define enc7_A 8        // GPA4
#define enc7_B 4        // GPA5
#define on 1
#define off 0

uint16_t currentClkState[7] = {0,0,0,0,0,0,0};
uint8_t Enc_B[7] = {0,0,0,0,0,0,0};
uint16_t lastClkState[7] = {0,0,0,0,0,0,0};
unsigned long lastInterruptTime = 0;
int accelerationFactor = 1;


// Encoder interrupt ----------------------------------------------
void Encoder_Interrupt()
{
    enc_action = true;
    current_GPIO1 = mcp1.readGPIOAB();
}


// read Encoder ------------------------------------------------
void read_encoder(void)
{

    if (enc_action)
    {
        uint8_t encID = 0;
        unsigned long interruptTime = millis();
        unsigned long timeDiff = interruptTime - lastInterruptTime;

        enc_action = false;

        // Encoder Acceleration
        // If the rotation is fast (< 50 ms), accelerate
        if (timeDiff < 50)
        {
            accelerationFactor = 3;
        }
        else
        {
            accelerationFactor = 1;
        }

        // Check MCP23017 GPIO for activity
        uint16_t changesBits = old_GPIO1 ^ current_GPIO1;

        // Encoder1 Clk Pin status
        if (changesBits == enc1_A)
        {
            currentClkState[0] = bitRead(current_GPIO1, 8);
            encID = 0;
        } // Encode1 DT Pin status
        else if (changesBits == enc1_B)
        {
            Enc_B[0] = bitRead(current_GPIO1, 9);
            encID = 0;
        }
        // Encoder1 Clk Pin status
        else if (changesBits == enc2_A)
        {
            currentClkState[1] = bitRead(current_GPIO1, 10);
            encID = 1;
        } // Encode2 DT Pin status
        else if (changesBits == enc2_B)
        {
            Enc_B[1] = bitRead(current_GPIO1, 11);
            encID = 1;
        }
        // Encoder3 Clk Pin status
        else if (changesBits == enc3_A)
        {
            currentClkState[2] = bitRead(current_GPIO1, 12);
            encID = 2;
        } // Encode3 DT Pin status
        else if (changesBits == enc3_B)
        {
            Enc_B[2] = bitRead(current_GPIO1, 13);
            encID = 2;
        }
        // Encoder4 Clk Pin status
        else if (changesBits == enc4_A)
        {
            currentClkState[3] = bitRead(current_GPIO1, 14);
            encID = 3;
        } // Encode4 DT Pin status
        else if (changesBits == enc4_B)
        {
            Enc_B[3] = bitRead(current_GPIO1, 15);
            encID = 3;
        }
        // Encoder5 Clk Pin status
        else if (changesBits == enc5_A)
        {
            currentClkState[4] = bitRead(current_GPIO1, 7);
            encID = 4;
        } // Encode5 DT Pin status
        else if (changesBits == enc5_B)
        {
            Enc_B[4] = bitRead(current_GPIO1, 6);
            encID = 4;
        }

        // save encoder values
        if (currentClkState[encID] != lastClkState[encID])
        {
            if (Enc_B[encID] != currentClkState[encID])
            {
                Enc_Pos[Menu_page][encID] += accelerationFactor;
                if (Enc_Pos[Menu_page][encID] >= 127)
                {
                    Enc_Pos[Menu_page][encID] = 127;
                }
            }
            else
            {
                Enc_Pos[Menu_page][encID] -= accelerationFactor;
                if (Enc_Pos[Menu_page][encID] <= 0)
                {
                    Enc_Pos[Menu_page][encID] = 0;
                }
            }

            // Menu page ---------
            if (Menu_page == 1)
            {
                if (encID == 0)
                {
                    tft.clearChangedArea();
                    tft.setFont(OpenSans_14);
                    tft.setTextColor(ST7735_WHITE);
                    tft.setCursor(50,50);
                    tft.fillRect(50,50,40,12,ST7735_RED);
                    tft.print(Enc_Pos[1][0]); // Encoder 1 value
                    tft.updateScreenAsync(false, true, true);

                }
                else if (encID == 1)
                {
                    cur_env1_attack = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 2)
                {
                    cur_env1_decay = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 3)
                {
                    cur_env1_sustain = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 4)
                {
                    cur_env1_release = Enc_Pos[Menu_page][encID];
                }

            }
         
            if (Menu_page == 4)
            {

                if (encID == 0)
                {
                    cur_env1_delay = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 1)
                {
                    cur_env1_attack = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 2)
                {
                    cur_env1_decay = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 3)
                {
                    cur_env1_sustain = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 4)
                {
                    cur_env1_release = Enc_Pos[Menu_page][encID];
                }
                boolean asyncUpdate = true;
                draw_envelope_line(asyncUpdate, curve, cur_env1_delay, cur_env1_attack, cur_env1_decay,
                    cur_env1_sustain, cur_env1_release, ST7735_GREEN);
            }
        }
        lastClkState[encID] = currentClkState[encID];
        old_GPIO1 = current_GPIO1;
        lastInterruptTime = interruptTime;
    }


// Setup -------------------------------------------------------
void setup(void)
{
    Serial.begin(9600);

// init Key/Encoder IO Expander
    mcp1.begin_I2C(0x22, &Wire1);
    // init Wire1 for Encoders
    Wire1.begin();
    Wire1.setClock(400000UL); // I2C speed 400KHz

    // init MPC23017Expander for Encoders
    mcp1.setupInterrupts(true, false, LOW);
    for (size_t i = 0; i < 16; i++)
    {
        mcp1.pinMode(i, INPUT_PULLUP);
        mcp1.setupInterruptPin(i, CHANGE);
       
    }
    mcp1.getCapturedInterrupt();
    mcp1.readGPIOAB();
    mcp1.clearInterrupts();
    delay(10);

    // init Encoder Interrupt
    attachInterrupt(digitalPinToInterrupt(PIN_ENC_INT), Encoder_Interrupt, FALLING);
}

void loop()
{
    
        read_encoder();    
   
}

}

I'm not curious at all ;) But what are you currently developing :unsure:
 
Last edited:
I'm not curious at all ;) But what are you currently developing :unsure:
Moving back on to the hardware for my clone of a clone

Started of simple, grew arms and legs or voices and filters. But nailed the supersaw and got some good filters. Ported on to my MicroDexed for now as that’s a solid build.

It’s now possible to load presets from the hardware, working on the final tweaks of that though.

Next sliders and pots (which I have done many times and failed. But that was my soldering

https://forum.pjrc.com/index.php?threads/jteensy8000-clone-of-a-clone-with-extra-filters.77776/
 
This is my first version of Jeannie - Polyphonic DIY Synthsizer. The case for Jeannie are made by Erica Synths

Link : https://github.com/rolfdegen/Jeannie-Open-source-Synthesizer
I seen it and you’ve done a lot of work. Well done it’s great to see. I take it you’ll prototype and then go for another kit?

This one for me will be a one off I think as the restrictions have been hard, hence the left over extras. But really surprised at what can be achieved
 
Back
Top