The 3 inputs are incrementing through the 8 different combinations constantly.
Yes. Put another way, each combination selects a different group of seven buttons. There being eight groups of seven buttons, means 7×8 = 56 possible buttons.
One of the 7 inputs on the MCU gets pulled down and, in code, matches which pin is pulled down with the output that happened at the same time,
Yes. Put another way, if any of the 7 inputs is low, it means the corresponding button in the currently selected group is being pressed. Otherwise the buttons in this group are not being pressed.
ideally using interrupts so the check doesn't mess up
The check needs to be often enough, at least a few times each second, so that a quick peck on the key is not missed.
One way is to use a timer interrupt to update the key states independently of your main code, so that the main code can simply check if there are any pending key press events. The simplest possible one is a matrix of button states, say an eight-byte array where each bit reflects the button state, so the main code can simply check each bit to find if a key is being pressed or not, but this way it is too easy to miss a keypress. A much better option is to have the interrupt keep track of the states internally, and post button press events in a simple circular queue of bytes. (I showed an example of one for RP2040
here with 32-bit words, but it is trivially modified for 8-bit bytes on ESP32.)
The other way is to check each group of seven buttons per main loop iteration. You still maintain an array of button states, so you can determine if a low indicates a new button press, or whether it was already handled; or whether it is time to autorepeat the press because it has been kept pressed for long enough.
Generally, the timer interrupt is easier, because it occurs at regular intervals, so instead of measuring actual time elapsed, you can use the number of timer interrupts as a measurement of time. For example, that when a button press is initially detected, for
N interrupts its state is not checked and is only assumed to be pressed, to ignore button bounce. Then, if it is still being pressed, and
M interrupts pass, it is time to do the first autorepeat. After each autorepeat, every
K interrupts of the button being still pressed, emit a new autorepeat event.
Your main code then simply looks at the key press queue whenever it has time, and processes the press events when it has time. This means no presses are missed, if the interrupt occurs often enough. A rate of 1000 to 10000 interrupts per second would be best, leading to each button checked once every 8ms to 0.8ms, depending on the interrupt rate.
If the main loop iterations vary in duration/interval, you have to use a timer like
millis() or
micros() to measure the duration between iterations to measure time. Otherwise your dead interval (
N) varies, and autorepeat repeats at varying intervals, making for really weird-feeling button interface.
Others disagree, of course, and there indeed are other kinds of ways to do this.
My point is that doing it right with a regular (timer) interrupt is easy, and have shown example code for an event queue and how simple it is –– in a polymorphic C form, which allows any number of such queues in a firmware in C. In practice, the keyboard event queue does not need to be that complicated or that large, something like 16 or 32 bytes (key press events) should suffice. For the internal state (tracking the keys), you need at least a byte per button, or 56 or 64 bytes. I would use a 32-bit word per button, though. In any case, the entire static RAM use is something like 96 bytes to 288 bytes for the event queue approach, giving you button autorepeat and no missed keypresses, as long as the keyboard scanning interrupt runs often enough so it won't miss quick pecks.
Plus, for the programmer, the main loop code then changes from "check the buttons" to "if there is a queued button press, do the corresponding action", which ends up being easier to design around and implement, anyway. You might even use an array of function pointers, one per possible queued event.
If you want, I can show you example code of how that might look like. I don't have an ESP32 dev environment set up, so it would not be
exact, only a general pattern.