« 新しい事務所 | トップページ | ZYNQのSPIをLinuxから使うspidevの解析 »

2018.12.16

ZYNQのSPIをLinuxから使う方法

ZYNQのPSにはSPIのポートが出ています。このSPIを使えば周辺ICを簡単にコントロールできるでしょう。

Spidev0

しかし、公式Wikiで紹介されているSPIの使用方法

https://xilinx-wiki.atlassian.net/wiki/spaces/A/pages/18842255/Linux+SPI+Driver

では、標準的なSPI Flash ROMやEEPROMを接続する場合しか書かれていません。

私はDAコンバータの内蔵レジスタを操作したかったので、より低レベルな操作ができるドライバを必要としていました。

LinuxでSPIを使うためにspidevというドライバが用意されています。概要は、「LinuxでSPI (spidev)を使う」のページで紹介されているのですが、open("/dev/spidev0.0", O_RDWR)で開いた後、ioctlで操作するという素敵なものです。

これは、ユーザモードドライバといって、デバイスに対する具体的な操作をカーネルモードでやらずに、ユーザモードのプログラムで組むことができるものです。これによって誰でもプログラムを作ることができるようになり、開発とテストが飛躍的に容易になります。

このspidevをZYNQで使いたいので、普通のLinuxカーネルではspidevは入っていないので、カーネルを再構築しなければなりません。

Linuxのコンフィグは、cat /proc/config.gz | ungip -c で取得することができるので、得られたconfigというファイルを.configにリネームしてLinuxのカーネルをビルドできる環境に持っていけば、現在のLinuxカーネルのその他の状態は変えずに新たなドライバを組み込みこんだカーネルを作れる。

spidevを有効にするには、Linuxのコンフィグで、

CONFIG_SPI_SPIDEV=y

にするか、menuconfigで、Device Drivers => SPI supportの中にあるUser mode SPI device driver supportを有効にします。

Spidev1

これでビルドしたら、次にdevice treeにSPIの下のspidevを有効にするように記述を追加します。

spi@e0006000 {
    compatible = "xlnx,zynq-spi-r1p6";
    reg = <0xe0006000 0x1000>;
    status = "okay";
    interrupt-parent = <0x4>;
    interrupts = <0x0 0x1a 0x4>;
    clocks = <0x1 0x19 0x1 0x22>;
    clock-names = "ref_clk", "pclk";
    num-cs = <3>;
    is-decoded-cs = <0>;
    #address-cells = <0x1>;
    #size-cells = <0x0>;
    spidev@0x00 {
        compatible = "spidev";
        spi-max-frequency = <1000000>;
        reg = <0>;
    };
};

これでspidevが有効になります。

dmesgで見てみると、起動時に認識されていることがわかります。

[    0.763436] spidev spi32766.0: buggy DT: spidev listed directly in DT

実際にspiのドライバがどう認識されてどんなデバイスが作られたかを見てみると、/sys/bus/spi/devices/spi32766.0 というバスが認識されていて、/dev/spidev32766.0 というデバイスがあることがわかります。

Spidev2

このドライバを開くには、open("/dev/spidev32766.0",O_RDWR)とすればよいことになります。

作ったプログラムの全体を載せます。

#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <linux/spi/spidev.h>
 
#define SPI_SPEED_HZ    500000
#define SPI_DELAY_USECS 1
#define SPI_BITS        8
 
#define DEVICE_NAME "/dev/spidev32766.0"
 
