Confused on button matrix design - Page 1
Forum section: electronics / beginners

author: VagrantBungler
author_rank: Newbie
timestamp: January 27, 2024, 11:20:03 am
content: Hi,I recently purchased a Cardputer from M5 Stack which is a card-sized ESP32 with a screen and keyboard. It's on its way and I'm quite excited. One might say it's quite the bobby dazzler lol. I was looking at the schematic and noticed it used a lot of pins for the keyboard and I was a bit bummed cause I wanted to use those pins. I wanted to see why they didn't just opt for an Atmega to the button matrix and then send the data via I2C.The design uses a (74HC138) 3 to 8 decoder and somehow uses 10 pins instead of 18. I'm assuming they did this to reduce cost and keep the pin usage for the keyboard low, but I was at a bit of a loss as to how it worked. I took to the internet to see if someone had a video or forum post on a similar design, but didn't find much. But I did find some videos of people using 3 to 8 decoders as 8 to-1 multiplexers on YouTube.Well, I'm not sure if this is the case for this design. I understand the button layout, but I don't get how you would use a decoder IC backward (I definitely don't understand how that would work). The IC is also in its enable state (with E3 high and E1 and E2 low), which I also don't quite understand what this means concerning how it interacts with the MCU.Can someone explain how this design for a button matrix works?Cardputer documentation from M5:https://docs.m5stack.com/en/core/CardputerCardputer schematic:https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/products/core/Cardputer/Sch_M5Cardputer.pdf74HC138 datasheet:https://www.diodes.com/assets/Datasheets/74HC138.pdfthis is my first question here. hopefully, more to come!

author: SeanB
author_rank: Super Contributor
timestamp: January 27, 2024, 12:19:33 pm
content: Very simple. The microcontroller sets all ports G3 to G15 attached to the keyboard as inputs, but with a weak pull up resistor inside the MCU. Then it simply counts up in binary on G8,9,11 and looks at the inputs to see if any are low, as the decoder is active low. Remember the bit that is low, scan the rest of the 3 bit address. Repeat, and after a few scans, to debounce, you get a valid X and Y value for the key that is pressed, and then the MCU looks up the keymap to see which key is pressed, and passes to the application, generally after the key is seen as being released. Generally has a routine that scans the keyboard a few dozen times a second, and exits with a key pressed and value of key, or exits with a null response, no key pressed.

author: Nominal Animal
author_rank: Super Contributor
timestamp: January 27, 2024, 01:02:50 pm
content: 74HC138 has eight outputs. Seven of them are high, and one low. The one that is low is determined by the three input pins to 74HC138: G8, G9, and G11.Each of those eight output lines are connected to seven buttons. In each set of seven buttons, the other side is connected to G3, G4, G5, G6, G7, G13, and G15. These seven pins have internal pullups. When a button is pressed, but it is not selected (its 74HC138 line is high), nothing happens. When one is pressed and it is on the set with low output from the 74HC138, it pulls its G3/G4/G5/G6/G7/G13/G15 line down.The procedure in pseudocode is as follows:Set pins G8, G9, G11 as outputs.Set pins G3, G4, G5, G6, G7, G13, G15 as inputs.For i in 0, 1, 2, 3, 4, 5, 6, 7:Set pin G8 state to (i & 1).Set pin G9 state to ((i >> 1) & 1).Set pin G11 state to ((i >> 2) & 2).Wait a tiny bit, so the wire states have time to stabilize.Read pin G3. Button[i*7+0] is pressed if low, not pressed if high.Read pin G4. Button[i*7+1] is pressed if low, not pressed if high.Read pin G5. Button[i*7+2] is pressed if low, not pressed if high.Read pin G6. Button[i*7+3] is pressed if low, not pressed if high.Read pin G7. Button[i*7+4] is pressed if low, not pressed if high.Read pin G13. Button[i*7+5] is pressed if low, not pressed if high.Read pin G15. Button[i*7+6] is pressed if low, not pressed if high.End ForIn practice, this is easiest to do in a timer interrupt, where the input pins are read first, then the output pins set for the next interrupt, each time reading the states of 7 buttons. Or, if you have a main loop, the button states are checked early in the iteration, with the output pin states set for the next iteration.

author: Ian.M
author_rank: Super Contributor
timestamp: January 27, 2024, 01:10:44 pm
content: Its a crappy design, because if you press two switches in the same column of the matrix simultaneously, you short 74HC138 outputs together and when one is active (low), an excessive current flows from the inactive one (high) to the active one. Yes, there are 22 ohm resistors in series with each output to limit the current, but if all the keys in a column are held, the active output current is likely to approach its abs. max .limit, with the only thing saving it being the low duty cycle.Replacing R27 - R34 with Schottky diodes, cathodes towards the '138 to prevent it sourcing current would totally prevent this, or it could be mitigated by simply using 330 ohm resistors.Note that as is, it can only detect a single key press in each column simultaneously, which has implications if you need to implement shift keys (i.e. a modifier key held while you press another key). With diodes in place of the resistors any two keys on the whole keyboard can be pressed simultaneously and detected individually without side effects. If three are pressed, with two in the same column and two in the same row, you get a fourth phantom key press at the remaining corner of the rectangle. The only way to avoid that is a diode in series with each individual key.

author: Nominal Animal
author_rank: Super Contributor
timestamp: January 27, 2024, 02:38:41 pm
content: Back in 2017, I designeda simple carrier for Teensy LC(which has native USB, thus can be a native USB keyboard/gamepad/etc. without OS support, costing only $12) for 32 individual buttons plus 9 potentiometers, using 2- and 3-pin 0.1" connectors. Since even I can design such (I'm a bumblefuck hobbyist!), it isn't difficult.Each button has an individual diode, using dual common cathode in SOT-23 (BAS70-05, BAT54C, or similar; Nexperia BAS70-05,215 at JLCPCB assembly is about $0.03 apiece), with current limiting resistors on the outputs/rows, assuming internal pulldowns are used on the inputs/columns. Each button state is fully independent, and with software debounce, the reaction latency can be on the order of a couple of milliseconds, suitable for all kinds of controllers.BAS40DW-05/BAS40CDW-AU/BAT54CDW (common cathode) and BAS40DW-06/BAS40ADW-AU/BAT54ADW (common anode) are even more dense, handling four buttons per SOT23-6/SC-74 or SOT363-6/SC-88 package. When using board prototyping and assembly services like JLCPCB, for $0.15 apiece (assembled, Nexperia BAS40-05V,115 in SOT666-6, takes about 2mm×2mm board space, or Diodes BAT54ADW-7-F in SOT363-6, a bit larger but well under 3mm×3mm) makes for pretty tight boards, four diodes per package.Even with such tiny quad packages, you'd need 14 of them on the Cardputer, which explains why it has none.

author: VagrantBungler
author_rank: Newbie
timestamp: January 27, 2024, 10:44:36 pm
content: Quote from: Nominal Animal on January 27, 2024, 01:02:50 pmok so just to get a bit more clarificationThe 3 inputs are incrementing through the 8 different combinations constantly. 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, ideally using interrupts so the check doesn't mess up. is this right?

author: djsb
author_rank: Super Contributor
timestamp: January 27, 2024, 11:03:39 pm
content: So how would a CD4532 priority encoder work in any way differently if used in this application?https://www.ti.com/lit/ds/symlink/cd4532b.pdf?ts=1706396510688&ref_url=https%253A%252F%252Fwww.ti.com%252Fproduct%252FCD4532B

author: Ian.M
author_rank: Super Contributor
timestamp: January 28, 2024, 03:09:10 am
content: You'd have to rearrange things considerably for a CD4532 to be useful in this application, as its got active high inputs. OTOH a74HC148which has active low inputs, could be connected (with pullup resistors) to encode the seven columns of the matrix. Don't use '148 input 0 (tie high), so an output of 111 on A2-A0 can indicate no key pressed, without the need to monitor its EO or GS pins. That would let you run a 56 key matrix keyboard with only six MCU I/O pins.The down-side is, it is inherently incapable of detecting multiple simultaneous keypresses in the same row, so even with the 22 ohm resistors replaced with diodes, there would be severe limitations on which pairs of key presses would be simultaneously detectable, which would limit the use of shift keys.However at this point, you've got two 16 pin 74HC ICs, eight diodes and eight pullups just to run a keyboard, and it still needs six I/Os, and a scanning routine on the main MCU. If you are short of I/O pins and using a higher pin count MCU is not an option, its probably worth considering a low cost 8 bit slave MCU to manage a keyboard matrix connected direct to its pins, with a serial interface (SPI, I2C, or asynch.) to communicate with the main MCU.

author: Nominal Animal
author_rank: Super Contributor
timestamp: January 28, 2024, 06:28:54 am
content: Quote from: VagrantBungler on January 27, 2024, 10:44:36 pmYes. 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.Quote from: VagrantBungler on January 27, 2024, 10:44:36 pmYes. 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.Quote from: VagrantBungler on January 27, 2024, 10:44:36 pmThe 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 RP2040herewith 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, forNinterrupts its state is not checked and is only assumed to be pressed, to ignore button bounce. Then, if it is still being pressed, andMinterrupts pass, it is time to do the first autorepeat. After each autorepeat, everyKinterrupts 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 likemillis()ormicros()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 beexact, only a general pattern.