気の向くままに辿るIT/ICT/IoT
IoT・電子工作

ESP32/FIFO無OV7670/TFT1.8/ブラウザ/Linuxで映像表示デモ

ホーム前へ次へ
ESP8266って?

ESP32/FIFO無OV7670/TFT1.8/ブラウザ/Linuxで映像表示デモ

ESP32/FIFO無OV7670/TFT1.8/ブラウザ/Linuxで映像表示デモ

ESP32/OV7670で撮影ブラウザで表示したガムボトル
2019/03/31

 Linux上でWi-Fi(wifi)モジュールESP8266の内、ESP32/ESP-WROOM-32の開発ボードとCMOSカメラOV7670(FIFOなし・without FIFO)を使ってTFT1.8インチ液晶とPCのブラウザでストリーミング(リアルタイム映像表示)のデモをしてみるページ。

 当初、Arduinoで探すとWindowsにおけるライブラリや手順の情報などは多々見かけたのですが、FIFOなしとなると、詳細となると尚、激減、何れにしてもLinuxに関する情報がなかなかなく、Linuxでもなんとかできないかとダメ元でWindows用ライブラリで試してはみたのですが、失敗、UNOやNanoでダメならばと、FIFOなしOV7670のためだけにMegaまで買ってみましたが、やはりダメと惨敗でした。

 それなら、より大容量のメモリを積むESP8266/ESP32なら...と探してみると、ここで使わせていただいたライブラリ含め、マルチプラットフォーム対応っぽいライブラリが1つ、2つ見つかり、試行錯誤の上、ストリーミングに成功しました。

 当初、手動でレンズを回してフォーカスを合わせるのが、とてつもなく難しいなと思ったら、どうやら配線不良に起因していたようです。

 正常な時は、回路に電源を供給(ESP32をUSBケーブルで接続)すると自動でWiFi接続、撮影が始まり、被写体との距離や光の射し方、OV7670自体のズーム位置などにもよるのでフォーカス状態はさておき、相応に映り、後はピントを合わせるだけの状態になりました。

 とりあえず、撮ったのは、手近にあったキシリ○シュ ディープミントのガムボトルです。

 ネットで調べた感じでは、モノクロというか、フルカラーではないのかもと思ってたのですが、実際には、フルカラーでした。

 ここでは、ブラウザで表示したスクリーンショット(写真)掲載に留めましたが、実際には、リアルタイムムービー(後段の備考に注記あり)であり、ESP32のメモリ量のおかげか、カクカクするかと思いきや、かなり動きも円滑です(気づけば、解像度によって大きく異なり、160x120だとカクカク、80x60だと円滑ですが、後者だとブラウザでは画質は落ちるもののサイズ自体は変わりませんが、TFT液晶だと画面の1/4程度で小さくなります)。

 アクセスポイントを宅内・社内回線上にした場合、回線の影響もあるかもしれないので一応、自身の環境を書いておくと光ONU(≒モデム・ルーター)、スイッチングハブ、アクセスポイントとしての無線ルーターまでが有線で光とはいえ、スイッチングハブ・無線LANルータ共に、ギガビット対応はしておらず、10BASE-T/100BASE-TX対応のものを使っています。

 というか、映像を見るとCMOSカメラって反転して(素直な向きに)映るものなのか...?とザッと調べてみるとそうではなさそう、設定できそうなところもないですし、安いから目で見た状態にする機能がないだけなのか...と思ったら、後述のようにTFT液晶では見た目通りに映るので、これはどうやら、ブラウザや液晶側の機能によるようです。

 それもあってか、カメラの向きと合わせ、被写体を捕えるのも結構難しく、慣れるまでは、被写体の方を動かして撮りました。

 被写体と距離をとれば、もっと鮮明に映りそうでしたが、USBケーブル長、かさばる配線類、デスクの奥行き、あまり動かすと映像が...などの制約から、あの程度になりました。

