はじめに:本格的なネットワーク時計を作ろう

電子工作の定番プロジェクトであるデジタル時計。今回は、ESP32とプリント基板を使って、本格的なネットワーク対応デジタル時計を作成します。

本記事は2部構成の第1弾で、回路設計からプリント基板の発注までを解説します。

このプロジェクトの特徴

ネットワーク時刻同期: NTPサーバーから自動で正確な時刻を取得
RTC搭載: 電源オフ時も時刻を保持(バックアップ電池使用)
大型ディスプレイ: 視認性抜群の7セグ風LCDパネル6枚使用
I/Oエキスパンダー活用: MCP23017でピン数不足を解決
プリント基板化: ブレッドボード卒業!美しい仕上がり
目覚まし機能: ブザー+ランプで実用的な時計に

プロジェクト構成

  • 第1弾(本記事): 回路設計 → KiCadでPCB設計 → JLCPCB発注
  • 第2弾: 基板実装 → ケース作成 → 完成

それでは、ワクワクする基板設計の世界へ!


完成イメージと目標仕様

まずは、どんな時計を作るのかイメージを共有しましょう。

完成した時計の様子

時刻が正確に表示され、大型ディスプレイで視認性も抜群です!

目標仕様

項目 仕様
時刻取得 NTPサーバー経由で自動取得(Wi-Fi接続)
時刻保持 RTC-8564NB(I2C接続)でバックアップ
表示デバイス 7セグ風LCDパネル TZ-250A × 6枚
マイコン ESP32-WROOM-32(Wi-Fi/Bluetooth搭載)
I/O拡張 MCP23017 I/Oエキスパンダー × 3個
追加機能 目覚まし用ブザー+24Vランプ(リレー制御)
設置場所 壁掛け対応

なぜブレッドボードではダメなのか?

プロトタイプ段階では、こんな感じのブレッドボード実装でした:

配線むき出しの試作機(改善前)

配線むき出しの試作機(改善前)

問題点:

  • 配線がごちゃごちゃで見た目が悪い
  • 接触不良が起きやすい
  • 壁掛けできない
  • 長期使用に不向き

プリント基板化のメリット:

  • ✅ コンパクトで美しい仕上がり
  • ✅ 信頼性の高い接続
  • ✅ 実用的な製品レベルに
  • ✅ 基板設計スキルの習得

さあ、本格的な製作に入りましょう!


必要な部品と工具

電子部品リスト

部品名 個数 概算価格(2026年) 購入先
LCD表示ユニット TZ-250A 6 ¥3,000~(中古) ORIGINAL MIND
ESP32-WROOM-32 開発ボード 1 ¥600~ 秋月電子 / Amazon
RTCモジュール RTC-8564NB 1 ¥280 秋月電子
I2C I/Oエキスパンダー MCP23017 3 ¥120 × 3 = ¥360 秋月電子
DIPソケット 28ピン 3 ¥50 × 3 = ¥150 秋月電子
ボタン電池 CR2032 1 ¥100 コンビニ/100円ショップ
24Vリレーモジュール 1 ¥300~ Amazon
圧電ブザー 1 ¥100 秋月電子
抵抗・コンデンサセット 適量 ¥500~ 秋月電子
プリント基板(JLCPCB発注) 5枚 $7.90~ JLCPCB

合計概算: 約¥5,000~7,000(LCD中古価格による)

ソフトウェア・ツール

ツール バージョン 用途
Arduino IDE 2.3以降 プログラム開発
KiCad 8.0以降 プリント基板設計
必要なArduinoライブラリ - Adafruit MCP23017、Wire、WiFi

💡 ヒント: ESP32開発環境がまだの方は、先にESP32開発環境構築ガイドをご覧ください。

このプロジェクトの主役:大型LCDパネル

視認性抜群の7セグ風LCDパネル TZ-250A

視認性抜群の7セグ風LCDパネル TZ-250A

TZ-250Aの特徴:

  • 大きさ: 約60mm × 90mm(1枚あたり)
  • 表示: 7セグメント風の大型ディスプレイ
  • 制御: 4bitパラレル通信+制御信号
  • 消費電力: 非常に低消費電力

このLCDパネルを6枚使用して、HH:MM:SS(時:分:秒)を表示します。

入手について:
オリジナルマインドなどで中古品として販売されることがあります。在庫が不安定なため、見つけたらチャンス!代替として、MAX7219制御の7セグLEDモジュールも使用可能です(回路は要変更)。


システム構成の理解

プリント基板設計の前に、システム全体の構成を理解しましょう。

ブロック図

┌─────────────┐
│   Wi-Fi     │ ← NTPサーバーから時刻取得
│   Router    │
└──────┬──────┘
       │ Wi-Fi
┌──────▼─────────┐
│  ESP32-WROOM   │ ← メインマイコン
│  ・時刻管理    │
│  ・Wi-Fi通信   │
│  ・I2C制御     │
└────┬───┬───┬──┘
     │   │   │ I2C通信
 ┌───▼┐ ┌▼──┐ ┌▼──┐
 │MCP │ │MCP│ │MCP│ ← I/Oエキスパンダー
 │#0  │ │#1 │ │#2 │   (GPIOピン拡張)
 └┬──┬┘ └┬─┬┘ └┬─┬┘
  │  │   │ │   │ │
┌▼┐┌▼┐ ┌▼┐▼┐ ┌▼┐▼┐
│L││L│ │L││L│ │L││L│ ← LCD × 6枚
│C││C│ │C││C│ │C││C│   (時:分:秒)
│D││D│ │D││D│ │D││D│
└─┘└─┘ └─┘─┘ └─┘─┘

なぜI/Oエキスパンダーが必要なのか?

問題: LCDパネル1枚あたり7本のピンが必要
→ 6枚 × 7本 = 42本のGPIOピンが必要

ESP32のGPIOピン数: 実用可能なピンは約20本程度
全く足りない!

解決策: MCP23017 I/Oエキスパンダーを使用

  • I2C通信(2本)で16本のGPIOを追加
  • 3つ使用すれば48本のGPIOが確保できる
  • I2Cアドレスを変更して複数個接続可能

時刻管理の仕組み

  1. 起動時: Wi-Fi接続 → NTPサーバーから現在時刻取得
  2. RTCに時刻設定: 取得した時刻をRTC-8564NBに書き込み
  3. 通常動作: RTCから時刻を読み取ってLCDに表示
  4. 停電時: RTCのバックアップ電池で時刻保持

💡 ポイント: Wi-Fiが使えない環境でも、RTCがあれば時計として機能します。


プロトタイプ回路の構築

プリント基板を作る前に、ブレッドボードでプロトタイプを作成しました。

試作回路の全体図

ブレッドボード試作回路

ブレッドボード試作回路

実装した基板(改善前)

ユニバーサル基板での試作

ユニバーサル基板での試作

ジャンパ線が多く、見た目が雑ですが、機能確認には十分です。

回路構成のポイント

1. ESP32とMCP23017の接続(I2C)

ESP32ピン 接続先 説明
GPIO21 (SDA) MCP23017 SDA I2Cデータライン
GPIO22 (SCL) MCP23017 SCL I2Cクロック
3.3V MCP23017 VDD 電源
GND MCP23017 VSS グランド

2. MCP23017のアドレス設定

3つのMCP23017を区別するため、I2Cアドレスを変更します。

デバイス A0 A1 A2 I2Cアドレス
MCP #0 GND GND GND 0x20
MCP #1 VCC GND GND 0x21
MCP #2 VCC VCC GND 0x23

