Community project

Japanese Writing Practice Cards

ESP32
Photo of Japanese Writing Practice Cards
Generated with AI

Oscar Rodriguez

Published August 28, 2026

This project turns an ESP32 into an interactive flashcard system for learning Japanese hiragana characters. The device displays one character at a time on its built-in screen along with its reading and meaning, letting learners test themselves and mark cards as known or unknown to track progress.

Builders will receive a complete parts list, wiring diagram, and ready-to-flash firmware loaded with all 46 basic hiragana characters organized by group. The guide covers physical assembly, button configuration, and how to navigate through cards using simple button actions to practice at your own pace.

Wiring diagram

Assemble it in 2 steps

1. Set out the Paper Color

Place the MD5Stack Paper Color on a clean, dry desk with the screen facing up. This project uses the screen and the three buttons already built into the unit, so you do not need breadboards, loose wires, or extra electronic parts.

  • The buttons are along the bottom edge: A is left, B is middle, and C is right.
  • Keep drinks away from the board; liquid can damage it.

2. Learn the button actions

Use button B in the middle to show or hide the answer. Press button C on the right after you knew the answer, or button A on the left when you need more practice. Five correct answers in a row unlock the next, harder writing set.

  • Say the sound or meaning aloud before pressing B. This makes it a real recall exercise rather than only reading the answer.
  • Do not press the screen with sharp objects; the e-paper panel can be damaged.

Deploy the firmware

#include <Arduino.h>
#include <M5Unified.h>

struct Card {
  const char* glyph;
  const char* reading;
  const char* meaning;
  const char* group;
};


// Forward declarations
int unlockedCards();
void drawScreen();
void chooseNextCard();
void markKnown(bool known);

const Card cards[] = {
  // Basic hiragana: all 46 gojuon characters.
  {"あ", "a", "ah", "Hiragana"}, {"い", "i", "ee", "Hiragana"}, {"う", "u", "oo", "Hiragana"}, {"え", "e", "eh", "Hiragana"}, {"お", "o", "oh", "Hiragana"},
  {"か", "ka", "kah", "Hiragana"}, {"き", "ki", "kee", "Hiragana"}, {"く", "ku", "koo", "Hiragana"}, {"け", "ke", "keh", "Hiragana"}, {"こ", "ko", "koh", "Hiragana"},
  {"さ", "sa", "sah", "Hiragana"}, {"し", "shi", "shee", "Hiragana"}, {"す", "su", "soo", "Hiragana"}, {"せ", "se", "seh", "Hiragana"}, {"そ", "so", "soh", "Hiragana"},
  {"た", "ta", "tah", "Hiragana"}, {"ち", "chi", "chee", "Hiragana"}, {"つ", "tsu", "tsoo", "Hiragana"}, {"て", "te", "teh", "Hiragana"}, {"と", "to", "toh", "Hiragana"},
  {"な", "na", "nah", "Hiragana"}, {"に", "ni", "nee", "Hiragana"}, {"ぬ", "nu", "noo", "Hiragana"}, {"ね", "ne", "neh", "Hiragana"}, {"の", "no", "noh", "Hiragana"},
  {"は", "ha", "hah", "Hiragana"}, {"ひ", "hi", "hee", "Hiragana"}, {"ふ", "fu", "foo", "Hiragana"}, {"へ", "he", "heh", "Hiragana"}, {"ほ", "ho", "hoh", "Hiragana"},
  {"ま", "ma", "mah", "Hiragana"}, {"み", "mi", "mee", "Hiragana"}, {"む", "mu", "moo", "Hiragana"}, {"め", "me", "meh", "Hiragana"}, {"も", "mo", "moh", "Hiragana"},
  {"や", "ya", "yah", "Hiragana"}, {"ゆ", "yu", "yoo", "Hiragana"}, {"よ", "yo", "yoh", "Hiragana"},
  {"ら", "ra", "rah", "Hiragana"}, {"り", "ri", "ree", "Hiragana"}, {"る", "ru", "roo", "Hiragana"}, {"れ", "re", "reh", "Hiragana"}, {"ろ", "ro", "roh", "Hiragana"},
  {"わ", "wa", "wah", "Hiragana"}, {"を", "wo", "particle pronounced o", "Hiragana"}, {"ん", "n", "n", "Hiragana"},

  // Basic katakana: all 46 gojuon characters.
  {"ア", "a", "ah", "Katakana"}, {"イ", "i", "ee", "Katakana"}, {"ウ", "u", "oo", "Katakana"}, {"エ", "e", "eh", "Katakana"}, {"オ", "o", "oh", "Katakana"},
  {"カ", "ka", "kah", "Katakana"}, {"キ", "ki", "kee", "Katakana"}, {"ク", "ku", "koo", "Katakana"}, {"ケ", "ke", "keh", "Katakana"}, {"コ", "ko", "koh", "Katakana"},
  {"サ", "sa", "sah", "Katakana"}, {"シ", "shi", "shee", "Katakana"}, {"ス", "su", "soo", "Katakana"}, {"セ", "se", "seh", "Katakana"}, {"ソ", "so", "soh", "Katakana"},
  {"タ", "ta", "tah", "Katakana"}, {"チ", "chi", "chee", "Katakana"}, {"ツ", "tsu", "tsoo", "Katakana"}, {"テ", "te", "teh", "Katakana"}, {"ト", "to", "toh", "Katakana"},
  {"ナ", "na", "nah", "Katakana"}, {"ニ", "ni", "nee", "Katakana"}, {"ヌ", "nu", "noo", "Katakana"}, {"ネ", "ne", "neh", "Katakana"}, {"ノ", "no", "noh", "Katakana"},
  {"ハ", "ha", "hah", "Katakana"}, {"ヒ", "hi", "hee", "Katakana"}, {"フ", "fu", "foo", "Katakana"}, {"ヘ", "he", "heh", "Katakana"}, {"ホ", "ho", "hoh", "Katakana"},
  {"マ", "ma", "mah", "Katakana"}, {"ミ", "mi", "mee", "Katakana"}, {"ム", "mu", "moo", "Katakana"}, {"メ", "me", "meh", "Katakana"}, {"モ", "mo", "moh", "Katakana"},
  {"ヤ", "ya", "yah", "Katakana"}, {"ユ", "yu", "yoo", "Katakana"}, {"ヨ", "yo", "yoh", "Katakana"},
  {"ラ", "ra", "rah", "Katakana"}, {"リ", "ri", "ree", "Katakana"}, {"ル", "ru", "roo", "Katakana"}, {"レ", "re", "reh", "Katakana"}, {"ロ", "ro", "roh", "Katakana"},
  {"ワ", "wa", "wah", "Katakana"}, {"ヲ", "wo", "particle pronounced o", "Katakana"}, {"ン", "n", "n", "Katakana"},

  {"日", "nichi", "sun / day", "Kanji"}, {"月", "getsu", "moon / month", "Kanji"},
  {"人", "hito", "person", "Kanji"}, {"山", "yama", "mountain", "Kanji"},
  {"水", "mizu", "water", "Kanji"}, {"木", "ki", "tree", "Kanji"}
};
const int CARD_COUNT = sizeof(cards) / sizeof(cards[0]);

int cardIndex = 0;
int score = 0;
int streak = 0;
int level = 1;
bool answerVisible = false;
String statusText = "Press B to reveal";

int unlockedCards() {
  // Level 1 is the complete basic hiragana chart; level 2 adds katakana.
  if (level == 1) return 46;
  if (level == 2) return 92;
  return CARD_COUNT;
}

void drawScreen() {
  const Card &card = cards[cardIndex];
  M5.Display.fillScreen(WHITE);
  M5.Display.setTextColor(BLACK, WHITE);
  M5.Display.setTextDatum(top_left);
  M5.Display.setTextSize(1);
  M5.Display.setTextFont(2);
  M5.Display.drawString("JAPANESE LEARNING CHEATSHEET", 18, 16);
  M5.Display.drawFastHLine(18, 43, M5.Display.width() - 36, BLACK);

  M5.Display.setTextFont(4);
  M5.Display.drawString(String(card.group) + "  |  Level " + String(level), 24, 60);
  M5.Display.drawString("Score: " + String(score) + "   Streak: " + String(streak), 24, 100);

  M5.Display.setTextDatum(middle_center);
  M5.Display.setTextFont(8);
  M5.Display.drawString(card.glyph, M5.Display.width() / 2, 245);

  M5.Display.setTextFont(4);
  if (answerVisible) {
    M5.Display.drawString("Reading: " + String(card.reading), M5.Display.width() / 2, 375);
    M5.Display.drawString("Meaning: " + String(card.meaning), M5.Display.width() / 2, 415);
  } else {
    M5.Display.drawString("Say the sound or meaning aloud", M5.Display.width() / 2, 395);
  }

  M5.Display.setTextDatum(middle_center);
  M5.Display.setTextFont(2);
  M5.Display.drawString(statusText, M5.Display.width() / 2, 500);
  M5.Display.drawFastHLine(18, 535, M5.Display.width() - 36, BLACK);
  M5.Display.drawString("A: Not yet        B: Reveal        C: I knew it", M5.Display.width() / 2, 565);
}

void chooseNextCard() {
  int available = unlockedCards();
  cardIndex = (cardIndex + 1 + (esp_random() % (available - 1))) % available;
  answerVisible = false;
}

void markKnown(bool known) {
  if (known) {
    score++;
    streak++;
    if (streak > 0 && streak % 5 == 0 && level < 3) {
      level++;
      statusText = "Great work! A harder set is unlocked.";
    } else {
      statusText = "Correct — next card";
    }
  } else {
    streak = 0;
    statusText = "No problem — this card will come around again.";
  }
  chooseNextCard();
  drawScreen();
}

void setup() {
  auto config = M5.config();
  M5.begin(config);
  M5.Display.setRotation(1);
  drawScreen();
}

void loop() {
  M5.update();
  if (M5.BtnA.wasPressed()) markKnown(false);
  if (M5.BtnB.wasPressed()) {
    answerVisible = !answerVisible;
    statusText = answerVisible ? "Check the answer, then grade yourself." : "Press B to reveal";
    drawScreen();
  }
  if (M5.BtnC.wasPressed()) markKnown(true);
  delay(10);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik