PR

Freenove ESP32-WROVER CAMボード【FNK0060 & FNK0060B】を試してみました【Arduino使用】

Raspberry Pi/電子工作
スポンサーリンク

こんにちは、あろっちです。

ESP32-WROVER CAMボード(Freenove FNK0060 または FNK0060B)はESP32-WROVER-E(技適マーク付き)とカメラが搭載されたボードです。
こちらを入手したので情報をまとめてみました。

FNK0060BはSDカードスロット搭載モデル(v3.0)です。

ESP32-WROVER CAMボードには、Freenoveのオリジナル品とジェネリック品があります。

オリジナル品は基板にFREENOVEの文字がシルク印刷されています。

左: Freenove FNK0060 右: ジェネリック品

参考サイト:

https://lang-ship.com/blog/work/esp32-cam-fnk0060/(Lang-ship)

上記にたなかさんの情報がありますので参考にしてください。
こちらによるとジェネリック品のESP32-WROVER-Eモジュールはクローン品(いわゆる偽物)とのことで筆者の方でも挙動の違いを確認しました。

本記事ではオリジナル品であるFreenove FNK0060 & FNK0060Bの情報をお届けしたいと思います。

本記事は以下を参考にしています。

GitHub - Freenove/Freenove_ESP32_WROVER_Board: Apply to FNK0060
Apply to FNK0060. Contribute to Freenove/Freenove_ESP32_WROVER_Board development by creating an account on GitHub.

本記事ではArduino(C/C++)で試してみます。

ESP32-S3-WROOM-1を搭載したモデルが入手できましたので記事を書きました。ぜひご覧ください。

スポンサーリンク

特徴

項目仕様詳細
ESP32モジュールESP32-WROVER-E
(技適マーク付き)
工事設計認証番号:
211-200403
CPUスピード 240MHz
フラッシュメモリ 4MB
PSRAM 8MB※1
ワイヤレス通信規格 802.11n
(周波数帯は2.4GHzのみ)
カメラオンボードカメラ
(コネクタ)
付属カメラモジュール
FNK0060(v1.6)
OV2640 66°

FNK0060B(v3.0)
OV3660
USBシリアルv1.6
Micro USB

v3.0
Type-C
シリアル変換IC
CH340※2
SDカードスロット(v3.0のみ)オンボード
Micro SDカードスロット
SDカード(1GB)
USB SDカードリーダー
が付属していてすぐに試せる
※1 Arduinoで使用可能な容量は4MBまでです。
※2 シリアルドライバをインストールしていない場合は別途インストールが必要です。

画像ギャラリー

Freenove ESP32-WROVER Board FNK0060(v1.6)

外箱
ボード本体の他、USBケーブルとリーフレットが付属しています。
袋からボードを取り出したところ

Freenove ESP32-WROVER Board FNK0060B(v3.0)

外箱 FNK0060B
FNK0060の内容に加えSDカード(1GB)とSDカードリーダーが付属しています
カメラはOV3660です
SDカードスロットが確認できます

サイズ

普通のブレッドボード(400穴)だと片側1ピン分の穴が確保できます。

ブレッドボード2枚を連結して設置すると左右どちらもピン穴が確保できました。

サンハヤトのニューブレッドボードでは、3ピン分(片側1ピン、他方2ピン)ピン穴が確保できました。
このボードを使うのにオススメのブレッドボードです。
※このブレッドボードの使用例を後述のサンプルプログラムに掲載しています。

\楽天ポイント4倍セール!/
楽天市場
\商品券4%還元!/
Yahooショッピング

カメラモジュール(OV2640)例

カメラモジュール(OV2640)には様々なバリエーションがあります。
こちらは別途購入したもの。

左から
66° 21mm(付属カメラ) 120°21mm 160°21mm
68°75mm 160°75mm

カメラはコネクタ接続なので換装して使えます。

120° 21mmを取り付けたところ
160° 21mmを取り付けたところ

ピン配列

v1.6 (FNK0060)

v3.0 (FNK0060B)

※画像のバー付きのGPIOはカメラ(コネクタ)に接続されています。

使用できるGPIOについて

カメラ使用時

以下が使用できると思います。

12〜15 (HSPI対応)
32、33

v3.0の場合 (SDカードスロット使用時)

カメラに加えSDカードスロットも使った場合、以下となります。

12、13、32、33

カメラ未使用時について

このボードは、通常のESP32開発ボードとしても利用可能です。

例えば、OLEDクロックを動かしてみたところです。

I2Cのピンとして、SCL 22、SDA 21を使用
GitHub - aloseed/esp32_oled_clock
Contribute to aloseed/esp32_oled_clock development by creating an account on GitHub.

参考サイト:

Arduinoボード設定

Arduino IDEにESP32ボード(安定リリース版)を追加しておきます。

追加方法については、以下の記事をご参照ください。

  • ボードインストール

[ツール] > [ボード] > [ボードマネージャ]からESP32ボードをインストールします。

  • ボードの選択

[ツール] > [ボード] > [ESP32 Arduino※] > [ESP32 Wrover Module]を選択します。
※Arduinoの表記はArduino IDEバージョン1系のみ

  • シリアルポートの選択

ボードが接続されているポートを選択します。

環境ポート名
Mac/dev/cu.wchusbserial*
WindowsCOM*
*(アスタリスク)は任意の文字列を表しています。

※シリアルポートが認識されない場合はCH340用シリアルドライバを以下からダウンロードしてインストールしてください。

GitHub
https://github.com/Freenove/Freenove_ESP32_WROVER_Board/tree/main/CH340

書き込みモード(Download Boot)

BOOTボタン」を押しながら、「リセットボタン」を押すと書き込みモードになります。

自動書き込みに失敗する場合にお試しください。

v3.0の場合

GPIO2とGNDをジャンパーワイヤーで接続(以下参照)して上記手順を実施します。
書き込み後はジャンパーワイヤーを取り外します。

サンプルプログラム (Arduinoスケッチ)

開発環境

Arduino IDE バージョン1.8.19 (※1系)
2系(2.x.x)は最新版でOK
ESP32 Arduinoボード基本的に最新版でOK

以下にpdfやサンプルスケッチがあります。

Freenove_ESP32_WROVER_Board/C at main ?? Freenove/Freenove_ESP32_WROVER_Board
Apply to FNK0060. Contribute to Freenove/Freenove_ESP32_WROVER_Board development by creating an account on GitHub.

CameraWebServer (Arduino IDEスケッチ例を使用する場合)

CameraWebServerスケッチを書き込む場合、スケッチサイズの関係上[ツール] > [Partition Scheme] > [Huge APP (3MB No OTA/1MB SPIFFS)]を選択してください。

GitHubのサンプルスケッチにSketch_06.1_CameraWebServerがありますが、Arduino IDEのスケッチ例([スケッチ例] > [ESP32] > [Camera] > [CameraWebServer])を使用する場合、次のように修正します。

  1. カメラモデルの指定
    CAMERA_MODEL_WROVER_KITのコメントをはずし、CAMERA_MODEL_ESP_EYE(※)をコメントアウトします。
    ※ESP32ボードのバージョンによってはCAMERA_MODEL_ESP_EYEではない場合があります。
    この手順のポイントはCAMERA_MODEL_WROVER_KITのコメントを外して、これ以外のCAMERA_MODELをコメントアウトするということです。

  2. Wi-Fi設定
    ssidとpasswordを接続先Wi-Fiのssidとpasswordに書き換えます。

スケッチを書き込んだのち、シリアルモニターを開きリセットボタンを押すとCameraWebServerのURL(画像赤枠参照)が確認できます。

ブラウザからシリアルモニターに表示されたURLにアクセスすると以下のような画面が表示されます。

動作例

映像を液晶ディスプレイに表示する(HSPI使用例)

ボタンを押すと映像が液晶ディスプレイに表示されるというスケッチ例です。

素材はこちら

液晶ディスプレイ

今回のスケッチ例ではST7789の液晶ディスプレイを使用します。

左: 1.54インチ 右: 1.3インチ

いずれも解像度は240×240です。

ちなみに1.3インチの方はCSピンがありません。そのためスケッチを動かす場合は修正が必要です。

製作例

1.54インチディスプレイの例

1.54インチディスプレイ使用
右の箱は今回の被写体に使っている素材で製作には無関係です。

1.3インチディスプレイの例

1.3インチディスプレイ使用
パーツについて
  • 液晶ディスプレイ
    ST7789のものを使っています。

  • ボタン
    1.54インチディスプレイの例で使用しているボタンはタッチでON/OFFができるトグルスイッチ式のものです。
    1.3インチディスプレイの例ではスライドスイッチを使用してみました。

  • ブレッドボード
    ニューブレッドボード(サンハヤト)を使っています。
\楽天ポイント4倍セール!/
楽天市場
\商品券4%還元!/
Yahooショッピング

スケッチ1 (Adafruit GFXライブラリ使用)

スケッチで使用しているライブラリについて
  • Adafruit ST7735 and ST7789 Library
  • Adafruit GFX Library
  • TJpg_Decoder

いずれのライブラリもArduino IDEのライブラリマネージャーから検索してインストールできます。

#include "esp_camera.h"
#include <SPI.h>
#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7789.h> // Hardware-specific library for ST7789
#include <TJpg_Decoder.h>

// Pin definition for CAMERA_MODEL_WROVER_KIT
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 21
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27

#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 19
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 5
#define Y2_GPIO_NUM 4
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22

// Pin definition for TFT HSPI
#define TFT_SCLK 14
#define TFT_MISO -1
#define TFT_MOSI 13
#define TFT_CS 15 // Chip select control pin
#define TFT_DC 32 // Data Command control pin
#define TFT_RST 33

// Pin button
#define PIN_BTN 12

Adafruit_ST7789 tft = Adafruit_ST7789(&SPI, TFT_CS, TFT_DC, TFT_RST);

bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap)
{
  // Stop further decoding as image is running off bottom of screen
  if (y >= tft.height())
    return 0;
  tft.drawRGBBitmap(x, y, bitmap, w, h);
  // Return 1 to decode next block
  return 1;
}

void init_camera()
{
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.frame_size = FRAMESIZE_UXGA;
  config.pixel_format = PIXFORMAT_JPEG; // for streaming
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 1;

  // if PSRAM IC present, init with UXGA resolution and higher JPEG quality
  //                      for larger pre-allocated frame buffer.
  if (config.pixel_format == PIXFORMAT_JPEG)
  {
    if (psramFound())
    {
      config.jpeg_quality = 10;
      config.fb_count = 2;
      config.grab_mode = CAMERA_GRAB_LATEST;
    }
    else
    {
      // Limit the frame size when PSRAM is not available
      config.frame_size = FRAMESIZE_SVGA;
      config.fb_location = CAMERA_FB_IN_DRAM;
    }
  }
  else
  {
    // Best option for face detection/recognition
    config.frame_size = FRAMESIZE_QVGA;
#if CONFIG_IDF_TARGET_ESP32S3
    config.fb_count = 2;
#endif
  }

  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK)
  {
    Serial.printf("Camera init failed with error 0x%x", err);
    for (;;)
      ;
  }

  sensor_t *s = esp_camera_sensor_get();
  // initial sensors are flipped vertically and colors are a bit saturated
  if (s->id.PID == OV3660_PID)
  {
    s->set_vflip(s, 1);       // flip it back
    s->set_brightness(s, 1);  // up the brightness just a bit
    s->set_saturation(s, -2); // lower the saturation
  }
  // drop down frame size for higher initial frame rate
  if (config.pixel_format == PIXFORMAT_JPEG)
  {
    s->set_framesize(s, FRAMESIZE_QVGA);
  }
}

void setup()
{
  Serial.begin(115200);
  Serial.println("ESP32-WROVER-CAM Picture");

  init_camera();

  // Use HSPI
  SPI.begin(TFT_SCLK, TFT_MISO, TFT_MOSI);
  // if the display has CS pin try with SPI_MODE0
  tft.init(240, 240); // Init ST7789 display 240x240 pixel
  tft.fillScreen(ST77XX_BLACK);

  TJpgDec.setJpgScale(1);
  // The decoder must be given the exact name of the rendering function above
  TJpgDec.setCallback(tft_output);

  pinMode(PIN_BTN, INPUT);
}

void take_picture()
{
  Serial.println("Taking picture..");
  camera_fb_t *fb = NULL;

  fb = esp_camera_fb_get();
  if (!fb)
  {
    Serial.println("Camera capture failed");
    return;
  }

  static uint16_t w = 0, h = 0;
  TJpgDec.getJpgSize(&w, &h, fb->buf, fb->len);
  Serial.print("- Width = ");
  Serial.print(fb->width);
  Serial.print(", height = ");
  Serial.println(fb->height);

  // Draw the image, top left at 0,0
  TJpgDec.drawJpg(0, 0, fb->buf, fb->len);

  // Free buffer
  esp_camera_fb_return(fb);
}

void loop()
{
  static int state = LOW;
  if (digitalRead(PIN_BTN) == HIGH)
  {
    Serial.println("Button pressed");
    take_picture();
    state = HIGH;
  }
  else if (digitalRead(PIN_BTN) == LOW && state == HIGH)
  {
    tft.fillScreen(ST77XX_BLACK);
    state = LOW;
  }

  delay(500);
}

ピン接続は次のようになっています。

パーツボードGPIO備考
TFT SCL14HSPI SCLK
TFT SDA13HSPI MOSI
TFT CS15HSPI CS
1.3インチLCDでは配線不要
TFT DC32
TFT RST33
BUTTON(スイッチ)12ボタン(トグルスイッチ)
INPUT
※TFTは液晶ディスプレイ(LCD)を表しています

液晶ディスプレイはHSPIで接続するようにしています。

スイッチに割り当てるGPIOが不足したため、GPIO12(HSPI MISO)を入力ピンに割り当てています。

スケッチの修正について

1.3インチディスプレイを使用する場合、CSピンがないためSPIをMODE 2に設定します。

tft.initの箇所を以下のように修正します。

  tft.init(240, 240, SPI_MODE2); // Init ST7789 display 240x240 pixel

スライドスイッチを使用する場合

以下のようにON側に3.3V(赤ワイヤー)、OFF側にGND(黒ワイヤー)、入力(青ワイヤー)をGPIO12に接続すればON/OFFを実現できます。

今回の製作例のようにON側の3.3V(赤ワイヤー)のみ配線する場合、以下のようにOFF側にスイッチを入れているとき入力ピン(青ワイヤー)の状態が不安定になります。

そこで、setup関数内のボタン入力ピンの設定を以下のようにINPUT_PULLDOWNにするとスイッチがOFF側(入力ピン(青ワイヤー)が開放された状態)の時に内部プルダウンされるようになるのでGNDの配線をしなくても動作するようになるかと思います。

  pinMode(PIN_BTN, INPUT_PULLDOWN);

1.3インチディスプレイ動作例

スケッチ2 (LovyanGFXライブラリ使用)

上のスケッチをLovyanGFXライブラリに置き換えたものです。
このスケッチは1.3インチ(CSなし)ディスプレイに対応しています。

スケッチで使用しているライブラリ
  • LovyanGFX
#include "esp_camera.h"
#include <LovyanGFX.hpp>

// Pin definition for CAMERA_MODEL_WROVER_KIT
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 21
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27

#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 19
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 5
#define Y2_GPIO_NUM 4
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22

// Pin definition for TFT HSPI
#define TFT_SCLK 14
#define TFT_MISO -1
#define TFT_MOSI 13
#define TFT_CS 15  // Chip select control pin
#define TFT_DC 32  // Data Command control pin
#define TFT_RST 33

// Pin button
#define PIN_BTN 12

int32_t lcd_width;
int32_t lcd_height;
int32_t y;

// LCD表示画像の倍率
float zoom;

// カメラ画像フレームバッファ
camera_fb_t *fb;

// ST7789 1.3インチ CSなし
class LGFX : public lgfx::LGFX_Device {
  lgfx::Panel_ST7789 _panel_instance;
  lgfx::Bus_SPI _bus_instance;  // SPIバスのインスタンス

public:
  LGFX(void) {
    {                                     // バス制御の設定を行います。
      auto cfg = _bus_instance.config();  // バス設定用の構造体を取得します。

      // SPIバスの設定
      cfg.spi_host = SPI3_HOST;
      cfg.spi_mode = 0;           // SPI通信モードを設定 (0 ~ 3)
      cfg.freq_write = 40000000;  // 送信時のSPIクロック (最大80MHz, 80MHzを整数で割った値に丸められます)
      cfg.freq_read = 20000000;   // 受信時のSPIクロック

      //      cfg.spi_3wire  = true;        // 受信をMOSIピンで行う場合はtrueを設定
      //      cfg.use_lock   = true;        // トランザクションロックを使用する場合はtrueを設定
      cfg.dma_channel = SPI_DMA_CH_AUTO;  // 使用するDMAチャンネルを設定 (0=DMA不使用 / 1=1ch / 2=ch / SPI_DMA_CH_AUTO=自動設定)

      cfg.pin_sclk = TFT_SCLK;  // SPIのSCLKピン番号を設定
      cfg.pin_mosi = TFT_MOSI;  // SPIのMOSIピン番号を設定
      cfg.pin_miso = TFT_MISO;  // SPIのMISOピン番号を設定 (-1 = disable)
      cfg.pin_dc = TFT_DC;      // SPIのD/Cピン番号を設定  (-1 = disable)

      _bus_instance.config(cfg);               // 設定値をバスに反映します。
      _panel_instance.setBus(&_bus_instance);  // バスをパネルにセットします。
    }

    {                                       // 表示パネル制御の設定を行います。
      auto cfg = _panel_instance.config();  // 表示パネル設定用の構造体を取得します。

      cfg.pin_cs = -1;    // CSが接続されているピン番号   (-1 = disable)
      cfg.pin_rst = -1;   // RSTが接続されているピン番号  (-1 = disable)
      cfg.pin_busy = -1;  // BUSYが接続されているピン番号 (-1 = disable)

      cfg.panel_width = 240;   // 実際に表示可能な幅
      cfg.panel_height = 240;  // 実際に表示可能な高さ

      cfg.invert = true;  // パネルの明暗が反転してしまう場合 trueに設定
      // cfg.rgb_order = true; // パネルの赤と青が入れ替わってしまう場合 trueに設定

      _panel_instance.config(cfg);
    }

    setPanel(&_panel_instance);  // 使用するパネルをセットします。

    lgfx::pinMode(TFT_RST, lgfx::pin_mode_t::output);
    lgfx::pinMode(TFT_SCLK, lgfx::pin_mode_t::output);

    lgfx::gpio_lo(TFT_RST);
    lgfx::gpio_hi(TFT_SCLK);
    lgfx::gpio_hi(TFT_RST);
  }
};

static LGFX tft;

void init_camera() {
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.frame_size = FRAMESIZE_UXGA;
  config.pixel_format = PIXFORMAT_JPEG;  // for streaming
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 1;

  // if PSRAM IC present, init with UXGA resolution and higher JPEG quality
  //                      for larger pre-allocated frame buffer.
  if (config.pixel_format == PIXFORMAT_JPEG) {
    if (psramFound()) {
      config.jpeg_quality = 10;
      config.fb_count = 2;
      config.grab_mode = CAMERA_GRAB_LATEST;
    } else {
      // Limit the frame size when PSRAM is not available
      config.frame_size = FRAMESIZE_SVGA;
      config.fb_location = CAMERA_FB_IN_DRAM;
    }
  } else {
    // Best option for face detection/recognition
    config.frame_size = FRAMESIZE_QVGA;
#if CONFIG_IDF_TARGET_ESP32S3
    config.fb_count = 2;
#endif
  }

  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    for (;;)
      ;
  }

  sensor_t *s = esp_camera_sensor_get();
  // initial sensors are flipped vertically and colors are a bit saturated
  if (s->id.PID == OV3660_PID) {
    s->set_vflip(s, 1);        // flip it back
    s->set_brightness(s, 1);   // up the brightness just a bit
    s->set_saturation(s, -2);  // lower the saturation
  }
  // drop down frame size for higher initial frame rate
  if (config.pixel_format == PIXFORMAT_JPEG) {
    s->set_framesize(s, FRAMESIZE_QVGA);
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println("ESP32-WROVER-CAM Picture");

  // スイッチの入力設定
  pinMode(PIN_BTN, INPUT_PULLDOWN);

  tft.init();
  // 画面回転はLCDに合わせて設定してください
  // tft.setRotation(1);

  lcd_width = tft.width();
  lcd_height = tft.height();

  // カメラ初期化
  init_camera();
  // カメラ画像の横幅を取得
  fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Camera capture failed");
    // カメラ画像が取得できない場合は停止
    for (;;)
      yield();
  }
  // LCD画像の倍率をセット
  zoom = lcd_width / (float)fb->width;
  y = (lcd_height - fb->height * zoom) / 2;
}

void loop() {
  static int state = LOW;
  if (digitalRead(PIN_BTN) == HIGH) {
    // Serial.println("Button pressed");

    // フレームバッファ解放
    esp_camera_fb_return(fb);

    // カメラのフレームを取得
    fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Camera capture failed");
      // カメラ画像が取得できない場合は停止
      for (;;)
        yield();
    }
    // Serial.println("Taking picture..");

    tft.drawJpg(fb->buf, fb->len, 0, y, lcd_width, lcd_height, 0, 0, zoom);

    state = HIGH;
  } else if (digitalRead(PIN_BTN) == LOW && state == HIGH) {
    tft.clear();
    state = LOW;
  }

  delay(100);
}

お好みのLCDに対応させるには、LGFXクラスの定義を変えればよいでしょう。
以下の記事を参考にしてください。

プリズムを使ってみました

見た目は透明なブロックみたいな感じです。1辺25.4mmの正方形です。

このプリズムを液晶ディスプレイに乗せるとプリズムに映像が投影されます。

投影している様子がこちら

ピクチャーフレーム (v3.0向け)

SDカード内の画像ファイル(jpgかpng)をLCDに表示するスケッチです。
画像ファイルが複数あると順番に表示します。
v3.0搭載SDカードスロットを使います。
LCDは1.3インチ(CSなし)ディスプレイに対応しています。

\楽天ポイント4倍セール!/
楽天市場
\商品券4%還元!/
Yahooショッピング
スケッチで使用しているライブラリ
  • LovyanGFX
#include "SD_MMC.h"
#include <LovyanGFX.hpp>

#define SD_MMC_CMD 15  //Please do not modify it.
#define SD_MMC_CLK 14  //Please do not modify it.
#define SD_MMC_D0 2    //Please do not modify it.

// ESP32 for ST7789 1.3LCD
#define TFT_MISO -1
#define TFT_MOSI 13
#define TFT_SCLK 12
#define TFT_DC 32
#define TFT_RST 33

class LGFX : public lgfx::LGFX_Device {
  lgfx::Panel_ST7789 _panel_instance;
  lgfx::Bus_SPI _bus_instance;  // SPIバスのインスタンス

public:
  LGFX(void) {
    {                                     // バス制御の設定を行います。
      auto cfg = _bus_instance.config();  // バス設定用の構造体を取得します。

      // SPIバスの設定
      cfg.spi_host = SPI3_HOST;
      cfg.spi_mode = 0;           // SPI通信モードを設定 (0 ~ 3)
      cfg.freq_write = 40000000;  // 送信時のSPIクロック (最大80MHz, 80MHzを整数で割った値に丸められます)
      // cfg.freq_read = 20000000;   // 受信時のSPIクロック

      cfg.spi_3wire = true;               // 受信をMOSIピンで行う場合はtrueを設定
      cfg.use_lock = true;                // トランザクションロックを使用する場合はtrueを設定
      cfg.dma_channel = SPI_DMA_CH_AUTO;  // 使用するDMAチャンネルを設定 (0=DMA不使用 / 1=1ch / 2=ch / SPI_DMA_CH_AUTO=自動設定)

      cfg.pin_sclk = TFT_SCLK;  // SPIのSCLKピン番号を設定
      cfg.pin_mosi = TFT_MOSI;  // SPIのMOSIピン番号を設定
      cfg.pin_miso = TFT_MISO;  // SPIのMISOピン番号を設定 (-1 = disable)
      cfg.pin_dc = TFT_DC;      // SPIのD/Cピン番号を設定  (-1 = disable)

      _bus_instance.config(cfg);               // 設定値をバスに反映します。
      _panel_instance.setBus(&_bus_instance);  // バスをパネルにセットします。
    }

    {                                       // 表示パネル制御の設定を行います。
      auto cfg = _panel_instance.config();  // 表示パネル設定用の構造体を取得します。

      cfg.pin_cs = -1;    // CSが接続されているピン番号   (-1 = disable)
      cfg.pin_rst = -1;   // RSTが接続されているピン番号  (-1 = disable)
      cfg.pin_busy = -1;  // BUSYが接続されているピン番号 (-1 = disable)

      cfg.panel_width = 240;   // 実際に表示可能な幅
      cfg.panel_height = 240;  // 実際に表示可能な高さ

      cfg.readable = false;  // データ読出しが可能な場合 trueに設定
      cfg.invert = true;     // パネルの明暗が反転してしまう場合 trueに設定
      // cfg.rgb_order = true; // パネルの赤と青が入れ替わってしまう場合 trueに設定
      // cfg.bus_shared = true;

      _panel_instance.config(cfg);
    }
    setPanel(&_panel_instance);  // 使用するパネルをセットします。

    lgfx::pinMode(TFT_RST, lgfx::pin_mode_t::output);
    lgfx::pinMode(TFT_SCLK, lgfx::pin_mode_t::output);

    lgfx::gpio_lo(TFT_RST);
    lgfx::gpio_hi(TFT_SCLK);
    lgfx::gpio_hi(TFT_RST);
  }
};

const unsigned long displayDuration = 2000;  // Display time (2 seconds)

LGFX lcd;  // LGFX instance
std::vector<String> fileList;
int currentIndex = 0;
int32_t screenWidth;
int32_t screenHeight;

void initFileSystem() {
  // SDカードスロット初期化
  SD_MMC.setPins(SD_MMC_CLK, SD_MMC_CMD, SD_MMC_D0);
  if (!SD_MMC.begin("/sdcard", true)) {
    Serial.println("Card Mount Failed");
    for (;;) yield();
  }
}

void createFileList() {
  File root = SD_MMC.open("/");
  File file = root.openNextFile();

  while (file) {
    String filePath = file.path();
    if (!filePath.startsWith("/.") && (filePath.endsWith(".jpg") || filePath.endsWith(".png"))) {
      fileList.push_back(filePath);
    }
    file = root.openNextFile();
  }
  root.close();

  if (fileList.empty()) {
    Serial.println("No image files found in SD.");
    for (;;) yield();
  }
}

// Get JPEG image size
bool getJpgSize(File &file, int &width, int &height) {
  uint8_t data[5];
  file.read(data, 2);

  if (data[0] == 0xFF && data[1] == 0xD8) {
    while (file.read(data, 4) == 4) {
      if (data[0] == 0xFF && (data[1] >= 0xC0 && data[1] <= 0xC3)) {
        file.read(data, 5);
        height = data[1] << 8 | data[2];
        width = data[3] << 8 | data[4];
        return true;
      } else {
        uint16_t size = data[2] << 8 | data[3];
        file.seek(file.position() + size - 2);
      }
    }
  }
  return false;
}

// Get PNG image size
bool getPngSize(File &file, int &width, int &height) {
  uint8_t data[24];
  if (file.read(data, 24) == 24) {
    if (data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47) {
      width = data[16] << 24 | data[17] << 16 | data[18] << 8 | data[19];
      height = data[20] << 24 | data[21] << 16 | data[22] << 8 | data[23];
      return true;
    }
  }
  return false;
}

void drawImage(const String &fileName) {
  File file = SD_MMC.open(fileName, FILE_READ);
  if (!file) {
    Serial.println("Failed to open file: " + fileName);
    return;
  }

  // Serial.printf("%s\n", fileName.c_str());
  int imgWidth = 0, imgHeight = 0;
  bool validImage = false;
  bool isJpg = false;

  // サイズ取得
  if (fileName.endsWith(".jpg")) {
    validImage = getJpgSize(file, imgWidth, imgHeight);
    isJpg = true;
  } else if (fileName.endsWith(".png")) {
    validImage = getPngSize(file, imgWidth, imgHeight);
  }

  if (!validImage) {
    file.close();
    Serial.println("Invalid or unsupported image format.");
    return;
  }

  // ファイル開始位置を戻す
  file.seek(0);

  // スケーリング
  float scaleX = static_cast<float>(screenWidth) / imgWidth;
  float scaleY = static_cast<float>(screenHeight) / imgHeight;
  float scale = min(scaleX, scaleY);

  // x座標 センタリング
  int32_t x = (screenWidth - imgWidth * scale) / 2;

  lcd.startWrite();
  lcd.clear();
  if (isJpg) {
    lcd.drawJpg(&file, x, 0, screenWidth, screenHeight, 0, 0, scale);
  } else {
    lcd.drawPng(&file, x, 0, screenWidth, screenHeight, 0, 0, scale);
  }
  lcd.endWrite();
  file.close();
}

void setup() {
  Serial.begin(115200);

  // LCD初期化
  lcd.init();

  screenWidth = lcd.width();
  screenHeight = lcd.height();

  // SDカードスロット初期化
  initFileSystem();
  // 画像ファイルリスト取得
  createFileList();
}

void loop() {
  // 画像を描画
  drawImage(fileList[currentIndex]);
  // 次の画像(インデックス)
  currentIndex = (currentIndex + 1) % fileList.size();
  delay(displayDuration);
}

対応LCD 1.3インチ ST7789 CSなし

LCDピン/GPIO
GNDGND
VCC3.3V
SCL12
SDA13
RES33
DC32
BLK未接続

HSPIを使用していますが、HSPI_CLKがSDカードスロットのCLKと被るため、HSPI_MISO(12)をCLKに割り当てています。

表示できる画像ファイルについて

.jpgと.pngの画像に対応しています。
お好きな画像ファイルをSDカードのルートフォルダに保存し、ESP32-WROVER CAMボードのSDカードスロットに挿し込むだけで、LCD画面に表示できます。
ぜひお試しください!

映像を液晶ディスプレイに表示する シャッター機能付き (v3.0向け)

カメラ画像が液晶ディスプレイに表示されるというスケッチ例です。
v3.0のSDカードスロットに挿入されているSDカードに画像を保存する機能を追加しています。

\楽天ポイント4倍セール!/
楽天市場
\商品券4%還元!/
Yahooショッピング

BOOTボタンを押すと現在のカメラ画像がSDカードに保存されます。

サンプル画像

画像サイズはVGA(640×480)です。

#include "esp_camera.h"
#include "SD_MMC.h"
#include <LovyanGFX.hpp>

// Pin definition for CAMERA_MODEL_WROVER_KIT
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 21
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27

#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 19
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 5
#define Y2_GPIO_NUM 4
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22

// Pin definition for SD MMC
#define SD_MMC_CMD 15  //Please do not modify it.
#define SD_MMC_CLK 14  //Please do not modify it.
#define SD_MMC_D0 2    //Please do not modify it.

// Pin definition for TFT HSPI
// #define TFT_SCLK 14
#define TFT_MISO -1
#define TFT_MOSI 13
#define TFT_SCLK 12
// #define TFT_CS 15  // Chip select control pin
#define TFT_DC 32  // Data Command control pin
#define TFT_RST 33

// カメラ画像の更新間隔 (ミリ秒で指定) 500ミリ秒間隔
#define UPDATE_INTERVAL (500u)

// Shutter button
#define SHUTTER_BUTTON 0

int32_t lcd_width;
int32_t lcd_height;
int32_t y;

// LCD表示画像の倍率
float zoom;

// カメラ画像フレームバッファ
camera_fb_t* fb;

// シャッター
volatile bool shutter = false;

// ST7789 1.3インチ CSなし
class LGFX : public lgfx::LGFX_Device {
  lgfx::Panel_ST7789 _panel_instance;
  lgfx::Bus_SPI _bus_instance;  // SPIバスのインスタンス

public:
  LGFX(void) {
    {                                     // バス制御の設定を行います。
      auto cfg = _bus_instance.config();  // バス設定用の構造体を取得します。

      // SPIバスの設定
      cfg.spi_host = SPI3_HOST;
      cfg.spi_mode = 0;           // SPI通信モードを設定 (0 ~ 3)
      cfg.freq_write = 40000000;  // 送信時のSPIクロック (最大80MHz, 80MHzを整数で割った値に丸められます)
      // cfg.freq_read = 20000000;   // 受信時のSPIクロック

      cfg.spi_3wire = true;               // 受信をMOSIピンで行う場合はtrueを設定
      cfg.use_lock = true;                // トランザクションロックを使用する場合はtrueを設定
      cfg.dma_channel = SPI_DMA_CH_AUTO;  // 使用するDMAチャンネルを設定 (0=DMA不使用 / 1=1ch / 2=ch / SPI_DMA_CH_AUTO=自動設定)

      cfg.pin_sclk = TFT_SCLK;  // SPIのSCLKピン番号を設定
      cfg.pin_mosi = TFT_MOSI;  // SPIのMOSIピン番号を設定
      cfg.pin_miso = TFT_MISO;  // SPIのMISOピン番号を設定 (-1 = disable)
      cfg.pin_dc = TFT_DC;      // SPIのD/Cピン番号を設定  (-1 = disable)

      _bus_instance.config(cfg);               // 設定値をバスに反映します。
      _panel_instance.setBus(&_bus_instance);  // バスをパネルにセットします。
    }

    {                                       // 表示パネル制御の設定を行います。
      auto cfg = _panel_instance.config();  // 表示パネル設定用の構造体を取得します。

      cfg.pin_cs = -1;    // CSが接続されているピン番号   (-1 = disable)
      cfg.pin_rst = -1;   // RSTが接続されているピン番号  (-1 = disable)
      cfg.pin_busy = -1;  // BUSYが接続されているピン番号 (-1 = disable)

      cfg.panel_width = 240;   // 実際に表示可能な幅
      cfg.panel_height = 240;  // 実際に表示可能な高さ

      cfg.readable = false;  // データ読出しが可能な場合 trueに設定
      cfg.invert = true;     // パネルの明暗が反転してしまう場合 trueに設定
      // cfg.rgb_order = true; // パネルの赤と青が入れ替わってしまう場合 trueに設定
      // cfg.bus_shared = true;

      _panel_instance.config(cfg);
    }
    setPanel(&_panel_instance);  // 使用するパネルをセットします。

    lgfx::pinMode(TFT_RST, lgfx::pin_mode_t::output);
    lgfx::pinMode(TFT_SCLK, lgfx::pin_mode_t::output);

    lgfx::gpio_lo(TFT_RST);
    lgfx::gpio_hi(TFT_SCLK);
    lgfx::gpio_hi(TFT_RST);
  }
};

static LGFX lcd;

void init_camera() {
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.frame_size = FRAMESIZE_UXGA;
  config.pixel_format = PIXFORMAT_JPEG;  // for streaming
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 1;

  // if PSRAM IC present, init with UXGA resolution and higher JPEG quality
  //                      for larger pre-allocated frame buffer.
  if (config.pixel_format == PIXFORMAT_JPEG) {
    if (psramFound()) {
      config.jpeg_quality = 10;
      config.fb_count = 2;
      config.grab_mode = CAMERA_GRAB_LATEST;
    } else {
      // Limit the frame size when PSRAM is not available
      config.frame_size = FRAMESIZE_SVGA;
      config.fb_location = CAMERA_FB_IN_DRAM;
    }
  } else {
    // Best option for face detection/recognition
    config.frame_size = FRAMESIZE_QVGA;
#if CONFIG_IDF_TARGET_ESP32S3
    config.fb_count = 2;
#endif
  }

  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    for (;;)
      yield();
  }

  sensor_t* s = esp_camera_sensor_get();
  // initial sensors are flipped vertically and colors are a bit saturated
  if (s->id.PID == OV3660_PID) {
    s->set_vflip(s, 1);        // flip it back
    s->set_brightness(s, 1);   // up the brightness just a bit
    s->set_saturation(s, -2);  // lower the saturation
  }
  // drop down frame size for higher initial frame rate
  if (config.pixel_format == PIXFORMAT_JPEG) {
    s->set_framesize(s, FRAMESIZE_VGA);
  }
}

void sdmmcInit(void) {
  SD_MMC.setPins(SD_MMC_CLK, SD_MMC_CMD, SD_MMC_D0);
  if (!SD_MMC.begin("/sdcard", true)) {
    Serial.println("Card Mount Failed");
    for (;;) yield();
  }
  uint8_t cardType = SD_MMC.cardType();
  if (cardType == CARD_NONE) {
    Serial.println("No SD_MMC card attached");
    for (;;) yield();
  }
  Serial.print("SD_MMC Card Type: ");
  if (cardType == CARD_MMC) {
    Serial.println("MMC");
  } else if (cardType == CARD_SD) {
    Serial.println("SDSC");
  } else if (cardType == CARD_SDHC) {
    Serial.println("SDHC");
  } else {
    Serial.println("UNKNOWN");
  }
  uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
  Serial.printf("SD_MMC Card Size: %lluMB\n", cardSize);
  Serial.printf("Total space: %lluMB\r\n", SD_MMC.totalBytes() / (1024 * 1024));
  Serial.printf("Used space: %lluMB\r\n", SD_MMC.usedBytes() / (1024 * 1024));
}

void createDir(fs::FS& fs, const char* path) {
  Serial.printf("Creating Dir: %s\n", path);
  if (fs.mkdir(path)) {
    Serial.println("Dir created");
  } else {
    Serial.println("mkdir failed");
  }
}

int readFileNum(fs::FS& fs, const char* dirname) {
  File root = fs.open(dirname);
  if (!root) {
    Serial.println("Failed to open directory");
    return -1;
  }
  if (!root.isDirectory()) {
    Serial.println("Not a directory");
    return -1;
  }

  File file = root.openNextFile();
  int num = 0;
  while (file) {
    file = root.openNextFile();
    num++;
  }
  return num;
}

void writejpg(fs::FS& fs, const char* path, const uint8_t* buf, size_t size) {
  File file = fs.open(path, FILE_WRITE);
  if (!file) {
    Serial.println("Failed to open file for writing");
    return;
  }
  file.write(buf, size);
  file.close();
  Serial.printf("Saved file to path: %s\r\n", path);
}

void shutterTask(void* arg) {
  for (;;) {
    if (!shutter && digitalRead(SHUTTER_BUTTON) == LOW) {
      shutter = true;
    }
    yield();
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println("ESP32-WROVER-CAM Picture");

  // シャッターボタン
  pinMode(SHUTTER_BUTTON, INPUT_PULLUP);

  // LCD初期化
  lcd.init();
  // 画面回転はLCDに合わせて設定してください
  lcd.setRotation(1);

  lcd_width = lcd.width();
  lcd_height = lcd.height();

  // カメラ初期化
  init_camera();
  // カメラ画像の横幅を取得
  fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Camera capture failed");
    // カメラ画像が取得できない場合は停止
    for (;;)
      yield();
  }
  // LCD画像の倍率をセット
  zoom = lcd_width / (float)fb->width;
  y = (lcd_height - fb->height * zoom) / 2;

  // SDカード初期化
  sdmmcInit();
  createDir(SD_MMC, "/camera");

  //Start Shutter tasks
  xTaskCreateUniversal(shutterTask, "shutterTask", 8192, NULL, 1, NULL, CONFIG_ARDUINO_RUNNING_CORE);
}

void loop() {
  static auto last = millis();

  if (millis() - last > UPDATE_INTERVAL) {
    // フレームバッファ解放
    esp_camera_fb_return(fb);

    // カメラのフレームを取得
    fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Camera capture failed");
      // カメラ画像が取得できない場合は停止
      for (;;)
        yield();
    }
    Serial.println("Taking picture..");

    lcd.drawJpg(fb->buf, fb->len, 0, y, lcd_width, lcd_height, 0, 0, zoom);

    last = millis();
  }
  if (shutter) {
    while (digitalRead(SHUTTER_BUTTON) == LOW) yield();
    if (fb != NULL) {
      int photo_index = readFileNum(SD_MMC, "/camera");
      if (photo_index != -1) {
        String path = "/camera/" + String(photo_index) + ".jpg";
        // グレイに塗りつぶし シャッターを表現
        lcd.clear(TFT_DARKGREY);
        Serial.println("camera shutter");

        // 画像保存
        writejpg(SD_MMC, path.c_str(), fb->buf, fb->len);
        // LCDをクリア
        lcd.clear(TFT_BLACK);

        shutter = false;
      }
    }
  }
}

対応LCD 1.3インチ ST7789 CSなし

LCDピン/GPIO
GNDGND
VCC3.3V
SCL12
SDA13
RES33
DC32
BLK未接続

ピンを全て使ったため、止むなくシャッターボタンにBOOTボタン(GPIO 0)を割り当てました。

関連記事

当ブログのマイコン記事です。ぜひご覧ください。

コメント

  1. 匿名 より:

    参考になる記事を書いていただきありがとうございます。
    モジュールについてですが、本物のFreenove FNK0060はESP32-WROVER-Eの正しい証明番号(211-200403)ですが、ジェネリックというか偽物の方はESP-WROOM32の証明番号(211-161007)を適当に書いてるだけなのでモジュールの相違というより単に違反品のようです。

    • あろっち あろっち より:

      ご覧いただきましてありがとうございます。

      ご指摘の点についてこちらでも確認できましたので記事の内容を一部修正いたしました。

      今後もより正確で役に立ちそうな情報を発信していきたいと思っていますのでよろしくお願いします。

  2. 匿名 より:

    コメント失礼します。

    ピン配列の説明について、
    「カメラ使用時は以下が外部ピンとして使用できるかと思います。

    GPIO12〜GPIO15(HSPI対応)
    GPIO32、GPIO33」
    これはピン配置画像の「CAM_〇〇」以外のGPIOピンが全て外部ピンとして使用できるということでしょうか。また、FLASHピンもカメラと連動して外部ピンとしては使えなくなってしまうのでしょうか。

    • あろっち あろっち より:

      ご覧いただきましてありがとうございます。

      カメラ使用時は、
      CAM_〇〇が使用できないということでOKです。

      それで、これ以外のGPIOピンが使えるかというところですが、
      カメラ非使用時においても、
      FLASHピンは内部フラッシュメモリの接続に使用されている関係で常時使用不可かと思います。
      また、LEDに接続されているピン(LED_〇〇)も使用できないと考えます。
      GPIO0に関してはBOOTボタンに接続されており起動時書き込みモードにするために使われるため、使わない方が良いかと考えます。

      従いまして、カメラ使用時に使用できるGPIOは本文にも記載してある通り以下のGPIOピンになるかと思います。
      GPIO12〜GPIO15(HSPI対応)
      GPIO32、GPIO33

タイトルとURLをコピーしました