さくらのジャンク箱ロゴ

Sakura87がほぼ月刊でお届けするPCや電子工作と写真の備忘録てきなブログ @なんと20周年

Raspberry Pi 4にてMCP3208を使用して測距センサーの値を得る

この記事は4年ほど前に投稿されました。内容が古くなっている可能性がありますので更新日時にご注意ください。

コロナ禍のなか皆様いかがお過ごしでしょうか。
今回はシャープ製の距離センサーGP2Y0A21YKの測定値をRaspberry Piで読んでみたいと思います。

今回使用する部品はRaspberry Pi4以外は以下のものです。
なお、4で動作を確認していますが、Raspberry Pi 2の頃から使用していた技術をそのまま利用していますので、4以前のRaspberry Piでもそのまま利用可能かと思われます。(少なくともピン配列が同じ2以降であれば利用できるかと。)

シャープが製造している赤外線方式の距離センサーです。10~80cmの距離が測定でき、距離に反比例した非線形の電圧値を吐きます。

PICで有名なマイクロチップ社が出しているAD変換ICです。
12bitの分解能があり最大で8chの電圧計測器が内蔵されています。

今回はこの測距センサーの値をこのAD変換ICを利用して読み取ってみたいと思います。
その前に、AD変換と距離センサーに関して簡単に。

AD変換IC

電圧値などのアナログの信号をデジタルの信号として変換することが出来るデバイスです。

例えば空気の振動によって電流が変化する素子から得られた電流の変化をAD変換することで音声入力、光の強弱を電流に変換する素子で得られる電流の変化をAD変換することでディスプレイの調光機能などが作成でき、これを規則正しく大量に並べることでスキャナやデジタルカメラといったものが作成できます。このようにAD変換は現在身の回りのあらゆるものに使用されていて、その電圧等のアナログ的な変化をデジタル信号として読み取る機能自体を抜き出したものが「AD変換IC」です。詳しい説明は殆ど気にする必要がなく、必要な変換範囲に応じてデバイスを選択します。

その中で今回のICは「逐次変換」型で12ビットの分解能があります。

距離センサー

電波、光、音波など、いろいろなものをデバイスから発してその信号が伝わる状況を感知して距離を測ることが出来るセンサーです。
発するものは大きく訳で以下のものがあります。

光学方式

赤外線などの不可視光線を放ち、その光の到達時間や到達する位置を測って距離を算出する方式です。
条件が良ければ非常に精度が高い方式ですが、ものの色や透過性に影響を受けやすく、外光による影響も少なからずあります。

一般的なパーツショップで手に入るものはこのタイプと音響方式のものが多いです。
測定範囲は数cm~数m程度。用途としては近距離での距離測定を主に扱います。また、安価に作れることから非接触方式のセンサーに使用することも考えられます。

今回使用する距離計もこのタイプで、検出はPSD(Position Sensing Device)にて反射された光の位置ずれを計算して距離を出しています。

音波方式

超音波などの指向性の高い音を放ってその音が到達する時間を計測して距離をはかる方式です。
特定の音を放ってから到達するまでの時間を計るだけですので、処理や設計が容易で比較的安価に作成することが可能。
また、精度は時間測定の精度に依存しますがあまり高くはなく、それなりに大きな被検体が必要な事が欠点ですが、ちりや埃、熱や光といった外的要因による影響を受けにくいと言う特徴があります。

