Built-in project · ESP32-C3-DevKitM-1

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.

Typewriter · ESP32-C3-DevKitM-1live0.000 s 0.00x
Click to open it in the editor
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · ESP32-C3-DevKitM-1
// 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.