C++
Linux
電子工作
RaspberryPi
組み込み

Raspberry PiとLinux的方法によるデジタルカメラの作成

この記事について

Raspberry Piでデジタルカメラを作成したので、その内容を記します。本記事と同じものは、Python + OpenCVで簡単に出来ます。もしかしたらシェルスクリプトだけでもできてしまうかもしれません。が、あえてC++とLinuxの標準的な方法だけを使用してデジカメを作ってみようと思います。
STM32での作成記事も同日にアップロードしました。こちらも併せてみると面白いと思います(https://qiita.com/take-iwiw/items/212ddb6faa05412c83b7 )。

動いている様子: https://youtu.be/xP2iDa2bzRc

目的

デバイスファイル(/dev/XXX)へシステムコール(openやwrite、ioctl等)を使用してアクセスするというLinuxの標準的方法によって、デバイス制御をする。カメラ画像の取得にはLinuxに搭載されているVideo for Linux 2 (V4L2) APIを使用する。これらを使用して、デジタルカメラを作る。

本記事ではカーネル空間には触れず、ユーザ空間のアプリケーションについてのみ触れる。

お願いと注意

この記事は、今までずっとRTOSやOSレスの組込み開発をやってきた人間が、「Linuxでもやってみるか」、と思って書き始めたものです。もしも間違っていたり、認識違いがあったりしたら、ぜひ教えてください。

紙面の関係で、本記事中のコードではエラーチェックや終了処理は全て省略しています。実際に実装するときには適切なエラーハンドリングをする必要があります。完成形のコードはGitHubからご確認をお願いいたします。

「デバイスドライバ」というとき、ほとんどの場合はカーネル空間の本当にデバイス制御するモジュールを指します。本記事では便宜上、ユーザプログラムでデバイスファイルにアクセスするモジュールをデバイスドライバと記載している箇所があります。

作成するデジタルカメラの仕様

  • ライブビュー表示 (フレームレートは出来高。実力20fps程度)
  • 静止画撮影
  • 静止画再生
  • 対応フォーマットはJPEG, 320 x 240
    • 画サイズが小さいのは使用する液晶ディスプレイ(LCD)のサイズに合わせるため
  • タッチパネルから操作
    • 撮影モード中に中央付近をタップすると、静止画撮影
    • 画面端っこをタップすると、モード遷移 (撮影モード↔再生モード)
    • 再生モード中に中央付近をタップすると、次ファイルの再生

使用するデバイス

  • Raspberry Pi Zero W
    • Raspberry Pi 2, 3でもOK
    • OS: 2017-09-07-raspbian-stretch-lite。特に依存はないはずだけど、古いとv4l2が入っていないかも
  • カメラモジュール
    • Raspberry Pi Camera V2 (V4L2をサポートしているデバイスなら何でも動くはず)
  • 液晶ディスプレイ(LCD) モジュール (HiLetgo 2.8 "TFT LCDディスプレイ タッチパネル SPIシリアル240 * 320 ILI9341 5V / 3.3V STM32 [並行輸入品] @Amazon)
    • 320 x 240
    • ILI9341 SPI インタフェース
  • タッチパネル(TP)
    • TSC2046 SPI インタフェース (ADS7846 互換)

デバイス制御 (準備)

Linuxでのデバイス制御の方法

通常、マイコンからハードウェア制御をする場合には、CPUがアクセス可能なメモリ空間に割り当てられたレジスタ(メモリマップドレジスタ)にRead/Writeをすると思います。Linuxのユーザー空間アプリケーションからでも、対象アドレスをmmapすることで、同様のことは可能です。

しかし、この方法だとハードウェア(例えば、ラズパイのコアであるSoC(bcm2835))が変わるたびに、書き直す必要があり移植性が悪いです。Linuxではハードウェアデバイスを抽象化して共通に使えるような仕組みがあります。Linuxがデバイスを/dev下に疑似ファイルとして見せてくれます。我々はその疑似ファイル(デバイスファイル)に対して、通常のファイルアクセスと同じように、open, close, read, writeといったシステムコールでアクセスできます。しかし、デバイス独自の機能や設定があり、これらのシステムコールだけではカバーしきれません。これに対してはioctlというシステムコールを使用して、多様なコントロールができるようになっています。これらのシステムコールを叩くと、カーネル空間にいる本当のデバイスドライバがハードウェアアクセスを行います。本記事で扱うような一般的なデバイスドライバ(GPIO, SPI, Camera)は標準で搭載されています。なお、カーネル空間内のデバイスドライバは本記事ではスコープ外とします。本記事ではシステムコールを使用してデバイス制御をするクラス(モジュール)を便宜上デバイスドライバと呼んでいます。

GPIO制御

先ほど、デバイスアクセスは/dev/XXXに対してシステムコールでアクセスすると述べましたが、実は同様の疑似ファイルが他にもあります。/sys/XXXも同様にデバイスに関する情報を見せるための仕組みになります。/sys/XXXの方が扱いやすい形になっています。GPIOへは/sys/class/gpio/XXXを読み書きすることでアクセスします。
GPIO26に1(High)を出力するコードを下記に記します。解説はコード中のコメントをご覧ください。実行可能なコードはこちらをご参考ください(https://github.com/take-iwiw/DigitalCamera_RaspberryPi/blob/master/sampleCode/sampleGPIO.cpp/ )。

int fd;

/* 1. GPIO26を使用可能にする */
fd = open("/sys/class/gpio/export", O_WRONLY);
write(fd, "26", 2);
close(fd);

/* 2. GPIO26を出力設定する */
fd = open("/sys/class/gpio/gpio26/direction", O_WRONLY);
write(fd, "out", 3);
close(fd);

/* 3. GPIO26に1(High)を出力する */
fd = open("/sys/class/gpio/gpio26/value", O_WRONLY);
write(fd, "1", 1);
close(fd);

[メモ] よく使う関連コマンド

sudo echo 26 > /sys/class/gpio/export
sudo echo out > /sys/class/gpio/gpio26/direction
sudo echo 1 > /sys/class/gpio/gpio5/value
sudo echo 26 > /sys/class/gpio/unexport

SPI制御

SPIへは/dev/spidevXXを読み書きすることでアクセスします。Raspberry Piでは、SPI0.0とSPI0.1が汎用ポートから使用可能です、それぞれ/dev/spidev0.0/dev/spidev0.1になります。また、SPIの場合はビットレートやCSの設定などGPIOよりも複雑なことが必要になります。これらの設定のためにioctlを使用します。ioctlの第2引数がリクエスト(コマンドのようなもの)、第3引数がパラメータになります。どういうコマンドやパラメータがあるかはヘッダを見ると書いてあります。
SPI0.0に62.5Mbpsで0xAA, 0xBBの2Byteを送信し、同時に受信するコードを下記に記します。実行可能なコードはこちらをご参考ください(https://github.com/take-iwiw/DigitalCamera_RaspberryPi/blob/master/sampleCode/sampleSPI.cpp )。

int fd;
uint16_t spiMode     = SPI_MODE_0;
uint8_t  bitsPerWord = 8;
uint32_t spiSpeed    = 125.0 / 2 * 1000 * 1000;

/* 1. デバイスファイルを開いてアクセスできるようにする */
fd = open("/dev/spidev0.0", O_RDWR);

/* 2. SPI通信設定 */
ioctl(fd, SPI_IOC_WR_MODE, &spiMode);
ioctl(fd, SPI_IOC_RD_MODE, &spiMode);
ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bitsPerWord);
ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bitsPerWord);
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &spiSpeed);
ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &spiSpeed);

