diff --git a/examples/C/keyboardLedMatrix.c b/examples/C/keyboardLedMatrix.c
new file mode 100644
index 000000000..5e4d081fe
--- /dev/null
+++ b/examples/C/keyboardLedMatrix.c
@@ -0,0 +1,170 @@
+#include "ripes_system.h"
+
+/*
+ * keyboardDualDisplay.c
+ * Keyboard input demo with dual display output
+ *
+ * =========================================================
+ * PREREQUISITE (IMPORTANT):
+ * Before running this program, the user MUST configure the
+ * Ripes I/O table and add the following peripherals:
+ *
+ * 1) LED Matrix:
+ * - ID: LED_MATRIX_0
+ * - Required symbols:
+ * LED_MATRIX_0_BASE
+ * LED_MATRIX_0_WIDTH
+ * LED_MATRIX_0_HEIGHT
+ *
+ * 2) Keyboard:
+ * - ID: KEYBOARD_0
+ * - Required symbols:
+ * KEYBOARD_0_BASE
+ *
+ * Without these peripherals properly added and mapped,
+ * the program will either:
+ * - read invalid memory
+ * - or produce undefined behavior in I/O access
+ * =========================================================
+ */
+
+#define MATRIX_W LED_MATRIX_0_WIDTH
+#define MATRIX_H LED_MATRIX_0_HEIGHT
+#define CHAR_W 5
+#define CHAR_H 7
+#define COLOR_ON 0x00FF00
+#define COLOR_OFF 0x000000
+#define SEG_COUNT SEVEN_SEGMENT_0_N_DIGITS
+
+// LED Matrix Font: 0-9, a-z (5x7 bitmap, 1 bit = 1 pixel)
+static const unsigned char font[36][CHAR_H] = {
+ // 0-9
+ {0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E}, // 0
+ {0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E}, // 1
+ {0x0E, 0x11, 0x01, 0x06, 0x08, 0x10, 0x1F}, // 2
+ {0x1F, 0x01, 0x02, 0x06, 0x01, 0x11, 0x0E}, // 3
+ {0x11, 0x11, 0x11, 0x1F, 0x01, 0x01, 0x01}, // 4
+ {0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E}, // 5
+ {0x0E, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x0E}, // 6
+ {0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x10}, // 7
+ {0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E}, // 8
+ {0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C}, // 9
+ // A-Z
+ {0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x00}, // a
+ {0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E}, // b
+ {0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E}, // c
+ {0x1E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1E}, // d
+ {0x1F, 0x10, 0x1E, 0x10, 0x10, 0x10, 0x1F}, // e
+ {0x1F, 0x10, 0x1E, 0x10, 0x10, 0x10, 0x10}, // f
+ {0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0F}, // g
+ {0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}, // h
+ {0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E}, // i
+ {0x01, 0x01, 0x01, 0x01, 0x01, 0x11, 0x0E}, // j
+ {0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11}, // k
+ {0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F}, // l
+ {0x11, 0x1B, 0x15, 0x11, 0x11, 0x11, 0x11}, // m
+ {0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11}, // n
+ {0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E}, // o
+ {0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10}, // p
+ {0x0E, 0x11, 0x11, 0x11, 0x11, 0x0E, 0x01}, // q
+ {0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11}, // r
+ {0x0E, 0x11, 0x10, 0x0E, 0x01, 0x11, 0x0E}, // s
+ {0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}, // t
+ {0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E}, // u
+ {0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04}, // v
+ {0x11, 0x11, 0x15, 0x15, 0x15, 0x15, 0x0A}, // w
+ {0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11}, // x
+ {0x11, 0x11, 0x11, 0x0A, 0x04, 0x04, 0x04}, // y
+ {0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F} // z
+};
+
+// 7-Segment Display Font: common-cathode encoding
+static const unsigned char seg_digit[10] = {
+ 0x3F, 0x06, 0x5B, 0x4F, 0x66,
+ 0x6D, 0x7D, 0x07, 0x7F, 0x6F
+};
+
+static const unsigned char seg_alpha[26] = {
+ 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71, 0x3D, 0x76, 0x06, 0x1E,
+ 0x76, 0x38, 0x37, 0x54, 0x3F, 0x73, 0x67, 0x50, 0x6D, 0x78,
+ 0x3E, 0x1C, 0x3E, 0x76, 0x6E, 0x5B
+};
+
+// Set single pixel on LED matrix
+static inline void set_pixel(volatile unsigned int *led, int x, int y, unsigned int color) {
+ if (x >= 0 && x < MATRIX_W && y >= 0 && y < MATRIX_H)
+ *(led + y * MATRIX_W + x) = color;
+}
+
+// Draw character from font bitmap
+static void draw_char_matrix(volatile unsigned int *led, int start_x, int start_y, int char_idx, unsigned int color) {
+ const unsigned char *bitmap = font[char_idx];
+ for (int dy = 0; dy < CHAR_H; dy++) {
+ unsigned char row = bitmap[dy];
+ for (int dx = 0; dx < CHAR_W; dx++)
+ if (row & (1 << (CHAR_W - 1 - dx)))
+ set_pixel(led, start_x + dx, start_y + dy, color);
+ }
+}
+
+// Clear character area (5x7)
+static void clear_char_area(volatile unsigned int *led, int start_x, int start_y) {
+ for (int dy = 0; dy < CHAR_H; dy++)
+ for (int dx = 0; dx < CHAR_W; dx++)
+ set_pixel(led, start_x + dx, start_y + dy, COLOR_OFF);
+}
+
+// Convert ASCII to 7-segment code
+static unsigned char encode_seg(unsigned char ch) {
+ if (ch >= '0' && ch <= '9') return seg_digit[ch - '0'];
+ if (ch >= 'A' && ch <= 'Z') return seg_alpha[ch - 'A'];
+ if (ch >= 'a' && ch <= 'z') return seg_alpha[ch - 'a'];
+ return 0;
+}
+
+// Convert ASCII to font index (0-35), -1 if unsupported
+static int char_to_index(unsigned char ch) {
+ if (ch >= '0' && ch <= '9') return ch - '0';
+ if (ch >= 'A' && ch <= 'Z') return 10 + (ch - 'A');
+ if (ch >= 'a' && ch <= 'z') return 10 + (ch - 'a');
+ return -1;
+}
+
+int main() {
+ volatile unsigned int *led = (volatile unsigned int *)LED_MATRIX_0_BASE;
+ volatile unsigned int *seg = (volatile unsigned int *)SEVEN_SEGMENT_0_BASE;
+ volatile unsigned int *kbd = (volatile unsigned int *)KEYBOARD_0_BASE;
+
+ int prev_char_idx = -1;
+ int center_x = (MATRIX_W - CHAR_W) / 2;
+ int center_y = (MATRIX_H - CHAR_H) / 2;
+ int seg_pos = 0;
+
+ for (int i = 0; i < MATRIX_W * MATRIX_H; i++)
+ *(led + i) = COLOR_OFF;
+
+ for (int i = 0; i < SEG_COUNT; i++)
+ *(seg + i) = 0;
+
+ while (1) {
+ if (*(kbd + 1) == 0)
+ continue;
+
+ unsigned char key = (unsigned char)(*kbd);
+ int char_idx = char_to_index(key);
+ unsigned char seg_code = encode_seg(key);
+
+ if (char_idx >= 0 && char_idx < 36 && seg_code != 0) {
+ if (prev_char_idx >= 0)
+ clear_char_area(led, center_x, center_y);
+ draw_char_matrix(led, center_x, center_y, char_idx, COLOR_ON);
+ prev_char_idx = char_idx;
+
+ *(seg + seg_pos) = seg_code;
+ seg_pos++;
+ if (seg_pos >= SEG_COUNT)
+ seg_pos = 0;
+ }
+ }
+ return 0;
+}
\ No newline at end of file
diff --git a/examples/examples.qrc b/examples/examples.qrc
index 1a76a3512..60c635d2f 100644
--- a/examples/examples.qrc
+++ b/examples/examples.qrc
@@ -8,6 +8,7 @@
assembly/consolePrinting.s
C/leds.c
C/switchesAndLeds.c
+ C/keyboardLedMatrix.c
assembly/leds.s
ELF/RanPi-RV32
ELF/RanPi-RV64
diff --git a/src/io/iokeyboard.cpp b/src/io/iokeyboard.cpp
new file mode 100644
index 000000000..cf709dc73
--- /dev/null
+++ b/src/io/iokeyboard.cpp
@@ -0,0 +1,191 @@
+#include "iokeyboard.h"
+#include "ioregistry.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Ripes {
+
+IOKeyboard::IOKeyboard(QWidget *parent) : IOBase(IOType::KEYBOARD, parent) {
+ m_parameters[BUFSIZE] = IOParam(BUFSIZE, "Buffer size", 16, true, 1, 256);
+
+ setFocusPolicy(Qt::StrongFocus);
+ buildLayout();
+ refreshRegMap();
+}
+
+QString IOKeyboard::description() const {
+ QStringList desc;
+ desc << "Memory-mapped keyboard with configurable FIFO buffer.";
+ desc << "KEY_DATA (offset 0x00, R): dequeue next ASCII code (0 if empty).";
+ desc << "KEY_STATUS (offset 0x04, R/W): read returns buffer count, "
+ "write non-zero to clear.";
+ return desc.join('\n');
+}
+
+void IOKeyboard::buildLayout() {
+ auto *root = new QVBoxLayout(this);
+ root->setSpacing(4);
+ root->setContentsMargins(6, 6, 6, 6);
+
+ auto addKeyButton = [&](QHBoxLayout *row, char ch, int w = 32) {
+ auto *btn = new QPushButton(QString(QChar(ch)), this);
+ btn->setFixedSize(w, 32);
+ btn->setFocusPolicy(Qt::NoFocus);
+ const uint8_t ascii = static_cast(ch);
+ connect(btn, &QPushButton::clicked, this,
+ [this, ascii]() { enqueueKey(ascii); });
+ row->addWidget(btn);
+ };
+
+ auto *numRow = new QHBoxLayout();
+ numRow->setAlignment(Qt::AlignCenter);
+ for (int i = 1; i <= 10; ++i)
+ addKeyButton(numRow, '0' + (i % 10));
+ root->addLayout(numRow);
+
+ const char *rows[] = {"QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"};
+ for (const char *row : rows) {
+ auto *rowLayout = new QHBoxLayout();
+ rowLayout->setAlignment(Qt::AlignCenter);
+ for (int i = 0; row[i]; ++i)
+ addKeyButton(rowLayout, row[i]);
+ root->addLayout(rowLayout);
+ }
+
+ auto *spaceRow = new QHBoxLayout();
+ spaceRow->setAlignment(Qt::AlignCenter);
+ addKeyButton(spaceRow, ' ', 200);
+ root->addLayout(spaceRow);
+
+ m_statusLabel = new QLabel(this);
+ root->addWidget(m_statusLabel);
+ refreshStatusLabel();
+}
+
+void IOKeyboard::refreshRegMap() {
+ const unsigned bufSize = m_parameters.at(BUFSIZE).value.toUInt();
+
+ m_regDescs.clear();
+ m_regDescs.push_back(RegDesc{"KEY_DATA", RegDesc::RW::R, 8, 0, true});
+ m_regDescs.push_back(RegDesc{"KEY_STATUS", RegDesc::RW::RW, 32, 4, true});
+
+ m_extraSymbols.clear();
+ m_extraSymbols.push_back(IOSymbol{"BUF_SIZE", bufSize});
+
+ emit regMapChanged();
+}
+
+void IOKeyboard::enqueueKey(uint8_t ascii) {
+ {
+ QMutexLocker lock(&m_bufMutex);
+ const unsigned maxSize = m_parameters.at(BUFSIZE).value.toUInt();
+ if (static_cast(m_keyBuffer.size()) < maxSize)
+ m_keyBuffer.enqueue(ascii);
+ m_lastKey = ascii;
+ }
+ refreshStatusLabel();
+}
+
+void IOKeyboard::refreshStatusLabel() {
+ if (!m_statusLabel)
+ return;
+
+ int count;
+ uint8_t ch;
+
+ {
+ QMutexLocker lock(&m_bufMutex);
+ count = m_keyBuffer.size();
+ ch = m_lastKey;
+ }
+
+ const unsigned bufSize = m_parameters.at(BUFSIZE).value.toUInt();
+ const QString charStr =
+ (ch >= 0x20 && ch < 0x7F) ? QString(QChar(ch)) : QString("--");
+
+ m_statusLabel->setText(QString("Last: %1 (0x%2) | Buffer: %3/%4")
+ .arg(charStr)
+ .arg(ch, 2, 16, QChar('0'))
+ .arg(count)
+ .arg(bufSize));
+}
+
+void IOKeyboard::keyPressEvent(QKeyEvent *event) {
+ if (event->isAutoRepeat()) {
+ event->ignore();
+ return;
+ }
+
+ const int key = event->key();
+ uint8_t ascii = 0;
+
+ if (key >= Qt::Key_A && key <= Qt::Key_Z)
+ ascii = static_cast('A' + (key - Qt::Key_A));
+ else if (key >= Qt::Key_0 && key <= Qt::Key_9)
+ ascii = static_cast('0' + (key - Qt::Key_0));
+ else if (key == Qt::Key_Space)
+ ascii = static_cast(' ');
+
+ if (ascii != 0) {
+ enqueueKey(ascii);
+ event->accept();
+ } else {
+ event->ignore();
+ }
+}
+
+void IOKeyboard::parameterChanged(unsigned) {
+ refreshRegMap();
+ refreshStatusLabel();
+}
+
+VInt IOKeyboard::ioRead(AInt offset, unsigned) {
+ if (offset == 0) {
+ uint8_t val;
+
+ {
+ QMutexLocker lock(&m_bufMutex);
+ val = m_keyBuffer.isEmpty() ? 0 : m_keyBuffer.dequeue();
+ }
+
+ QMetaObject::invokeMethod(
+ this, [this]() { refreshStatusLabel(); }, Qt::QueuedConnection);
+
+ return val;
+ }
+
+ if (offset == 4) {
+ QMutexLocker lock(&m_bufMutex);
+ return static_cast(m_keyBuffer.size());
+ }
+
+ return 0;
+}
+
+void IOKeyboard::ioWrite(AInt offset, VInt value, unsigned) {
+ if (offset == 4 && value != 0) {
+ {
+ QMutexLocker lock(&m_bufMutex);
+ m_keyBuffer.clear();
+ }
+
+ QMetaObject::invokeMethod(
+ this, [this]() { refreshStatusLabel(); }, Qt::QueuedConnection);
+ }
+}
+
+void IOKeyboard::reset() {
+ {
+ QMutexLocker lock(&m_bufMutex);
+ m_keyBuffer.clear();
+ m_lastKey = 0;
+ }
+ refreshStatusLabel();
+}
+
+} // namespace Ripes
\ No newline at end of file
diff --git a/src/io/iokeyboard.h b/src/io/iokeyboard.h
new file mode 100644
index 000000000..1d16f602f
--- /dev/null
+++ b/src/io/iokeyboard.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include "iobase.h"
+
+QT_FORWARD_DECLARE_CLASS(QLabel);
+QT_FORWARD_DECLARE_CLASS(QPushButton);
+
+namespace Ripes {
+
+class IOKeyboard : public IOBase {
+ Q_OBJECT
+
+ enum Parameters { BUFSIZE };
+
+ enum RegMap : AInt {
+ KEY_DATA = 0x0,
+ KEY_STATUS = 0x4
+ };
+
+public:
+ IOKeyboard(QWidget *parent);
+ ~IOKeyboard() { unregister(); };
+
+ virtual unsigned byteSize() const override { return 8; }
+ virtual QString description() const override;
+ virtual QString baseName() const override { return "Keyboard"; }
+
+ virtual const std::vector ®isters() const override {
+ return m_regDescs;
+ };
+ virtual const std::vector *extraSymbols() const override {
+ return &m_extraSymbols;
+ }
+
+ virtual VInt ioRead(AInt offset, unsigned size) override;
+ virtual void ioWrite(AInt offset, VInt value, unsigned size) override;
+ virtual void reset() override;
+
+protected:
+ virtual void parameterChanged(unsigned) override;
+ void keyPressEvent(QKeyEvent *event) override;
+
+private:
+ void buildLayout();
+ void refreshRegMap();
+ void refreshStatusLabel();
+ void enqueueKey(uint8_t ascii);
+
+ QQueue m_keyBuffer;
+ mutable QMutex m_bufMutex;
+ std::optional m_lastKey;
+
+ QLabel *m_statusLabel = nullptr;
+
+ std::vector m_regDescs;
+ std::vector m_extraSymbols;
+};
+
+} // namespace Ripes
\ No newline at end of file
diff --git a/src/io/ioregistry.h b/src/io/ioregistry.h
index edf95a142..ad73c5c3e 100644
--- a/src/io/ioregistry.h
+++ b/src/io/ioregistry.h
@@ -4,6 +4,7 @@
#include
#include "iodpad.h"
+#include "iokeyboard.h"
#include "ioledmatrix.h"
#include "ioswitches.h"
@@ -18,7 +19,7 @@
namespace Ripes {
-enum IOType { LED_MATRIX, SWITCHES, DPAD, NPERIPHERALS };
+enum IOType { LED_MATRIX, SWITCHES, DPAD, KEYBOARD, NPERIPHERALS };
template
IOBase *createIO(QWidget *parent) {
@@ -31,11 +32,13 @@ using IOFactory = std::function;
const static std::map IOTypeTitles = {
{IOType::LED_MATRIX, "LED Matrix"},
{IOType::SWITCHES, "Switches"},
- {IOType::DPAD, "D-Pad"}};
+ {IOType::DPAD, "D-Pad"},
+ {IOType::KEYBOARD, "Keyboard"}};
const static std::map IOFactories = {
{IOType::LED_MATRIX, createIO},
{IOType::SWITCHES, createIO},
- {IOType::DPAD, createIO}};
+ {IOType::DPAD, createIO},
+ {IOType::KEYBOARD, createIO}};
} // namespace Ripes
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index f3270d323..e989cf068 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -25,4 +25,5 @@ create_qtest(tst_assembler)
create_qtest(tst_expreval)
create_qtest(tst_cosimulate)
create_qtest(tst_reverse)
-create_qtest(tst_stall)
\ No newline at end of file
+create_qtest(tst_io_keyboard)
+create_qtest(tst_stall)
diff --git a/test/tst_io_keyboard.cpp b/test/tst_io_keyboard.cpp
new file mode 100644
index 000000000..758c3524f
--- /dev/null
+++ b/test/tst_io_keyboard.cpp
@@ -0,0 +1,89 @@
+#include
+#include
+#include
+
+#include "io/iobase.h"
+#include "io/iokeyboard.h"
+
+using namespace Ripes;
+
+namespace {
+IOKeyboard *makeKeyboard() {
+ auto *p = new IOKeyboard(nullptr);
+ QObject::connect(p, &IOBase::aboutToDelete, p,
+ [](std::atomic &ok) { ok = 1; });
+ return p;
+}
+} // namespace
+
+class tst_io_keyboard : public QObject {
+ Q_OBJECT
+
+private slots:
+ void keyboard_keyPressEnqueuesAndStatusReflectsBuffer() {
+ auto *kb = makeKeyboard();
+
+ QCOMPARE(kb->ioRead(IOKeyboard::RegMap::KEY_STATUS, 4), VInt{0});
+ QCOMPARE(kb->ioRead(IOKeyboard::RegMap::KEY_DATA, 1), VInt{0});
+
+ const QList> keys = {
+ {Qt::Key_A, 'A'}, {Qt::Key_B, 'B'}, {Qt::Key_5, '5'}};
+
+ for (const auto &k : keys) {
+ QKeyEvent ev(QEvent::KeyPress, k.first, Qt::NoModifier, QString(k.second));
+ QCoreApplication::sendEvent(kb, &ev);
+ }
+
+ QCOMPARE(kb->ioRead(IOKeyboard::RegMap::KEY_STATUS, 4), VInt{3});
+
+ for (const auto &k : keys)
+ QCOMPARE(static_cast(kb->ioRead(IOKeyboard::RegMap::KEY_DATA, 1)), k.second);
+
+ QCOMPARE(kb->ioRead(IOKeyboard::RegMap::KEY_DATA, 1), VInt{0});
+
+ QKeyEvent ignored(QEvent::KeyPress, Qt::Key_F1, Qt::NoModifier, QString());
+ QCoreApplication::sendEvent(kb, &ignored);
+
+ QCOMPARE(kb->ioRead(IOKeyboard::RegMap::KEY_STATUS, 4), VInt{0});
+
+ delete kb;
+ }
+
+ void keyboard_writeStatusClearsBuffer() {
+ auto *kb = makeKeyboard();
+
+ QKeyEvent ev(QEvent::KeyPress, Qt::Key_Q, Qt::NoModifier, "Q");
+
+ QCoreApplication::sendEvent(kb, &ev);
+ QCOMPARE(kb->ioRead(IOKeyboard::RegMap::KEY_STATUS, 4), VInt{1});
+
+ kb->ioWrite(IOKeyboard::RegMap::KEY_STATUS, 1, 4);
+ QCOMPARE(kb->ioRead(IOKeyboard::RegMap::KEY_STATUS, 4), VInt{0});
+
+ QCoreApplication::sendEvent(kb, &ev);
+ QCOMPARE(kb->ioRead(IOKeyboard::RegMap::KEY_STATUS, 4), VInt{1});
+
+ kb->reset();
+ QCOMPARE(kb->ioRead(IOKeyboard::RegMap::KEY_STATUS, 4), VInt{0});
+
+ delete kb;
+ }
+
+ void keyboard_bufferOverflowIsBounded() {
+ auto *kb = makeKeyboard();
+
+ kb->setParameter(0, 4);
+
+ QKeyEvent ev(QEvent::KeyPress, Qt::Key_X, Qt::NoModifier, "X");
+
+ for (int i = 0; i < 10; ++i)
+ QCoreApplication::sendEvent(kb, &ev);
+
+ QCOMPARE(kb->ioRead(IOKeyboard::RegMap::KEY_STATUS, 4), VInt{4});
+
+ delete kb;
+ }
+};
+
+QTEST_MAIN(tst_io_keyboard)
+#include "tst_io_keyboard.moc"
\ No newline at end of file