必要なもの

 今回使ったものは、次の通りです。

 後述の回路を見るとWeMOS LOLIN32 V1.0.0を使っているようですが、使うピンを見て38ピンバージョンでもいけると踏んでこれをチョイスしました。

 OV7670は、FIFOありの方が圧倒的に扱いやすく手軽でストリーミングもよりスムースなようです。

 そんな違いがあることを知らずに以前、安さにつられて結果、FIFOなしを買ったものの、それにより、こんなこと自身でも初ですが、買って届いてから1年ちょっと、ようやく今になって動作確認、TFT液晶やブラウザ上でのストリーム配信・スクリーンショット撮影ができました。

 以前買ったTFT1.8液晶ではピン配列が異なり、マッピングにつまづいた為、同じ系統と思われるものの、以前のものとは別のピンホールがあり、そっちにのみピンヘッダがはんだ付けされたTFT1.8インチ液晶(根元が黄色いピンヘッダ8ピン)をこのために追加で調達しました。

 Amazon/Amazonマーケットプレイス/100均価格で2500円前後、Aliexpress価格で6〜7割くらいでしょうか。

前提

 ここでは、Arduino IDEを使うのでArduino IDEの[ツール] => [ボード]から[espressif/arduino-esp32]を選択、ESPにスケッチをアップロードできる状態であること。

 Arduino IDE 1,8,6で追加された[ツール] => [ライブラリを管理...]メニューか、従来の[スケッチ] => [ライブラリをインクルード...] => [ライブラリを管理...]メニューを辿ってライブラリ管理画面を開き、TFT 1.8液晶用に[Adafruit GFX Library]、[Adafruit ST7735 and ST7789 Library]を検索、インストールしておくこと。

 参考までに自身の使用しているOSは、Debian(Linux)、Arduino IDEのバージョンは、1.8.8。

回路

ESP32TFT1.8OV7670備考
2D/C-
4-D7
5CS-
12-D6
13-D5
14-D4
15-D3
16-D2
17-D1
18CLK-
21-SIOD経路上と3.3Vとの間に抵抗4.7kΩ
22-SIOC
23DIN-
27-D0
32-XCLK
33-PCLK
34-VSYNC
35-HREF
ENRSTRESET
3.3VVCC3.3Vただし、TFT1.8液晶は基板裏のJ1をジャンパしていない場合、別途5Vから取る
BL
GNDGND

 配線は、ESP32 I2S Camera (OV7670)にある通り...というのも何なのでESP32のピン番号昇順に書いてみると、こんな感じです。

 と言ってもESP32の実際のピン配列は、通し番号順じゃないですが。

スケッチ・ライブラリ

#include "OV7670.h"
 
#include <Adafruit_GFX.h>    // Core graphics library version 1.2.2
#include <Adafruit_ST7735.h> // Hardware-specific library version 1.1.0
 
#include <WiFi.h>
#include <time.h>
 
#include <WiFiMulti.h>
#include <WiFiClient.h>
#include "BMP.h"
 
const int SIOD = 21; //SDA
const int SIOC = 22; //SCL
 
const int VSYNC = 34;
const int HREF = 35;
 
const int XCLK = 32;
const int PCLK = 33;
 
const int D0 = 27;
const int D1 = 17;
const int D2 = 16;
const int D3 = 15;
const int D4 = 14;
const int D5 = 13;
const int D6 = 12;
const int D7 = 4;
 
const int TFT_DC = 2;
const int TFT_CS = 5;
//DIN <- MOSI 23
//CLK <- SCK 18
 
#define ssid1        "SSID"
#define password1    "PASSPHRASE"
//#define ssid2        ""
//#define password2    ""
 
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS,  TFT_DC, 0/*no reset*/);
OV7670 *camera;
 
//WiFiMulti wifiMulti;
WiFiServer server(80);
 
unsigned char bmpHeader[BMP::headerSize];
 
void serve()
{
  WiFiClient client = server.available();
  if (client)
  {
    //Serial.println("New Client.");
    String currentLine = "";
    while (client.connected())
    {
      if (client.available())
      {
        char c = client.read();
        //Serial.write(c);
        if (c == '\n')
        {
          if (currentLine.length() == 0)
          {
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println();
            client.print(
              "<style>body{margin: 0}\nimg{height: 100%; width: auto}</style>"
              "<img id='a' src='/camera' onload='this.style.display=\"initial\"; var b = document.getElementById(\"b\"); b.style.display=\"none\"; b.src=\"camera?\"+Date.now(); '>"
              "<img id='b' style='display: none' src='/camera' onload='this.style.display=\"initial\"; var a = document.getElementById(\"a\"); a.style.display=\"none\"; a.src=\"camera?\"+Date.now(); '>");
            client.println();
            break;
          }
          else
          {
            currentLine = "";
          }
        }
        else if (c != '\r')
        {
          currentLine += c;
        }
        
        if(currentLine.endsWith("GET /camera"))
        {
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:image/bmp");
            client.println();
            
            client.write(bmpHeader, BMP::headerSize);
            client.write(camera->frame, camera->xres * camera->yres * 2);
        }
      }
    }
    // close the connection:
    client.stop();
    //Serial.println("Client Disconnected.");
  }  
}
 
void setup()
{
  Serial.begin(115200);
  // Use this initializer if you're using a 1.8" TFT
  tft.initR(INITR_BLACKTAB);   // initialize a ST7735S chip, black tab
 
  Serial.println("init");
//  tft.writecommand(ST7735_DISPON);
 
  uint16_t time = millis();
//  tft.fillScreen(BLACK);
  tft.fillScreen(ST77XX_BLACK);
  time = millis() - time;
  
  tft.setRotation(1);
  
  Serial.println(time, DEC);
  delay(500);
  Serial.println("done");
  delay(1000);
 
  // WiFi starting
  Serial.println("WiFi connecting...");
  WiFi.begin(ssid1, password1);
  while(WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }
  Serial.println();
  Serial.printf("Connected, IP address: ");
  Serial.println(WiFi.localIP());
  Serial.println("WiFi connected!");
 
/*
  wifiMulti.addAP(ssid1, password1);
  //wifiMulti.addAP(ssid2, password2);
  Serial.println("Connecting Wifi...");
  if(wifiMulti.run() == WL_CONNECTED) {
      Serial.println("");
      Serial.println("WiFi connected");
      Serial.println("IP address: ");
      Serial.println(WiFi.localIP());
  }
 
  wifiMulti.addAP(ssid1, password1);
  //wifiMulti.addAP(ssid2, password2);
  Serial.println("Connecting Wifi...");
  while(wifiMulti.run() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.printf("Connected, IP address: ");
  Serial.println(WiFi.localIP());
*/
  
//  camera = new OV7670(OV7670::Mode::QQVGA_RGB565, SIOD, SIOC, VSYNC, HREF, XCLK, PCLK, D0, D1, D2, D3, D4, D5, D6, D7);
  camera = new OV7670(OV7670::Mode::QQQVGA_RGB565, SIOD, SIOC, VSYNC, HREF, XCLK, PCLK, D0, D1, D2, D3, D4, D5, D6, D7);
  BMP::construct16BitHeader(bmpHeader, camera->xres, camera->yres);
  
//  tft.initR(INITR_BLACKTAB);
  tft.fillScreen(0);
  server.begin();
}
 
void displayY8(unsigned char * frame, int xres, int yres)
{
  tft.setAddrWindow(0, 0, yres - 1, xres - 1);
  int i = 0;
  for(int x = 0; x < xres; x++)
    for(int y = 0; y < yres; y++)
    {
      i = y * xres + x;
      unsigned char c = frame[i];
      unsigned short r = c >> 3;
      unsigned short g = c >> 2;
      unsigned short b = c >> 3;
      tft.pushColor(r << 11 | g << 5 | b);
    }  
}
 
void displayRGB565(unsigned char * frame, int xres, int yres)
{
/*
  tft.setAddrWindow(0, 0, yres - 1, xres - 1);
  int i = 0;
  for(int x = 0; x < xres; x++)
    for(int y = 0; y < yres; y++)
    {
      i = (y * xres + x) << 1;
      tft.pushColor((frame[i] | (frame[i+1] << 8)));
    }  
*/
  tft.setAddrWindow(0, 0, xres - 1, yres - 1);
  int i = 0;
  for(int y = 0; y < yres; y++)
    for(int x = 0; x < xres; x++)
    {
      i = (y * xres + x) << 1;
      tft.pushColor((frame[i] | (frame[i+1] << 8)));
    }  
}
 
void loop()
{
  camera->oneFrame();
  Serial.println("oneFrame()");
  serve();
  Serial.println("serv done.");
  displayRGB565(camera->frame, camera->xres, camera->yres);
  Serial.println("display done.");
}

 スケッチは、ライブラリbitluni/ESP32CameraI2SESP32_I2S_Camera.inoを使わせていただきました。

 ただし、以下のような修正が必要でした。

 先のスケッチ変更理由は、次のようなものです。

 DMABuffer.h/I2C.h/Log.hといったこれらヘッダファイルには、Arduino.hをインクルードしないとプログラム・スケッチのアップロード時に標準的な関数が存在しない旨のエラーが表示されました。

 XClk.cppの修正は、vivian-ng氏のコメントに倣いましたが、これがないとArduino IDEでスケッチをアップ、シリアルモニタを開き、そのまま、またはリセットを押下、WiFi接続、IPアドレス払い出し後、[ledc: ledc_set_duty_with_hpoint(383): hpoint argument is invalid]のようなエラー(らしき)表示が出て先に進みませんでした。

 自身の環境に起因するのか、ここでwifiMultiを使うと、なぜかアクセスポイントも立たず、よって当然、ステーションモードのIPも取得できなかったため、ステーションモードのみのWiFi.begin()で接続を確立しました。

 tft.writecommand()については、確かライブラリで使用のAdafruit_ST7735.hが特殊なのか、バージョンが異なるのか、存在しなかった気がします。

 tft.fillScreen()についてもライブラリで使用のAdafruit_ST7735.hが特殊なのか、バージョンが異なるのか、引数名が異なり、ヘッダファイルを参照したところ、BLACKではなく、ST77XX_BLACKでした。

 OV7670のインスタンス生成時の引数については、この場合、変更する必要はないとは思いますが、ソースファイルOV7670.cppとヘッダファイルOV7670.hを参照し、ここでは、QQVGA_RGB565(160x120)から、より解像度の低いQQQVGA_RGB565(80x60)に変更しました...と思ったらブラウザではともかく、TFT液晶上では、画面の1/4程度に映写されてしまった為、元に戻しました。

WiFi搭載マイコンESP32/CMOSカメラOV7670/TFT1.8液晶でまだノイズしか表示できない様子

 また、このライブラリは、TFT1.8液晶に対応で、自身も使うべく、先の通り、TFT1.8インチ液晶を追加調達、液晶自体の表示・動作確認はできたものの、ブラウザ上でのストリーミング表示は確認できましたが、液晶上での映像表示は、ノイズ状態のみでキレイな映像がでない...。

 この状態でもブラウザでは表示できているため、配線の接触不良ではなさそう、液晶側のライブラリか、スケッチ...[//DIN <- MOSI 23]、[//CLK <- SCK 18]をなんとかしないといけないのか、それともトリッキーな配線でもあるのか...。

WiFi搭載マイコンESP32/CMOSカメラOV7670/TFT1.8液晶で撮影に成功した時の様子

 と思いつつ、情報を探したところ、自身はCMOSカメラOV7670をAliexpressで買ったわけですが、秋月OV7670にESP32を繋いでみるの情報がまさにビンゴでした(感謝)。

 おかげさまでOV7670で撮影したものを1.8インチTFT液晶にストリーミングできました。

 撮影時の全体像はこんな感じです。

 リンク先の方がスケッチも提示してくださっていますが、概略としては、最終的に逆さまにはなってしまうものの、TFT液晶に表示する際、前掲のスケッチ内の太字部分、2つのAdafruitライブラリのバージョンは、特定の古いものを暫定的に使用、displayRGB565()関数内の修正、必要に応じてsetup関数内のtft.setRotation()で表示向きの設定が必要とのことです。

 また、撮影できたバージョンと最新バージョンの.cpp/.hファイルのそれぞれをタブ数違いとかは考慮せず、ザッとcommコマンドで比較してみると、かなりのステップ数書き換えが行われているようで自身も原因究明する意欲は湧きませんでした...。

WiFi搭載マイコンESP32/CMOSカメラOV7670/TFT1.8液晶で撮影に成功した被写体(ガムボトル)

 OV7670で撮影、TFT液晶1.8インチに映った映像が、今度は味やラベル色の異なるキシリ○シュ クリスタルミントのガムボトルです。

 ちょっと見づらいですが、ブラウザの時と異なり、映像は、反転して(そのままの向きにはなって)おらず、目で見たとおりに映っています。

 ということは、映り方の向きは、カメラではなく、映写機側の機能によるもののようです。

 液晶においては、書き換え時、割と高速ではあるものの、画面の切り替わりが見えてしまう為、スライド、というより、パラパラマンガのような感じになっています。

 最初にブラウザで表示確認した際は、インスタンス生成(new OV7670())時、QQQVG_RGB565(80x60)にしていたものの、液晶では小さすぎたため、QQVGA_RGB565(160x120)に戻した関係だと思いますが、TFT液晶はもとより、ブラウザでも、まるでオートフォーカスが働いているかのようにピンぼけが少なくなったというか、鮮明に映るようになりました。

 逆に、サイズを大きくした分、サクサク流れるような映像だったブラウザでは、液晶以上に切り替えが遅くなり、カクカクした映像になりました。

備考

 ブラウザに表示する場合、URL表示・入力欄にArduino IDEのシリアルモニタ、ifconfig/iw/iwconfigやnmapコマンドなどで調べたIPアドレスを入力すれば、OV7670カメラで捉えた映像が表示されます。

WiFi搭載マイコンESP32/CMOSカメラOV7670/PCブラウザでストリーミング調整中

 冒頭触れた通り、配線の接触不良などで映りが著しく低下することがありました。

 その際、調整中は、ドット絵風や真っ白、まっピンク、真っ青、IP接続できている間、この写真のように様々な色合いのカラフルな何かがうじゃうじゃ動いている...等々いろんな感じの映像が表示されますが、うまくピントが合うと、前段や後掲の写真の通り、そこそこキレイに映写されました。

WiFi搭載マイコンESP32/CMOSカメラOV7670/PCブラウザでマルチメータDT-830Bを撮影

 が、配線の接触不良に起因していたようで何も問題がなければ、前述の通り、ピントがぴったりあっているか否かは別として、このように、それなりの映像がすぐに表示されます。

 たまたま、周囲やマルチメータも白黒ばかりでモノクロっぽく写っていますが、ダイヤル上部中央の枠は微妙ながらも赤であり、映像もカラーです。

 ただ、実際、光の射す方向の加減なのか、接触の問題なのか、被写体に関わらず、モノクロになることもありました。

 尚、結構な時間継続してムービー/動画が撮れることもあれば、単発で写真撮影になってしまうこともあります。

 無線なので当然、PC以外のUSBポートに挿してもパソコンのブラウザに映像を表示できるわけですが、AC接続USB充電器(5V/1A・ダイソー200円商品)に挿した時は、いつまで経っても一貫して動画状態で撮影できているのでPCのUSBポート(max 300〜500mA)では、ESP32(の無線)による一時的な電流の急上昇に保護回路が働き、切断することがあるのかもしれません。

 さて、ブラウザにもTFT液晶にも表示できましたが、これの使い道はどうしよう...フォーカスに問題がなければ、ドアフォンならぬ、モニタ付きドアベルにするとか...鮮明かつスムース映像は無理にしても見守りカメラにするとか...。

ホーム前へ次へ