LogiClover開発ブログ

LogiCloverは、趣味でFPGAを使った電子工作をしているサークルです。主に開発中の出来事や技術メモを投稿します

CP2130でC#からUSB-SPIを実現する

FPGAマイコンでアプリケーションを作成しているときに悩ましい項目としてPCとの通信方法があります。

低速なものであればFT232に代表されるようなUSB-UARTでいいのですが、UARTはRTLと相性がよくありません。

項目 利点 欠点
USB-UART 手軽 遅い
USB-I2C I2C I/Fのデバイスを直接制御できる、マルチスレーブ対応 PC側でアービトレーションを実装する必要があり
USB-SPI SPI I/Fのデバイスを直接制御できる、ある程度速く出来る、実装が簡単

FPGAで実装するにはSPIを使うとかなり簡単にアクセスできそうです。今回はSilicon Laboratory社が販売しているUSB-SPI Bridge IC CP2130を使ってみます。このICは最大12MHzでUSB-SPIを実現することが出来ます。

jp.silabs.com

余談ですが、もっと高速なUSB通信を行う場合Cypress社のEZ-USB FXに代表されるような製品が使われます。

http://japan.cypress.com/products/ez-usb-fx3-superspeed-usb-30-peripheral-controller

評価ツールをダウンロード

評価ボードのダウンロード先がどういうわけかCP210xの別製品のものになっているので*1、サポートページからダウンロードします。

CP2130 Classic USB Bridge | Silicon Labs

上記サイト下側にある「CP2130 Software package for Windows」をダウンロードしてインストールします。Linuxの場合はfor Linuxを使用します。

サンプルアプリケーションを眺める

以後Windowsで解説します。インストールが完了すると “C:\SiliconLabs\MCU\CP2130_SDK\Software"にライブラリとサンプルアプリケーションが追加されています。

CP2130 Demo ApplicationがC#+WPFで記述されたソースコード付きのサンプルになっています。

細かな制御の方法や作例はこちらを眺めると良いでしょう。

動作させてみる

サンプルアプリケーションを読んでみると、SLAB_USB_SPI.csがDllImport経由でSLAB_USB_SPI.dllの関数を呼び出していることがわかります。

このままでは扱いづらいので転送だけ出来るWrapperを作成します。

CSをGPIO経由で手動で上げ下げすることも考慮してIsCSEnableとGPIO制御もつけておきます。

using SLAB_USB_SPI_DLL;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LogiClover.Lib {
    class CP2130SPI : IDisposable {
        private IntPtr deviceHandle = IntPtr.Zero;
        public bool IsAvailable => deviceHandle != IntPtr.Zero;

        #region Static Method
        /// <summary>
        /// 現在接続されているデバイス数を返します
        /// </summary>
        /// <returns></returns>
        public static int GetNumDevices() {
            uint n = 0;
            SLAB_USB_SPI.CP213x_GetNumDevices(ref n);
            return (int)n;
        }
        /// <summary>
        /// デバイス名のリストを返します
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<string> GetDeviceNames() {
            var n = GetNumDevices();
            for (uint i = 0; i < n; ++i) {
                var sb = new StringBuilder(SLAB_USB_SPI.MAX_PATH);
                SLAB_USB_SPI.CP213x_GetDevicePath(i, sb);
                yield return sb.ToString();
            }
        }
        #endregion

        #region Device Connection

        /// <summary>
        /// デバイスと接続します
        /// </summary>
        /// <param name="index"></param>
        /// <returns>正常に接続できた場合true</returns>
        public bool Open(int index) {
            if (index < 0) return false;
            this.Close();
            var result = SLAB_USB_SPI.CP213x_OpenByIndex((uint)index, ref deviceHandle);
            if (result == SLAB_USB_SPI.USB_SPI_ERRCODE_SUCCESS) {
                return true;
            } else {
                Debug.WriteLine($"#Exception Errorcode:{result}");
                Close();
                return false;
            }
        }

        /// <summary>
        /// デバイスを切断します
        /// </summary>
        public void Close() {
            if (IsAvailable) {
                SLAB_USB_SPI.CP213x_Close(deviceHandle);
                deviceHandle = IntPtr.Zero;
            }
        }

        #endregion

        #region Parameters
        public enum ClockRate {
            F12M = SLAB_USB_SPI.SPICTL_CLKRATE_12M,
            F6M = SLAB_USB_SPI.SPICTL_CLKRATE_6M,
            F3M = SLAB_USB_SPI.SPICTL_CLKRATE_3M,
            F1M5 = SLAB_USB_SPI.SPICTL_CLKRATE_1M5,
            F750K = SLAB_USB_SPI.SPICTL_CLKRATE_750K,
            F375K = SLAB_USB_SPI.SPICTL_CLKRATE_375K,
            F187K5 = SLAB_USB_SPI.SPICTL_CLKRATE_187K5,
            F93K75 = SLAB_USB_SPI.SPICTL_CLKRATE_93K75,
        };
        /// <summary>
        /// 転送周波数
        /// </summary>
        public ClockRate SCKRate { get; set; } = ClockRate.F12M;
        /// <summary>
        /// CS番号
        /// </summary>
        public int CSIndex { get; set; } = 0;
        /// <summary>
        /// CSを駆動するか
        /// </summary>
        public bool IsCSEnable { get; set; } = true;

        /// <summary>
        /// タイムアウト時間[ms]
        /// </summary>
        public uint TimeOutMS { get; set; } = 1000;

        #endregion

        public bool Configuration() {
            if (!IsAvailable) return false;
            if (IsCSEnable) {
                SLAB_USB_SPI.CP213x_SetChipSelect(deviceHandle, (byte)CSIndex, SLAB_USB_SPI.CSMODE_ACTIVE_OTHERS_IDLE);
            } else {
                //CS無効化
                SLAB_USB_SPI.CP213x_SetChipSelect(deviceHandle, (byte)CSIndex, SLAB_USB_SPI.CSMODE_IDLE);
            }

            return true;
        }

        /// <summary>
        /// SPI通信を行います
        /// </summary>
        /// <param name="srcData">転送したいデータ、受信だけ行う場合はダミーデータを渡す</param>
        /// <returns>受信したデータ、データ長はsrcDataと同じ</returns>
        public byte[] Transfer(params byte[] srcData) {
            if (!IsAvailable) return new byte[] { };
            uint bytesTransferred = 0;
            var dstData = new byte[srcData.Length];

            SLAB_USB_SPI.CP213x_TransferWriteRead(
                deviceHandle,
                srcData,
                dstData,
                (uint)srcData.Length,
                true,
                TimeOutMS,
                ref bytesTransferred);

            return dstData;
        }

        /// <summary>
        /// GPIOに値を書き込みます
        /// </summary>
        /// <param name="index">制御するGPIO番号</param>
        /// <param name="value"></param>
        public void WriteIO(int index, bool value) {
            if (IsAvailable) {
                SLAB_USB_SPI.CP213x_SetGpioModeAndLevel(deviceHandle, (byte)index, SLAB_USB_SPI.GPIO_MODE_OUTPUT_PP, (byte)(value ? 0x1 : 0x0));
            }
        }
        /// <summary>
        /// GPIOの値を読み込みます
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public bool? ReadIO(int index) {
            if (!IsAvailable) return null;
            byte mode = 0;
            byte level = 0;

            SLAB_USB_SPI.CP213x_GetGpioModeAndLevel(deviceHandle, (byte)index, ref mode, ref level);
            return level != 0x0;
        }

        public void Dispose() {
            this.Close();
        }
    }
}


あとはこのクラスの利用例ですが

ユーザーにデバイスを選ばせる

コンボボックスなどで表示しておくとわかりやすいかもしれません。

var numofDevice = CP2130SPI.GetNumDevices();//デバイス数を取得
var deviceNames = CP2130SPI.GetDeviceNames();//デバイス名一覧を取得

バイスと通信を行う

開いて保持したままにしてもいいのですが、常時SPIを送り続けるアプリケーションではないので今回はお行儀良く送ったらCloseする制御にします。

//送る
int index = 0;//通信に使うデバイスインデックス
using (var spiBridge = new CP2130SPI()) {
    try {
        if (!spiBridge.Open(index)) {
            //デバイスオープンに失敗
            return;
        }
    } catch (Exception ex) {
        //ほかの例外
        return;
    }

    //SPIの設定
    spiBridge.CSIndex = 0x0;//CSとして駆動させるピン番号
    spiBridge.IsCSEnable = true;//CSは転送時に自動駆動させる
    spiBridge.SCKRate = CP2130SPI.ClockRate.F375K;//転送周波数
    spiBridge.TimeOutMS = 1000;//タイムアウト時間
    spiBridge.Configuration();

    //適当なデータを作る
    var sendData = Enumerable.Range(0, 0xff).ToArray();
    //データ転送
    var recvData = spiBridge.Transfer(sendData);//byte[]が返ってくる
}

動作させる

SLAB_USB_SPI.dllが実行ディレクトリに無いとDllImportに失敗するのでプロジェクトに入れて、プロパティを出力ディレクトリに新しい場合はコピーするようにします。

スクリーンショットはComet*2に使われているエフェクトの検証ツールで使用している例です。

f:id:logiclover:20170612013548p:plain

終わりに

これでc#から直接SPIデバイスを制御できるようになりました!最高ですね!

SPI FlashやSPI DAC/ADCなどを繋いでも良いですし、FPGAの自作ペリフェラル検証に使用してもいいと思います。