Arduino Nano R4 と1.8インチLCDとDS3231で三毛猫時計を作ってみた

表示デバイス開発のPoCとして、画像表示・RTC連携・GUI描画技術の検証を行うという想定で、今回も「エレクトロニクスを簡単に」をテーマに、Arduino Nano R4と、1.8インチ128×160 RGB TFT LCD (ST7735)、そして高精度RTCのDS3231を使って、机の上に置きたくなる「かわいい三毛猫時計」を作ってみました。
Arduino Nano R4に搭載される Renesas RA4M1は、組み込み機器のUI表示やセンサ連携にも適しており、本作例はルネサスマイコン活用の入門事例としても有効です。

大きな時刻表示に加えて、青空を背景にした三毛猫のイラスト、曜日・日付・気温の同時表示、電源を切っても時刻を覚えているRTCという、少し凝った専用デバイスに仕上げています。

目次

はじめに:どんな困りごとに対するソリューションなのか

「時計」は単なる時間表示ではなく、作業のリズムを作るためのデスクツールとして役立ちます。

特に次のような困りごとに対して効果があります。

  • スマホを開かずに時間・日付・室温を知りたい

  • PC作業に集中していても、一瞬で時刻が確認できるようにしたい

  • デスクのインテリアに馴染む、見ていて“かわいい”時計が欲しい

  • 電源が落ちても、時刻の再設定をやり直したくない

  • 表示が単調ではなく、少し温度を感じるデバイスが欲しい

つまり今回のソリューションは、

「電源を切っても狂わない正確な時刻が欲しい」
「デスクに置きたくなる見た目のデバイスが欲しい」
「デスクに置きたくなる見た目のデバイスが欲しい」

という困りごとに対するものです。

今回のソリューション概要

今回作るのは、Arduino Nano R4 + 1.8インチTFT LCD (ST7735) + DS3231 RTC で構成する三毛猫背景のかわいいデジタル時計です。

本記事では以下を実現します。

  1. DS3231 RTCから正確な時刻を取得(電池バックアップ対応)

  2. ST7735 TFTに三毛猫のイラスト背景+大きな時刻表示を描画

  3. 曜日・日付・気温の同時表示

  4. RTCが読めなくても内部カウンタで動作するフォールバック

  5. コンパイル時刻による自動初期化

なぜArduino Nano R4が向いているのか

今回のテーマに Arduino Nano R4 が向いている理由は、何より処理性能と手軽さのバランスです。

公式ドキュメントによると、Arduino Nano R4は Renesas RA4M1 (Arm Cortex-M4 / 48MHz / 256kB Flash / 32kB SRAM / 8KB EEPROM) を搭載した、Nanoフォームファクタの新しいボードです。USB-C給電・書き込みに対応し、QwiicコネクタやCAN、DAC、オペアンプまで搭載しています。

時計程度の処理ならスペック的には余裕で、むしろ

  • Nanoサイズでブレッドボードにそのまま挿せる

  • USB Type-Cで給電・書き込み・シリアルが1本で済む

  • 5V動作で扱いやすい (TFTやRTCなどの汎用モジュールと親和性が高い)

  • 256kBのFlashがあるので、128×160ドットのフルカラー背景画像をPROGMEMに置ける

という手軽さが強みです。特に最後の点はポイントで、AVR系NanoではFlash容量的にフルカラー背景画像を持たせるのが厳しいのですが、Nano R4なら約60KBの32色パレット背景画像を余裕で内蔵できます。

開発環境と実装のポイント

開発環境には Visual Studio Code と PlatformIO IDE を使用しました。PlatformIOでは、board = nano_r4, platform = renesas-ra, framework = arduino の構成で利用できます。

背景画像をPROGMEM に持たせる

三毛猫のイラストは、32色パレット+インデックスカラーという形式で cat_frames.h に格納し、PROGMEM (Flash) に置いています。

  • パレット: uint16_t CAT_PALETTE[32] (64バイト、RGB565)

  • 画像本体: uint8_t CAT_STATIC[160][128] (20,480バイト、パレット参照インデックス)

こうすることで、フルカラー (uint16_t 直接) だと40KB必要なところを約20KBに半減でき、Nano R4のFlash 256KB に余裕で収まります。

透過テキスト描画 (背景を潰さない)