💡 設定方法: MCP23017のA0、A1、A2ピンをVCCまたはGNDに接続してアドレスを決定します。

3. MCP23017とLCDの接続

各MCP23017から2枚のLCDパネルを制御します。

LCDパネル1枚あたりの接続:

MCP23017ピン LCDピン 信号名 説明
GPA0 D0 データ0 データビット0
GPA1 D1 データ1 データビット1
GPA2 D2 データ2 データビット2
GPA3 D3 データ3 データビット3
GPA4 BSY ビジー信号 LCD処理中フラグ(入力)
GPA5 RST リセット LCD初期化用
GPA6 STR ストローブ データ書き込みトリガー

1つのMCP23017に16個のGPIOがあるため、8ピン × 2枚 = 16ピンで2枚のLCDを制御できます。

4. RTCモジュールの接続

RTC-8564NB ESP32 説明
SDA GPIO21 I2Cデータライン(MCP23017と共有)
SCL GPIO22 I2Cクロック(MCP23017と共有)
VDD 3.3V 電源
VSS GND グランド

バックアップ電池: CR2032を接続しておけば、電源オフ時も時刻を保持します。


Arduino プログラムの作成

プログラムの構成

プログラムは以下の処理を行います:

  1. Wi-Fi接続: 設定したSSID/パスワードで接続
  2. NTP時刻取得: configTime()でNTPサーバーから現在時刻取得
  3. RTC初期化: 取得した時刻をRTCに書き込み
  4. MCP23017初期化: I/Oエキスパンダー3個を設定
  5. LCD初期化: リセット信号を送信
  6. ループ処理: RTCから時刻を読み取り、LCDに表示

必要なライブラリ

Arduino IDE 2.xのライブラリマネージャーから以下をインストール:

  • Adafruit MCP23017 Arduino Library by Adafruit
  • WiFi (ESP32標準ライブラリ、インストール不要)
  • Wire (I2C通信、標準ライブラリ)

プログラムコード

💡 注意: 実際のコードは800行以上あるため、重要部分を抜粋して解説します。

📄 クリックして全文コードを表示(長いので折りたたみ)
#include <WiFi.h>
#include <Wire.h>
#include<time.h>
#include <stdlib.h>
#include <stdio.h>
#include "Adafruit_MCP23017.h"
Adafruit_MCP23017 mcp0;
Adafruit_MCP23017 mcp1;
Adafruit_MCP23017 mcp2;
char *ssid = "SSID";
char *pass = "PASSWORD";
int ihour, imin, isec, i;
int h1, h2, m1, m2, s1, s2;
char hour, minute, sec;
int RegTbl[16];
byte RTC8564 = 0x51;
struct tm timeInfo;//時刻を格納するオブジェクト
char s[20];//文字格納用

int s0_D0_Pin = 0; //データライン0 out
int s0_D1_Pin = 1; //データライン1 out
int s0_D2_Pin = 2; //データライン2 out
int s0_D3_Pin = 3; //データライン3 out
int s0_Bsy_Pin = 4; //BUSY信号 in
int s0_Rst_Pin = 5; //RESET信号 通常LOW out
int s0_Str_Pin = 6; //STROBE信号 out

int s1_D0_Pin = 8; //データライン0 out
int s1_D1_Pin = 9; //データライン1 out
int s1_D2_Pin = 10; //データライン2 out
int s1_D3_Pin = 11; //データライン3 out
int s1_Bsy_Pin = 12; //BUSY信号 in
int s1_Rst_Pin = 13; //RESET信号 通常LOW out
int s1_Str_Pin = 14; //STROBE信号 out

int s2_D0_Pin = 0; //データライン0 out
int s2_D1_Pin = 1; //データライン1 out
int s2_D2_Pin = 2; //データライン2 out
int s2_D3_Pin = 3; //データライン3 out
int s2_Bsy_Pin = 4; //BUSY信号 in
int s2_Rst_Pin = 5; //RESET信号 通常LOW out
int s2_Str_Pin = 6; //STROBE信号 out

int s3_D0_Pin = 8; //データライン0 out
int s3_D1_Pin = 9; //データライン1 out
int s3_D2_Pin = 10; //データライン2 out
int s3_D3_Pin = 11; //データライン3 out
int s3_Bsy_Pin = 12; //BUSY信号 in
int s3_Rst_Pin = 13; //RESET信号 通常LOW out
int s3_Str_Pin = 14; //STROBE信号 out

int s4_D0_Pin = 0; //データライン0 out
int s4_D1_Pin = 1; //データライン1 out
int s4_D2_Pin = 2; //データライン2 out
int s4_D3_Pin = 3; //データライン3 out
int s4_Bsy_Pin = 4; //BUSY信号 in
int s4_Rst_Pin = 5; //RESET信号 通常LOW out
int s4_Str_Pin = 6; //STROBE信号 out

int s5_D0_Pin = 8; //データライン0 out
int s5_D1_Pin = 9; //データライン1 out
int s5_D2_Pin = 10; //データライン2 out
int s5_D3_Pin = 11; //データライン3 out
int s5_Bsy_Pin = 12; //BUSY信号 in
int s5_Rst_Pin = 13; //RESET信号 通常LOW out
int s5_Str_Pin = 14; //STROBE信号 out

//データの定義
int disp_0[8] = {0, 0, 0, 0, 0, 0, 0, 1};
int disp_1[8] = {1, 0, 0, 0, 0, 0, 0, 1};
int disp_2[8] = {0, 1, 0, 0, 0, 0, 0, 1};
int disp_3[8] = {1, 1, 0, 0, 0, 0, 0, 1};
int disp_4[8] = {0, 0, 1, 0, 0, 0, 0, 1};
int disp_5[8] = {1, 0, 1, 0, 0, 0, 0, 1};
int disp_6[8] = {0, 1, 1, 0, 0, 0, 0, 1};
int disp_7[8] = {1, 1, 1, 0, 0, 0, 0, 1};
int disp_8[8] = {0, 0, 0, 0, 1, 0, 0, 1};
int disp_9[8] = {1, 0, 0, 0, 1, 0, 0, 1};
int disp_A[8] = {0, 1, 0, 0, 1, 0, 0, 1};
int disp_B[8] = {1, 1, 0, 0, 1, 0, 0, 1};
int disp_C[8] = {0, 0, 1, 0, 1, 0, 0, 1};
int disp_D[8] = {1, 0, 1, 0, 1, 0, 0, 1};
int disp_E[8] = {0, 1, 1, 0, 1, 0, 0, 1};
int disp_F[8] = {1, 1, 1, 0, 1, 0, 0, 1};

int disp_H[8] = {1, 0, 0, 0, 0, 1, 0, 1};
int disp_I[8] = {0, 1, 0, 0, 0, 1, 0, 1};
int disp_J[8] = {1, 1, 0, 0, 0, 1, 0, 1};
int disp_L[8] = {1, 0, 1, 0, 0, 1, 0, 1};
int disp_O[8] = {0, 0, 0, 0, 1, 1, 0, 1};
int disp_P[8] = {1, 0, 0, 0, 1, 1, 0, 1};
int disp_S[8] = {0, 0, 1, 0, 1, 1, 0, 1};
int disp_U[8] = {0, 1, 1, 0, 1, 1, 0, 1};
int disp_BL[8] = {0, 0, 1, 0, 0, 0, 1, 1};
int disp_RR[8] = {0, 1, 1, 0, 0, 0, 1, 1};
int disp_RL[8] = {1, 1, 1, 0, 0, 0, 1, 1};


// DECIMAL -> BCD
byte dec2bcd( byte data )
{
  return ((( data / 10) << 4) + (data % 10));
}
// BCD -> DECIMAL
byte BCDtoDec(byte data) {
  return ((data >> 4) * 10) + (data & 0x0F) ;
}


void init_pin() {
  //各ピンLowにセット
  mcp0.digitalWrite(s0_D0_Pin, LOW);
  mcp0.digitalWrite(s0_D1_Pin, LOW);
  mcp0.digitalWrite(s0_D2_Pin, LOW);
  mcp0.digitalWrite(s0_D3_Pin, LOW);
  mcp0.digitalWrite(s0_Str_Pin, LOW);
  mcp0.digitalWrite(s0_Rst_Pin, LOW);
  mcp0.digitalWrite(s1_D0_Pin, LOW);
  mcp0.digitalWrite(s1_D1_Pin, LOW);
  mcp0.digitalWrite(s1_D2_Pin, LOW);
  mcp0.digitalWrite(s1_D3_Pin, LOW);
  mcp0.digitalWrite(s1_Str_Pin, LOW);
  mcp0.digitalWrite(s1_Rst_Pin, LOW);
  delay(100);
  mcp1.digitalWrite(s2_D0_Pin, LOW);
  mcp1.digitalWrite(s2_D1_Pin, LOW);
  mcp1.digitalWrite(s2_D2_Pin, LOW);
  mcp1.digitalWrite(s2_D3_Pin, LOW);
  mcp1.digitalWrite(s2_Str_Pin, LOW);
  mcp1.digitalWrite(s2_Rst_Pin, LOW);
  mcp1.digitalWrite(s3_D0_Pin, LOW);
  mcp1.digitalWrite(s3_D1_Pin, LOW);
  mcp1.digitalWrite(s3_D2_Pin, LOW);
  mcp1.digitalWrite(s3_D3_Pin, LOW);
  mcp1.digitalWrite(s3_Str_Pin, LOW);
  mcp1.digitalWrite(s3_Rst_Pin, LOW);
  delay(100);
  mcp2.digitalWrite(s4_D0_Pin, LOW);
  mcp2.digitalWrite(s4_D1_Pin, LOW);
  mcp2.digitalWrite(s4_D2_Pin, LOW);
  mcp2.digitalWrite(s4_D3_Pin, LOW);
  mcp2.digitalWrite(s4_Str_Pin, LOW);
  mcp2.digitalWrite(s4_Rst_Pin, LOW);
  mcp2.digitalWrite(s5_D0_Pin, LOW);
  mcp2.digitalWrite(s5_D1_Pin, LOW);
  mcp2.digitalWrite(s5_D2_Pin, LOW);
  mcp2.digitalWrite(s5_D3_Pin, LOW);
  mcp2.digitalWrite(s5_Str_Pin, LOW);
  mcp2.digitalWrite(s5_Rst_Pin, LOW);
  delay(100);
  //リセットかける
  mcp0.digitalWrite(s0_Rst_Pin, HIGH);
  mcp0.digitalWrite(s1_Rst_Pin, HIGH);
  delay(100);
  mcp1.digitalWrite(s2_Rst_Pin, HIGH);
  mcp1.digitalWrite(s3_Rst_Pin, HIGH);
  delay(100);
  mcp2.digitalWrite(s4_Rst_Pin, HIGH);
  mcp2.digitalWrite(s5_Rst_Pin, HIGH);
  delay(1000);
  mcp0.digitalWrite(s0_Rst_Pin, LOW);
  mcp0.digitalWrite(s1_Rst_Pin, LOW);
  delay(100);
  mcp1.digitalWrite(s2_Rst_Pin, LOW);
  mcp1.digitalWrite(s3_Rst_Pin, LOW);
  delay(100);
  mcp2.digitalWrite(s4_Rst_Pin, LOW);
  mcp2.digitalWrite(s5_Rst_Pin, LOW);
  delay(1000);
}


void write_display0(int disp_data) {
  int i;
  int val = 0;
  int data[8];

  switch (disp_data) {
    case 0:    memcpy(data, disp_0, sizeof(int) * 8);    break;
    case 1:    memcpy(data, disp_1, sizeof(int) * 8);    break;
    case 2:    memcpy(data, disp_2, sizeof(int) * 8);    break;
    case 3:    memcpy(data, disp_3, sizeof(int) * 8);    break;
    case 4:    memcpy(data, disp_4, sizeof(int) * 8);    break;
    case 5:    memcpy(data, disp_5, sizeof(int) * 8);    break;
    case 6:    memcpy(data, disp_6, sizeof(int) * 8);    break;
    case 7:    memcpy(data, disp_7, sizeof(int) * 8);    break;
    case 8:    memcpy(data, disp_8, sizeof(int) * 8);    break;
    case 9:    memcpy(data, disp_9, sizeof(int) * 8);    break;
  }

  while (mcp0.digitalRead(s0_Bsy_Pin) != LOW)
    continue;
  //LOWDATA書き込み
  mcp0.digitalWrite(s0_D0_Pin, data[0]);
  mcp0.digitalWrite(s0_D1_Pin, data[1]);
  mcp0.digitalWrite(s0_D2_Pin, data[2]);
  mcp0.digitalWrite(s0_D3_Pin, data[3]);
  mcp0.digitalWrite(s0_Str_Pin, HIGH);
  while (mcp0.digitalRead(s0_Bsy_Pin) != HIGH)
    continue;
  mcp0.digitalWrite(s0_Str_Pin, LOW);
  while (mcp0.digitalRead(s0_Bsy_Pin) != LOW)
    continue;
  mcp0.digitalWrite(s0_D0_Pin, data[4]);
  mcp0.digitalWrite(s0_D1_Pin, data[5]);
  mcp0.digitalWrite(s0_D2_Pin, data[6]);
  mcp0.digitalWrite(s0_D3_Pin, data[7]);
  delay(10);
  mcp0.digitalWrite(s0_Str_Pin, HIGH);
  while (mcp0.digitalRead(s0_Bsy_Pin) != HIGH)
    continue;
  mcp0.digitalWrite(s0_Str_Pin, LOW);

}

void write_display1(int disp_data) {
  int i;
  int val = 0;
  int data[8];

  switch (disp_data) {
    case 0:    memcpy(data, disp_0, sizeof(int) * 8);    break;
    case 1:    memcpy(data, disp_1, sizeof(int) * 8);    break;
    case 2:    memcpy(data, disp_2, sizeof(int) * 8);    break;
    case 3:    memcpy(data, disp_3, sizeof(int) * 8);    break;
    case 4:    memcpy(data, disp_4, sizeof(int) * 8);    break;
    case 5:    memcpy(data, disp_5, sizeof(int) * 8);    break;
    case 6:    memcpy(data, disp_6, sizeof(int) * 8);    break;
    case 7:    memcpy(data, disp_7, sizeof(int) * 8);    break;
    case 8:    memcpy(data, disp_8, sizeof(int) * 8);    break;
    case 9:    memcpy(data, disp_9, sizeof(int) * 8);    break;
  }

  while (mcp0.digitalRead(s1_Bsy_Pin) != LOW)
    continue;
  mcp0.digitalWrite(s1_D0_Pin, data[0]);
  mcp0.digitalWrite(s1_D1_Pin, data[1]);
  mcp0.digitalWrite(s1_D2_Pin, data[2]);
  mcp0.digitalWrite(s1_D3_Pin, data[3]);
  mcp0.digitalWrite(s1_Str_Pin, HIGH);
  while (mcp0.digitalRead(s1_Bsy_Pin) != HIGH)
    continue;
  mcp0.digitalWrite(s1_Str_Pin, LOW);
  while (mcp0.digitalRead(s1_Bsy_Pin) != LOW)
    continue;
  mcp0.digitalWrite(s1_D0_Pin, data[4]);
  mcp0.digitalWrite(s1_D1_Pin, data[5]);
  mcp0.digitalWrite(s1_D2_Pin, data[6]);
  mcp0.digitalWrite(s1_D3_Pin, data[7]);
  delay(10);

  mcp0.digitalWrite(s1_Str_Pin, HIGH);
  while (mcp0.digitalRead(s1_Bsy_Pin) != HIGH)
    continue;
  mcp0.digitalWrite(s1_Str_Pin, LOW);

}



void write_display2(int disp_data) {
  int i;
  int val = 0;
  int data[8];

  switch (disp_data) {
    case 0:    memcpy(data, disp_0, sizeof(int) * 8);    break;
    case 1:    memcpy(data, disp_1, sizeof(int) * 8);    break;
    case 2:    memcpy(data, disp_2, sizeof(int) * 8);    break;
    case 3:    memcpy(data, disp_3, sizeof(int) * 8);    break;
    case 4:    memcpy(data, disp_4, sizeof(int) * 8);    break;
    case 5:    memcpy(data, disp_5, sizeof(int) * 8);    break;
    case 6:    memcpy(data, disp_6, sizeof(int) * 8);    break;
    case 7:    memcpy(data, disp_7, sizeof(int) * 8);    break;
    case 8:    memcpy(data, disp_8, sizeof(int) * 8);    break;
    case 9:    memcpy(data, disp_9, sizeof(int) * 8);    break;
  }
  while (mcp1.digitalRead(s2_Bsy_Pin) != LOW)
    continue;
  //LOWDATA書き込み

  mcp1.digitalWrite(s2_D0_Pin, data[0]);
  mcp1.digitalWrite(s2_D1_Pin, data[1]);
  mcp1.digitalWrite(s2_D2_Pin, data[2]);
  mcp1.digitalWrite(s2_D3_Pin, data[3]);
  mcp1.digitalWrite(s2_Str_Pin, HIGH);
  while (mcp1.digitalRead(s2_Bsy_Pin) != HIGH)
    continue;
  mcp1.digitalWrite(s2_Str_Pin, LOW);
  while (mcp1.digitalRead(s2_Bsy_Pin) != LOW)
    continue;
  mcp1.digitalWrite(s2_D0_Pin, data[4]);
  mcp1.digitalWrite(s2_D1_Pin, data[5]);
  mcp1.digitalWrite(s2_D2_Pin, data[6]);
  mcp1.digitalWrite(s2_D3_Pin, data[7]);
  delay(10);
  mcp1.digitalWrite(s2_Str_Pin, HIGH);
  while (mcp1.digitalRead(s2_Bsy_Pin) != HIGH)
    continue;
  mcp1.digitalWrite(s2_Str_Pin, LOW);

}

void write_display3(int disp_data) {
  int i;
  int val = 0;
  int data[8];

  switch (disp_data) {
    case 0:    memcpy(data, disp_0, sizeof(int) * 8);    break;
    case 1:    memcpy(data, disp_1, sizeof(int) * 8);    break;
    case 2:    memcpy(data, disp_2, sizeof(int) * 8);    break;
    case 3:    memcpy(data, disp_3, sizeof(int) * 8);    break;
    case 4:    memcpy(data, disp_4, sizeof(int) * 8);    break;
    case 5:    memcpy(data, disp_5, sizeof(int) * 8);    break;
    case 6:    memcpy(data, disp_6, sizeof(int) * 8);    break;
    case 7:    memcpy(data, disp_7, sizeof(int) * 8);    break;
    case 8:    memcpy(data, disp_8, sizeof(int) * 8);    break;
    case 9:    memcpy(data, disp_9, sizeof(int) * 8);    break;
  }

  while (mcp1.digitalRead(s3_Bsy_Pin) != LOW)
    continue;
  mcp1.digitalWrite(s3_D0_Pin, data[0]);
  mcp1.digitalWrite(s3_D1_Pin, data[1]);
  mcp1.digitalWrite(s3_D2_Pin, data[2]);
  mcp1.digitalWrite(s3_D3_Pin, data[3]);
  mcp1.digitalWrite(s3_Str_Pin, HIGH);

  while (mcp1.digitalRead(s3_Bsy_Pin) != HIGH)
    continue;
  mcp1.digitalWrite(s3_Str_Pin, LOW);
  while (mcp1.digitalRead(s3_Bsy_Pin) != LOW)
    continue;
  mcp1.digitalWrite(s3_D0_Pin, data[4]);
  mcp1.digitalWrite(s3_D1_Pin, data[5]);
  mcp1.digitalWrite(s3_D2_Pin, data[6]);
  mcp1.digitalWrite(s3_D3_Pin, data[7]);
  delay(10);

  mcp1.digitalWrite(s3_Str_Pin, HIGH);
  while (mcp1.digitalRead(s3_Bsy_Pin) != HIGH)
    continue;
  mcp1.digitalWrite(s3_Str_Pin, LOW);

}



void write_display4(int disp_data) {
  int i;
  int val = 0;
  int data[8];

  switch (disp_data) {
    case 0:    memcpy(data, disp_0, sizeof(int) * 8);    break;
    case 1:    memcpy(data, disp_1, sizeof(int) * 8);    break;
    case 2:    memcpy(data, disp_2, sizeof(int) * 8);    break;
    case 3:    memcpy(data, disp_3, sizeof(int) * 8);    break;
    case 4:    memcpy(data, disp_4, sizeof(int) * 8);    break;
    case 5:    memcpy(data, disp_5, sizeof(int) * 8);    break;
    case 6:    memcpy(data, disp_6, sizeof(int) * 8);    break;
    case 7:    memcpy(data, disp_7, sizeof(int) * 8);    break;
    case 8:    memcpy(data, disp_8, sizeof(int) * 8);    break;
    case 9:    memcpy(data, disp_9, sizeof(int) * 8);    break;
  }

  while (mcp2.digitalRead(s4_Bsy_Pin) != LOW)
    continue;
  //LOWDATA書き込み
  mcp2.digitalWrite(s4_D0_Pin, data[0]);
  mcp2.digitalWrite(s4_D1_Pin, data[1]);
  mcp2.digitalWrite(s4_D2_Pin, data[2]);
  mcp2.digitalWrite(s4_D3_Pin, data[3]);
  mcp2.digitalWrite(s4_Str_Pin, HIGH);
  while (mcp2.digitalRead(s4_Bsy_Pin) != HIGH)
    continue;
  mcp2.digitalWrite(s4_Str_Pin, LOW);
  while (mcp2.digitalRead(s4_Bsy_Pin) != LOW)
    continue;
  mcp2.digitalWrite(s4_D0_Pin, data[4]);
  mcp2.digitalWrite(s4_D1_Pin, data[5]);
  mcp2.digitalWrite(s4_D2_Pin, data[6]);
  mcp2.digitalWrite(s4_D3_Pin, data[7]);
  delay(10);
  mcp2.digitalWrite(s4_Str_Pin, HIGH);
  while (mcp2.digitalRead(s4_Bsy_Pin) != HIGH)
    continue;
  mcp2.digitalWrite(s4_Str_Pin, LOW);

}

void write_display5(int disp_data) {
  int i;
  int val = 0;
  int data[8];

  switch (disp_data) {
    case 0:    memcpy(data, disp_0, sizeof(int) * 8);    break;
    case 1:    memcpy(data, disp_1, sizeof(int) * 8);    break;
    case 2:    memcpy(data, disp_2, sizeof(int) * 8);    break;
    case 3:    memcpy(data, disp_3, sizeof(int) * 8);    break;
    case 4:    memcpy(data, disp_4, sizeof(int) * 8);    break;
    case 5:    memcpy(data, disp_5, sizeof(int) * 8);    break;
    case 6:    memcpy(data, disp_6, sizeof(int) * 8);    break;
    case 7:    memcpy(data, disp_7, sizeof(int) * 8);    break;
    case 8:    memcpy(data, disp_8, sizeof(int) * 8);    break;
    case 9:    memcpy(data, disp_9, sizeof(int) * 8);    break;
  }

  while (mcp2.digitalRead(s5_Bsy_Pin) != LOW)
    continue;
  mcp2.digitalWrite(s5_D0_Pin, data[0]);
  mcp2.digitalWrite(s5_D1_Pin, data[1]);
  mcp2.digitalWrite(s5_D2_Pin, data[2]);
  mcp2.digitalWrite(s5_D3_Pin, data[3]);
  mcp2.digitalWrite(s5_Str_Pin, HIGH);
  while (mcp2.digitalRead(s5_Bsy_Pin) != HIGH)
    continue;
  mcp2.digitalWrite(s5_Str_Pin, LOW);
  while (mcp2.digitalRead(s5_Bsy_Pin) != LOW)
    continue;
  mcp2.digitalWrite(s5_D0_Pin, data[4]);
  mcp2.digitalWrite(s5_D1_Pin, data[5]);
  mcp2.digitalWrite(s5_D2_Pin, data[6]);
  mcp2.digitalWrite(s5_D3_Pin, data[7]);
  delay(10);

  mcp2.digitalWrite(s5_Str_Pin, HIGH);
  while (mcp2.digitalRead(s5_Bsy_Pin) != HIGH)
    continue;
  mcp2.digitalWrite(s5_Str_Pin, LOW);

}

void setup() {
  // initialize the digital pin as an output.
  Serial.begin(115200);
  Wire.begin();

  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  if (WiFi.begin(ssid, pass) != WL_DISCONNECTED) {
    ESP.restart();
  }
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
  }

  Serial.println("Connected to the WiFi network!");
  delay(1000);
  configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");//NTPの設定
  getLocalTime(&timeInfo);//tmオブジェクトのtimeInfoに現在時刻を入れ込む


  delay(1);
  ihour = timeInfo.tm_hour;
  imin = timeInfo.tm_min;
  isec = timeInfo.tm_sec;
  sprintf(s, " %04d/%02d/%02d %02d:%02d:%02d",
          timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
          timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);//人間が読める形式に変換
  Serial.println(s);//時間をシリアルモニタへ出力




  Wire.beginTransmission(RTC8564);
  Wire.write(0x00);
  Wire.write(0x00);// [00]Control1
  Wire.write(0x02);// [01]Control2
  Wire.write(byte(dec2bcd(isec)));// [02]Seconds(15秒)
  Wire.write(byte(dec2bcd(imin))); // [03]Minutes(20分)
  Wire.write(byte(dec2bcd(ihour)));// [04]Hours(12時)
  Wire.write(0x25);// [05]Days(25日)
  Wire.write(0x01);    // [06]Weekdays(月)
  Wire.write(0x12 | 0x80); // [07]Month/Century(21世紀の12月)
  Wire.write(0x17);// [08]Years(2017年)
  Wire.endTransmission();
  delay(10);

  Wire.beginTransmission(RTC8564);
  Wire.write(0x00);
  Wire.endTransmission();
  Wire.requestFrom(RTC8564, 16);
  for (i = 0; i < 16; i++) {
    while (Wire.available() == 0 ) {}
    RegTbl[i] = Wire.read();
  }
  ihour = (BCDtoDec(RegTbl[4] & 0x3F));
  imin = (BCDtoDec(RegTbl[3] & 0x7F));
  isec = (BCDtoDec(RegTbl[2] & 0x7F));

  
  mcp0.begin();
  mcp1.begin(0x01);
  mcp2.begin(0x03);

  mcp0.pinMode(s0_D0_Pin, OUTPUT);
  mcp0.pinMode(s0_D1_Pin, OUTPUT);
  mcp0.pinMode(s0_D2_Pin, OUTPUT);
  mcp0.pinMode(s0_D3_Pin, OUTPUT);
  mcp0.pinMode(s0_Bsy_Pin, INPUT);
  mcp0.pinMode(s0_Str_Pin, OUTPUT);
  mcp0.pinMode(s0_Rst_Pin, OUTPUT);

  mcp0.pinMode(s1_D0_Pin, OUTPUT);
  mcp0.pinMode(s1_D1_Pin, OUTPUT);
  mcp0.pinMode(s1_D2_Pin, OUTPUT);
  mcp0.pinMode(s1_D3_Pin, OUTPUT);
  mcp0.pinMode(s1_Bsy_Pin, INPUT);
  mcp0.pinMode(s1_Str_Pin, OUTPUT);
  mcp0.pinMode(s1_Rst_Pin, OUTPUT);

  mcp1.pinMode(s2_D0_Pin, OUTPUT);
  mcp1.pinMode(s2_D1_Pin, OUTPUT);
  mcp1.pinMode(s2_D2_Pin, OUTPUT);
  mcp1.pinMode(s2_D3_Pin, OUTPUT);
  mcp1.pinMode(s2_Bsy_Pin, INPUT);
  mcp1.pinMode(s2_Str_Pin, OUTPUT);
  mcp1.pinMode(s2_Rst_Pin, OUTPUT);

  mcp1.pinMode(s3_D0_Pin, OUTPUT);
  mcp1.pinMode(s3_D1_Pin, OUTPUT);
  mcp1.pinMode(s3_D2_Pin, OUTPUT);
  mcp1.pinMode(s3_D3_Pin, OUTPUT);
  mcp1.pinMode(s3_Bsy_Pin, INPUT);
  mcp1.pinMode(s3_Str_Pin, OUTPUT);
  mcp1.pinMode(s3_Rst_Pin, OUTPUT);

  mcp2.pinMode(s4_D0_Pin, OUTPUT);
  mcp2.pinMode(s4_D1_Pin, OUTPUT);
  mcp2.pinMode(s4_D2_Pin, OUTPUT);
  mcp2.pinMode(s4_D3_Pin, OUTPUT);
  mcp2.pinMode(s4_Bsy_Pin, INPUT);
  mcp2.pinMode(s4_Str_Pin, OUTPUT);
  mcp2.pinMode(s4_Rst_Pin, OUTPUT);

  mcp2.pinMode(s5_D0_Pin, OUTPUT);
  mcp2.pinMode(s5_D1_Pin, OUTPUT);
  mcp2.pinMode(s5_D2_Pin, OUTPUT);
  mcp2.pinMode(s5_D3_Pin, OUTPUT);
  mcp2.pinMode(s5_Bsy_Pin, INPUT);
  mcp2.pinMode(s5_Str_Pin, OUTPUT);
  mcp2.pinMode(s5_Rst_Pin, OUTPUT);

  init_pin();
}

void loop() {
  Serial.println("LOOP");
  Wire.beginTransmission(RTC8564);
  Wire.write(0x00);
  Wire.endTransmission();
  Wire.requestFrom(RTC8564, 16);
  for (i = 0; i < 16; i++) {
    while (Wire.available() == 0 ) {}
    RegTbl[i] = Wire.read();
  }

  ihour = (BCDtoDec(RegTbl[4] & 0x3F));
  imin = (BCDtoDec(RegTbl[3] & 0x7F));
  isec = (BCDtoDec(RegTbl[2] & 0x7F));
  h1 = ihour / 10;
  h2 = ihour % 10;
  m1 = imin / 10;
  m2 = imin % 10;
  s1 = isec / 10;
  s2 = isec % 10;

  write_display0(h2);
  write_display1(h1);
  write_display2(s2);
  write_display3(s1);
  write_display4(m2);
  write_display5(m1);

  delay(100);
}

コードの重要ポイント解説

1. Wi-Fi接続とNTP時刻取得

// Wi-Fi接続
WiFi.begin(ssid, pass);
while (WiFi.status() != WL_CONNECTED) {
  delay(1000);
}

// NTP時刻取得(日本標準時 GMT+9)
configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");
getLocalTime(&timeInfo);

使用NTPサーバー:

  • ntp.nict.jp: 日本の公式NTPサーバー(情報通信研究機構)
  • time.google.com: Googleの公開NTPサーバー
  • ntp.jst.mfeed.ad.jp: 日本のインターネットマルチフィード

💡 ポイント: 複数のNTPサーバーを指定すると、1つが応答しなくても他から取得できます。

2. RTCへの時刻書き込み

// 10進数 → BCD変換
byte dec2bcd(byte data) {
  return (((data / 10) << 4) + (data % 10));
}

// RTCに時刻を書き込み
Wire.beginTransmission(RTC8564);
Wire.write(0x00);
Wire.write(0x00);                      // Control1
Wire.write(0x02);                      // Control2
Wire.write(byte(dec2bcd(isec)));       // 秒
Wire.write(byte(dec2bcd(imin)));       // 分
Wire.write(byte(dec2bcd(ihour)));      // 時
Wire.endTransmission();

BCD(Binary-Coded Decimal): RTCは時刻をBCD形式で保存します。例えば、23時は0x23として保存されます。

3. MCP23017の初期化

// 3つのMCP23017を初期化(アドレス: 0x20, 0x21, 0x23)
mcp0.begin();       // デフォルトアドレス 0x20
mcp1.begin(0x01);   // アドレス 0x21
mcp2.begin(0x03);   // アドレス 0x23

// GPIOピンの入出力設定
mcp0.pinMode(s0_D0_Pin, OUTPUT);   // データピン(出力)
mcp0.pinMode(s0_Bsy_Pin, INPUT);   // ビジー信号(入力)
// ... 以下、各ピンを設定

4. LCD表示関数

void write_display0(int disp_data) {
  // 表示したい数字に対応するデータ配列を選択
  switch (disp_data) {
    case 0: memcpy(data, disp_0, sizeof(int) * 8); break;
    case 1: memcpy(data, disp_1, sizeof(int) * 8); break;
    // ... 0~9の定義
  }
  
  // LCD busy待ち
  while (mcp0.digitalRead(s0_Bsy_Pin) != LOW) continue;
  
  // 下位4ビットを送信
  mcp0.digitalWrite(s0_D0_Pin, data[0]);
  mcp0.digitalWrite(s0_D1_Pin, data[1]);
  mcp0.digitalWrite(s0_D2_Pin, data[2]);
  mcp0.digitalWrite(s0_D3_Pin, data[3]);
  mcp0.digitalWrite(s0_Str_Pin, HIGH);  // ストローブ信号
  
  // busy待ち → 上位4ビット送信
  // ... 同様の処理
}

💡 LCDタイミング制御: BSY信号がLOWになるまで待つことで、LCD側の処理完了を確認しています。

5. メインループ

void loop() {
  // RTCから時刻を読み取り
  Wire.beginTransmission(RTC8564);
  Wire.write(0x00);
  Wire.endTransmission();
  Wire.requestFrom(RTC8564, 16);
  
  // BCD → 10進数変換
  ihour = BCDtoDec(RegTbl[4] & 0x3F);
  imin = BCDtoDec(RegTbl[3] & 0x7F);
  isec = BCDtoDec(RegTbl[2] & 0x7F);
  
  // 各桁に分解
  h1 = ihour / 10;  // 時の10の位
  h2 = ihour % 10;  // 時の1の位
  m1 = imin / 10;   // 分の10の位
  m2 = imin % 10;   // 分の1の位
  s1 = isec / 10;   // 秒の10の位
  s2 = isec % 10;   // 秒の1の位
  
  // 6枚のLCDに表示(左から順に HH:MM:SS)
  write_display5(h1);
  write_display4(h2);
  write_display3(m1);
  write_display2(m2);
  write_display1(s1);
  write_display0(s2);
  
  delay(100);  // 100ms待機
}

動作確認

プログラムを書き込んで動作確認しましょう。

確認手順:

  1. Arduino IDE 2.xでコードを開く
  2. Wi-FiのSSID/パスワードを自分の環境に合わせて変更
  3. ESP32ボードを選択([ツール] → [ボード] → [ESP32 Dev Module])
  4. ポート選択([ツール] → [Port])
  5. アップロード(左上の「→」ボタン)
  6. シリアルモニタ(115200 baud)で動作確認

シリアルモニタ出力例:

Connected to the WiFi network!
 2026/02/18 14:23:45
LOOP
LOOP
...

試作機の動作

正確に時刻が表示されています!機能的には完璧です。

でも…見た目がイマイチ

配線がむき出しで美しくない…

配線がむき出しで美しくない…

問題点:

  • 配線が複雑で見た目が悪い
  • 基板が吊るされていて不安定
  • 長期使用に耐えられるか不安
  • 壁掛けできない

解決策: プリント基板化で美しく信頼性の高い製品に!


KiCad 8.xでプリント基板を設計する

いよいよ、プリント基板(PCB)の設計に入ります。2026年現在、KiCad 8.xが最新版で、以前のバージョンより大幅に使いやすくなっています。

KiCad 8.xのインストール

  1. KiCad公式サイトにアクセス
  2. お使いのOS(Windows/Mac/Linux)向けの最新版をダウンロード
  3. インストーラーを実行

KiCad 8.xの新機能(2026年版):

  • ✅ 改善されたユーザーインターフェース
  • ✅ 高速な3Dビューア
  • ✅ Python APIの強化
  • ✅ より豊富な部品ライブラリ
  • ✅ Git統合機能

💡 初心者向けヒント: KiCadは無料かつオープンソースの本格的なPCB設計ツールです。商用利用も可能!

プリント基板設計の流れ

PCB設計は以下の手順で進めます:

  1. 回路図作成 (Schematic Editor)
  2. フットプリント割り当て (Symbol to Footprint Assignment)
  3. 基板レイアウト (PCB Editor)
  4. 配線(Routing)
  5. デザインルールチェック(DRC)
  6. ガーバーデータ出力

ステップ1:回路図設計

まず、試作機で作った回路をKiCadの回路図エディタで描きます。

追加機能の実装

試作機では実現できなかった目覚まし機能を追加します。

追加部品:

  • 圧電ブザー: アラーム音用
  • 24Vリレー: ランプ点灯制御用
  • トランジスタ(2SC1815など): リレー駆動用
  • ダイオード(1N4148): リレー逆起電力保護

完成した回路図

KiCad 8.xで作成した回路図

KiCad 8.xで作成した回路図

回路図のポイント:

