こんにちは、あろっちです。
ESP32-WROVER CAMボード(Freenove FNK0060 または FNK0060B)はESP32-WROVER-E(技適マーク付き)とカメラが搭載されたボードです。
こちらを入手したので情報をまとめてみました。
FNK0060BはSDカードスロット搭載モデル(v3.0)です。
ESP32-WROVER CAMボードには、Freenoveのオリジナル品とジェネリック品があります。
オリジナル品は基板にFREENOVEの文字がシルク印刷されています。

参考サイト:
https://lang-ship.com/blog/work/esp32-cam-fnk0060/(Lang-ship)
上記にたなかさんの情報がありますので参考にしてください。
こちらによるとジェネリック品のESP32-WROVER-Eモジュールはクローン品(いわゆる偽物)とのことで筆者の方でも挙動の違いを確認しました。
本記事ではオリジナル品であるFreenove FNK0060 & FNK0060Bの情報をお届けしたいと思います。
本記事は以下を参考にしています。
本記事では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カードリーダー が付属していてすぐに試せる |
※2 シリアルドライバをインストールしていない場合は別途インストールが必要です。
画像ギャラリー
Freenove ESP32-WROVER Board FNK0060(v1.6)



Freenove ESP32-WROVER Board FNK0060B(v3.0)


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

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


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

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

66° 21mm(付属カメラ) 120°21mm 160°21mm
68°75mm 160°75mm
カメラはコネクタ接続なので換装して使えます。
ピン配列
v1.6 (FNK0060)

v3.0 (FNK0060B)

※画像のバー付きのGPIOはカメラ(コネクタ)に接続されています。
使用できるGPIOについて
カメラ使用時
以下が使用できると思います。
12〜15 (HSPI対応)
32、33
v3.0の場合 (SDカードスロット使用時)
カメラに加えSDカードスロットも使った場合、以下となります。
12、13、32、33
カメラ未使用時について
このボードは、通常のESP32開発ボードとしても利用可能です。
例えば、OLEDクロックを動かしてみたところです。


参考サイト:
Arduinoボード設定
Arduino IDEにESP32ボード(安定リリース版)を追加しておきます。
追加方法については、以下の記事をご参照ください。
- ボードインストール
[ツール] > [ボード] > [ボードマネージャ]からESP32ボードをインストールします。
- ボードの選択
[ツール] > [ボード] > [ESP32 Arduino※] > [ESP32 Wrover Module]を選択します。
※Arduinoの表記はArduino IDEバージョン1系のみ
- シリアルポートの選択
ボードが接続されているポートを選択します。
環境 | ポート名 |
---|---|
Mac | /dev/cu.wchusbserial* |
Windows | COM* |
※シリアルポートが認識されない場合は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やサンプルスケッチがあります。
CameraWebServer (Arduino IDEスケッチ例を使用する場合)
GitHubのサンプルスケッチにSketch_06.1_CameraWebServerがありますが、Arduino IDEのスケッチ例([スケッチ例] > [ESP32] > [Camera] > [CameraWebServer])を使用する場合、次のように修正します。

- カメラモデルの指定
CAMERA_MODEL_WROVER_KITのコメントをはずし、CAMERA_MODEL_ESP_EYE(※)をコメントアウトします。
※ESP32ボードのバージョンによってはCAMERA_MODEL_ESP_EYEではない場合があります。
この手順のポイントはCAMERA_MODEL_WROVER_KITのコメントを外して、これ以外のCAMERA_MODELをコメントアウトするということです。 - 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.3インチディスプレイの例



スケッチ1 (Adafruit GFXライブラリ使用)
いずれのライブラリも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 SCL | 14 | HSPI SCLK |
TFT SDA | 13 | HSPI MOSI |
TFT CS | 15 | HSPI CS 1.3インチLCDでは配線不要 |
TFT DC | 32 | |
TFT RST | 33 | |
BUTTON(スイッチ) | 12 | ボタン(トグルスイッチ) INPUT |
液晶ディスプレイは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なし)ディスプレイに対応しています。

#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なし)ディスプレイに対応しています。


#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 |
---|---|
GND | GND |
VCC | 3.3V |
SCL | 12 |
SDA | 13 |
RES | 33 |
DC | 32 |
BLK | 未接続 |
HSPIを使用していますが、HSPI_CLKがSDカードスロットのCLKと被るため、HSPI_MISO(12)をCLKに割り当てています。
表示できる画像ファイルについて
.jpgと.pngの画像に対応しています。
お好きな画像ファイルをSDカードのルートフォルダに保存し、ESP32-WROVER CAMボードのSDカードスロットに挿し込むだけで、LCD画面に表示できます。
ぜひお試しください!
映像を液晶ディスプレイに表示する シャッター機能付き (v3.0向け)
カメラ画像が液晶ディスプレイに表示されるというスケッチ例です。
v3.0のSDカードスロットに挿入されているSDカードに画像を保存する機能を追加しています。


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 |
---|---|
GND | GND |
VCC | 3.3V |
SCL | 12 |
SDA | 13 |
RES | 33 |
DC | 32 |
BLK | 未接続 |
ピンを全て使ったため、止むなくシャッターボタンにBOOTボタン(GPIO 0)を割り当てました。
関連記事
当ブログのマイコン記事です。ぜひご覧ください。
コメント
参考になる記事を書いていただきありがとうございます。
モジュールについてですが、本物のFreenove FNK0060はESP32-WROVER-Eの正しい証明番号(211-200403)ですが、ジェネリックというか偽物の方はESP-WROOM32の証明番号(211-161007)を適当に書いてるだけなのでモジュールの相違というより単に違反品のようです。
ご覧いただきましてありがとうございます。
ご指摘の点についてこちらでも確認できましたので記事の内容を一部修正いたしました。
今後もより正確で役に立ちそうな情報を発信していきたいと思っていますのでよろしくお願いします。
コメント失礼します。
ピン配列の説明について、
「カメラ使用時は以下が外部ピンとして使用できるかと思います。
GPIO12〜GPIO15(HSPI対応)
GPIO32、GPIO33」
これはピン配置画像の「CAM_〇〇」以外のGPIOピンが全て外部ピンとして使用できるということでしょうか。また、FLASHピンもカメラと連動して外部ピンとしては使えなくなってしまうのでしょうか。
ご覧いただきましてありがとうございます。
カメラ使用時は、
CAM_〇〇が使用できないということでOKです。
それで、これ以外のGPIOピンが使えるかというところですが、
カメラ非使用時においても、
FLASHピンは内部フラッシュメモリの接続に使用されている関係で常時使用不可かと思います。
また、LEDに接続されているピン(LED_〇〇)も使用できないと考えます。
GPIO0に関してはBOOTボタンに接続されており起動時書き込みモードにするために使われるため、使わない方が良いかと考えます。
従いまして、カメラ使用時に使用できるGPIOは本文にも記載してある通り以下のGPIOピンになるかと思います。
GPIO12〜GPIO15(HSPI対応)
GPIO32、GPIO33