/* 3. 2バイト(0xAA, 0xBB)を送受信してみる */
uint8_t tx[2] = {0xAA, 0xBB};
uint8_t rx[2];
struct spi_ioc_transfer tr = {0};
tr.tx_buf = (uint64_t)tx;
tr.rx_buf = (uint64_t)rx;
tr.len = 2;
tr.speed_hz = spiSpeed;
tr.delay_usecs = 0;
tr.bits_per_word = bitsPerWord;
ioctl(fd, SPI_IOC_MESSAGE(1), &tr);

/* 4. 使い終わったので閉じる */
close(fd);

IC2制御(おまけ)

今回は使用しないのですが、自分の備忘録も兼ねてI2Cへのアクセス方法も書いておきます。もしかしたらI2C用のデバイスドライバライブラリのインストールが必要かもしれません(sudo apt-get install libi2c-dev)。
スレーブアドレスが0x18のデバイス(例えば、LIS3DH)の0x0F番地のレジスタの値をREADする例になります。実行可能なコードはこちらをご参考ください(https://github.com/take-iwiw/DigitalCamera_RaspberryPi/blob/master/sampleCode/sampleI2C.cpp )。

int fd;
uint8_t slaveAddress = 0x18;
uint8_t registerAddress = 0x0F;
uint8_t readData;

/* 1. デバイスファイルを開いてアクセスできるようにする */
fd = open("/dev/i2c-1", O_RDWR);

/* 2. スレーブアドレスの設定 */
ioctl(fd, I2C_SLAVE, slaveAddress);

/* 3. "0x0F"を送信してみる。(たいていの場合、これはデバイス内のレジスタアドレスを指定する) */
write(fd, &registerAddress, 1);

/* 4. 1Byte Readしてみる。(たいていの場合、先ほど指定したアドレス(0x0F)の値が読み出される) */
read(fd, &readData, 1);
printf("%02X\n", readData);

/* 4. 使い終わったので閉じる */
close(fd);

[メモ] よく使う関連コマンド

I2Cデバイスを使うときには、まずは下記コマンドでデバイス情報の取得や通信確認をすると捗ると思います。

sudo apt-get install libi2c-dev
i2cdetect -y 1
i2cget -y 1 0x18 0x0f b

デバイス制御 (実践)

LCD用デバイスドライバの開発

今回使用するディスプレイモジュールにはILI9341というコントローラが搭載されています。ILI9341はインタフェースとして8, 16, 18-bitパラレルとSPIインタフェースが使用可能ですが、実際に液晶モジュールとして購入すると、ピン制約もありいずれかのインタフェースに固定されていると思います。ラズパイで使うにはパラレルインタフェースだとピン数がもったいないので、SPIインタフェースになっている液晶モジュールを使用します。

LCD制御のためにはSPIでデータ/コマンドを送信します。それともう一つ、DCピンというものがあります。これは、SPIバスで通信する内容が、データなのかコマンドなのかをCPUからLCD(ILI9341)に教えてあげるものです。データ通信するときは1(High)、コマンド通信するときは0(Low)にしてあげます。この制御のためにはGPIOを使用します。

前述のGPIOとSPI制御のサンプルコードでは全てまとめて書いてしまいましたが、実際に使うときには、初期化時(あるいは起動時)に一度だけopenしたり設定を行い、デバイスファイルにアクセスするためのファイルディスクリプタ(m_fd)を保持しておきます。そして、普段使うときには保持したm_fdを使用してアクセスします。データ送信とコマンド送信する関数を下記に記します。

void DdIli9341Spi::writeData(uint8_t data)
{
    write(m_fdGpioDc, "1", 1);

    struct spi_ioc_transfer tr = {0};
    tr.tx_buf = (uint64_t)&data;
    tr.rx_buf = 0;
    tr.len = 1;
    ioctl(m_fdSpi, SPI_IOC_MESSAGE(1), &tr);
}

void DdIli9341Spi::writeCommand(uint8_t cmd)
{
    write(m_fdGpioDc, "0", 1);

    struct spi_ioc_transfer tr = {0};
    tr.tx_buf = (uint64_t)&cmd;
    tr.rx_buf = 0;
    tr.len = 1;
    ioctl(m_fdSpi, SPI_IOC_MESSAGE(1), &tr);
}

これらの関数を使用して、LCDモジュールとやり取りします。初期化シーケンスや描画方法は既にネットに大量に転がっているので、ここでは省略します。

タッチパネル用デバイスドライバの開発

今回購入した液晶モジュールには4線抵抗式タッチパネルが装着されていました。タッチパネル単体だと、自分で電圧かけて、ADCして、また別軸に対しても同じことして、とちょっと面倒なのですが、タッチパネルコントローラも搭載されていました。私が購入したモジュールにはTSC2046というコントローラICがついていましたが、これは一般に使用されているADS7846と互換性があります。

このタッチパネルもSPIとGPIOの2つを使用します。全体の流れは以下のようになります。
0. ラズパイ ← TP: タッチ状態が変化したよ (IRQピン(GPIO)で通知)
1. ラズパイ → TP: X軸のタッチ位置を教えてください
2. ラズパイ ← TP: X位置は〇〇だよ
3. ラズパイ → TP: Y軸のタッチ位置を教えてください
4. ラズパイ ← TP: Y位置は〇〇だよ
5. ラズパイ → TP: タッチの強さ(圧力: z軸)を教えてください
6. ラズパイ ← TP: 圧力は〇〇だよ

Z軸(圧力)を確認することで、タッチされていないとき(圧力小)のゴミデータを取り除きます。

一般のユーザ操作をイメージするとわかると思うのですが、ユーザは常にタッチ操作しているわけではありません。できれば、ユーザがタッチした時だけタッチ位置取得を行いたいです。それを教えてくれるピン(IRQ)があります。IRQピンは常時1(High)で、状態変化時に0(Low)になります。つまり、ラズパイはこのピンを監視することでタッチ状態を取得します。タッチ(あるいは指離れ)時のみタッチ位置を取得するようにします。

このIRQピンの監視には割り込みハンドラを使用したいところなのですが、ユーザ空間からは割り込みハンドラを定義することが出来ないようです。しかし、GPIOのエッジ変化をポーリングで取得する方法はあります。そのため、タッチパネルに関しては別スレッドを立ち上げて、そのスレッド内でIRQピンのエッジ変化をチェックして、変化があったらSPI通信でタッチ位置を取得する、という仕組みにします。コードを下記に記します。(getPosition関数内でSPI通信することでタッチ位置を取得していますが、前述のコードと同様に通信しているだけなので省略します。)

void* threadFunc(void)
{
    int fd;
    struct pollfd pfd;
    float x, y;
    bool pressed;

    fd = open("/sys/class/gpio/gpio19/value", O_RDONLY);
    pfd.fd = fd;
    pfd.events = POLLPRI;

    while(1) {
        lseek(fd, 0, SEEK_SET);
        if(poll(&pfd, 1, 1000) != 0) {
            pressed = getPosition(&x, &y);
            if(pressed) {
                printf("Pressed %f %f\n", x, y);
            }
            char c;
            read(fd, &c, 1);    // need to read to reset irq status?
        }
    }

    close(fd);
    return 0;
}

Camera(V4L2)制御

カメラ制御にはVideo4Linux2(V4L2)というAPIを使用します。カメラ(この場合にはラズパイのカメラ)のデバイスドライバがV4L2 APIとして実装されている、という言い方の方が正しいかもしれません。なので我々は同じV4L2 APIを使用してカメラ制御を行います。「V4L2 APIを使う」といっても、実際に使用する関数は前述のioctlになります。V4L2ではioctlの引数のリクエストとパラメータが規定されています。

V4L2を使用するためには、事前にコマンドでモジュールを組込んでおく必要があります。これを実行するとカメラがビデオデバイスとして認識されます(/dev/video0)

sudo modprobe bcm2835-v4l2

カメラ制御が今までのデバイスと異なるところは、デバイスが画像データあるいはストリームを出力する点です。この点もV4L2で考慮されており、我々はカメラデバイスに対して、「〇〇というフォーマットの画像用にバッファを〇面確保してください。そして、そのバッファに対してキャプチャした画像を出力してください」と指示します。で、このバッファ面をmmapしてあげることで、我々が作るユーザプログラムから画像データにアクセス可能になります。実際にカメラに対して「バッファに対してキャプチャした画像を出力してください」と指示するのは、バッファをエンキューすることになります。カメラはエンキューされたバッファに毎フレーム、画を出力します。キューイングされているバッファ全てに書き終わったら止まるようです(リングバッファのようにはならない模様)。その後、画が書き込み済みのバッファにユーザプログラムがアクセスするときには、対象バッファをデキューします。そしてそのバッファを使い終わったら再度エンキューします。これによってカメラはそのバッファに対してまた書き込みを行います。

320x240でJPEG撮影をするようにカメラのキャプチャ設定をして(startCapture)、画像データを取得して(copyBuffer)、キャプチャを停止する(stopCapture)というコードが下記になります。実行可能なコードはこちらをご参考ください(https://github.com/take-iwiw/DigitalCamera_RaspberryPi/blob/master/sampleCode/sampleV4L2.cpp )。ここではCっぽく書いていますが、カメラ用デバイスドライバとしてはこれを使いやすくしたものを実装しました。

int fd;
const int v4l2BufferNum = 2;
void *v4l2Buffer[v4l2BufferNum];
uint32_t v4l2BufferSize[v4l2BufferNum];

void startCapture()
{
    fd = open("/dev/video0", O_RDWR);

    /* 1. フォーマット指定。320 x 240のJPEG形式でキャプチャしてください */
    struct v4l2_format fmt;
    memset(&fmt, 0, sizeof(fmt));
    fmt.type                = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width       = 320;
    fmt.fmt.pix.height      = 240;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_JPEG;
    ioctl(fd, VIDIOC_S_FMT, &fmt);

    /* 2. バッファリクエスト。バッファを2面確保してください */
    struct v4l2_requestbuffers req;
    memset(&req, 0, sizeof(req));
    req.count  = v4l2BufferNum;
    req.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    ioctl(fd, VIDIOC_REQBUFS, &req);

    /* 3. 確保されたバッファをユーザプログラムからアクセスできるようにmmapする */
    struct v4l2_buffer buf;
    for (uint32_t i = 0; i < v4l2BufferNum; i++) {
        /* 3.1 確保したバッファ情報を教えてください */
        memset(&buf, 0, sizeof(buf));
        buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index  = i;
        ioctl(fd, VIDIOC_QUERYBUF, &buf);

        /* 3.2 取得したバッファ情報を基にmmapして、後でアクセスできるようにアドレスを保持っておく */
        v4l2Buffer[i] = mmap(NULL, buf.length, PROT_READ, MAP_SHARED, fd, buf.m.offset);
        v4l2BufferSize[i] = buf.length;
    }

    /* 4. バッファのエンキュー。指定するバッファをキャプチャするときに使ってください */
    for (uint32_t i = 0; i < v4l2BufferNum; i++) {
        memset(&buf, 0, sizeof(buf));
        buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index  = i;
        ioctl(fd, VIDIOC_QBUF, &buf);
    }

    /* 5. ストリーミング開始。キャプチャを開始してください */
    enum v4l2_buf_type type;
    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(fd, VIDIOC_STREAMON, &type);

    /* この例だと2面しかないので、2フレームのキャプチャ(1/30*2秒?)が終わった後、新たにバッファがエンキューされるまでバッファへの書き込みは行われない、はず */
}

void copyBuffer(uint8_t *dstBuffer, uint32_t *size)
{
    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(fd, &fds);

    /* 6. バッファに画データが書き込まれるまで待つ */
    while(select(fd + 1, &fds, NULL, NULL, NULL) < 0);

    if (FD_ISSET(fd, &fds)) {
        /* 7. バッファのデキュー。もっとも古くキャプチャされたバッファをデキューして、そのインデックス番号を教えてください */
        struct v4l2_buffer buf;
        memset(&buf, 0, sizeof(buf));
        buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        ioctl(fd, VIDIOC_DQBUF, &buf);

        /* 8. デキューされたバッファのインデックス(buf.index)と書き込まれたサイズ(buf.byteused)が返ってくる */
        *size = buf.bytesused;

        /* 9. ユーザプログラムで使いやすいように、別途バッファにコピーしておく */
        memcpy(dstBuffer, v4l2Buffer[buf.index], buf.bytesused);

        /* 10. 先ほどデキューしたバッファを、再度エンキューする。カメラデバイスはこのバッファに対して再びキャプチャした画を書き込む */
        ioctl(fd, VIDIOC_QBUF, &buf);
    }
}

void stopCapture()
{
    /* 11. ストリーミング終了。キャプチャを停止してください */
    enum v4l2_buf_type type;
    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(fd, VIDIOC_STREAMOFF, &type);

    /* 12. リソース解放 */
    for (uint32_t i = 0; i < v4l2BufferNum; i++) munmap(v4l2Buffer[i], v4l2BufferSize[i]);

    /* 13. デバイスファイルを閉じる */
    close(fd);
}

[メモ] よく使う関連コマンド

sudo modprobe bcm2835-v4l2
v4l2-ctl -d /dev/video0 --info
v4l2-ctl -d /dev/video0 --all
v4l2-ctl -d /dev/video0 --list-formats-ext

デジタルカメラシステムの開発

全体の設計

記事の趣旨的にボトムアップで考えていきます。
デバイスドライバとして、カメラドライバ(DdCamera)、LCDドライバ(DdIli9341Spi)、タッチパネルドライバ(DdTpTsc2046Spi)を用意します。これらの中身は上述したとおりです。
今回は、機能仕様として、ライブビューや静止画撮影を行う撮影モードと、再生を行う再生モードがあります。これらのモードは独立なので、それぞれ別のクラスに管理させます。それぞれCameraCtrlとPlaybackCtrlとします。各Ctrlクラスは使用するデバイスドライバを持ちます。つまり、CameraCtrlはカメラドライバとLCDドライバ、PlaybackCtrlはLCDドライバを持ちます。各Ctrlクラスは自分のモード内の制御だけを考えます。
そのため、ユーザのタッチパネル操作に応じて適切なCtrlクラスを呼び出す人が必要です。それをAppクラスとします(本当はモードMgrという名前にしたかった)。そのため、Appクラスはタッチパネルドライバを常に持ちます。モードによってCameraCtrlかPlaybackCtrlの生成/削除をします。

overview.jpg

よりよい設計

今回は趣味のプロジェクトなので上記のような設計にしました。
この設計だと各Ctrlクラスが直接LCDドライバを持ってしまっていますが、これでは移植性、性能ともに悪くなります。例えば、液晶ディスプレイを別のものに変えたときに叩くドライバを変える必要があります。抽象化したHALみたいなレイヤをかますことでこれは解消できるかもしれません。これでもまだ問題があります。各CtrlがそれぞれLCDドライバを持っているため、モード切替時に毎回デバイスの初期化などが行われるため、モード切り替えに時間がかかっています。
これに対しては、別途DisplayCtrlのようなものを用意して、DisplayCtrlが液晶ディスプレイ(あるいはHDMI出力も?)を隠ぺいします。で、CameraCtrlやPlaybackCtrl、あるいは将来的にモードが増えたときに追加するであろうCtrlは、DisplayCtrlに表示してほしいバッファを通知するという形が良いかなと思います。さらにちゃんとやるには、CameraやDisplayを別々のサービスとして立ち上げて、/dev/fb経由でやり取りするのだと思いますが、そこは勉強中。

タッチパネルも同様で、今回はたまたまユーザ入力はタッチパネルでしたが、将来的にはボタンやダイヤル入力にすることも考えられます。そのため、InputCtrlみたいなものをかませて、そいつがすべての入力を取りまとめるのがいいかと思います。
(そして、こうやって〇〇Mgrや〇〇Ctrlといったモジュールが増えていく。。。)

今回、大きいバッファだけでなく、モジュールクラスまでも、動作中にnew/deleteしていますが、普段(OSレスやRTOSでの開発)なら絶対にやりません。理由は、メモリ使用量が読めない(大きいモジュールは静的確保にしたい)、フラグメンテーション、new/deleteの時間的コスト、解放忘れのリスク、があります。が、モード切替とかそういった粒度であれば有りかな、とも思います。各Ctrlが同時に動くことはないので、メモリ使用量的にも有利になりますので。実際にはモード切替時間とメモリ使用量、その他制約(開発期間、要求レベル)を天秤にかけて決めるのだと思います。

実際に作って動かしてみる

ハードウェア接続

カメラモジュールはラズパイのCSIコネクタに接続してください。
液晶ディスプレイ(LCD)とタッチパネル(TP)の各ピンを、以下のようにラズパイと接続してください。
- LCD (SPI0.1 (/dev/spidev0.0))
- LCD_SCLK: SPI0_SCLK
- LCD_MOSI: SPI0_MOSI
- LCD_MISO: SPI0_MISO
- LCD_CS: SPI0_CE0_N
- DC: GPIO 26 (/sys/class/gpio/gpio26)
- VDD, LED, RESET: 3.3V
- GND: GND
- TP (SPI0.1 (/dev/spidev0.1))
- TP_SCLK: SPI0_SCLK
- TP_MOSI: SPI0_MOSI
- TP_MISO: SPI0_MISO
- TP_CS: SPI0_CE1_N
- IRQ = GPIO 19 (/sys/class/gpio/gpio19)

ラズパイの設定

こんな記事を読んでいる方はすでにやっていると思いますが、sudo raspi-config等でCameraとSPIを有効にしておいてください。

また、libjpegをインストールする必要があります。JPEGエンコードはV4L2がやってくれるのですが、再生時のJPEGデコードにlibjpegを使用しています。

sudo apt-get install libjpeg-dev

コードの取得とビルド

ラズパイ上のターミナルからコードを取得してください。git clone https://github.com/take-iwiw/DigitalCamera_RaspberryPi.git

とりあえず試す

下記コマンドでビルド、実行が行われます。

cd DigitalCamera_RaspberryPi
make
sudo modprobe bcm2835-v4l2
./a.out

起動時に自動実行させる

ラズパイ起動時に自動的に本カメラアプリを実行させて、実際のプロダクトのようにさせます。cronに対して起動時ジョブを指定しているだけです。

cd DigitalCamera_RaspberryPi
sh ./install.sh

おわりに

言い訳

コードを見るとわかると思うのですが、無駄なバッファのコピーがあります。これによって、メモリ使用量が増えてしまうことに加えて、ライブビューのディレイ増加につながります。ガチガチに設計することで、最適化はできると思うのですが、今回は実装の分かりやすさを優先してこのようにしました。静止画撮影するときに、一度ライブビュー用(RGB888出力)のカメラドライバをdeleteして、静止画撮影用(JPEG出力)のカメラドライバを新たにnewしています。当然撮影時のタイムラグが生じますが、これも同様に実装の分かりやすさを優先した結果です。

他マイコン(STM32 + RTOS)と比較して

デバイス制御について

LCD制御やTP制御はSTM32でやるのとほぼ同じ感覚で実装できました。最下層のSPIやGPIOアクセスする部分は異なりますが、STM32でも一番下のSPI通信部分やDCピンの設定なんかは関数化してすぐに隠してしまうので、そこさえ書き換えれば、ほぼSTM32と同様のコードで行けました。
カメラ制御はSTM32とは全く異なります。V4L2の使い方を覚えるのが全て、といった感じでした。とはいえ、カーネル層の本当のデバドラ部分になるとまた変わってくるのだと思います。

開発期間

STM32でもほぼ同じ機能を持つデジタルカメラを作りました(https://github.com/take-iwiw/DigitalCamera_STM32/ )。この時は実働約3週間かかりました。今回のLinuxでは1週間かからずにできました。ある程度下回りが出来ると、メモリ制約の少ないラズパイ+Linuxの方が実装は圧倒的に楽でした。

資料など

ソースコード

デモビデオ

https://youtu.be/xP2iDa2bzRc

参考資料