背景画像がある状態で単純に fillRect で消して文字を描くと、四角い塗り潰しが背景に浮いて見えて汚くなります。そこで今回は、

  • 文字を書き直したい領域だけ、PROGMEMの背景画像から画素を復元

  • その上に透過 (背景色を指定しない) モードで文字を描画

という手順にしています。これにより、背景の三毛猫のイラストを潰さずに時刻・日付・気温を重ねる表示が実現できます。

RTCが読めなくても止まらないフォールバック

RTCモジュールの配線ミスや電池切れで通信できなくなるケースに備え、今回は millis() ベースの内部時計をバックアップとして持たせています。

  • 起動時に rtc.begin() を試し、成功したらRTC値を採用

  • 失敗した場合、または読み出し値が不正(年<2024、分≥60など) だった場合は、コンパイル時刻を起点に millis() で内部時計を進める

これでRTCが無くても時計として動く耐障害性を持たせています。

使用した主な部品

  • Arduino Nano R4 (ピンヘッダ実装済み)

  • 1.8インチ 128×160 TFT LCDモジュール (ST7735コントローラ)

  • DS3231 RTCモジュール (CR2032 コイン電池付き)

  • ブレッドボード

  • ジャンパーワイヤ

  • USB Type-C ケーブル

配線情報

部品

信号

接続先(Nano R4)

補足

TFT

VCC

5V

モジュールによっては3V3対応

TFT

GND

GND

TFT

LED

3V3

バックライト (PWM調光可能)

TFT

SCK

D13

SPI クロック

TFT

SDA

D11

SPI MOSI

TFT

DC

D9

データ/コマンド切替

TFT

RST

D8

リセット

TFT

CS

D10

チップセレクト

DS3231

VCC

5V

モジュールのプルアップ対応

DS3231

GND

GND

DS3231

SDA

A4

I2C データ

DS3231

SCL

A5

I2C クロック

配線のポイント

  • SPIはNano R4のハードウェアSPIピン (D13 = SCK / D11 = SDA) を使います。CS / DC / RST は任意のデジタルピンでOKです。

  • I2Cは A4 (SDA) と A5 (SCL) を使います。Nano R4はQwiicコネクタ (3.3V I2C) も備えているため、Qwiic対応センサならそちらに差すだけでも動きます。

  • 1.8インチ ST7735の表示オフセット: 128×160モジュールでは初期状態で表示が2px程度ずれることがあります。今回のコードでは setColRowStart(2, 1) でオフセットを補正しています。

実際のプログラム

今回使用したプログラムは以下の通りです。
本プログラムは、main.cpp と背景画像を持つ cat_frames.h の2ファイル構成です。
※本プログラムは、生成AIを用いて作成しております。

main.cpp

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <RTClib.h>
#include "cat_frames.h"

// ピン
#define TFT_CS   10
#define TFT_DC    9
#define TFT_RST   8

// setColRowStart を公開 (1.8" 128x160 のオフセット補正用)
class MyTFT : public Adafruit_ST7735 {
public:
  using Adafruit_ST7735::Adafruit_ST7735;
  void setOffset(int8_t c, int8_t r) { setColRowStart(c, r); }
};

MyTFT tft = MyTFT(TFT_CS, TFT_DC, TFT_RST);
RTC_DS3231 rtc;
bool rtcok = false;

//---- 色 (RGB565) ----
const uint16_t WHITE = 0xFFFF, GRAY = 0x8410, GOLD = 0xFD60, MINT = 0x3FEF;
const int SCALE = 4;
const int DX[] = {17, 39, 71, 93};
const int DY = 24, COLX = 61;

//---- 5x7 フォント (0-9) ----
const uint8_t FONT[10][7] = {
  {0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E},
  {0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E},
  {0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F},
  {0x0E, 0x11, 0x01, 0x02, 0x01, 0x11, 0x0E},
  {0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02},
  {0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E},
  {0x0E, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x0E},
  {0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08},
  {0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E},
  {0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C}
};

const char *WEEK[7] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};

//---- 内部時計 (RTC無しでもmillis()で進む) ----
uint32_t baseEpoch = 0;
unsigned long baseMs = 0;

DateTime nowTime()
{
  if (rtcok) {
    DateTime t = rtc.now();
    if (t.year() >= 2024 && t.month() >= 1 && t.month() <= 12 &&
        t.minute() < 60 && t.second() < 60 && t.hour() < 24) return t;
  }
  return DateTime(baseEpoch + (millis() - baseMs) / 1000);
}