精度や反応速度は光学方式よりは劣ります。(基本的には音速になるため光速で測る光学式の方が高速である。

基本的にはこの2つのセンサーを用途や環境によって使い分けることになります。

電波方式

所謂レーダー。

非常に直進性が高い数十GHzといった電波を放ってその電波が反射してくる時間などを利用して距離を測定します。

電波なので視界が優れない状態や音響・光学の機器が使えない場所でも使用できますが、基本的にホームユースでは入手する機会がなく、あっても高価なので使うことはないと思います。最近流行の自動ブレーキとか車のコーナーセンサーに用いられるほか、様々な製品に用いられている。

画像認識方式

「センサー」とは違いますが。例えばカメラを2つ用意してそれから得られた画像の視差を計算するなど様々な方法で画像を処理して距離を計測する方法。方式的には光学センサーに分類されるんだろうか。人間の目と同じように距離以外のいろいろなものが検知できる今話題のナウい方式。
カメラ式の自動ブレーキやロボットなど様々なものに応用されている。

マイコンで使用するのであれば余り縁のない方式だが、ラズパイであれば初代でも十分処理できる性能はあると思われる。
ただし面倒なのでこのサイトで扱うことは多分ない。

以上が測定方式の違いによるもの。このほか、データを取得する方式がアナログ的な電圧変化によるもの、通信インターフェイスを持ち、データとして取得可能なものがあり。今回利用するものは前者である。

文字数稼ぎの為の説明はこの程度にして、各デバイスの使い方を説明します。

使い方

測距モジュール GP2Y0A21YK

5Vの電源で動作します。接続は電源と出力の3ピン構成で非常にシンプル。Arduinoなどのアナログ入力があるマイコンであればこれ単体で使用可能。
データシートによると10cm少し手前(8cmくらい?)がピークで2.6V、10cmで2.45V程度、80cmで0.4Vの電圧が出力されます。

ただし電圧出力が線形ではないのが難点です。

メーカー仕様書にある距離と出力電圧の関係グラフです。
見ての通り非線形なので扱いにくいものになりますが、下記のサイトに近似値を利用した計算式があります。

こちらによると

  • 26.549 × 電圧-1.2091

の計算式で近似値が計算出来るようです。

ここまで書いておいてなんですが、現状同価格でデジタルで距離が取得できるセンサーがあるのでこのセンサーを使用して距離を測るのはオススメしません。どちらかと言えば一定の距離の電圧を閾値に処理を行うトリガースイッチとして使用するのが良いと思います。

この距離モジュールを秋月で買った場合に付いてくるワイヤーを用いて以下のように接続します。

ピン番号 役割 接続先
1 Vo MCP3208 CH0~7のいずれか
2 GND ラズパイのGND
3 Vcc ラズパイの5V出力

Voが電圧出力で、これはAD変換ICの入力ピンにつなぎます。
残りはラズパイのGNDと5V電源につなぎますが。もしAD変換ICの基準電源を別から取る場合はそちらの5V電源につなぎます。

なお、ここで注意が必要なのは、Voが白なのはともかくとして茶色がGND、黒がVccと標準的な配色と逆の状態になっているという非常に残念な設計なので、接続する際は注意してください。

設置位置やその他注意事項などはデータシートを参照してください。

AD変換IC MCP3208-CI/P

3.3および5Vの電源で動作するICですが、SPIの信号が電源電圧に依存するようなのでRaspberry Piで利用する際は3.3Vの出力で使用します。
このICを解説しているページを色々見ていると3V系を電源。Vrefとして使用しているサイトと5V系を電源・Vrefとしているサイトが存在します。
データシートによれば概ね1V程度電圧降下があるようなので4V~4.5VくらいのSPI出力になると思われますが、Raspberry PiのGPIOは3.3V以上の電圧に対応していないので3.3Vの電源で使用をお勧めします。もしくは抵抗分圧による分圧か、2MHz程度のスイッチングですのでトランジスタなどを使用して入力します。

接続は様々なパターンがあり、基本的には下図のようにRaspberry Pi4のSPIポートを用いてデータを送受信します。
ソフトウェア的には特に難しいことはなく、SPIで決まったフォーマットにデータを成形して送ってあげれば測定値を返してきます。

回路図の画像を拡大

作例を見ていると、デジタル側に0.1μFのコンデンサを配している作例も結構見かけます。回路図にも推奨回路として載っているようですが、一応回路には記載しますがなくても問題なさそうです。ただし、どちらかと言えばアナログ側のノイズ軽減が目的のようで、あった方が誤動作の心配はないのかなと。ここに掲げる写真ではつけていません。何らかの作品に組み込む場合はつけておきましょう。

なお、GNDですが、データシートに以下のようなイラストがあります。

この通りに接続するのが良いと思います。
また、センサーのGNDはラズパイ~AGND迄のどこかで取れば良いでしょうが、AGNDに近い方が良いと思います。

今回はアナログ側の基準電圧はRaspberry Piの3.3Vに接続しています。

測定される電圧について

このICはVrefの電圧を基準として各CHの電圧を0~100%で数値化し12bitの分解能でデジタルデータとして出力します。
例えばVrefに入力した電圧が5V、3.3V、1.5Vだったときの1.5Vの値はそれぞれ1229、1862、4095になります。よって、このICを扱う場合はVrefの電圧は一定で、この電圧を把握しておいてプログラムに反映する必要があります。

0Vの数値は当然0ですが、最高値である4095が何ボルトであるかはVrefの電圧に準拠します。
Vrefが5Vなら4095は5V、3.3Vなら3.3Vです。なので上の回路図であれば0V~3.3Vの間を測定することになります。
このICには疑似差動入力機能がありますが、これはグランドループ回避やノイズ耐性の向上目的で用意されているモードのようで、これを使ったからといって倍の電圧を測れたり、マイナス電圧が測れたりするわけじゃないようです。

Vrefに5Vを入れればラズパイでも0~5Vで測れそうですが、ラズパイの5Vから電源を取ると何故かうまくいかなかったです。
ICが発熱したりラズパイが起動しないなどの問題がありました。なので5Vを取るときは間に絶縁型DC-DCコンバーターを挟むなどして別の電源から取ってください。

測定ポート、チャンネル選択コマンドについて

前項でも若干触れましたが、このICの測定ポートは8chあり、シングルエンドモードで全てのポートを単独で使用し8ch、疑似差動入力機能を用いて両隣のポートを使用して4ch計測する機能があります。詳細は下表の通りです。

ピン シングルエンド時 疑似差動入力時
CH 設定ビット CH 設定ビット
1 CH 0 1000 CH 0 IN + 0000 または 0001
2 CH 1 1001 CH 0IN –
3 CH 2 1010 CH 1 IN + 0010 または 0011
4 CH 3 1011 CH 1 IN –
5 CH 4 1100 CH 2 IN + 0100 または 0101
6 CH 5 1101 CH 2 IN –
7 CH 6 1110 CH 3 IN + 0110 または 0111
8 CH 7 1111 CH 3 IN –

設定ビットは後述するコマンド送信の際に、チャンネルを指定するためのビットです。
ビットで書いているのでわかりにくい部分もありますが、基本的には0~7の数値に加えてシングルエンドか差動入力かの設定を行うだけです。
シングルエンドと差動入力の切り替えは4ビット目で行っており、差動入力を使用する場合は4ビット目はL(0)を、シングルエンドの場合はH(1)を指定します。

つまり、シングルエンド入力の場合はチャンネル数に8(1000)を足します。
なお、差動入力の場合はシングルエンドの0・1 2・3 4・5 6・7チャンネルの指定地で同じ数値が返るようです。
なので例えば上の表でCH 0に該当する信号を差動入力で取得したい場合、10進数で0または1を設定するようになります。

上記の回路図の例であれば、1番ピンに距離計が接続されているので「8(1000)」を設定します。
差動入力を使用する場合は下図のような回路になります。(測距センサーの回路のみを記載しています。)

実際どちらが良いかですが、どちらでも良いと思います。

制御コードの作成

前置きが非常に長くなりましたが、以上で一通りの説明は終わります。
後はソフトウェア部分の話になってきます。

今回は前回同様C++、spidev.hを使用して制御します。

宣言関係

必要な定数やインクルードなどを定義します。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>
#include <cstring>
#include <cstdint>
#include <math.h>

#define kHz *1000
#define MHz *1000000

static char SPIMode = 0, SPIbit = 8; //SPIモード3 bit数8
static int SPISpeed = 1 MHz;         // SPI転送速度設定

//***** SPIデバイス定義
// # ls -l /dev/spidev* で確認できる。
// Pi2の場合CE1に接続した場合は0.0
//          CE2に接続した場合は0.1 だった。
const char *SPIDevice = "/dev/spidev0.0";

int spidev = -1;

//***** 転送用バッファ
uint8_t *tbuf = (uint8_t *)calloc(16, 1);
uint8_t *rbuf = tbuf + 8;
//***** 転送するデータの準備
struct spi_ioc_transfer tr_setting;

//***** ここまでSPI関係

//***** プロトタイプ

int init();
ushort getValue(int);

殆ど前回と同じで、SPIModeが「0」SPISpeedが「1 MHz」になっているところと、
今回は転送バッファと転送データの設定(spi_ioc_transfer)が外に出ているところが大きく違います。

このICは2.7V動作時に1MHzの処理速度があるのでそれに合わせた設計です。

また、バッファや設定は今回はグローバル化していますが、組み込む先の状況に合わせて適宜変更してください。

SPIを初期化して開く

//***** 初期化命令
int init()
{
    //***** 転送データ設定のパラメーターを定義
    tr_setting.tx_buf = (unsigned long)tbuf; // 送信するデータ
    tr_setting.rx_buf = (unsigned long)rbuf; // 受信データ
    tr_setting.len = 3;                      // 転送するサイズ
    tr_setting.delay_usecs = 0;              // 最終ビット転送後のディレイ
    tr_setting.speed_hz = SPISpeed;          // 転送速度
    tr_setting.bits_per_word = SPIbit;       // ワードあたりのビット数指定
    tr_setting.cs_change = 0;                // CS切り替えを行うかどうか

    //***** 開いたSPIデバイスの番号を返す。
    return open(SPIDevice, O_RDWR);
}

SPIの初期化命令です。
コードを見直したところ、spi_ioc_transferにて転送速度などを設定していれば最初の設定は不要だと言うことが分かりましたので、spi_ioc_transferを設定してSPIデバイスをオープンしているだけです。

データ取得関数

データを取得するための関数です。

ushort getValue(int ch)
{
    //***** チャンネル設定
    // 1バイト目、StartBit、取得モード、CH最上位ビット
    // 2バイト目、CH2ビット目、CH最下位ビット、以降および2バイト目はゼロ

    tbuf[0] = 0x4 | ((ch >> 2) & 3);
    tbuf[1] = ((char)ch << 6) & 0xC0;

    //***** データ送受信
    int r = ioctl(spidev, SPI_IOC_MESSAGE(1), &tr_setting); // 何もなければ3(lenに設定した送受信サイズ)を返す

    //***** 受信したデータを結合して返す。
    // 受信は送信と同時に行われ、コマンド送信後、1クロック空いて、
    // NULL BITの後にデータが送信される。つまり受信データ2バイト目
    // 下4ビットと3バイト目を結合すれば値が取れる(0~4095の4096段階)
    return (ushort)(*(rbuf + 1) & 0xf) << 8 | *(rbuf + 2);
}

送受信するデータ用のバッファは送信8バイト・受信8バイトの16バイト(unsigned longのポインタで渡すため)確保し、先頭3バイトを送信します。3バイト目はデータ受信のための余白なので常に0で有効なデータがあるのは1・2バイト目になります。送信するデータは以下の通りです。

1バイト目のデータ

  • 7~3ビット目
    常に0
  • 2ビット目
    スタートビット 常に1
  • 1ビット目
    シングルエンド・差動入力切り替え
    差動入力:0
    シングルエンド:1
  • 0ビット目
    チャンネル数の最上位ビット

2バイト目のデータ

  • 7ビット目
    チャンネル数の中央ビット
  • 6ビット目
    チャンネル数の最下位ビット
  • 5~0ビット目
    常に0(検知しない)

つまりシングルエンドで5ch目を使用したい場合のビット配列は以下の通りになります。

バイト 1 2 3
ビット 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7-0
設定値 0 0 0 0 0 1 1 1 0 1 0 0 0 0 0 0 0

セルに色()が付き、ビットに下線が引いてあるものがチャンネルのビットで下線のみのものがシングルエンド・差動入力の切り替えビット、斜体のものがスタートビットとなります。スタートビットの前の5ビットは常にL(0)である必要があるようです。

データの受信と受信されたデータ

データの受信は受信コマンドが終わった次のビットをNULLビットとして(この間にAD変換が行われます)その次のビットから送信されます。
データシートをみると色々とタイミングが書いてありますが。ユーザーとしては受信した3バイトのうち2バイト目の下位4ビットと3バイト目を見てやればAD変換の値が取得できるので特に難しく考える必要はないです。1バイト目を捨て、2バイト目と0x0FをAND変換して8ビットシフトし、3バイト目とつなげるだけです。

これらを行うことで、AD変換を行った電圧データがVref電圧に対しての比率で取得できます。
一応表にするとこう。

バイト 1 2 3
ビット 7-0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
受信値 不定値 N 測定データ

N= Null ビット(0)

データの使用方法

取得したデータは以下のように計算します。

  • 測定値 ÷ 4096 × 基準電圧(Vrefの電圧)

ゆえにVrefが3.3Vで測定値が1865の場合は

  • 1865 ÷ 4096 × 3.3=1.5025635V

となる。

マイコンでVrefの電圧が一定の場合は

  • 測定値 × ( 1 ÷ 4096 ) × 基準電圧

とし、下線部を事前に計算しておくと処理が早くなる。(ラズパイで感じられるほど早くなる事はないと思うが。

  1. 1 ÷ 4096 × 基準電圧
  2. =1 ÷ 4096 × 3.3
  3. ‭=0.000244140625‬ × 3.3
  4. =‭0.0008056640625‬

ということで

  1. 測定値 × ‭0.0008056640625‬
  2. =1865 × ‭0.0008056640625‬
  3. =‭1.5025635V

ということになる。かけ算を1回するだけなので割って掛けるより早い。
ラズパイなら初代でもこれくらいの計算実感が出来るほど変わらないと思いますが、Arduinoかそれより低性能なマイコンならこの方法は有効なのかなと。これくらいの最適化コンパイラがやってくれそうですが。

全体のコード

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>
#include <cstring>
#include <cstdint>
#include <math.h>

#define kHz *1000
#define MHz *1000000

static char SPIMode = 0, SPIbit = 8; //SPIモード3 bit数8
static int SPISpeed = 1 MHz;         // SPI転送速度設定

//***** SPIデバイス定義
// # ls -l /dev/spidev* で確認できる。
// Pi2の場合CE1に接続した場合は0.0
//          CE2に接続した場合は0.1 だった。
const char *SPIDevice = "/dev/spidev0.0";

int spidev = -1;

//***** 転送用バッファ
uint8_t *tbuf = (uint8_t *)calloc(16, 1);
uint8_t *rbuf = tbuf + 8;
//***** 転送するデータの準備
struct spi_ioc_transfer tr_setting;

//***** ここまでSPI関係

//***** プロトタイプ

int init();
ushort getValue(int);

//***** メイン関数
int main()
{

    //***** 初期化
    spidev = init();

    //***** データ取得。
    // このプログラムでは1msごとに情報を表示している。
    while (1)
    {
        //***** 先にディレイを設けると動作が安定する。
        usleep(1000);

        // 情報の取得
        ushort v = getValue(8);


        float voltage = (float)v * 0.0008057f;

        float dist = 26.549f * powf(voltage, -1.2091f);

        //***** 生の値 算出値 の順で出力され、常に上書きされる。
        printf("%04d %1.3f D:%2.1fcm\r", v, voltage, dist);
    }
    return 0;
}

//***** 初期化命令
int init()
{
    //***** 転送データ設定のパラメーターを定義
    tr_setting.tx_buf = (unsigned long)tbuf; // 送信するデータ
    tr_setting.rx_buf = (unsigned long)rbuf; // 受信データ
    tr_setting.len = 3;                      // 転送するサイズ
    tr_setting.delay_usecs = 0;              // 最終ビット転送後のディレイ
    tr_setting.speed_hz = SPISpeed;          // 転送速度
    tr_setting.bits_per_word = SPIbit;       // ワードあたりのビット数指定
    tr_setting.cs_change = 0;                // CS切り替えを行うかどうか

    //***** 開いたSPIデバイスの番号を返す。
    return open(SPIDevice, O_RDWR);
}

//***** データ取得
// int ch:取得したいチャンネル
//         0・1 微分モードCH0 ch0 IN+ ch1 IN-    8:CH0   12:CH4
//         2・3 微分モードCH1 ch2 IN+ ch3 IN-    9:CH1   13:CH5
//         4・5 微分モードCH2 ch4 IN+ ch5 IN-   10:CH2   14:CH6
//         6・7 微分モードCH3 ch6 IN+ ch7 IN-   11:CH3   15:CH7
// 戻り値:取得した生値(0~4095)
// 【取得した情報の計算】
//   電圧=(取得値 ÷ 4096)      × 基準電圧
//      = 取得値 × (1÷4096)  × 基準電圧
//     = 取得値 × 0.000244‬1  × 基準電圧
//     = 取得値 × 0.0008057 (基準電圧=3.3Vの時
ushort getValue(int ch)
{
    //***** チャンネル設定
    // 1バイト目、StartBit、取得モード、CH最上位ビット
    // 2バイト目、CH2ビット目、CH最下位ビット、以降および2バイト目はゼロ

    tbuf[0] = 0x4 | ((ch >> 2) & 3);
    tbuf[1] = ((char)ch << 6) & 0xC0;

    //***** データ送受信
    int r = ioctl(spidev, SPI_IOC_MESSAGE(1), &tr_setting); // 何もなければ3(lenに設定した送受信サイズ)を返す

    //***** 受信したデータを結合して返す。
    // 受信は送信と同時に行われ、コマンド送信後、1クロック空いて、
    // NULL BITの後にデータが送信される。つまり受信データ2バイト目
    // 下4ビットと3バイト目を結合すれば値が取れる(0~4095の4096段階)
    return (ushort)(*(rbuf + 1) & 0xf) << 8 | *(rbuf + 2);
}

これを以下の写真(冒頭の写真)のように接続してコンパイルして(特に特別な引数は必要ないです)実行すれば「生の値 変換値V 測距センサーの数値cm」の順で表示されます。

測定試験例

出力サンプル

※この出力サンプルは上の写真の状態を実測した値ではないです。実際に22cm近辺に物体があります。

最後に

新型コロナウイルスによる外出自粛などで暇なのでずっと前(3年くらい前)に買っていたAD変換ICと距離センサーで遊んでみました。
もう既にAD変換が内蔵され距離がデータとして取れるタイプの距離計が多く発売されていてこれを使う意味はあまりないかもしれませんが、AD変換ICを扱う材料としてはよいものでしょう。

次回ですが、現状未定です。諸事情によりしばらく暇なのと、ストックがいくつかあるので何か近いうちに公開できれば良いなと思います。

それでは。

参考文献

総閲覧数:1,175 PV

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください