公開日:2020/12/26
title.png

14th Season


#14-2 いちたすいちは?に〜(OV7670)

 カメラモジュール(OV7670)をDUEで制御して、TFTシールドに画像を表示しようとしています。前回のハードウェア設計に引き続き、ソフトウェアの設計を続けて行きます。

ソフトウェアの設計

 完成したプログラムを見ていただくと、結構な物量が有ります。カメラ(OV7670)とTFTシールド(ILI9328)に加えてSDカードまで制御しているので、そりゃあ、大きくなります。本節では、ポイントとなる技術のみ、さらっと解説しておこうと思います。全体像は完成版プログラム(qvga_cam.ino)でご確認ください。

※新規に制御を追加するOV7670以外のモジュール(TFTシールドやSDカード)に関しては、過去記事で解説済みです。逆引き索引などから過去記事を当たってみてください。

入力クロック(XCLK)の生成

 「#13-2 MMLで三和音を演奏する」で使ったDUE(SAM3X8E)のPWMユニットを使って14MHzデューティ50%のPWMを作り、OV7670への入力クロックXCLKとします。
 前回示した接続表より、XCLKに割り当てたDUEの端子はPWML2(D43)なので、使用するのはPWMチャンネル2となります。

  // PORT setting
  digitalWrite (pin_XCLK, HIGH);
  pinMode (pin_XCLK, OUTPUT);

  // output clock XCLK=14MHz, D43(PA20/MCCDA/PWML2)
  // PMC setting (PWM peripheral id=36)
  REG_PMC_PCER1=PMC_PCER1_PID36;
  // PWM setting
  REG_PWM_CPRD2=F_CPU/14000000;
  REG_PWM_CDTY2=F_CPU/14000000/2;
  REG_PWM_CMR2=PWM_CMR_CPRE_MCK;
  REG_PWM_ENA=PWM_ENA_CHID2;
  // PWML2 (PA20) output
  REG_PIOA_PDR=PIO_PDR_P20;
  REG_PIOA_ABSR |= PIO_ABSR_P20;


 上のコードがPWMチャンネル2の初期設定該当箇所抜粋です。
 PWM_CPRD2レジスタがPWM周期、PWM_CDTY2レジスタがデューティです。デューティ50%のPWMにするためには、PWM_CPRD2=F_CPU/14000000が偶数である必要があります。例えば、12MHzのPWMを出力しようとすると、

  PWM_CPRD2=F_CPU/12000000=7 (周期レジスタ)
  PWM_CDTY2=F_CPU/12000000/2=3.5 (デューティレジスタ)

と、PWM_CDTY2レジスタ値(デューティ)が割り切れず、整数に切り下げるとデューティ=3/7≒42.9%になってしまうのです。14MHzなら、デューティレジスタ値が割り切れて綺麗にデューティ50%のPWMが出力できます。14MHz以外では、前回示した他の周波数でもOKです。
 ここで生成するクロックXCLKがOV7670制御の基本クロックとなるため、後でPCLKの周波数調整などを行う時のCLKRCレジスタ設定値などすべてがXCLKを基準に変化することになります。

OV7670のデータシートには、XCLKのデューティ=45(MIN)〜55(MAX)[%](TYP 50%)が規定されているので、12MHzの時のデューティ=42.9%だと、定格外使用となります。

OV7670からの出力データ取り込み手順

 データバスD7〜D0や、ストローブ信号(VSYNC,HREF,PCLK)など、同時に参照する必要のある信号が有りますので、I/Oポート1本づつしか参照できないdigitalRead()関数などは使用しません。直接、DUE搭載MCU、SAM3X8Eのレジスタを叩いて制御します。
 直接レジスタを叩いてI/Oポートを制御する方法については、「#12-1 DUE+TFTシールド」で解説済みなので、そちらを参照してください。
 また、こちらの接続表で、同時に参照する必要のあるデータバスD7〜D0がポートCの連続するビットに固まっていること、ストローブ信号(VSYNC,HREF,PCLK)もポートCに固まっていることを確認しておいてください。さすがに同じポートに割り当てておかないと同時参照できないので、最初の基本設計段階で考慮しておくことが必要です。

 以下に、OV7670データシートからデータ出力タイミング図を引用して、データの取り込み手順を記します。