int main (int argc, char *argv[]) {
    if(argc < 2) {
        printf("usage:spitest length");
        return 0;
    }
    int trsize = atoi(argv[1]);
 
    int fd = open(DEVICE_NAME, O_RDWR);
 
    uint8_t *tx = new unsigned char[trsize];
    uint8_t *rx = new unsigned char[trsize];
    struct spi_ioc_transfer tr[1];
 
    tr[0].tx_buf        = (__u64)tx;
    tr[0].rx_buf        = (__u64)rx;
    tr[0].len           = trsize;
    tr[0].delay_usecs   = SPI_DELAY_USECS;
    tr[0].speed_hz      = SPI_SPEED_HZ;
    tr[0].bits_per_word = 8;
    tr[0].cs_change     = 0;
 
    for(int i=0;i<trsize;i++) {
        tx[i] = i;
    }
    tx[0] = 0x80;
    tx[1] = 0x02;
    tx[2] = 0x03;
    tx[3] = 0x01;
    memset(rx,0 , trsize);
    uint8_t mode = SPI_MODE_0; // 効果あり
    ioctl(fd, SPI_IOC_WR_MODE, &mode);
 
    uint8_t wlen = 32; // 効果なし
    ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &wlen);
 
//  uint8_t lsbfirst = 1; // 変更不可
//  ioctl(fd, SPI_IOC_WR_LSB_FIRST, &lsbfirst);
    printf("TX:");
    for(int i=0;i<trsize;i++) {
        printf("%02X ",tx[i]);
    }
    printf("\n");
//  write(fd , tx, 2); // 動作はする
//  read(fd , rx, 3); // 動作はする
    printf("result=%d bytes\n",ioctl(fd, SPI_IOC_MESSAGE(1), tr));
    printf("RX:");
    for(int i=0;i<trsize;i++) {
        printf("%02X ",rx[i]);
    }
    printf("\n");
   
    delete[] tx;
    delete[] rx;
    return 0;
}

このプログラムを、g++ spitest.cpp -o spitest でコンパイルして、

# ./spitest 長さ

で実行できます。

ZYNQの中でどのような波形が出ているのかをキャプチャするロジックを作り、

Spidev5

その中でMOSIとMISOを接続してループバックて、実行してみた結果は、

Spidev3

となりました。256バイトの送信でも問題なくできているようです。

試しに、送受信の長さを4バイト程度にして、中の波形をMITOUJTAGを使ってみてみると、

Spidev4

となりました。

わかることは、

  • CSはSS0が使われる
  • MOSIからはMSBファーストでデータが出てくる
  • 1バイト(あるいは指定長)ごとにCSを上げることはない
  • 転送が行われている間、MOSI_TはL(出力許可)になる
  • MISO_TはH(出力禁止)だが、転送が行われた後、短い時間Lになる
  • MISOは挙動不審
  • SS_Tや、SCLK_Tは転送が行われている間、Lになる

というものでした。

なお、tr[0].cs_change = 0;を1にすると、1つのメッセージを送信するたびにCSを上げ下げするようになります。しかし、最後のメッセージでは0にしておかないとCSが下がりっぱなしになってしまうようです。

また、tr[0].delay_usecsは、SPIへの送信が終わったあと、CSを上げるまでのディレイ時間を設定するものです。通常は0でよいと思います。

ZYNQのSPI0の信号を外に出すには、下の図のように単純に出せばよく、

Spidev6

SCLKやSSに3ステート・バッファを使うと、転送が行われていない間はハイ・インピーダンスになってしまうので、特にこだわりがなければ、そのまま出力したほうがよいでしょう。

MISOは、ZYNQがマスターとなる場合は入力ですが、MISO_TがLになる期間があるので、これも3ステート・バッファを使わずに単純な入力ポートとしたほうがよいでしょう。

これで、SPIの単純な操作ならできるのですが、様々なオプションに効果があるのかとか、3線式SPIへの対応可能性、spidev32766という番号の変え方などを、デバイスドライバのソースコードを読みつつ探っていくことにします。

(続く)

|

« 新しい事務所 | トップページ | ZYNQのSPIをLinuxから使うspidevの解析 »

コメント

コメントを書く



(ウェブ上には掲載しません)




« 新しい事務所 | トップページ | ZYNQのSPIをLinuxから使うspidevの解析 »