ZYNQのSPIをLinuxから使う方法
ZYNQのPSにはSPIのポートが出ています。このSPIを使えば周辺ICを簡単にコントロールできるでしょう。
しかし、公式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を有効にします。
これでビルドしたら、次に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 というデバイスがあることがわかります。
このドライバを開くには、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の中でどのような波形が出ているのかをキャプチャするロジックを作り、
その中でMOSIとMISOを接続してループバックて、実行してみた結果は、
となりました。256バイトの送信でも問題なくできているようです。
試しに、送受信の長さを4バイト程度にして、中の波形をMITOUJTAGを使ってみてみると、
となりました。
わかることは、
- 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でよいと思います。
SCLKやSSに3ステート・バッファを使うと、転送が行われていない間はハイ・インピーダンスになってしまうので、特にこだわりがなければ、そのまま出力したほうがよいでしょう。
MISOは、ZYNQがマスターとなる場合は入力ですが、MISO_TがLになる期間があるので、これも3ステート・バッファを使わずに単純な入力ポートとしたほうがよいでしょう。
![]()
これで、SPIの単純な操作ならできるのですが、様々なオプションに効果があるのかとか、3線式SPIへの対応可能性、spidev32766という番号の変え方などを、デバイスドライバのソースコードを読みつつ探っていくことにします。
(続く)
| 固定リンク









コメント