ブロック 説明
電源部 5V入力(USBまたはACアダプタ)→ 3.3V変換
マイコン部 ESP32-WROOM-32 + 周辺回路
I2C部 MCP23017 × 3 + RTC-8564NB
LCD接続部 MCP23017 → LCDパネル × 6
アラーム部 ブザー + リレー回路

💡 設計のコツ:

  • 階層設計: 複雑な回路は階層シートで見やすく整理
  • ネットラベル: 配線を減らしてすっきり表示
  • 電源記号: VCC、GNDは専用記号を使用

KiCad 8.xでの回路図作成手順

  1. KiCad起動New Project → プロジェクト名を入力
  2. Schematic Editorを開く
  3. Add Symbol(A): 部品配置
    • ESP32-WROOM-32
    • MCP23017
    • RTC-8564NB
    • コネクタ(LCD接続用)
    • 抵抗、コンデンサなど
  4. Wire(W): 部品同士を配線
  5. Net Label(L): 配線にラベル付け(I2C_SDA、I2C_SCLなど)
  6. Electrical Rules Check(ERC): 回路図のエラーチェック

トラブルシューティング:

  • 部品が見つからない: KiCadの公式ライブラリや、SnapEDA、Ultra Librarianなどから部品をダウンロード
  • ESP32のフットプリント: 公式ライブラリまたはGitHubで"ESP32-WROOM-32"を検索

ステップ2:フットプリント割り当て

回路図の部品に、実際の基板上での形状(フットプリント)を割り当てます。

主要部品のフットプリント:

部品 フットプリント
ESP32-WROOM-32 ESP32-WROOM-32(38ピンSMD)
MCP23017 DIP-28(スルーホール)
RTC-8564NB DIP-8またはSMD
抵抗・コンデンサ 1206サイズ(SMD)または標準スルーホール
コネクタ ピンヘッダ 2.54mmピッチ

💡 ポイント: 初心者はスルーホール部品(足が穴に差し込むタイプ)が扱いやすいです。今回はMCP23017をスルーホールにしました。

ステップ3:基板レイアウト(部品配置)

PCB Editorで実際の基板上に部品を配置していきます。

基板サイズの決定

今回の基板サイズ: 200mm × 150mm(LCDパネルの配置を考慮)

💡 コスト削減のヒント: JLCPCBなどの基板メーカーは、100mm × 100mm以下だと最安価格になります。大きな基板は割高になるので注意。

部品配置のコツ

KiCad 8.xでの部品配置

KiCad 8.xでの部品配置

配置の原則:

  1. 機能ごとにグループ化: ESP32周辺、MCP23017グループ、電源部など
  2. 高さを考慮: LCD接続コネクタを基板端に配置
  3. 配線しやすい配置: I2C配線が短くなるように
  4. 放熱を考慮: ESP32周辺に空きスペース確保
  5. ネジ穴配置: 四隅に3mm穴を配置(M3ネジ対応)

KiCad 8.xの便利機能:

  • 3Dビューア: リアルタイムで立体表示(右上の3Dアイコン)
  • Push and Shove: 配線時に他の配線を自動で押しのける
  • Design Rules: 配線幅や間隔を自動チェック

基板の3Dプレビュー

KiCad基板表面
基板表面(部品面)
KiCad基板裏面
基板裏面(配線面)

完成イメージが見える!
3Dビューで確認することで、部品の干渉や実装ミスを事前に発見できます。

ステップ4:配線(Routing)

部品を配置したら、**配線(トレース)**を引いていきます。

配線の基本ルール

項目 設定値 備考
電源ライン幅 0.5mm~1.0mm 大電流に対応
信号ライン幅 0.25mm~0.4mm 一般的な信号
I2Cライン幅 0.3mm 高速通信対応
最小間隔 0.2mm JLCPCBの製造能力
ビア径 0.8mm スルーホール接続

配線のコツ:

  1. 電源・GND優先: 太い配線で最初に引く
  2. 信号線は短く: 特にI2C、SPI、高速信号
  3. 直角禁止: 配線は45度または曲線で
  4. 片面化を避ける: 2層基板を活用(表面+裏面)

KiCad 8.xの自動配線:

  • Route → Auto-route: 簡単な回路なら自動配線も可能
  • 今回は手動: 複雑な回路は手動の方が美しく仕上がります

GNDプレーンの作成

GNDプレーン(グランドベタ): 未配線部分をGNDで埋めることで、ノイズ対策と配線の安定化を図ります。

設定方法(KiCad 8.x):

  1. Add Filled Zone(Ctrl+Shift+Z)
  2. レイヤー: B.Cu(裏面銅箔)
  3. ネット: GND
  4. クリアランス: 0.2mm
  5. 基板外周に沿って範囲指定

ステップ5:デザインルールチェック(DRC)

配線が完了したら、**DRC(Design Rule Check)**でエラーがないか確認します。

DRC実行方法:

  1. Inspect → Design Rules Checker
  2. Run DRCをクリック
  3. エラーがあれば修正

よくあるエラー:

  • 配線間隔不足: 0.2mm以下の間隔
  • パッド干渉: 部品同士が近すぎる
  • 未接続: 配線漏れ
  • アンカット銅箔: GNDプレーンの設定ミス

💡 重要: DRCでエラーゼロにしてから発注しましょう!


ガーバーデータの生成(基板製造用データ)

DRCでエラーがなくなったら、基板メーカーに発注するためのガーバーデータを生成します。

ガーバーデータとは?

ガーバーファイル(Gerber Format): PCB製造業界の標準フォーマットで、基板の各層(銅箔、シルク、レジストなど)の情報を記述したファイルです。

KiCad 8.xでのガーバー出力手順

ステップ1:プロット設定

  1. PCB Editorで File → Plot を選択
  2. Plot dialogが開く
KiCad 8.x プロット設定画面

KiCad 8.x プロット設定画面

ステップ2: 出力レイヤーの選択

以下のレイヤーにチェックを入れます:

レイヤー 説明 必須
F.Cu 表面の銅箔(Front Copper)
B.Cu 裏面の銅箔(Back Copper)
F.SilkS 表面のシルク印刷(部品名など)
B.SilkS 裏面のシルク印刷
F.Mask 表面のソルダーマスク(緑色の部分)
B.Mask 裏面のソルダーマスク
Edge.Cuts 基板外形
F.Paste 表面のペースト(SMD実装用)
B.Paste 裏面のペースト

💡 ポイント: 今回は2層基板なので、F.CuとB.Cuの2層です。4層基板の場合はIn1.Cu、In2.Cuも追加します。

ステップ3: 出力設定

設定項目 推奨値 説明
Plot format Gerber ガーバー形式
Output directory gerber/ 出力先フォルダ
Use Protel filename extensions 一般的な拡張子を使用
Subtract soldermask from silkscreen シルクをパッド上に印刷しない
Coordinate format 4.6, unit mm 座標フォーマット

ステップ4: ガーバーファイル生成

  1. Plotボタンをクリック
  2. 出力メッセージを確認

正常に生成されると、以下のようなメッセージが表示されます:

ガーバーファイル生成完了

ガーバーファイル生成完了

生成されるファイル(例):

ESP32_Clock-F_Cu.gbr          # 表面銅箔
ESP32_Clock-B_Cu.gbr          # 裏面銅箔
ESP32_Clock-F_SilkS.gbr       # 表面シルク
ESP32_Clock-B_SilkS.gbr       # 裏面シルク
ESP32_Clock-F_Mask.gbr        # 表面マスク
ESP32_Clock-B_Mask.gbr        # 裏面マスク
ESP32_Clock-Edge_Cuts.gbr     # 基板外形

