From d90039bbe1096097a583d11d412bcb75fb9c7c17 Mon Sep 17 00:00:00 2001 From: zombieleet Date: Tue, 22 Sep 2026 01:04:46 +0200 Subject: [PATCH 1/2] keypad4x4: keep the rows apart while scanning Two problems make GetIndices report the wrong row. The scan drives one row low, reads the columns, and raises the row again. When it finds a key it returns straight away, so the row it was testing stays low. The next scan then has two rows low at once, and a column that reads low no longer says which row it came from. The row is now raised before the return. The columns are also read on the instruction after the row goes low. The line does not fall that fast, because the wiring has some capacitance, so the read can still show the row before it. The scan now waits 50us after a row changes. Together these report a key as the row below it, now and then. Tested on an ESP32 with a 4x4 membrane keypad. Pressing the same key five times gives the same row every time, and pressing down one column gives rows 0, 1, 2 and 3 in order. --- keypad4x4/keypad4x4.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/keypad4x4/keypad4x4.go b/keypad4x4/keypad4x4.go index f4adafb45..7c79af4d8 100644 --- a/keypad4x4/keypad4x4.go +++ b/keypad4x4/keypad4x4.go @@ -2,11 +2,17 @@ package keypad4x4 import ( "machine" + "time" ) // NoKeyPressed is used, when no key was pressed const NoKeyPressed = 255 +// settleTime is the wait after a row changes, before the columns are read. +// The row line does not fall immediately, because the wiring has some +// capacitance. A read that is too early can still show the previous row. +const settleTime = 50 * time.Microsecond + // Device is used as 4x4 keypad driver type Device interface { Configure() @@ -79,6 +85,7 @@ func (keypad *device) GetKey() uint8 { func (keypad *device) GetIndices() (int, int) { for rowIndex, rowPin := range keypad.rows { rowPin.Low() + time.Sleep(settleTime) for columnIndex := range keypad.columns { columnPin := keypad.columns[columnIndex] @@ -89,6 +96,11 @@ func (keypad *device) GetIndices() (int, int) { keypad.lastColumn = columnIndex keypad.lastRow = rowIndex + // Stop driving this row before leaving. A row left low makes + // two rows low on the next scan, and a low column then does + // not say which row it came from. + rowPin.High() + return keypad.lastRow, keypad.lastColumn } From e7bd7749128b98ecb0d78525f683c7f92e4e616e Mon Sep 17 00:00:00 2001 From: zombieleet Date: Tue, 22 Sep 2026 01:05:03 +0200 Subject: [PATCH 2/2] keypad4x4: let the caller set the key values and the scan polarity Two things the driver cannot guess, and had no way to be told. The value of a key. GetKey returns the position, 0 at the upper left end and 15 at the lower right end. A keypad prints something else on the keys, and only the caller knows what. The device already had a mapping field of the right shape, but nothing could write to it and Configure overwrote it on every call. Config.Mapping now fills it, Configure no longer touches it, and the default is still the position. The polarity of the scan. The columns rest high with a pull-up and each row is driven low. That fails when something on the board pulls a column down harder than the internal pull-up can hold it up. The column then rests low, the scan reads a key in that column on every row, and the driver latches and stops reporting anything at all. Config.Inverted turns the scan around, so the columns rest low and each row is driven high. Config.ColumnConfig gives the pin mode of the columns. Inverted wants an internal pull-down, but not every chip has one. AVR has pull-ups only, so naming PinInputPulldown here would break those targets. The caller passes it on a chip that has it. Nil means PinInputPullup, or PinInput when Inverted is set, which is enough when the board already holds the columns down. NewDevice and the Device interface do not change. NewDevice now calls NewDeviceWithConfig with an empty Config. Tested on an ESP32 with a 4x4 membrane keypad. The board pulls GPIO13 down, so the whole left column of the keypad never worked. With Inverted and a pull-down it reads 1, 4, 7 and * correctly, and the mapping gives those characters rather than 0, 4, 8 and 12. --- keypad4x4/keypad4x4.go | 112 +++++++++++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 16 deletions(-) diff --git a/keypad4x4/keypad4x4.go b/keypad4x4/keypad4x4.go index 7c79af4d8..43eae9a8b 100644 --- a/keypad4x4/keypad4x4.go +++ b/keypad4x4/keypad4x4.go @@ -13,6 +13,53 @@ const NoKeyPressed = 255 // capacitance. A read that is too early can still show the previous row. const settleTime = 50 * time.Microsecond +// defaultMapping gives the position of a key, 0 at the upper left end and 15 +// at the lower right end. +var defaultMapping = [4][4]uint8{ + {0, 1, 2, 3}, + {4, 5, 6, 7}, + {8, 9, 10, 11}, + {12, 13, 14, 15}, +} + +// Config holds the options of a keypad. +type Config struct { + // Mapping is the value that GetKey returns for each key, in reading + // order. Use it to give the value printed on the key, such as '7' or 'A', + // instead of its position. The zero value asks for the default mapping, + // which is the position of the key. + Mapping [4][4]uint8 + + // Inverted swaps the polarity of the scan. + // + // The default is false. A column rests high with a pull-up, each row is + // driven low in turn, and a pressed key pulls its column low. + // + // Set it to true when a column cannot rest high, because something on the + // board pulls that pin down harder than the internal pull-up can hold it + // up. A strapping pin such as GPIO12 on the ESP32 often has a pull-down + // fitted, and some keypad modules bring their own resistors. A column + // then rests low, each row is driven high, and a pressed key pulls its + // column high. + Inverted bool + + // ColumnConfig overrides how the column pins are configured. + // + // Leave it nil for the usual case. The driver then uses PinInputPullup, + // or PinInput when Inverted is set. + // + // Inverted wants an internal pull-down, but not every chip has one. AVR + // has pull-ups only, so the driver cannot name PinInputPulldown without + // breaking those targets. Pass it here on a chip that has it: + // + // pulldown := machine.PinConfig{Mode: machine.PinInputPulldown} + // cfg := keypad4x4.Config{Inverted: true, ColumnConfig: &pulldown} + // + // PinInput on its own is enough when the board already holds the columns + // at their resting level. + ColumnConfig *machine.PinConfig +} + // Device is used as 4x4 keypad driver type Device interface { Configure() @@ -28,20 +75,45 @@ type device struct { columns [4]machine.Pin rows [4]machine.Pin mapping [4][4]uint8 + inverted bool + columnConfig *machine.PinConfig } // takes r4 -r1 pins and c4 - c1 pins func NewDevice(r4, r3, r2, r1, c4, c3, c2, c1 machine.Pin) Device { + return NewDeviceWithConfig(Config{}, r4, r3, r2, r1, c4, c3, c2, c1) +} + +// NewDeviceWithConfig is NewDevice with the options in config. +func NewDeviceWithConfig(config Config, r4, r3, r2, r1, c4, c3, c2, c1 machine.Pin) Device { result := &device{} result.columns = [4]machine.Pin{c4, c3, c2, c1} result.rows = [4]machine.Pin{r4, r3, r2, r1} + result.inverted = config.Inverted + result.columnConfig = config.ColumnConfig + + // The mapping belongs to the device, not to Configure. Configure used to + // write it, which would throw away a mapping given here. + result.mapping = config.Mapping + if result.mapping == ([4][4]uint8{}) { + result.mapping = defaultMapping + } return result } // Configure sets the column pins as input and the row pins as output func (keypad *device) Configure() { + // PinInputPullup exists on every chip. A pull-down does not, so the + // inverted default is PinInput and the caller gives a pull-down through + // ColumnConfig when the chip has one. inputConfig := machine.PinConfig{Mode: machine.PinInputPullup} + if keypad.inverted { + inputConfig = machine.PinConfig{Mode: machine.PinInput} + } + if keypad.columnConfig != nil { + inputConfig = *keypad.columnConfig + } for i := range keypad.columns { keypad.columns[i].Configure(inputConfig) } @@ -49,14 +121,7 @@ func (keypad *device) Configure() { outputConfig := machine.PinConfig{Mode: machine.PinOutput} for i := range keypad.rows { keypad.rows[i].Configure(outputConfig) - keypad.rows[i].High() - } - - keypad.mapping = [4][4]uint8{ - {0, 1, 2, 3}, - {4, 5, 6, 7}, - {8, 9, 10, 11}, - {12, 13, 14, 15}, + keypad.releaseRow(keypad.rows[i]) } keypad.inputEnabled = true @@ -81,30 +146,45 @@ func (keypad *device) GetKey() uint8 { return keypad.mapping[row][column] } +// selectRow drives a row to the level that tests it. +func (keypad *device) selectRow(pin machine.Pin) { + pin.Set(keypad.inverted) +} + +// releaseRow puts a row back to its resting level. +func (keypad *device) releaseRow(pin machine.Pin) { + pin.Set(!keypad.inverted) +} + +// pressed reports whether a column reads as a key held down. +func (keypad *device) pressed(pin machine.Pin) bool { + return pin.Get() == keypad.inverted +} + // GetIndices returns the position of the pressed key func (keypad *device) GetIndices() (int, int) { for rowIndex, rowPin := range keypad.rows { - rowPin.Low() + keypad.selectRow(rowPin) time.Sleep(settleTime) for columnIndex := range keypad.columns { columnPin := keypad.columns[columnIndex] - if !columnPin.Get() && keypad.inputEnabled { + if keypad.pressed(columnPin) && keypad.inputEnabled { keypad.inputEnabled = false keypad.lastColumn = columnIndex keypad.lastRow = rowIndex - // Stop driving this row before leaving. A row left low makes - // two rows low on the next scan, and a low column then does - // not say which row it came from. - rowPin.High() + // Stop driving this row before leaving. A row left selected + // makes two rows selected on the next scan, and a column then + // does not say which row it came from. + keypad.releaseRow(rowPin) return keypad.lastRow, keypad.lastColumn } - if columnPin.Get() && + if !keypad.pressed(columnPin) && columnIndex == keypad.lastColumn && rowIndex == keypad.lastRow && !keypad.inputEnabled { @@ -112,7 +192,7 @@ func (keypad *device) GetIndices() (int, int) { } } - rowPin.High() + keypad.releaseRow(rowPin) } return -1, -1