Qtの基礎 - SPI通信

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

SPI (Serial Peripheral Interface) は、マイコンやSoCと周辺機器の間で使用される同期式シリアル通信プロトコルである。
高速かつ全二重通信が可能であり、主にセンサ、ディスプレイ、メモリチップ等との通信に利用される。

Qtフレームワーク自体に組み込まれた専用のクラスや機能は存在しない。
代わりに、OSが提供するSPIインターフェースを直接利用する、あるいは、サードパーティ製ライブラリを使用することになる。

多くの場合、OSが提供するインターフェースでの実装が一般的である。
Linuxの場合では、sys/ioctl.hファイル、linux/spi/spidev.hファイルを使用して、システムレベルのSPI操作を行う。
具体的には、SPIデバイスファイルを開いて、ioctlシステムコールを使用してSPIの設定を行い、read関数やwrite関数でデータの送受信を行う。

これらの低レベル操作をQtアプリケーションに統合する方法として、
QFileクラスを用いてデバイスファイルを操作、あるいは、QIODeviceクラスを継承した独自のクラスを定義することが想定される。
これにより、Qtのシグナルとスロットメカニズムやイベントループとの連携が可能になる。

非同期処理が必要な場合は、QtConcurrentモジュールを使用してSPI通信をバックグラウンドで実行して、結果をシグナルで通知する方法も有効である。
これにより、UIの応答性を維持しながらSPI通信を行うことができる。

ただし、SPIの実装はハードウェアやOSに大きく依存するため、具体的な実装方法はプロジェクトの要件やターゲットプラットフォームによって変わることに注意が必要となる。
場合によっては、クロスプラットフォーム対応のためのラッパークラスを定義して、プラットフォーム固有の実装を抽象化することも検討する。

セキュリティの観点からは、SPIデバイスへのアクセス権限の管理やデータの整合性の確認等も重要な考慮点となる。


SPI通信の設定

以下に示す設定は、マスターデバイス (マイコンやCPU) と スレーブデバイス (センサやメモリ等) の間で一致している必要がある。
正しく設定されていない場合、データの誤読や通信エラーが発生する可能性がある。

  • 速度
    SPIバスのクロック周波数を指す。
    これはデータ転送速度を決定する。
    高い周波数ほど速いデータ転送が可能になるが、デバイスの仕様や配線の品質などによって上限がある。
    一般的な範囲は、数百[kHz]から数十[MHz]である。

  • モード
    SPIには4つの動作モードがあり、これらはCPOL (クロック極性) と CPHA (クロック位相) の組み合わせで決まる。

    以下に示すのモードは、クロック信号のアイドル状態とデータのサンプリングタイミングを定義する。
    • モード0 - CPOL=0, CPHA=0
    • モード1 - CPOL=0, CPHA=1
    • モード2 - CPOL=1, CPHA=0
    • モード3 - CPOL=1, CPHA=1

  • ビット数
    1回のデータ転送で送受信するビット数を指定する。
    通常は8ビットであるが、デバイスによっては16ビットや他のビット数を使用することもある。

  • LSB First (Least Significant Bit First)
    データ転送時に最下位ビットから送信するかどうかを決定する。
    デフォルトでは、ほとんどのSPIデバイスはMSB (Most Significant Bit) ファーストである。

  • 3線式SPIモード / 4線式SPIモード
    通常のSPIは4線式 (MOSI、MISO、SCK、CS) であるが、3線式では双方向データラインを使用することもできる。

  • ループバックモード
    MISOとMOSIを内部的に接続して、送信したデータがそのまま受信される。
    主にテスト目的で使用される。

  • No Chip Select (CS)
    チップセレクト信号を使用するかどうかを指定する。
    単一のSPIデバイスを使用する場合やソフトウェアで別途CSを制御する場合に使用する。

  • Ready
    レディ信号を有効にするかどうかを指定する。
    一部のSPIデバイスでは、データの準備ができたことを示す信号を使用する。

  • CS Change
    各ワード転送後にCSをトグルするかどうかを指定する。
    通常、CSはトランザクション全体で低いままであるが、一部のデバイスでは各ワード間でCSをトグルする必要がある。



SPI通信の共通クラスの定義

SPI通信の設定を以下に示す。

  • LSB First
  • 3線式SPIモード / 4線式SPIモード
  • ループバックモード
  • No Chip Select (CS)
  • Ready
  • CS Change


※注意
一部のSPIデバイスやドライバでは、これらの設定の一部がサポートされていない場合がある。
使用する前に、ターゲットのハードウェアとドライバがこれらの設定をサポートしていることを確認すること。

 // SpiDevice.hファイル
 
 #include <QObject>
 #include <QFile>
 #include <QFuture>
 #include <QtConcurrent>
 #include <sys/ioctl.h>
 #include <linux/spi/spidev.h>
 #include <fcntl.h>
 #include <unistd.h>
 
 class SpiDevice : public QObject
 {
    Q_OBJECT
 
 protected:
    QString m_devicePath;
 
 public:
    enum class Mode {
       Mode0 = SPI_MODE_0,
       Mode1 = SPI_MODE_1,
       Mode2 = SPI_MODE_2,
       Mode3 = SPI_MODE_3
    };
 
    struct Config {
       Mode mode           = Mode::Mode0;
       bool lsbFirst       = false;
       bool threeWire      = false;
       bool loopback       = false;
       bool noCs           = false;
       bool ready          = false;
       bool csChange       = false;
       uint8_t bitsPerWord = 8;
       uint32_t speed      = 500000;
    };
 
    explicit SpiDevice(const QString& devicePath, QObject* parent = nullptr) : QObject(parent), m_devicePath(devicePath)
    {}
 
    bool configure(const Config& config)
    {
       QFile spiDevice(m_devicePath);
       if (!spiDevice.open(QIODevice::ReadWrite)) {
          emit errorOccurred("SPIデバイスを開けませんでした: " + spiDevice.errorString());
          return false;
       }
 
       int fd = spiDevice.handle();
       uint8_t mode = static_cast<uint8_t>(config.mode);
       if (config.lsbFirst)  mode |= SPI_LSB_FIRST;
       if (config.threeWire) mode |= SPI_3WIRE;
       if (config.loopback)  mode |= SPI_LOOP;
       if (config.noCs)      mode |= SPI_NO_CS;
       if (config.ready)     mode |= SPI_READY;
       if (config.csChange)  mode |= SPI_CS_HIGH;
 
       if (ioctl(fd, SPI_IOC_WR_MODE, &mode) < 0 || ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &config.bitsPerWord) < 0 ||
            ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &config.speed) < 0) {
          emit errorOccurred("SPIの設定に失敗しました");
          spiDevice.close();
          return false;
       }
 
       spiDevice.close();
       return true;
    }
 
 signals:
    void errorOccurred(const QString& error);
 };



SPI通信の受信

以下の例では、非同期およびストリーミング処理を使用して、SPI通信でデータを受信している。

 // SpiReader.hファイル
 
 #include <QObject>
 #include <QFile>
 #include <QFuture>
 #include <QtConcurrent>
 #include <sys/ioctl.h>
 #include <linux/spi/spidev.h>
 #include <fcntl.h>
 #include <unistd.h>
 #include "SpiDevice.h"
 
 class SpiReader : public SpiDevice
 {
    Q_OBJECT
 
 private:
    std::atomic<bool> m_stopReading{false};
 
 public:
    using SpiDevice::SpiDevice;
 
    void startReading(int bufferSize = 1024)
    {
       QFuture<void> future = QtConcurrent::run([this, bufferSize]() {
          QFile spiDevice(m_devicePath);
          if (!spiDevice.open(QIODevice::ReadOnly)) {
             emit errorOccurred("SPIデバイスのオープンに失敗: " + spiDevice.errorString());
             return;
          }
 
          int fd = spiDevice.handle();
          QByteArray buffer(bufferSize, 0);
          while (!m_stopReading) {
             ssize_t ret = read(fd, buffer.data(), buffer.size());
             if (ret < 0) {
                emit errorOccurred("SPIの読み込みに失敗");
                break;
             }
             else if (ret > 0) {
                emit dataRead(buffer.left(ret));
             }
          }
 
          spiDevice.close();
          emit readingFinished();
       });
    }
 
    void stopReading()
    {
       m_stopReading = true;
    }
 
 signals:
    void dataRead(const QByteArray& data);
    void readingFinished();
 };


 // main.cppファイル
 
 #include <QCoreApplication>
 #include <QTimer>
 #include "SpiReader.h"
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    SpiReader reader("/dev/spidev0.0");
 
    SpiDevice::Config config;
    config.mode        = SpiDevice::Mode::Mode0;
    config.lsbFirst    = true;
    config.threeWire   = true;
    config.loopback    = false;
    config.noCs        = true;
    config.ready       = false;
    config.csChange    = false;
    config.bitsPerWord = 8;
    config.speed       = 1000000;
 
    if (!reader.configure(config) || !writer.configure(config)) {
       qDebug() << "SPIデバイスの設定に失敗";
       return -1;
    }
 
    QObject::connect(&reader, &SpiReader::dataRead, [](const QByteArray& data) {
       qDebug() << "受信したデータ: " << data.toHex();
    });
 
    QObject::connect(&reader, &SpiReader::errorOccurred, [](const QString& error) {
       qDebug() << "エラーが発生: " << error;
    });
 
    reader.startReading();
 
    QTimer::singleShot(10000, [&reader, &a]() {
       reader.stopReading();
       a.quit();
    });
 
    return a.exec();
 }



SPI通信の送信

以下の例では、非同期処理を使用して、SPI通信でデータを送信している。

 // SpiWriter.hファイル
 
 class SpiWriter : public SpiDevice
 {
    Q_OBJECT
 
 public:
    using SpiDevice::SpiDevice;
 
    void writeData(const QByteArray& data)
    {
       QFuture<void> future = QtConcurrent::run([this, data]() {
          QFile spiDevice(m_devicePath);
          if (!spiDevice.open(QIODevice::WriteOnly)) {
             emit errorOccurred("SPIデバイスのオープンに失敗: " + spiDevice.errorString());
             return;
          }
 
          int fd = spiDevice.handle();
          ssize_t ret = write(fd, data.constData(), data.size());
          if (ret < 0) {
             emit errorOccurred("SPIの書き込みに失敗");
          }
          else {
             emit dataWritten(ret);
          }
 
          spiDevice.close();
       });
    }
 
 signals:
    void dataWritten(int bytes);
 };


 // main.cppファイル
 
 #include <QCoreApplication>
 #include <QTimer>
 #include "SpiDevice.h"
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    SpiWriter writer("/dev/spidev0.0");
 
    SpiDevice::Config config;
    config.mode        = SpiDevice::Mode::Mode0;
    config.lsbFirst    = true;
    config.threeWire   = true;
    config.loopback    = false;
    config.noCs        = true;
    config.ready       = false;
    config.csChange    = false;
    config.bitsPerWord = 8;
    config.speed       = 1000000;
 
    QObject::connect(&writer, &SpiWriter::dataWritten, [](int bytes) {
       qDebug() << bytes << "バイトのデータを送信";
    });
 
    QObject::connect(&writer, &SpiWriter::errorOccurred, [](const QString& error) {
       qDebug() << "エラーが発生: " << error;
    });
 
    QTimer::singleShot(5000, [&writer]() {
       writer.writeData(QByteArray::fromHex("0102030405"));
    });
 
    return a.exec();
 }