ドリルファイルの生成

スルーホール(穴)の情報を別途生成します。

ステップ1: ドリルファイル出力

  1. Plotダイアログで Generate Drill Files… をクリック
ドリルファイル設定画面

ドリルファイル設定画面

ステップ2: 設定確認

設定項目 推奨値 説明
Drill file format Excellon 標準形式
Drill units Millimeters ミリメートル単位
Zeros format Decimal format 小数点形式
Drill origin Absolute 絶対座標
Drill map file format Gerber マップをガーバー形式で

ステップ3: 生成

  1. Generate Drill Fileボタンをクリック
  2. ESP32_Clock.drlファイルが生成される

JLCPCB用のファイル名変更(2026年版対応)

2026年現在、JLCPCBは柔軟なファイル形式に対応していますが、念のため以下の変更を行います:

変更前 変更後 理由
xxx.drl xxx.txt 一部の古いシステム対応
xxx-Edge_Cuts.gbr xxx.gml または xxx.gko 外形レイヤーの明確化

💡 2026年版アップデート: 最新のJLCPCBシステムは.drlをそのまま認識できますが、.txtに変更する方が確実です。

ZIPファイルの作成

生成したガーバーファイルとドリルファイルをまとめてZIP圧縮します。

ファイル一覧(圧縮対象):

ESP32_Clock-F_Cu.gbr
ESP32_Clock-B_Cu.gbr
ESP32_Clock-F_SilkS.gbr
ESP32_Clock-B_SilkS.gbr
ESP32_Clock-F_Mask.gbr
ESP32_Clock-B_Mask.gbr
ESP32_Clock-Edge_Cuts.gml
ESP32_Clock.txt (ドリルファイル)
ESP32_Clock-drl_map.gbr (オプション)

ZIP圧縮方法(Windows):

  1. すべてのファイルを選択
  2. 右クリック → 送る圧縮(ZIP形式)フォルダー
  3. ESP32_Clock_Gerber.zipとして保存

💡 注意: フォルダごと圧縮せず、ファイルを直接ZIP内に配置してください。


JLCPCBでプリント基板を発注する

いよいよ、実際の基板を発注します!今回は**JLCPCB(中国の基板メーカー)**を使用します。

JLCPCBとは?

JLCPCB: 世界最大級のPCBメーカーで、以下の特徴があります:

項目 詳細
価格 2層基板 100×100mm 5枚で $2~
納期 24時間製造 + 配送3~7日
品質 高品質(IPC規格準拠)
オプション 基板色、表面処理、厚みなど選択可能
送料 日本まで約 $15~(2026年現在)

💡 コスト例(2026年):
基板5枚(200×150mm)+ 送料 = 約$30~40(約¥4,500~6,000)

発注手順(2026年版)

ステップ1: アカウント作成

  1. JLCPCB公式サイトにアクセス
  2. 右上のSign Upからアカウント作成
  3. メールアドレスで登録(Googleアカウント連携も可能)

ステップ2: ガーバーファイルのアップロード

  1. トップページの Order Now をクリック
JLCPCB アップロード画面(2026年版)

JLCPCB アップロード画面(2026年版)

  1. Add Gerber Fileボタンをクリック
  2. 先ほど作成したESP32_Clock_Gerber.zipを選択
  3. アップロード完了を待つ(数秒~1分)

ステップ3: 基板仕様の確認・変更

アップロードが完了すると、自動的に基板サイズなどが検出されます。

基板プレビューが表示され、正しく読み込まれたか確認できます。

基板プレビューと仕様設定

基板プレビューと仕様設定

主要な設定項目:

項目 デフォルト 推奨設定 説明
Base Material FR-4 FR-4 標準的な基板材質
Layers 2 2 2層基板
PCB Qty 5 5~10 枚数(5枚が最安)
PCB Color Green お好み 緑/赤/青/黒/白など
PCB Thickness 1.6mm 1.6mm 標準厚み
Surface Finish HASL ENIG 表面処理(ENIGが高品質)
Copper Weight 1oz 1oz 銅箔厚(標準)
Remove Order Number No Yes 基板番号の非表示(+$1.5)

💡 おすすめカスタマイズ:

  • PCB Color: 黒や青だとプロっぽい見た目に
  • Surface Finish: ENIG(金メッキ)にすると酸化しにくく長持ち
  • Remove Order Number: Yesにすると基板に製造番号が印字されない

価格確認:
設定を変更すると、右側にリアルタイムで価格が表示されます。
例: 2層基板 200×150mm × 5枚 = $7.90~

ステップ4: カートに追加

  1. Save to Cartボタンをクリック
  2. ショッピングカートに移動

💡 2026年版の便利機能: 複数のプロジェクトをまとめて注文すると送料が節約できます。

ステップ5: 配送方法と支払い

配送・支払い画面

配送・支払い画面

配送方法の選択:

配送方法 料金(2026年) 納期 追跡
Global Standard Direct Line 無料~$5 10~20日
DHL Express $20~ 3~5日
Registered Air Mail $10~ 7~15日

💡 おすすめ: 急ぎでなければGlobal Standardが安い。確実に早く欲しい場合はDHL

支払い方法:

  • PayPal (推奨)
  • クレジットカード (Visa/MasterCard/JCB対応)

ステップ6: 住所入力と発注

  1. Shipping Addressに配送先住所を入力(日本語不可、ローマ字で)
  2. 内容を最終確認
  3. Submit Orderをクリック

発注完了! 🎉

発注後の流れ(2026年版)

  1. 審査 (Review): JLCPCB側で基板データを確認(数時間~1日)
  2. 製造 (In Production): 基板製造開始(1~2日)
  3. 出荷 (Shipped): 配送開始
  4. 到着: 日本到着(配送方法により3~20日)

進捗確認: マイページのOrder Historyで随時確認可能。

関税について(2026年版)

基板の関税:

  • 2026年現在、¥10,000以下の個人輸入は関税・消費税が免除される場合が多い
  • ただし、¥16,666以上(CIF価格)は課税対象
  • DHLの場合、通関手数料が別途かかることがある

💡 節約テクニック: 基板代が高額になりそうな場合、複数回に分けて発注すると関税を回避できます。


プリント基板発注の完了とまとめ

お疲れさまでした!これで基板の設計から発注まで完了です。

本記事で学んだこと

ESP32時計の回路設計: I/OエキスパンダーとRTCを活用
KiCad 8.xの使い方: 回路図 → PCBレイアウト → ガーバー出力
プリント基板の設計手法: 部品配置、配線、DRC
JLCPCB発注方法: ガーバーデータのアップロードから支払いまで
2026年最新情報: KiCad 8.x、JLCPCBの最新仕様

基板到着までにやっておくこと

基板が届くまでの間に、以下の準備をしておきましょう:

  1. 部品の購入: 秋月電子やAmazonで必要部品を注文
  2. ハンダ付け練習: 久しぶりの人は練習基板で感覚を取り戻す
  3. プログラムの最終調整: コードの動作確認
  4. ケース設計: 3Dプリンタや木材でケース製作

次回予告:第2弾

次回は基板実装編!

届いたプリント基板に部品を実装し、動作確認からケース作成まで一気に完成させます。

続編: 【ESP32で電子工作】デジタル時計の作成(2)【プリント基板への実装】

関連記事

ESP32プロジェクトに興味がある方は、こちらもどうぞ:

参考リンク


See You Next Time! 🚀
基板が届いたら、実装編でお会いしましょう!