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.
Teaching? Assign it to your class in one click.
Advanced About 25 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $27. On Mokxi they're free.
What you'll learn
- How a PS/2 keyboard sends bits with its own clock
- Reading a serial signal on a falling-edge interrupt
- Open-collector lines and pull-ups inside the device
- Keeping a text page in memory and redrawing a small LCD
- Powering a 5 V part from 3.3 V on an ESP32-C3
- Difficulty: Advanced
- Time: About 25 minutes
- Board: ESP32-C3-DevKitM-1
- Parts: 4
How it works
A PS/2 keyboard makes its own clock on pin 3 and sends data on pin 2. A helper library reads each bit on an interrupt, so the main loop never has to be fast. The keyboard's lines are open collector, so no extra resistors are needed.
At startup the sketch sends the keyboard a reset command and prints whether the self test passed. After that, loop asks for one key at a time.
The sketch keeps a two-line, 16-column page in memory. Typed characters go into it, and the LCD on pins 4 and 5 is redrawn. Backspace erases, Enter starts a new line, and a full bottom line scrolls up. Everything typed also goes to the Serial Monitor.
Parts list
4 parts, plus the jumper wires. They retail for about $27; on Mokxi they're free.
- 1 × ESP32-C3-DevKitM-1In Handheld Console Kit, Desk Buddy Kit.
- 1 × Full-size breadboard
- 1 × PS/2 keyboard
- 1 × Character LCD with I2C backpackIn Claw Machine Kit, Vending Machine Kit, Rhythm Game Kit.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| PS/2 keyboard pin VCC | ESP32-C3-DevKitM-1 pin 3V3 |
| Character LCD with I2C backpack pin VCC | ESP32-C3-DevKitM-1 pin 3V3 |
| PS/2 keyboard pin DATA | ESP32-C3-DevKitM-1 pin 2 |
| PS/2 keyboard pin CLK | ESP32-C3-DevKitM-1 pin 3 |
| PS/2 keyboard pin GND | ESP32-C3-DevKitM-1 pin GND |
| Character LCD with I2C backpack pin GND | ESP32-C3-DevKitM-1 pin GND |
| Character LCD with I2C backpack pin SDA | ESP32-C3-DevKitM-1 pin 4 |
| Character LCD with I2C backpack pin SCL | ESP32-C3-DevKitM-1 pin 5 |
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();
}
Code walkthrough
The key functions of the sketch, and what each one does.
putCharacter sketch.ino
Stores one character at the cursor and moves the cursor right. If the line is already full, it first starts a new line.
static void putCharacter(char c)
{
if (column >= COLUMNS) {
newline();
}
page[row][column] = c;
column++;
}newline sketch.ino
Moves to the start of the next line. On the bottom line there is nowhere to go, so it calls scroll.
static void newline()
{
if (row + 1 < LINES) {
row++;
column = 0;
} else {
scroll();
}
}scroll sketch.ino
Throws away the top line by copying the bottom line up, then blanks the bottom line and puts the cursor at its start.
/** 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;
}loop sketch.ino
Asks the keyboard for a key. Escape clears the page, and the left and right arrows move the cursor. Enter, backspace, tab and printable characters change the page and echo to serial. Then redraw updates the LCD.
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) {
// ... 29 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The Serial Monitor says no answer from the keyboard This project
Check that CLK goes to pin 3 and DATA to pin 2. They are easy to swap. - The LCD stays blank This project
The LCD backpack uses SDA on pin 4 and SCL on pin 5. Check its power and ground too. - Keys type nothing in the simulator This project
Click the keyboard on the canvas first so it takes your keystrokes. Escape hands it back to the editor. - No keys register at all. PS/2 keyboard
A PS/2 keyboard needs 5 V power and ground, with clock and data lines pulled up to 5 V (about 10 k). Use an interrupt-capable pin for clock. A 3.3 V board needs level shifting. - Wrong or doubled characters appear. PS/2 keyboard
Scan code set 2 sends a code on press and F0 followed by the code on release. Extended keys send E0 first. Use a PS/2 library to decode, rather than reading raw codes. - The screen stays blank or shows only a row of blocks. Character LCD with I2C backpack
The I2C address is usually 0x27 or 0x3F. Run an I2C scanner to find yours. Then turn the contrast trimmer on the backpack slowly until text appears. - The backlight is on but there is no text, or text is garbled. Character LCD with I2C backpack
Wire SDA and SCL to the right pins (A4 and A5 on an Uno), power it from 5 V, and call lcd.init() and the backlight call in setup. - Upload fails or the board is not found by the computer. ESP32-C3-DevKitM-1
Pick the ESP32-C3 board in your tools and the right port. If it still fails, hold BOOT, tap RESET, then release BOOT to enter download mode and try again.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.