I am creating an audio sampler/sequencer with Teensy 4.1 and I am running out of pins, so instead continuing adding multiplexers, I have thought about using an inexpensive ATMEGA4809 (the Arduino Every microchip) to outsource some simple tasks. Specifically I have thought about using it for reading about 16 buttons and controlling 12 LEDs.
The idea is that the ATMEGA4809 will be permanently scanning if any button has been pushed and, if so, communicate it to the Teensy. On the other hand, the Teensy will send data to the ATMEGA4809 indicating that a specific LED has to be turned on or off. I have thought that liberating the Teensy of permanently scanning those buttons would free some processing power, too.


My first doubt was if it would be better to use I2C or SPI. I know SPI is faster, but on the other hand it takes more pins, is a bit more complicated to implement and is more prone to noise issues. And given the small amount of information that I have to transmit, I don't think that the difference in speed will be relevant. So I think it would be more appropriate to use I2C.

My main concern is how to implement the communication without causing any issue with the main task of the application, i.e. the audio stuff, and, at the same time without too much latency in the readings.

Ideally, I would like to send and receive messages only when new data is available, but I have seen that this behaviour is only possible for the direction Master --> Slave. Nevertheless, for getting data sent by the Slave, the Master must explicitly request it.

My main doubt is, is it a good idea to call the Wire.requestFrom() in every loop iteration so that the data is fetched as soon as possible, or would it be a better idea to do the call every X milliseconds? I have read that Wire.read() is a blocking operation, so could it mess up the timing in the audio?

Something that I have noticed is that after calling Wire.requestFrom(), Wire.available() seems to return true (and reads 0) even when the slave haven't written anything.


I have tested the following code using an Arduino Pro Micro instead of the ATMEGA4809 and it seems to work well, but I don't know how well it would scale. In this example I read the state of 4 buttons in the Teensy (Master) and another 4 buttons in the Arduino (Slave).
When a button is pushed in the Teensy (Master) it sends a new byte with the number of the button.
When a button is pushed in the Arduino (Slave), as it has to wait for the Master to make request, I am storing the pushed button in an array and sending their numbers in the following requests until there are no more in the array. The array is acting as a queue. I would try changing it for a CircularBuffer in case I continue with this approach.

Master - Teensy 4.1
Code:
/*
  Board: Teensy 4.1
  I2C Master
  SDA: 18
  SCL: 19
 */

#include <Wire.h>
#include <Bounce.h>

#define BTN_1_PIN 25  
#define BTN_2_PIN 26
#define BTN_3_PIN 27
#define BTN_4_PIN 28
#define ADDRESS 8

Bounce teensyBtn1 = Bounce(BTN_1_PIN, 20);
Bounce teensyBtn2 = Bounce(BTN_2_PIN, 20);
Bounce teensyBtn3 = Bounce(BTN_3_PIN, 20);
Bounce teensyBtn4 = Bounce(BTN_4_PIN, 20);
Bounce *teensyBtnArray[4] = {&teensyBtn1, &teensyBtn2, &teensyBtn3, &teensyBtn4};

void setup() {
  Serial.begin(9600);
  
  pinMode(BTN_1_PIN, INPUT_PULLUP);
  pinMode(BTN_2_PIN, INPUT_PULLUP);
  pinMode(BTN_3_PIN, INPUT_PULLUP);
  pinMode(BTN_4_PIN, INPUT_PULLUP);
  
  Wire.begin();
}

void loop() {

  // Check if there are buttons clicked
  for(byte i=0; i<4; i++) {
    teensyBtnArray[i]->update();
    if (teensyBtnArray[i]->fallingEdge()) {
      // Send button number to Arduino
      Wire.beginTransmission(ADDRESS);
      Wire.write(i + 1);
      Wire.endTransmission();
    }
  }

  // Request new data from Arduino
  Wire.requestFrom(ADDRESS, 1);
  
  // Read data sent from Arduino
  while (Wire.available()) { 
    int x = Wire.read();
    if (x > 0) {
      Serial.print("Clicked button ");
      Serial.print(x);
      Serial.println(" on Arduino.");
    }
  }
}

Slave - Arduino Pro Micro
Code:
/*
  I2C Slave
  
  Board: Arduino Pro Micro (generic brand)
  Choose --> Tools / Board / Arduino Leonardo

  Conn  | Pro Micro | Teensy
  ---------------------------
  SDA   | pin 2     | pin 18  (pullup resistor 2K2-4K7 to 3.3V)
  SCL   | pin 3     | pin 19  (pullup resistor 2K2-4K7 to 3.3V)
  GND   | GND       | GND
 */

#include <Wire.h>
#include "Bounce.h"

#define BTN_1_PIN 10  
#define BTN_2_PIN 16
#define BTN_3_PIN 14  
#define BTN_4_PIN 15
#define ADDRESS 8

Bounce arduinoBtn1 = Bounce(BTN_1_PIN, 20);
Bounce arduinoBtn2 = Bounce(BTN_2_PIN, 20);
Bounce arduinoBtn3 = Bounce(BTN_3_PIN, 20);
Bounce arduinoBtn4 = Bounce(BTN_4_PIN, 20);
Bounce *arduinoBtnArray[4] = {&arduinoBtn1, &arduinoBtn2, &arduinoBtn3, &arduinoBtn4};

// Array for storing clicked buttons
bool buttonClicked[4] = {false, false, false, false};

void setup(){
  Serial.begin(9600);
  
  pinMode(BTN_1_PIN, INPUT_PULLUP);
  pinMode(BTN_2_PIN, INPUT_PULLUP);
  pinMode(BTN_3_PIN, INPUT_PULLUP);
  pinMode(BTN_4_PIN, INPUT_PULLUP);

  Wire.begin(ADDRESS);
  Wire.onRequest(requestFromTeensy);
  Wire.onReceive(receiveFromTeensy); 
}

void loop() {
  // Check if there are buttons clicked
  for(byte i=0; i<4; i++) {
    arduinoBtnArray[i]->update();
    if (arduinoBtnArray[i]->fallingEdge()) {
      // Store flag in array
      buttonClicked[i] = true;
    }
  }
}

void requestFromTeensy() {
  // Check array of clicked buttons
  for(byte i=0; i<4; i++) {
    if (buttonClicked[i]) {
      // Send button number to Teensy
      Wire.write(i + 1);
      buttonClicked[i] = false;
      break;
    }
  }
}

void receiveFromTeensy() {
  // Read data sent from Teensy
  while (Wire.available()) {
    int x = Wire.read();
    Serial.print("Clicked button ");
    Serial.print(x);
    Serial.println(" on Teensy.");
  }
}
Any recommendations for enhancing the code above?

Thanks.