twitter icon   twitter icon   rss icon

Linux.com Japan

Home Linux Jp チュートリアル BeagleBone BlackのSPIからチップにアクセスする方法

BeagleBone BlackのSPIからチップにアクセスする方法

原文へのリンクは、こちらです。

BeagleBone Blackは、ARM CPU、2GB フラッシュメモリ、512MB の RAM を搭載し、他電子機器と接続できるさまざまなインタフェースを備えた 45 米ドルのシングルボード コンピューターです。Arduino と異なり、BeagleBone Blackはフルセットの Linux が動作し、任意の言語を使って外部電子機器と通信が可能で、搭載 RAM を活用することができます。以前、私が投稿したチュートリアルでは、BeagleBone Black の GPIO の活用方法を解説しました。今回は、シリアル ペリフェラル インタフェース (SPI) を使った EEPROM 形式の不揮発性ストレージ アクセスについて解説します。

SPIは、バス マスター (コントローラー) と、バスに接続した各種チップとの間で双方向にデータをやりとりできます。バス上にさまざまなチップが存在するため、複数チップによる同時通信や、同じバス上の別のチップに送信したコマンドへの応答を停止する方法が必要です。チップ セレクト線を用いて、アクティブなチップを確認したり、通信したいチップを選択します。

インタフェースには、マスターからバスへのデータ送信線、逆方向のデータ送信線、ビット同期制御のためのクロック線と各チップ用にチップ セレクト線があります。マスターからチップ (Slave) へのデータ送信線を MOSI (Master Out, Slave In) と呼び、逆方向の送信線を MISO (Master In, Slave Out) と呼びます。各バイトは、1 ビット単位で送信されます。1 ビット送信するときには、まず、MOSI にビットをセットし、クロック線を使って該当ビットの取得を指示します。同様にして、次々とビットを送信します。1 ビット送信ごとに 1 クロック必要で、チップも MISO 線を使って同様に 1 ビットずつ送信します。
 

Arduino を使って EEPRO をテストする

SPI インタフェース (訳注: SPI の I はインタフェースを意味するため重複表現になる) を持つ安価な EEPROM は、SPI アクセスのテストに最適で、高価なハードウエアを壊す心配がありません。ここでは、512KビットEEPROM を使いました。これは、数ドルで購入しました。クロックは 20MHz です。データ送信速度は、システムに組み込むものとしてはそれほど遅くありません。EEPROM チップが不揮発性ストレージに書き出すときは、もっと時間がかかります。

まず、Arduino を使って EEPROM にアクセスすることにしました。2 つ理由があります。Arduino ボードは BeagleBone Black より安価であること、そして、EEPROM を接続して電源投入したときの動作が、データシートから理解したとおりであることを確認できたことです。Arduino を使って SPI 経由で EEPROM に読み書きできると知ったことは、BeagleBone Black から EEPROM にアクセスしてデバッグするときにも役立ちました。

EEPROM の 8 ピンのうち、4 本を電源につなぎます (3 本は、3.3V 電源線、1 本は接地線) 。3.3V 線は赤色、接地線には青色を使いました。クロック線は橙色、チップ セレクト線は青色、MOSI と MISO はそれぞれ黒、白を使いました。

arduino-eeprom

写真: 赤色は 3.3V 線、青色は接地線、橙色はクロック線、青色はチップ セレクト線、黒色は MOSI、白色は MISO 線。

次に EEPROM 読み書き用の Arduino プログラムを掲載します。1 ビット単位で送信するため、ビット順序を知っておく必要があります。つまり、MSB (Most Significant Bit) を最初に送信するか、それとも最後に送信するかです。EEPROMの データシートによれば、MSB から送信するように指定されているため、setup() 関数でセットします。チップ セレクト ピン (10) を出力にセットして、EEPROMに 送信できるようにします。

