-
-
Notifications
You must be signed in to change notification settings - Fork 371
Add Keyboard I/O peripheral with tests and example #453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| #include "ripes_system.h" | ||
|
|
||
| /* | ||
| * keyboardLedMatrix.c | ||
| * Reads characters from the keyboard peripheral and displays the most recent | ||
| * one in the center of the LED matrix using a 5x7 bitmap font. | ||
| */ | ||
|
|
||
| #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 | ||
|
|
||
| // 5x7 bitmap font for 0-9 and a-z (case-insensitive). | ||
| static const unsigned char font[36][CHAR_H] = { | ||
| {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 | ||
| {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 | ||
| }; | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| static void draw_char(volatile unsigned int *led, int x, int y, int idx, unsigned int color) { | ||
| const unsigned char *bitmap = font[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, x + dx, y + dy, color); | ||
| } | ||
| } | ||
|
|
||
| static void clear_area(volatile unsigned int *led, int x, int y) { | ||
| for (int dy = 0; dy < CHAR_H; dy++) | ||
| for (int dx = 0; dx < CHAR_W; dx++) | ||
| set_pixel(led, x + dx, y + dy, COLOR_OFF); | ||
| } | ||
|
|
||
| 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 *kbd = (volatile unsigned int *)KEYBOARD_0_BASE; | ||
|
|
||
| const int center_x = (MATRIX_W - CHAR_W) / 2; | ||
| const int center_y = (MATRIX_H - CHAR_H) / 2; | ||
| int prev_idx = -1; | ||
|
|
||
| for (int i = 0; i < MATRIX_W * MATRIX_H; i++) | ||
| *(led + i) = COLOR_OFF; | ||
|
|
||
| while (1) { | ||
| if (*(kbd + 1) == 0) | ||
| continue; | ||
|
|
||
| unsigned char key = (unsigned char)(*kbd); | ||
| int idx = char_to_index(key); | ||
| if (idx < 0) | ||
| continue; | ||
|
|
||
| if (prev_idx >= 0) | ||
| clear_area(led, center_x, center_y); | ||
| draw_char(led, center_x, center_y, idx, COLOR_ON); | ||
| prev_idx = idx; | ||
| } | ||
| return 0; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| #include "iokeyboard.h" | ||
| #include "ioregistry.h" | ||
|
|
||
| #include <QHBoxLayout> | ||
| #include <QKeyEvent> | ||
| #include <QLabel> | ||
| #include <QMutexLocker> | ||
| #include <QPushButton> | ||
| #include <QVBoxLayout> | ||
|
|
||
| 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<uint8_t>(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<unsigned>(m_keyBuffer.size()) < maxSize) | ||
| m_keyBuffer.enqueue(ascii); | ||
| m_lastKey = ascii; | ||
| } | ||
| refreshStatusLabel(); | ||
| } | ||
|
|
||
| void IOKeyboard::refreshStatusLabel() { | ||
| if (!m_statusLabel) | ||
| return; | ||
|
|
||
| QMutexLocker lock(&m_bufMutex); | ||
| const int count = m_keyBuffer.size(); | ||
| const uint8_t ch = m_lastKey; | ||
| lock.unlock(); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use a scoped lock instead of a lock/unlock pattern. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I replaced the manual lock.unlock() pattern with a scoped block, so the mutex is now released automatically when the block ends. |
||
|
|
||
| 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<uint8_t>('A' + (key - Qt::Key_A)); | ||
| else if (key >= Qt::Key_0 && key <= Qt::Key_9) | ||
| ascii = static_cast<uint8_t>('0' + (key - Qt::Key_0)); | ||
| else if (key == Qt::Key_Space) | ||
| ascii = static_cast<uint8_t>(' '); | ||
|
Comment on lines
+124
to
+132
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. huh - is there really no built-in Qt function for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Qt provides QKeyEvent::text(), which gives the actual character generated by the key press while taking the current keyboard layout and modifiers into account. That turned out to be exactly what we needed here, so I replaced the manual Qt::Key_* to ASCII mapping with event->text().toLatin1(). |
||
|
|
||
| if (ascii != 0) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Zero checks aren't the best. If you insist on using this pattern, do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. I removed the sentinel 0 check and switched to std::optional<uint8_t>, so the presence or absence of a decoded character is now represented explicitly. |
||
| enqueueKey(ascii); | ||
| event->accept(); | ||
| } else { | ||
| event->ignore(); | ||
| } | ||
| } | ||
|
|
||
| void IOKeyboard::parameterChanged(unsigned) { | ||
| refreshRegMap(); | ||
| refreshStatusLabel(); | ||
| } | ||
|
|
||
| VInt IOKeyboard::ioRead(AInt offset, unsigned) { | ||
| if (offset == 0) { | ||
| QMutexLocker lock(&m_bufMutex); | ||
| const uint8_t val = m_keyBuffer.isEmpty() ? 0 : m_keyBuffer.dequeue(); | ||
| lock.unlock(); | ||
| QMetaObject::invokeMethod( | ||
| this, [this]() { refreshStatusLabel(); }, Qt::QueuedConnection); | ||
| return val; | ||
| } | ||
| if (offset == 4) { | ||
| QMutexLocker lock(&m_bufMutex); | ||
| return static_cast<VInt>(m_keyBuffer.size()); | ||
| } | ||
| return 0; | ||
|
Comment on lines
+148
to
+167
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a RegMap enum for the register offsets, replaced the if chain with a switch over KEY_DATA and KEY_STATUS, and moved to a single scoped QMutexLocker at the start of the function to avoid both magic constants and repeated locking logic. |
||
| } | ||
|
|
||
| void IOKeyboard::ioWrite(AInt offset, VInt value, unsigned) { | ||
| if (offset == 4 && value != 0) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use the enum register map value that you are going to define. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After introducing the RegMap enum, I updated the code to use RegMap::KEY_DATA and RegMap::KEY_STATUS everywhere instead of the raw offset values. |
||
| QMutexLocker lock(&m_bufMutex); | ||
| m_keyBuffer.clear(); | ||
| lock.unlock(); | ||
| QMetaObject::invokeMethod( | ||
| this, [this]() { refreshStatusLabel(); }, Qt::QueuedConnection); | ||
| } | ||
| } | ||
|
|
||
| void IOKeyboard::reset() { | ||
| { | ||
| QMutexLocker lock(&m_bufMutex); | ||
| m_keyBuffer.clear(); | ||
| m_lastKey = 0; | ||
| } | ||
| refreshStatusLabel(); | ||
| } | ||
|
|
||
| } // namespace Ripes | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| #pragma once | ||
|
|
||
| #include <QMutex> | ||
| #include <QQueue> | ||
| #include <QWidget> | ||
|
|
||
| #include "iobase.h" | ||
|
|
||
| QT_FORWARD_DECLARE_CLASS(QLabel); | ||
| QT_FORWARD_DECLARE_CLASS(QPushButton); | ||
|
|
||
| namespace Ripes { | ||
|
|
||
| class IOKeyboard : public IOBase { | ||
| Q_OBJECT | ||
|
|
||
| enum Parameters { BUFSIZE }; | ||
|
|
||
| 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<RegDesc> ®isters() const override { | ||
| return m_regDescs; | ||
| }; | ||
| virtual const std::vector<IOSymbol> *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<uint8_t> m_keyBuffer; | ||
| mutable QMutex m_bufMutex; | ||
| uint8_t m_lastKey = 0; | ||
|
|
||
| QLabel *m_statusLabel = nullptr; | ||
|
|
||
| std::vector<RegDesc> m_regDescs; | ||
| std::vector<IOSymbol> m_extraSymbols; | ||
| }; | ||
|
|
||
| } // namespace Ripes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be obvious, but i like to make things super obvious - can you write here that, as a prerequisite, users should instantiate a matrix and keyboard peripheral before running this program?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ve updated the header comment to explicitly state the prerequisite.
It now mentions that, before running the program, the user must add both the LED Matrix and Keyboard peripherals to the Ripes I/O tab, using the default identifiers (LED_MATRIX_0_* and KEYBOARD_0_*).