Typewriter on an ESP32-C3
A PS/2 keyboard read on an interrupt and printed on a 16x2 LCD: type on it for real and watch the scan codes become letters. The whole build, an ESP32-C3 and 2 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
AdvancedRuns in your browser. Free, and no account needed.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Typewriter: a PS/2 keyboard and a 16x2 LCD. Type, and it prints.
//
// Wiring, board to modules:
// keyboard CLK -> GPIO 3 DATA -> GPIO 2 VCC -> 3V3 GND -> GND
// LCD backpack SCL -> GPIO 5 SDA -> GPIO 4 VCC -> 3V3 GND -> GND
//
// Click the keyboard on the canvas while this is running and your own
// keystrokes become scan codes on the wire. Escape hands the keyboard back to
// the editor. Everything typed also goes out of the serial port, so the monitor
// at the bottom of the window is a second copy of the page.
//
// Backspace rubs out the character before the cursor, Enter starts a new line,
// and when the bottom line fills the top one is thrown away and everything
// moves up, which is the whole of what a two-line screen can do for a page.
//
// # Two wires and an interrupt
//
// A PS/2 keyboard generates its own clock and the host reads DATA on the
// falling edge of it, so the receiving half of this sketch is one interrupt
// handler inside mokxi_ps2.h and nothing in loop() has to be quick. Both lines
// are open collector with the pull-ups inside the keyboard, which is why the
// two GPIOs are plain inputs and there is not a resistor on the board.
//
// # Talking to the keyboard
//
// setup() sends the Reset command, 0xFF, and waits for the two bytes that
// answer it: 0xFA to say the keyboard heard, and 0xAA when its self test
// passes about six hundred milliseconds later. That is the host-to-device half
// of PS/2 (hold the clock down, pull the data down, let the clock go, and the
// keyboard clocks eleven bits in and acknowledges with a twelfth), and the
// serial monitor says whether it worked. A keyboard that is not wired up says
// so instead of the sketch pretending.
//
// # Supply
//
// A PS/2 keyboard is nominally a 5 V part. This one is wired to 3V3 because an
// ESP32-C3's pins are not 5 V tolerant and the model works down to 3 V; see
// /docs/parts/ps2keyboard, which says so plainly rather than leaving it to be
// found out.
#include "mokxi_lcd1602_i2c.h"
#include "mokxi_ps2.h"
const uint8_t CLK_PIN = 3;
const uint8_t DATA_PIN = 2;
const uint8_t SCL_PIN = 5;
const uint8_t SDA_PIN = 4;
Ps2Keyboard keyboard(CLK_PIN, DATA_PIN);
// The display goes on the PCF8574 backpack rather than six data lines: the
// keyboard has taken two pins already and a two-wire bus next to a two-wire
// keyboard is the tidier board.
Lcd1602I2c lcd(SCL_PIN, SDA_PIN);
const uint8_t COLUMNS = 16;
const uint8_t LINES = 2;
/** The page, as it stands. Two lines of sixteen and a cursor. */
char page[LINES][COLUMNS + 1];
uint8_t row = 0;
uint8_t column = 0;
static void blankLine(uint8_t line)
{
for (uint8_t i = 0; i < COLUMNS; i++) {
page[line][i] = ' ';
}
page[line][COLUMNS] = '\0';
}
static void redraw()
{
for (uint8_t line = 0; line < LINES; line++) {
lcd.setCursor(0, line);
lcd.print(page[line]);
}
lcd.setCursor(column, row);
}
/** Throw the top line away and move everything up one. */
static void scroll()
{
for (uint8_t i = 0; i < COLUMNS; i++) {
page[0][i] = page[1][i];
}
blankLine(1);
row = 1;
column = 0;
}
static void newline()
{
if (row + 1 < LINES) {
row++;
column = 0;
} else {
scroll();
}
}
static void putCharacter(char c)
{
if (column >= COLUMNS) {
newline();
}
page[row][column] = c;
column++;
}
static void backspace()
{
if (column > 0) {
column--;
} else if (row > 0) {
row--;
column = COLUMNS - 1;
} else {
return;
}
page[row][column] = ' ';
}
void setup()
{
Serial.begin(115200);
lcd.begin();
lcd.cursor(true);
blankLine(0);
blankLine(1);
lcd.setCursor(0, 0);
lcd.print("PS/2 typewriter");
lcd.setCursor(0, 1);
lcd.print("resetting...");
keyboard.begin();
bool ready = keyboard.reset();
Serial.println(ready ? "keyboard: self test passed" : "keyboard: no answer, check CLK and DATA");
Serial.println("type on the keyboard; the LCD is the page");
lcd.clear();
redraw();
}
void loop()
{
int key = keyboard.readKey();
if (key == PS2_NONE) {
return;
}
if (key == PS2_ESCAPE) {
// A fresh page, which is what a typewriter's Escape may as well do.
blankLine(0);
blankLine(1);
row = 0;
column = 0;
Serial.println();
Serial.println("-- new page --");
lcd.clear();
redraw();
return;
}
if (key == PS2_LEFT) {
if (column > 0) {
column--;
}
lcd.setCursor(column, row);
return;
}
if (key == PS2_RIGHT) {
if (column + 1 < COLUMNS) {
column++;
}
lcd.setCursor(column, row);
return;
}
if (key >= 0x100) {
return; // a key with no character on it: nothing to print
}
char c = (char)key;
if (c == '\r') {
newline();
Serial.println();
} else if (c == '\b') {
backspace();
Serial.print("\b \b");
} else if (c == '\t') {
for (uint8_t i = 0; i < 4; i++) {
putCharacter(' ');
}
Serial.print(" ");
} else if (c >= ' ' && c < 127) {
putCharacter(c);
Serial.write(c);
} else {
return;
}
redraw();
}
Parts list
4 parts, plus the jumper wires. Every one is in the editor's parts bin.
How it is wired
6 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- ESP32-C3-DevKitM-1 pin 3V3; PS/2 keyboard pin VCC; Character LCD with I2C backpack pin VCC
- ESP32-C3-DevKitM-1 pin 2; PS/2 keyboard pin DATA
- ESP32-C3-DevKitM-1 pin 3; PS/2 keyboard pin CLK
- Ground: ESP32-C3-DevKitM-1 pin GND; PS/2 keyboard pin GND; Character LCD with I2C backpack pin GND
- ESP32-C3-DevKitM-1 pin 4; Character LCD with I2C backpack pin SDA
- ESP32-C3-DevKitM-1 pin 5; Character LCD with I2C backpack pin SCL
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.