loop() 関数はきわめてシンプルです。アドレス 10 から 1 バイトを読み込み、内容を表示し、ループ回数をアドレス 10 に書き出します。EEPROM からの読み込みは、まず EEPROM に READ 命令を送信します。そのあとに、読み込みアドレス 2 バイトを送信します。奇妙に思えるかもしれませんが、太字の transfer(0) は、0 を書き出すためのものではありません。SPI のクロックを 8 回進めるためにに 1 バイト送信しているのです。データ内容は関係ありません。これにより、チップが正常にデータを送信できるようにするためなのです。チップ セレクトを解放して、読み込みを終了させます。そうして、EEPROM に次の命令を送信できます。

EEPROM への書き出しには、INSTWREN (Write Enable) 命令をまず送信します。つづいて、WRITE命令、書き出しアドレス、書き出しデータの順に送信します。最後に、INSTWRDI (Write Disable) 命令を送信し、書き出しを完了させます。INSTWREN 命令を送信後、チップ セレクトを解放し、EEPROM が命令を実行するのを待ちます。チップ セレクト線を LOW にしたままにすると、EEPROM は WRITE コマンドを無視します。

#include <SPI.h>
const byte INSTREAD  = B0000011;
const byte INSTWRITE = B0000010;
const byte INSTWREN  = B0000110;
const byte INSTWRDI  = B0000100;
const int chipSelectPin = 10;
byte loopcount = 1;
void setup() 
{
  Serial.begin(9600);
  // start the SPI library:
  SPI.begin();
  SPI.setBitOrder( MSBFIRST );
  pinMode(chipSelectPin, OUTPUT);
}
void loop()
{
  byte data = 0;
  digitalWrite(chipSelectPin, LOW);
  // Read the byte at address 10
  SPI.transfer(INSTREAD); 
  SPI.transfer(0); 
  SPI.transfer(10);
  data = SPI.transfer(0); // <- clock SPI 8 bits
  digitalWrite(chipSelectPin, HIGH);
  // show the user what we have  
  Serial.print("read data:");
  Serial.print( data, BIN );
  Serial.print("\n");
  
  // Set write enable, write a byte with the current loop 
  // and disable write again
  digitalWrite(chipSelectPin, LOW);  
  SPI.transfer(INSTWREN);
  digitalWrite(chipSelectPin, HIGH);
  digitalWrite(chipSelectPin, LOW);  
  SPI.transfer(INSTWRITE);
  SPI.transfer(0);
  SPI.transfer(10);
  SPI.transfer(loopcount);
  digitalWrite(chipSelectPin, HIGH);
  digitalWrite(chipSelectPin, LOW);  
  SPI.transfer(INSTWRDI);
  digitalWrite(chipSelectPin, HIGH);
  
  loopcount++;
  delay(5000);
}

各 READ / WRITE 命令で、複数データの読み書きができます。たとえば、クロックを 8 進めることにより、新たな READ 命令を出さずにアドレス 11 のデータを読むことができます。他にも機能はありますが、本稿の目的にとしては、SPI 経由でのデータ読み書きができることを確認することで充分です。


BeagleBone Black と Linux

BeagleBone Black には、SPI0 と SPI1 という 2 つの SPI があります。SPI0 の 4 ピンは、P9 ヘッダーの中央下部にあり、下図のように接続されています。橙色線はクロック、青線はチップ セレクト線です。MOSI と MISO の代わりに、d0、d1 線があります。これらは、 Beaglebone から見た入出力方向をソフトウェア的に自由に設定できます。P8、P9 ヘッダー ピンの詳細については、システムリファレンスマニュアル(SRM) を見てください。ページ 84 に表が掲載されています (ドキュメントが変わっている可能性があるので、「Expansion Header P9 Pinout」で探したほうが良いかもしれません)。P9 ヘッダーの Multiplexing については、こちらのを参照してください。

bbb eeprom diagram

BeagleBone の SPI からアクセス可能なチップの一部は、Linux カーネルのデバイス ドライバが対応しています。例えば、SPI 上のリアルタイム クロックは /dev/rtc として使用できます。また、spidev デバイスドライバを使用して SPI を直接扱うこともできます。このドライバは、/dev/spidev1.0 というデバイスを提供しています。このデバイスに対して Open(2)、Read(2)、Write(2) システム コールを発行し、SPI を通してデータの読み書きをします。カーネルは自動的にチップ セレクト線を制御し、クロックを制御してspidev にセットしたデータを送信します。

EEPROMは若干複雑です。write(2) でデバイス ファイルに書き出すと、チップ セレクトがセットされ、データが書かれ、そしてチップ セレクトがセット解除されます。ここで、EEPROM から読み出すためには、EEPROM から読み込む間、SPI を通してチップ セレクトを保持する必要があります。チップ セレクトをダウンすると、EEPROM は読み込み操作が終了したものと認識して、処理を終えてしまいます。必要な時間だけチップ セレクトを保持するためには、SPI へのアクセスを続けることが必要です。

このために、ioctl(2) システム コールの SPI_IOC_MESSAGE インタフェースを使います。このインタフェースを使うことにより、送受信の同時実行、連続送受信中に SPI タイミングとチップ選択解除の挿入ができます。spidev_fdx.c に SPI を使ったデータの送受信例があります。spidev_fdx.c は半二重送受信の例ですが、この例で基本的な ioctl の使用方法が理解できます。spi_ioc_transfer 構造体の cs_change メンバーを使い、送受信操作中にチップ選択を行うことができます。

BeagleBone Black は、SPI0 と SPI1 の 2 つの SPI を持っていますが、両方とも複数チップ選択が可能です。SPI1 は HDMI インタフェースと共用しているため、SPI を 2 つ使用するときは HDMI を停止する必要があります。ヘッダー・ピンは複数用途を持っているため、カーネルを通して用途を設定します。これには device tree オーバーレイを用います。device tree を使い、あるピンが同時に 2 つの用途に使用されることを防ぎます。

cape manager を使い、/sys/devices/bone_capemgr.*/slots ファイルにオーバーレイ名を設定し、オーバーレイをロードします。cape manager は /lib/firmware ディレクトリを調べて、使用ピンと用途を記述している dbto ファイルを探します。このファイルを自分で作成することもできます。ピンとその用途を記述した可読形式のソース ファイルをコンパイルします。また、ブート時にロードするオーバーレイを指定可能です。これを指定すれば、それ以降オーバー レイファイルをロードする必要がなくなります。

eLinux サイトに BeagleBone Black 用の SPI0、SPI1 のd1を出力モードに設定するファイルが用意されています。このサイトの記述どおりにして、これらのオーバーレイを有効にします。また、spidev ファイルの所有者を変更し、root にならずに作業できるようにしました。Cloud9 GNOME イメージ 2013.05.27 を使い、uEnv.txt optargs を使った自動ブートは、うまく動きませんでした。ソフトウェアを更新すると、うまく動くようになりました。

root@beaglebone:~# dtc -O dtb -o BB-SPI0-01-00A0.dtbo -b 0 -@ BB-SPI0-01-00A0.dts
root@beaglebone:~# cp BB-SPI0-01-00A0.dtbo /lib/firmware/
root@beaglebone:~# echo BB-SPI0-01 > /sys/devices/bone_capemgr.*/slots
root@beaglebone:~# ls -lh /dev/spi*
crw------- 1 root root 153, 0 Oct 16 04:12 /dev/spidev1.0
root@beaglebone:~# chown ben /dev/spi*

udev が spidev ファイルを作成するときには常に所有者を root ではなく自ユーザにするよう、下記のように設定します。udevadm は手軽なコマンドで、設定済みの udev ルール ファイルから、デバイス作成時に適用されるルールとデバイス選択時に実行されるタスクを表示します。

# udevadm test /sys/devices/ocp.2/48030000.spi/spi_master/spi1/spi1.0/spidev/spidev1.0
...
SUBSYSTEM=spidev
...
# cat /etc/udev/rules.d/99-spidev.rules
SUBSYSTEM=="spidev", OWNER="ben"


SPI を利用した EEPROM 制御

ここまでで /dev/spidev1.0 ファイルが準備できたので、次に BeagleBone Black から SPI を使った EEPROM との通信作業に移ります。

Arduino で実現したことと同じ機能を実行する C++ プログラムのコアを以下に掲載します。固定アドレスからの読み込み、内容表示、回数の書き出しという処理を繰り返します。

int main( int argc, char** argv )
{
  EEPROMTest obj( argv[1] );
  address_t addr(305);
  for( uint8_t loopcount = 0; ; loopcount++ )
  {
    int d = (int)obj.read( addr );
    cerr << "MAIN: read data:"  << hex << d << endl;
    cerr << "MAIN: write data:" << hex << (int)loopcount << endl;
    obj.write( addr, loopcount );
    sleep( 1 );
  }
  return 0;
}

EEPROMTest クラスの重要なメソッドは、read()、wirte() メソッドです。EEPROMTest::write() メソッドを以下に掲載します。Aruduino プログラムと同様、書き出し可能にし、次に WRITE 命令、アドレス、書き出しデータの順に送信します。次に、書き出しを無効にします。tostr() メソッドは、spi_write() 関数が扱えるように命令を文字列に変換します。コードブロックの最後にある spi_write() は、std::string を指定ファイル ディスクリプタに書き出す write() 関数のラッパーです。

void write( address_t addr, uint8_t b )
{
  spi_write( m_fd, tostr(INSTWREN), true );
  {
    stringstream ss;
    ss << INSTWRITE << addr << flush;
    ss.write( (const char*)&b, sizeof(uint8_t));
    spi_write( m_fd, ss.str(), true );
  }
  spi_write( m_fd, tostr(INSTWRDI), true );
}
std::string tostr( instruction_t v )
{
  std::stringstream ss;
  ss << v;
  return ss.str();
}
static void spi_write( int fd, std::string v )
{
 write( fd, v.data(), v.size() );
}

write() 関数が呼ばれると、チップを選択し、バス上にデータを置き、チップ選択を解放します。EEPROM にデータを送るだけであれば、これで終わりです。しかし、EEPROM からデータを読み込むときは、READ 命令とアドレスを送信し、読み込む間、チップ セレクトを保持しなければなりません。EEPROMTest::read() メソッドは、spi_transfer() 関数を使用して、SPI を通して同時読み書きを実行します。つまり、4 バイト送信、8 ビット読込み、16 ビット アドレス、次に 8 ビットの文字 "d" を送信するわけです。実際には EEPROM は最後の "d" を無視します。8 ビットの文字 "d" を送信することにより、SPI バスは指定アドレスの内容 8 ビットを戻します。つまり、spi_transfer() 関数は、EEPROM の指定アドレスのデータを配列 data[3] に入れて戻します。

uint8_t read( address_t addr )
{
  uint8_t d = 77;
  stringstream ss;
  ss << INSTREAD << addr;
  int prefixlength = ss.str().length();
  ss.write( (const char*)&d, sizeof(uint8_t));
  std::string data = spi_transfer( m_fd, ss.str() );
  data = data.substr(prefixlength);
  return data[0];
}

spi_transfer() 関数は、送信データ文字列を引数として指定し、データ送信中に SPI を通して BeagleBone Black に送り返された文字列を戻します。受信文字列と送信文字列の長さは同じになります。そのため、最初の 24 ビットは意味がありません。しかし、24 ビット (READ 命令と 16 ビット アドレス) 送信後の1 バイトは、EEPROM が送り返した指定アドレスの内容です。同じサイズの送信バッファ (tx_buf) と受信バッファ (rx_buf) を用意して、ioctl() 関数を実行し、SPI を通した同時読み書きを実行します。

static std::string spi_transfer( int fd, std::string v )
{
  struct spi_ioc_transfer xfer[2];
  int status;
  memset(xfer, 0, sizeof xfer);
  std::string ret;
  ret.resize( v.size() );
  
  xfer[0].tx_buf = (unsigned long)v.data();
  xfer[0].rx_buf = (unsigned long)ret.data();
  xfer[0].len = v.size();
  xfer[0].delay_usecs = 200;
  status = ioctl(fd, SPI_IOC_MESSAGE(1), xfer);
  if (status < 0) {
     perror("SPI_IOC_MESSAGE");
     return "";
  }
  return ret;
}

プログラムの出力を以下に出します。この例では、以前 spibbb を走らせたときに 5 回繰り返したため、5 と表示されています。spidev_fdx.c の例にあった dumpstat() 関数を少し手直しした関数を使って、指定 spidev デバイスに対応した SPI の設定状態を表示しています。

$ ./spibbb /dev/spidev1.0 
opened file /dev/spidev1.0
before applying settings: spi mode 0, 8 bits (msb) per word, 500000 Hz max
after applying settings: spi mode 0, 8 bits (msb) per word, 500000 Hz max
MAIN: read data:5
MAIN: write data:0
MAIN: read data:0
MAIN: write data:1
MAIN: read data:1
MAIN: write data:2

EEPROMTest コンストラクタは、現在の SPI 設定の表示、データ送信のための設定、SPI に設定可能なクロック スピードの設定、EEPROM が使用する SPI モードの設定を行います。

dumpstat("before applying settings", m_fd );
uint8_t lsb = 0;
ioctl( m_fd, SPI_IOC_WR_LSB_FIRST, &lsb );
uint32_t speed = 500000;
ioctl( m_fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed );
uint32_t mode = SPI_MODE_0;
ioctl( m_fd, SPI_IOC_WR_MODE, &mode );
dumpstat("after applying settings", m_fd );

上記のようにして、命令とアドレスが std::iostreams に書き出されます。EEPROM が期待する 8 ビットと 16 ビット形式ではありませんが、instruction_t と address_t に対する出力演算子をオーバーローディングすることで、チップが期待しているデータ構造で書き出されます。

enum instruction_t
{
    INSTREAD  = 0b00000011,
    INSTWRITE = 0b00000010,
    INSTWREN  = 0b00000110,
    INSTWRDI  = 0b00000100,
    INSTRDSR  = 0b00000101
};
class address_t
{
public:
    uint16_t m_v;
    address_t( int v = 0 )
        : m_v(v)
    {
    }
};
std::ostream& operator<<( std::ostream& os, const instruction_t& v )
{
    os.write( (const char*)&v, sizeof(uint8_t));
    return os;
}
std::ostream& operator<<( std::ostream& os, const address_t& v )
{
    os.write( (const char*)&v.m_v, sizeof(uint16_t));
    return os;
}

次回は、SPI を使った 3 軸加速度計と ATMega328 を使った 7 セグメントのシリアル ディスプレイの制御をしたいと考えています。加速度計は、Beagle からモニターや制御が可能な割り込みピンを備えています。

以前、書いた文献 (訳注、近日翻訳予定)

Getting Started With the BeagleBone Black: A 1GHz ARM Linux Machine for $45
(日本語版:BeagleBone Black 事始め: 45 ドルで買える 1GHz の ARM Linux マシン)

BeagleBone Black Part 2: Linux Performance Tests
(日本語版: BeagleBone Black パート2 : Linux 性能テスト)

Linux Foundationメンバーシップ

30人のカーネル開発者

人気コンテンツ


Linux Foundationについて

Linux Foundation はLinux の普及,保護,標準化を進めるためにオープンソース コミュニティに資源とサービスを提供しています

 

The Linux Foundation Japan

サイトマップ

問い合わせ先

サイトに関するお問い合わせはこちらまで

Linux Foundation Japan

Linux Foundation

Linux Training

提案、要望

Linux.com JAPANでは広く皆様の提案、要望、投稿を受け付ける予定です。

乞うご期待!