diff --git a/src/kaleidoscope/driver/keyscanner/Simple.h b/src/kaleidoscope/driver/keyscanner/Simple.h
new file mode 100644
index 0000000000..5c71dec271
--- /dev/null
+++ b/src/kaleidoscope/driver/keyscanner/Simple.h
@@ -0,0 +1,215 @@
+/* -*- mode: c++ -*-
+ * kaleidoscope::driver::keyscanner::Simple --Straightforward Keyscanner for microcontrollers
+ * Copyright (C) 2018-2023 Keyboard.io, Inc
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ */
+
+#pragma once
+
+#include // for uint16_t, uint8_t, uint32_t
+#include "kaleidoscope/driver/keyscanner/Base.h" // for BaseProps
+#include "kaleidoscope/driver/keyscanner/None.h" // for None
+
+
+namespace kaleidoscope {
+namespace driver {
+namespace keyscanner {
+
+
+struct SimpleProps : kaleidoscope::driver::keyscanner::BaseProps {
+ static const uint32_t keyscan_interval_micros = 1500;
+ typedef uint16_t RowState;
+
+ /*
+ * The following two lines declare an empty array. Both of these must be
+ * shadowed by the descendant keyscanner description class.
+ */
+ static constexpr uint8_t matrix_row_pins[] = {};
+ static constexpr uint8_t matrix_col_pins[] = {};
+};
+
+
+#ifndef KALEIDOSCOPE_VIRTUAL_BUILD
+template
+class Simple : public kaleidoscope::driver::keyscanner::Base<_KeyScannerProps> {
+ protected:
+ /*
+ each of these variables are storing the state for a row of keys
+
+ so for key 0, the counter is represented by db0[0] and db1[0]
+ and the state in debounced_state[0].
+ */
+ struct debounce_t {
+ typename _KeyScannerProps::RowState db0; // counter bit 0
+ typename _KeyScannerProps::RowState db1; // counter bit 1
+ typename _KeyScannerProps::RowState debounced_state; // debounced state
+ };
+
+ struct row_state_t {
+ typename _KeyScannerProps::RowState previous;
+ typename _KeyScannerProps::RowState current;
+ debounce_t debouncer;
+ };
+
+ private:
+ typedef Simple<_KeyScannerProps> ThisType;
+ typedef _KeyScannerProps KeyScannerProps_;
+ static row_state_t matrix_state_[_KeyScannerProps::matrix_rows];
+ static uint32_t next_scan_at_;
+
+ public:
+ void setup() {
+ static_assert(
+ sizeof(_KeyScannerProps::matrix_row_pins) > 0,
+ "The key scanner description has an empty array of matrix row pins.");
+ static_assert(
+ sizeof(_KeyScannerProps::matrix_col_pins) > 0,
+ "The key scanner description has an empty array of matrix column pins.");
+
+
+ for (uint8_t i = 0; i < _KeyScannerProps::matrix_columns; i++) {
+ pinMode(_KeyScannerProps::matrix_col_pins[i], INPUT_PULLUP);
+ }
+
+ for (uint8_t i = 0; i < _KeyScannerProps::matrix_rows; i++) {
+ pinMode(_KeyScannerProps::matrix_row_pins[i], OUTPUT);
+ digitalWrite(_KeyScannerProps::matrix_row_pins[i], HIGH);
+ }
+ }
+
+
+ __attribute__((optimize(3))) void readMatrix(void) {
+ typename _KeyScannerProps::RowState any_debounced_changes = 0;
+
+ for (uint8_t current_row = 0; current_row < _KeyScannerProps::matrix_rows; current_row++) {
+ digitalWrite(_KeyScannerProps::matrix_row_pins[current_row], LOW);
+ typename _KeyScannerProps::RowState hot_pins = readCols();
+ digitalWrite(_KeyScannerProps::matrix_row_pins[current_row], HIGH);
+
+
+ any_debounced_changes |= debounce(hot_pins, &matrix_state_[current_row].debouncer);
+
+ if (any_debounced_changes) {
+ for (uint8_t current_row = 0; current_row < _KeyScannerProps::matrix_rows; current_row++) {
+ matrix_state_[current_row].current = matrix_state_[current_row].debouncer.debounced_state;
+ }
+ }
+ }
+ }
+ void scanMatrix() {
+ uint32_t current_micros_ = micros();
+ if (current_micros_ >= next_scan_at_) {
+ next_scan_at_ = current_micros_ + _KeyScannerProps::keyscan_interval_micros;
+ readMatrix();
+ }
+ actOnMatrixScan();
+ }
+
+ void __attribute__((optimize(3))) actOnMatrixScan() {
+ for (uint8_t row = 0; row < _KeyScannerProps::matrix_rows; row++) {
+ for (uint8_t col = 0; col < _KeyScannerProps::matrix_columns; col++) {
+ uint8_t keyState = (bitRead(matrix_state_[row].previous, col) << 0) | (bitRead(matrix_state_[row].current, col) << 1);
+ if (keyState) {
+ ThisType::handleKeyswitchEvent(Key_NoKey, typename _KeyScannerProps::KeyAddr(row, col), keyState);
+ }
+ }
+ matrix_state_[row].previous = matrix_state_[row].current;
+ }
+ }
+
+ uint8_t pressedKeyswitchCount() {
+ uint8_t count = 0;
+
+ for (int8_t r = 0; r < _KeyScannerProps::matrix_rows; r++) {
+ count += __builtin_popcount(matrix_state_[r].current);
+ }
+ return count;
+ }
+ bool isKeyswitchPressed(typename _KeyScannerProps::KeyAddr key_addr) {
+ return (bitRead(matrix_state_[key_addr.row()].current, key_addr.col()) != 0);
+ }
+
+ uint8_t previousPressedKeyswitchCount() {
+ uint8_t count = 0;
+
+ for (int8_t r = 0; r < _KeyScannerProps::matrix_rows; r++) {
+ count += __builtin_popcount(matrix_state_[r].previous);
+ }
+ return count;
+ }
+ bool wasKeyswitchPressed(typename _KeyScannerProps::KeyAddr key_addr) {
+ return (bitRead(matrix_state_[key_addr.row()].previous,
+ key_addr.col()) != 0);
+ }
+
+
+ private:
+ /*
+ * This function has loop unrolling disabled on purpose: we want to give the
+ * hardware enough time to produce stable PIN reads for us. If we unroll the
+ * loop, we will not have that, because even with the NOP, the codepath is too
+ * fast. If we don't have stable reads, then entire rows or columns will behave
+ * erratically.
+ *
+ * For this reason, we ask the compiler to not unroll our loop, which in turn,
+ * gives hardware enough time to produce stable reads, at the cost of a little
+ * bit of speed.
+ *
+ * Do not remove the attribute!
+ * TODO : see if this is true on ARM. Jesse thinks it was an AVRism
+ */
+ // __attribute__((optimize("no-unroll-loops")))
+ typename _KeyScannerProps::RowState
+ readCols() {
+ typename _KeyScannerProps::RowState hot_pins = 0;
+ for (uint8_t i = 0; i < _KeyScannerProps::matrix_columns; i++) {
+ // asm("NOP"); // We need to pause a beat before reading or we may read before the pin is hot
+ hot_pins |= (!digitalRead(_KeyScannerProps::matrix_col_pins[i]) << i);
+ }
+
+ return hot_pins;
+ }
+
+ static inline typename _KeyScannerProps::RowState debounce(
+ typename _KeyScannerProps::RowState sample, debounce_t *debouncer) {
+ typename _KeyScannerProps::RowState delta, changes;
+
+ // Use xor to detect changes from last stable state:
+ // if a key has changed, it's bit will be 1, otherwise 0
+ delta = sample ^ debouncer->debounced_state;
+
+ // Increment counters and reset any unchanged bits:
+ // increment bit 1 for all changed keys
+ debouncer->db1 = ((debouncer->db1) ^ (debouncer->db0)) & delta;
+ // increment bit 0 for all changed keys
+ debouncer->db0 = ~(debouncer->db0) & delta;
+
+ // Calculate returned change set: if delta is still true
+ // and the counter has wrapped back to 0, the key is changed.
+
+ changes = ~(~delta | (debouncer->db0) | (debouncer->db1));
+ // Update state: in this case use xor to flip any bit that is true in changes.
+ debouncer->debounced_state ^= changes;
+
+ return changes;
+ }
+};
+#else // ifndef KALEIDOSCOPE_VIRTUAL_BUILD
+template
+class Simple : public keyscanner::None {};
+#endif // ifndef KALEIDOSCOPE_VIRTUAL_BUILD
+
+} // namespace keyscanner
+} // namespace driver
+} // namespace kaleidoscope