🔬

簡易ロジアナを作り、Raspberry PiのI2Cアクセスを見る

2023/07/29に公開

どんなもの

ロジアナ(Logic Analyzer)は、デジタル信号のHIGH/LOWを記録し、表示・解析する測定器です。昔は非常に高価で手に入らないものでしたが、今は、サンプリング周波数が低いものは非常に安価になりました。また、Pulseview(sigrok)のオープンソースを使うと、市販されているUSBシリアル変換モジュール(USB UART/FIFO IC)で簡単にロジアナができます。

市販のFT232H USBシリアル変換モジュール

市販されている上のモジュールでもよかったのですが、今回、自分で基板を作りたかったのでFTDI FT232Hを使ったPCBを作成し、また、ブレッドボードの電源ラインにも直接させるようにしました。このロジアナで、Raspberry PiのI2Cインタフェースで接続したEEPROMへのアクセス信号を見てみました。

自作シリアル変換モジュールをブレッドボードに差した状態
ブレッドボードに差す

回路図、ガーバーデータ、テストコードはこちらにあります

PCB

PulseView(sigrok)

PulseView(sigrok)は、ロジアナのアダプタから信号をキャプチャし、それを表示解析してくれるオープンソースのソフトウェアです。以下のように、信号線からプロトコル解析もしてくれます。

I2Cのプロトコル解析表示
PulseView

このソフトウェアのインストールは、sigrokのダウンロード先から、ビルド済みのインストーラをインストールできますが、FT232HのUSBシリアル変換モジュール場合、パルス幅の表示が正しくされません。そのため、コードを修正し、ビルドしてインストールしました。

なぜパルス幅の時間が合わないか

PulseViewの機能に、Show Cursorがあり、パルス幅を計測できますが、既知のパルス幅を測定したところ短い時間で表示されます。(1MHzクロックで1.42MHz/700.000nsと表示される、正しくは1MHz/1000ns)

誤った表示

デジタルオシロで、確かめたところ、入力されている信号は正しく、PulseViewの表示する数値だけが誤っていました。そこで、sigrokのFT232Hのドライバコードを調べたところ、FT232Hのデバイスのサンプルレートデバイダsamplerate_divの値がFT232Rと同じ30でした。これをFT232Hの20にすれば修正できます。ただし、コードから再ビルドが必要になります。(ビルド、インストール方法は後述)

ビルド後、再インストールして、1MHzのクロック幅を測定したところ、正しく表示されました。
clock 1MHz

どこまで測定できるか

どこまでの周波数が測定できるか試してみました。信号の周波数を上げてみたところ、2MHzまでなんとか測定できました(Duty比がおかしいですが長さは正しい)。それ以上は、サンプリングとデータ転送が間に合わず、取りこぼしました。この簡易ロジアナは最大2MHzまで、正しく測定するなら1MHzまでとなりました。
これでも、いろいろ測定できますので、重宝します。
clock 2MHz

IC2アクセスを測定してみる

実際に、Raspberry PiからEEPROMへのI2Cアクセスをこのロジアナで取得してみます。
接続は以下です。
Raspberry PiとI2C接続

IC2の信号は、SDA,SCLの2本のインタフェースしかありません。SDAをADBUS0(D0)に、SCLをADBUS1(D1)に接続します。

実際の結線

Raspberry PiからI2Cアクセスするためのコードを書きます。今回はRustを使用しました。
テストコードはこちら(2023.8.7追記)

まずは、RustコンパイラをRaspberry Piにインストールします。

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
>1
ここで1を選択Enterキーを押し、インストール
そのあと、環境変数をセットする
$ source "$HOME/.cargo/env"

次に、Raspberry PiのI2Cインタフェースを有効にし、I2Cツールをインストールします。

$ sudo raspi-config nonint do_i2c 0
$ sudo apt-get -y install i2c-tools

Raspberry PiからI2C経由でEEPROMデバイスを認識できるかI2Cツールで確認します。

$ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: 30 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: 50 -- -- -- -- -- -- -- 58 -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --

EEPROMのA0~A2PinをGNDに接続していますので、30, 50, 58のアドレスのデバイスが見えています。デバイスが1つしかないのになぜか、3つ出て来ています。それは、30hは、Write Protectionレジスタ、50hがEEPROMメモリへのレジスタ、58Hがシリアルナンバー読み込み用レジスタになっています。EEPROMにデータを書込みするレジスタは50hになります。

次に、I2CでアクセスするRustコードを置く、Rustのひな型を作ります。

$ cargo new eeprom

以下のソースコードを書いていきます。

.
└── eeprom
    ├── Cargo.toml
    ├── src
    │   ├── at24mac.rs
    │   └── main.rs

はじめに、アドレス50hからデータをget, writeする部分です。rppalクレートを使用します。DEVADDRはI2Cのアドレスで50hと固定にしています。

at24mac.rs
use rppal::i2c::I2c;
use std::error::Error;
 
const DEVADDR: u16 = 0x50;
 
pub struct AT24EEPROM {
    bus: I2c,
}
 
impl AT24EEPROM {
    pub fn new(bus: I2c) -> Self {
        Self { bus }
    }
 
    pub fn get_data(&mut self, addr: u8) -> Option<u8> {
        self.bus
            .set_slave_address(DEVADDR)
            .expect("failed to set slave address");
        match self.bus.smbus_read_byte(addr) {
            Ok(n) => {
                Some(n)
            },
            Err(e) => {
                eprintln!("{e}");
                None
            }
        }
    }
 
    pub fn write_data(&mut self, addr: u8, value: u8) -> Result<(), Box<dyn Error>>{
        self.bus
            .set_slave_address(DEVADDR)
            .expect("failed to set slave address");
 
        match self.bus.smbus_write_byte(addr, value) {
            Ok(()) => {
                Ok(())
            },
            Err(e) => {
                eprintln!("{e}");
                Err(Box::new(e))
            }
        }
    }
}

mainの部分です。デバイスアクセスするインスタンスを作成し、write_dataで255から0までを0番地から255番地まで書込み、それぞれ、書込毎に5msecの待ち時間を入れます。書込みには5msecかかり、この待ち時間がないと、書込みがエラーとなります。次に、0から255番地までデータを読み、書き込んだ値と一致するか確認します。

main.rs
use rppal::i2c::I2c;
use std::{error::Error, thread, time::Duration};
 
mod at24mac;
 
fn main() -> Result<(), Box<dyn Error>> {
    let i2c_bus = I2c::new()?;
 
    let mut at24eeprom_dev = at24mac::AT24EEPROM::new(i2c_bus);
 
    let mut value = 255;
 
    for addr in 0..=255 {
        // Write data
        match at24eeprom_dev.write_data(addr, value){
            Ok(()) => {},
            Err(e) => {
                println!("{e}");
            }
        }
        value -= 1;
        thread::sleep(Duration::from_millis(5));
    }

    value = 255;
    for addr in 0..=255 {
        // Read data
        if let Some(ret_value) = at24eeprom_dev.get_data(addr) {
            let mut flag = "NG";
            if value == ret_value {
                flag = "OK";
            }
            println!("addr {addr} value: {value}  {ret_value} {flag}");
        }
        value -= 1;
    } 
    Ok(())
}

読み込むクレートを指定します。

Cargo.toml
[package]
name = "eeprom"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rppal = "0.14.1"

ビルド&実行してみます。

$ cargo run --release
   Compiling eeprom v0.1.0 (/home/pi/eeprom)
    Finished release [optimized] target(s) in 1.14s
     Running `target/release/eeprom`
addr 0 value: 255  255 OK
addr 1 value: 254  254 OK
:
addr 254 value: 1  1 OK
addr 255 value: 0  0 OK

EEPROMへの書込み、読み込みは成功しました。

今度は、PulseViewを起動し、1G sammples/10MHzでRunを押しキャプチャします。先ほどのプログラムを実行します。
キャプチャできたらstopし、記録したパルスの部分を拡大します。Raspberry PiのI2Cアクセスは100KHzのクロックでした。

キャプチャ

プロトコルの解析をしてみます。ラインの左端のADBUS0のラベルをクリックし、NameADBUS0からSDAに変更します。同様にADBUS1SCLに設定します。

ラベル変更

赤枠のadd protocol decoderをクリックします。
プロトコル追加

右側のリストにデコードするプロトコルのリストが出てきます。リストのMemory24xx I2C EEPROMを選択し、ダブルクリックします。

プロトコルリスト

ダイアログが出てきますので、i2c (Inter-Integrated Circuit)を選択してOKを押します。

I2cダイアログ

SCL,SDAの信号をデコードして表示しました。I2Cアドレス50hにWriteでメモリアドレス00hにFFhのByteデータを書き込むコマンドであることがわかります。パルスを見てコマンドを判断するのは大変ですので、このデコード機能は非常に助かります。

I2Cデコード・ライト

データをランダムリードした場合のデコードです。I2Cアドレス50hにメモリアドレスF3アドレスをWriteし、I2Cアドレス50からデータを0ChをReadする手順となっています。
I2Cデコード・リード

PulseView(sigrok)のビルド・インストール

最後に、PulseViewのFT232Hのドライバを修正のうえ、ビルドする方法をまとめておきます。

Linux用

Ubuntu 22.04 LTS

$ sudo apt-get install git-core gcc make autoconf automake libtool

$ sudo apt-get install git gcc g++ make autoconf autoconf-archive \
  automake libtool pkg-config libglib2.0-dev libglibmm-2.4-dev libzip-dev \
  libusb-1.0-0-dev libftdi1-dev libieee1284-3-dev libvisa-dev nettle-dev libavahi-client-dev \
  libhidapi-dev check doxygen python3-numpy python3-dev python-gi-dev python-setuptools swig default-jdk

$ sudo apt-get install git-core g++ make cmake libtool pkg-config \
  libglib2.0-dev libboost-test-dev libboost-serialization-dev \
  libboost-filesystem-dev libboost-system-dev libqt5svg5-dev qtbase5-dev\
  qttools5-dev sdcc

$ git clone git://sigrok.org/sigrok-util

FT232H用のドライバftdi-laにパッチをあてます。

$ git clone https://github.com/hnz1102/ft232hbrkout.git
$ cp ft232hbrkout/doc/patch.ftdi-la-api.linux sigrok-util/cross-compile/linux
$ cd sigrok-util/cross-compile/linux
$ patch < patch.ftdi-la-api.linux

ビルドします。

$ ./sigrok-cross-linux
$ cd sigrok-util/cross-compile/linux/build/libsigrok
$ ./autogen.sh
$ ./configure

インストールします。

$ sudo make install

$HOME/srのところにインストールされます。

Windows用のビルド

Windows用のビルドもLinux上で行います。

$ git clone git://sigrok.org/sigrok-util
$ git clone https://github.com/mxe/mxe.git mxe-git
$ git clone https://github.com/hnz1102/ft232hbrkout.git
$ cp ft232hbrkout/doc/patch.ftdi-la-api.mingw sigrok-util/cross-compile/mingw
$ cp ft232hbrkout/doc/patch.sigrok-cross-mingw.* igrok-util/cross-compile/mingw

$ cd mxe-git

$ patch -p1 < ../sigrok-util/cross-compile/mingw/libusb1_upgrade.patch 

$ sudo  ln -s /usr/bin/python3 /usr/bin/python

$ sudo  apt install p7zip-full autopoint intltool libtool lzip python3-mako ruby libtool-bin bison flex gperf libssl-dev nsis ruby-asciidoctor-pdf 

$ make MXE_TARGETS=i686-w64-mingw32.static.posix MXE_PLUGIN_DIRS=plugins/examples/qt5-freeze gcc glib libzip libusb1 libftdi1 hidapi glibmm qtbase qtimageformats qtsvg qttranslations boost check gendef libieee1284 qtbase_CONFIGURE_OPTS='-no-sql-mysql'

$ cd sigrok-util/cross-compile/mingw
$ patch < patch.sigrok-cross-mingw.1

一度、FT232Hのドライバの修正をせずにコードをビルドします。

$ ./sigrok-cross-mingw

ビルドが成功した後、FT232Hのドライバにパッチをあてます。

$ patch < patch.ftdi-la-api.mingw

2回目のビルドをします

$ patch < patch.sigrok-cross-mingw.2
$ ./sigrok-cross-mingw

以下にWindows用のインストーラが出来ますので、これをWindowsPCにインストールします。

sigrok-util/cross-compile/mingw/build_release_32/pulseview/contrib/pulseview-0.5.0-git-3903edb-installer.exe

Windows用のビルドはたいへんややこしいので、ビルドが大変な方は、ここに公式ビルドではないですが、おいておきます。

また、Windowsではドライバの設定が必要です。上記のインストーラで同時にZadigというツールがインストールされます。
メニューのsigrokの下にZadigがあり、それを起動します。
Zadigを起動後、OptionsList All Devicesを選択します。
Zadig1

デバイスの一覧からUSBシリアル変換モジュールを選択し、USBIDが0403/6014であることを確認し、DriverをWinUSBにしてReplace Driverでインストールされます。
Zadig2

これで、PulseViewから認識できるようになります。

PulseViewのデバイスを選択(Linux/Windows共通)

PulseViewを起動後、キャプチャするUSBシリアル変換モジュールを選択します。

デバイス選択

ダイアログが表示されたら、Step 1でFTDI LA(ftdi-la)を選択し、Step 2はUSBにチェック、Step 3でScan for device using driver aboveをクリックします。USBシリアル変換モジュールが検出されるとStep 4にデバイスが表示されますのでそれを選択してOKを押します。
デバイス選択

これでサンプリング数を適切なものに選択して、Runを押すとキャプチャがスタートします。Stopでキャプチャをストップできます。

さいごに

基板を作らなくてもFT232Hのモジュールはいろいろなところで市販されていて、こんなに簡単にロジアナでができてしまうことに驚きます。PulseViewの使った安価なロジアナもいくつか市販されているのを見つけます。ドライバを用意すれば独自のキャプチャモジュールも作れそうです。FPGAでさらに周波数の高いサンプリングができるものを作ってみるのもいいかもしれません。
次回は、ESP32とK型熱電対ICを使って300℃程度まで測定できる温度測定ロガーを作りましたので、紹介したいと思います。ここまで読んでくださり、ありがとうございました。

Discussion