void setNow(DateTime t) {
  baseEpoch = t.unixtime();
  baseMs = millis();
  if (rtcok) rtc.adjust(t);
}

//---- 背景描画 ----
void drawBg()
{
  uint16_t buf[CAT_W];
  for (int y = 0; y < CAT_H; y++) {
    for (int x = 0; x < CAT_W; x++)
      buf[x] = pgm_read_word(&CAT_PALETTE[pgm_read_byte(&CAT_STATIC[y][x])]);
    tft.drawRGBBitmap(0, y, buf, CAT_W, 1);
  }
}

// 背景から矩形を復元 (透過テキスト用)
void restore(int x0, int y0, int w, int h)
{
  uint16_t buf[CAT_W];
  for (int y = y0; y < y0 + h; y++) {
    for (int x = x0; x < x0 + w; x++)
      buf[x - x0] = pgm_read_word(&CAT_PALETTE[pgm_read_byte(&CAT_STATIC[y][x])]);
    tft.drawRGBBitmap(x0, y, buf, w, 1);
  }
}

//---- UI 描画 ----
void drawDigit(int i, int v)
{
  for (int r = 0; r < 7; r++)
    for (int c = 0; c < 5; c++)
      if (FONT[v][r] & (0x10 >> c))
      {
        tft.fillRect(DX[i] + c * SCALE + 1, DY + r * SCALE + 1, SCALE, SCALE, GRAY);
        tft.fillRect(DX[i] + c * SCALE,     DY + r * SCALE,     SCALE, SCALE, WHITE);
      }
}

void drawColon(bool on)
{
  restore(COLX, DY + 6, 8, 21);
  if (on) {
    tft.fillRect(COLX + 3, DY + 7,  4, 4, GRAY);
    tft.fillRect(COLX + 3, DY + 19, 4, 4, GRAY);
    tft.fillRect(COLX + 2, DY + 6,  4, 4, GOLD);
    tft.fillRect(COLX + 2, DY + 18, 4, 4, GOLD);
  }
}

void drawTime(DateTime t)
{
  restore(15, 22, 100, 32);
  drawDigit(0, t.hour() / 10);
  drawDigit(1, t.hour() % 10);
  drawDigit(2, t.minute() / 10);
  drawDigit(3, t.minute() % 10);
}

void drawText(int x, int y, uint16_t c, const char *s)
{
  tft.setTextSize(1);
  tft.setTextColor(GRAY); tft.setCursor(x + 1, y + 1); tft.print(s);
  tft.setTextColor(c);    tft.setCursor(x, y);         tft.print(s);
}

void drawDate(DateTime t)
{
  char b[16];
  sprintf(b, "%02d.%02d %s", t.month(), t.day(), WEEK[t.dayOfTheWeek()]);
  restore(31, 56, 76, 10);
  drawText(33, 57, WHITE, b);
}

void drawTemp()
{
  float c = rtcok ? rtc.getTemperature() : 25.0f;
  int i = (int)c, f = (int)((c - i) * 10.0f + 0.5f);
  if (f >= 10) { i++; f = 0; }
  char b[8];
  sprintf(b, "%d.%dC", i, f);
  restore(94, 65, 34, 10);
  drawText(96, 66, MINT, b);
}

DateTime compileTime()
{
  const char *M[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
  String d = __DATE__, tt = __TIME__;
  int m = 1;
  for (int i = 0; i < 12; i++) if (d.substring(0, 3) == M[i]) { m = i + 1; break; }
  return DateTime(d.substring(7).toInt(), m, d.substring(4, 6).toInt(),
                  tt.substring(0, 2).toInt(), tt.substring(3, 5).toInt(), tt.substring(6, 8).toInt());
}

unsigned long lastTick = 0;
int8_t lastMin = -1, lastDay = -1;
bool colonOn = true;

void setup()
{
  tft.initR(INITR_BLACKTAB);
  tft.setOffset(2, 1); // 1.8" 128x160 のオフセット補正
  tft.setRotation(0);

  rtcok = rtc.begin();
  setNow(compileTime());
  if (rtcok) {
    DateTime rt = rtc.now();
    if (rt.year() >= 2024 && rt.month() >= 1 && rt.month() <= 12 && rt.minute() < 60) {
      baseEpoch = rt.unixtime();
      baseMs = millis();
    }
  }

  drawBg();
}

void loop()
{
  unsigned long now = millis();
  if (now - lastTick < 1000) return;
  lastTick = now;

  DateTime t = nowTime();
  if (t.minute() != lastMin) {
    lastMin = t.minute();
    drawTime(t);
    drawTemp();
  }
  if (t.day() != lastDay) {
    lastDay = t.day();
    drawDate(t);
  }

  colonOn = !colonOn;
  drawColon(colonOn);
}

cat_frames.h (背景画像データ)

Pythonで32色パレット+インデックスカラーに変換し、PROGMEM 配列としてヘッダファイル化しています。データ量が大きいため (約60KB)、記事本文には概要のみ記載します。
※画像、画像データの生成は生成AIを用いて実施しました。

cpp

#ifndef CAT_FRAMES_H
#define CAT_FRAMES_H

#include <Arduino.h>

#define CAT_W 128
#define CAT_H 160

// 32色パレット (RGB565)
const uint16_t CAT_PALETTE[32] PROGMEM = {
  0x7DC5, 0xFD60, ... // 空色、金色、猫の三毛カラーなど計32色
};

// 128x160 のインデックスカラー画像
const uint8_t CAT_STATIC[CAT_H][CAT_W] PROGMEM = {
  { 0, 0, 0, ..., 0 }, // y=0 行
  ...
  { 5, 5, 3, ..., 12 }, // y=159 行
};

#endif

画像データの生成は、任意のイラストPNGを Pillow の quantize(colors=32, method=MEDIANCUT) で減色し、パレットとインデックスを出力するPythonスクリプトで自動化しています。

PlatformIO設定例

ini

[env:nano_r4]
platform = renesas-ra
board = nano_r4
framework = arduino
monitor_speed = 115200
upload_protocol = dfu
lib_deps =
    adafruit/Adafruit GFX Library
    adafruit/Adafruit ST7735 and ST7789 Library
    adafruit/RTClib

※Nano R4はPlatformIOのRenesas RAプラットフォームで nano_r4 として認識され、書き込みプロトコルはデフォルトで dfu です。

ライブラリの既知の問題への対処

Adafruit ST7735 and ST7789 Library の一部バージョンでは、Adafruit_ST77xx.cpp にAVR系専用の #include <wiring_private.h> が残っており、Nano R4 (Renesas RA) ではコンパイルエラーになることがあります。

その場合は、以下の1行をコメントアウトするか、条件付きインクルードにするだけでビルドが通ります。
・Adafruit_ST77xx.cpp ファイルの場所
.pio\libdeps\nano_r4\Adafruit ST7735 and ST7789 Library\Adafruit_ST77xx.cpp

// #include <wiring_private.h>

プログラムのポイント

  • Adafruit_ST7735 で1.8インチ128×160 TFT に直接描画しています。

  • RTClib でDS3231 から時刻を読み出し、getTemperature() で気温も取得しています。

  • Adafruit_ST7735 の setColRowStart() を継承クラスで公開し、setOffset(2, 1) で1.8インチモジュール特有の表示オフセットを補正しています。

  • 5×7ドットの自作フォントを4倍拡大し、既存フォントでは出せない大きな時刻表示を実現しています。

  • 32色パレット+インデックスカラーの背景画像をPROGMEM に格納し、drawRGBBitmap() で行単位に描画しています。

  • 透過テキスト描画 (背景画像を矩形単位で復元→上に文字)で、背景の三毛猫のイラストを潰さずに情報を重ねています。

  • millis() ベースの内部時計をフォールバックとして持ち、RTCが読めなくても時計として動作します。

動作確認

プログラムを書き込んで起動すると、三毛猫の背景画像が全画面に描画され、その上に時刻・日付・気温が透過表示されます。

画面の見え方

  • 上部: 青空の背景に、白+グレー影の大きな HH:MM 表示と、ゴールドのコロン点滅

  • 中央: 白文字で 09.09 WED (日付+曜日)、右側にミント色の 25.0C (気温)

  • 下部: 縁側に座る三毛猫と、隣に瓦屋根のイラスト

時刻の初期化

  • RTCが正常に動いている場合: RTCの値をそのまま採用

  • RTCが電池切れ・未接続の場合: コンパイル時刻を起点に millis() で内部時計を進めます

RTCの時刻を書き換えたい場合は、発展編で紹介している pollSerial() を組み込み、シリアルモニタから以下の形式で日時を入力します。

入力例:
2026-09-09 16:30:00

これにより、PCやスマホに頼らず、机の上ですべての情報を眺められる専用時計が完成します。

発展編:アニメーションや調光への拡張

背景画像+時刻・日付・気温だけでも十分楽しめる時計ですが、実際に使い込むなら次のような発展が考えられます。

  • 猫のあくびアニメーション
    背景の三毛猫を口だけパラパラアニメーションさせ、25~70秒ごとにランダムであくびする演出。口の矩形だけを差し替える方式なら、上半分の空や右下の草はまったく動かず、猫だけが動く自然な演出になります。

  • アラーム機能
    DS3231はアラーム機能 (ALARM1/ALARM2) を内蔵しており、設定時刻に割り込みを発生させられます。ブザーを追加して「時刻お知らせ」に使えます。

  • 自動調光
    フォトレジスタ (CdSセル) を追加して、暗い時にバックライト (TFT_BL ピンをPWM制御)を落とすと、夜間でも眩しくない時計になります。

  • 日本語フォント表示
    Adafruit GFX標準フォントは英数字のみです。日本語表示には、Shift-JIS対応のフルカラーフォントを埋め込む拡張が必要です。

  • 背景差し替え
    cat_frames.h を書き換えるだけで、季節ごと・時間帯ごとに背景を変えることもできます (春は桜、夜は月夜など)。

  • Wi-Fi連携
    Nano R4にはWi-Fiがありませんが、ESP32-S3等をSPI/UARTで接続すればNTP同期やスマホ通知、クラウド連携に広げられます。

  • シリアル通信による時刻設定
    RTCは初回書き込み時にコンパイル時刻で初期化されますが、運用中に日時を変更したい場合は、シリアル通信から時刻を設定することもできます。
    例えば、シリアルモニタから以下の形式で日時を送信します。
    「2026-09-09 16:30:00」
    この文字列を解析し、RTCへ反映する関数が pollSerial() です。
    ループ処理内で定期的に呼び出しておけば、パソコンのシリアルモニタから日時を書き換えられます。

void loop()
{
  pollSerial();
  updateDisplay();
  delay(1000);
}

実際の実装例は次のようになります。

void pollSerial()
{
  if (!Serial.available())
    return;

  String s = Serial.readStringUntil('\n');
  s.trim();
  int yy, mm, dd, hh, mi, ss;
  if (sscanf(s.c_str(), "%d-%d-%d %d:%d:%d", &yy, &mm, &dd, &hh, &mi, &ss) == 6)
  {
    rtc.adjust(DateTime(yy, mm, dd, hh, mi, ss));
    Serial.println("RTC updated.");
  }
}

関連技術参考トピック

今回はArduino Nano R4に搭載されているRenesas RA4M1マイコンを利用しました。
今回の時計は比較的シンプルな表示アプリケーションですが、今後Wi-Fi接続やGUI機能の拡張、AI連携などを検討する場合には、Renesas RAファミリやRZファミリへの展開も考えられます。
特にRZシリーズは高性能MPUとしてLinuxや画像処理アプリケーションにも活用できるため、表示機器やエッジデバイス開発の有力な選択肢になります。

まとめ

今回は、Arduino Nano R4と1.8インチ128×160 TFT LCD (ST7735)、DS3231 RTCを使って、机の上に置きたくなる「かわいい三毛猫時計」を作ってみました。

今回のテーマは、単に時刻を表示することではなく、

  • スマホやPCに頼らない専用の時計が欲しい

  • 時刻・日付・気温を一画面で確認したい

  • 電源を切っても時刻が狂わないようにしたい

  • デスクのインテリアに馴染む、見ていて“かわいい”デバイスにしたい

という困りごとに対して、シンプルだけれど実用的な可視化PoC を作ることにあります。

構成自体はシンプルですが、

  • Nano R4 + TFT + RTCという最小構成

  • 32色パレットのフルカラー背景画像と透過テキスト描画

  • 自作フォントによる大きな時刻表示

  • 曜日・日付・気温の同時表示

  • RTC + millis() フォールバックによる堅牢な計時

という流れを通して、“ただの時計”を少し楽しく、少しかわいくする題材になりました。

PCやスマホの時計よりも、机の上で静かに時間の流れを教えてくれる専用デバイスとして使いたい時に、ちょうどよいテーマだと思います。

参考リンク