a141_frmtmg.png
※OV7670データシートから引用

 OV7670は上図のタイミングで常にデータを垂れ流しているので、取り込む側がVSYNCなどでタイミングを取ってデータを取り込む必要があります。従って、ちょうどVSYNC=1→0になった瞬間に取り込みを開始した時が、待ち時間最大となり、ほぼ1フレーム分の時間待ちが発生します。

 また、要となるのは一番高速に変化するストローブ信号PCLKの周波数です。DUE @84MHzがI/Oポートの状態を確認しながら、PCLKのエッジを取りこぼさないように掴む必要があるので、PCLKが速すぎると失敗します。
 PCLKの周波数fPCLKから周期tPCLK(=1/fPCLK)が決まり、tP=2×tPCLK ⇒ tLINE=(640+144)×tP ⇒ tFRAME=(480+30)×tLINEと、次々に決定して行って、最終的なフレームレートtFRAMEが求まるので、PCLKの周波数をどこまで上げられるか?がカギとなります。

※OV7670に「待った」をかけるプロトコルが無いので、OV7670が垂れ流してくるデータをひたすら取り込み続けるしか有りません。一度取りこぼしたら最後、「あっ、今のデータもう一回……」と言っても聞いてはくれません。

画像の保存

 折角カメラで撮影した画像を、TFTシールドに表示して終わりでは味気ないので、SDカードI/Fを使って、画像データを保存する手段を持たせておこうと思います。
 カメラから取り込んだ画像は自然画像なので、一般的にはJPEG等の非可逆圧縮アルゴリズムでセーブするのが正解ですが、ArduinoでチマチマとJPEG保存プログラムを組む気にはならないので、WindowsのBMP形式でセーブしようと思います。
 WindowsのBMPファイル形式については、「#13-1 TFTシールドにBMPを表示する」にて解説してありますが、あの時はロード側だったので、以下表にBMPセーブ時の対応として改めてまとめます。

BMPファイル サイズ 内容 セーブ時対応
  BITMAPFILEHEADER
  bfType 2 BMPヘッダ識別子'BM' 0x4d42('BM')
bfSize 4 ファイルサイズ 0x00025836
(320×240×2+bfOffBits)
bfReserved1 2 リザーブ1 0x0000
bfReserved2 2 リザーブ2 0x0000
bfOffBits 4 画像データの先頭ファイルポジション 0x00000036(54)
BITMAPINFOHEADER
  biSize 4 sizeof(BITMAPINFOHEADER) 0x00000028(40)
biWidth 4 画像横サイズ 0x00000140(320)
biHeight 4 画像縦サイズ 0x000000f0(240)
biPlanes 2 プレーン数 0x0001(1)
biBitCount 2 ピクセルのビット数 0x0010(16)
biCompression 4 圧縮形式 0x00000000(0)
biSizeImage 4 画像データサイズ 0x00000000(0)
biXPelsPerMeter 4 横方向解像度 0x00000000(0)
biYPelsPerMeter 4 縦方向解像度 0x00000000(0)
biClrUsed 4 カラーテーブルの数 0x00000000(0)
biClrImportant 4 重要なカラーテーブルの数 0x00000000(0)
uncompressed bit map data
  16 bit/pixel 2 [MSB]I(1),R(5),G(5),B(5)[LSB] 320×240ドット繰り返し

 以上の様に、BMPヘッダの全項目が決め打ちの定数になったし、肝心なビットマップデータも非圧縮で丸ごと書き出したら終了なので、簡単に実装できそうです。以下、コード抜粋。あら、簡単。

  // bmp file header
  const uint8 bmp_head[ ]=
  {
    // BITMAPFILEHEADER
    0x42,0x4d,  // 0x4d42('BM')
    0x36,0x58,0x02,0x00,  // 0x00025836(320*240*2+bfOffBits)
    0x00,0x00,  // 0x0000
    0x00,0x00,  // 0x0000
    0x36,0x00,0x00,0x00,  // 0x00000036(54)
    // BITMAPINFOHEADER
    0x28,0x00,0x00,0x00,  // 0x00000028(40)
    0x40,0x01,0x00,0x00,  // 0x00000140(320)
    0xf0,0x00,0x00,0x00,  // 0x000000f0(240)
    0x01,0x00,  // 0x0001(1)
    0x10,0x00,  // 0x0010(16)
    0x00,0x00,0x00,0x00,  // 0x00000000(0)
    0x00,0x00,0x00,0x00,  // 0x00000000(0)
    0x00,0x00,0x00,0x00,  // 0x00000000(0)
    0x00,0x00,0x00,0x00,  // 0x00000000(0)
    0x00,0x00,0x00,0x00,  // 0x00000000(0)
    0x00,0x00,0x00,0x00  // 0x00000000(0)
  };

  // header
  for (ix=0; ix<int (sizeof (bmp_head)/sizeof (uint8)); ix++)
    fout.write (bmp_head[ix]);

  // bitmap
  for (iy=0; iy<tft_SIZH; iy++)
  {
    for (ix=0; ix<tft_SIZV; ix++)
    {
      // BGR565: bbbb bggg gggr rrrr
      pix=tft_getpix (ix,iy);
      // IRGB1555: irrr rrgg gggb bbbb
      pix=((pix<<10)&0x7c00) | ((pix>>1)&0x03e0) | ((pix>>11)&0x001f);
      fout.write (pix&0xff);
      fout.write ((pix>>8)&0xff);
    }
  }


 唯一、TFTシールドから読み出したBGR565フォーマットのピクセルデータから、BMPファイルの16ビットピクセル値、IRGB1555に変換が必要となります。
 BMPファイルは一番下のラインから順にデータを書き出すボトムアップなこともお忘れなく。

※BGR565からIRGB1555への変換で、Gデータを1ビット失っています。24ビットBMPにして、この1ビットもちゃんとセーブしたい方は、各自対応下さい。

シリアルモニタコマンド

 いつもの様にシリアルモニタで受け付けるコマンドを用意して、デバッグと評価を実施したいと思います。
 シリアルモニタを使わずに、TFTシールドのタッチパネルなどでGUIを形成すれば、所謂「デジカメ」が完成します。ただ、酷くバグバグのOV7670をこれ以上いじりたいと思わないので、「デバッグおよび試行用」と割り切りたいと思います。

 デバッグ中には、「このレジスタにこの値を設定したらどうなる(どう変わる)のだろう?」と、いうカット&トライが発生します。その度にプログラムを変更していては、都度、コンパイル&書き込みが必要となります。DUEへのプログラム書き込みはUNOに比べて時間がかかるので、最小限に抑えないとデバッグが面倒です。
 そこで、TFTシールドやカメラモジュールのレジスタを直接触ることができる様に、アクセスコマンドを用意しておきます。
 OV7670の一部レジスタには、1つの設定項目が複数のレジスタに分散して配置されていて、レジスタを個別に書き込む方法だけだと設定し難い値が存在します。例えば以下の項目がそうです。

 デバッグを容易にするため、これらの項目を設定するためのコマンドも準備したいと思います。以下に目標仕様を記します。

  シリアルモニタコマンドの目標仕様:

初期実験

 OV7670の設定レジスタ数が多くて、設定すべき値を絞り切れないので、まだ完成前ですが初期実験を実施して感触をつかんでおきたいと思います。

 こちら(前回)に従って外部回路を構築した後、プログラムをDUEにコンパイル&書き込みして実験しました。プログラムは未完成なので、今回はリンクせず、次回完成版をリンクします。以下、実験中の様子です。

a142_expr.jpg

  実験サンプル:【 】内はシリアルモニタからの送信データ
  INFO: OV7673.7fa2
  INFO: ILI9328
  RDY

  【take】
  OK: took a picture

 OV7670カメラモジュールからデータを取り込んで表示させてみました(以下図)。

a142_badcol.jpg

※モデルさんは筆者宅にお越しいただいたドロッセルお嬢様。サングラスが赤で、上画像見たままの方向で左下に映り込んだ万能基板が緑色です。

 まず、映像が表示されたので、VSYNC,HREF,PCLKなどのストローブ信号に従ってデータを取り込む方法に誤りが無いことが判りました。→OK
 しかし、色合い🎨が変です。どうも、赤色の被写体が緑になって、緑色の被写体が紫っぽくなっている模様。

 前回の検討でOV7670とTFTシールドのピクセル値の合わせ込み(RGB565で統一)を検討して、その通りに実装したつもりだったのですが、間違っているのでしょうか?

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【wrc,71,b5】
  OK: camera write [0x71]=0xb5

  【take】
  OK: took a picture

 OV7670のテスト機能で、カラーバーを表示するビットに1を立てました。元に戻す時は【wrc,71,35】としてください。読み出したカラーバーが以下図です。

a142_colbar.jpg

 期待値の正確なところはわからないのですが、R,G,B各原色ON/OFFの組み合わせで表示可能な8色:黒(0)、青(1)、赤(2)、マゼンタ(3)、緑(4)、シアン(5)、黄(6)、白(7)が順番通りに並んでいるので、恐らく、期待値通り、正しいと思われます。→OK
 と、言う事は、やはりOV7670とTFTシールド間のピクセルフォーマットは正しく合わせ込めているのでしょう。先ほどのカメラ映像で、色合いがおかしかったのは何か他の問題ですね。設定が足りていないのか、間違えているのか?


 OV7670の制御には、ここから更に長い道のりが待っていました。この先のドタバタは、また次回お話いたします。


btn_arduino.png

btn_prev.png btn_next.png

home