GPIOだけでSPI信号を送信してみる

前回ではWindows IoT Coreが用意してくれているSPIをやりとりするAPIを使ったプログラミングを紹介しました。電子パーツのお店で売っているLCDはI2CやSPI規格に則ったものが2,3千円くらいで売られています(表示できる内容や規模にもよります)が、ワゴンに無造作に入れられたような数百円で買える激安LCDは独自のシリアル通信を使っていたりします。今回はそういった信号のための学習として、GPIOだけによるSPI通信を試してみました。

配線図は前回とほとんど同じですが、Raspberry Pi側のピンをすべてSPIに対応していない箇所に移しています。データは書き込みのみで、読み取りは行わないので、この線は省略しています。


まずはどのようにしてSPI通信が行われているかを知る必要があります。MCP23S08のデータシートやウィキペディア英語版によると、CS信号を流していない間に、総ビット分のオン・オフをSCKで持続的におこない、SCKがオン(またはオフ)になるタイミングでSI(SO)に信号があるかないかで、シリアルデータが決定されるようです。

ここでWindows特有の問題が発生します。Windows IoT CoreはUniversal Windows Appベースのため、待機状態にするには精度の低い「await Task.Delay()」しかプログラム命令がない上に、Arduinoでのdelay、Linuxでのusleepのようにミリ秒未満を指定することができません(追記:先日発売されたトランジスタ技術2016年11月号の記事によると、Raspberry Pi自体がマイクロ秒のカウントに対応していないようです)。

こちらのブログによると、マイクロ秒単位での待機には、精度の高いStopwatchクラスのカウンターが一定数に達するまで無限ループでぶん回すという力技しか今のところ方法は無いようです。

以上の点を踏まえたプログラムコードがこちらです。つないだ各種ピンをGPIOとして宣言したうえで、信号を送るときはCSピンをオフにし、入力ピンのスイッチを切り替え、パルス信号を送信する手順を行っています。

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.ApplicationModel.Background;
using Windows.Devices.Gpio;

namespace SpiGpio
{
    public sealed class StartupTask : IBackgroundTask
    {
        private BackgroundTaskDeferral deferral;

        private const int GPIO_CS_PIN = 2;  // External chip select input 
        private const int GPIO_SI_PIN = 3;  // Serial data input
        private const int GPIO_CSK_PIN = 4; // Serial clock input
        private const int GPIO_SO_PIN = 17; // Serial data output
        private GpioPin cskpin, sipin, sopin, cspin;

        //Refer from : http://uepon.hatenadiary.com/entry/2016/08/19/234838
        private Stopwatch sw = new Stopwatch();
        private const long WAIT_SEC = 10;
        private void msDelay(long microSeconds)
        {
            long ticks = microSeconds * Stopwatch.Frequency / 1000000L;
            sw.Restart();
            while (sw.ElapsedTicks < ticks) ;
        }

        private GpioPin OpenPin(int pinno, bool input, bool high = false)
        {
            var pin = GpioController.GetDefault().OpenPin(pinno);
            if (input == false) {
                pin.SetDriveMode(GpioPinDriveMode.Output);
                pin.Write((high == true) ? GpioPinValue.High : GpioPinValue.Low);
            } else {
                pin.SetDriveMode(GpioPinDriveMode.Input);
            }
            return pin;
        }

        private void SendByte(byte bits)
        {
            for (int i = 0; i < 8; i++) {
                var pv = (((bits >> (7 - i)) & 0x1) != 0) ? GpioPinValue.High : GpioPinValue.Low;
                sipin.Write(pv);
                msDelay(WAIT_SEC / 2);
                cskpin.Write(GpioPinValue.High);
                msDelay(WAIT_SEC);
                cskpin.Write(GpioPinValue.Low);
                msDelay(WAIT_SEC / 2);
            }
        }

        private void SendCommand(params byte[] bytes)
        {
            cspin.Write(GpioPinValue.Low);
            msDelay(WAIT_SEC);

            foreach(byte b in bytes) {
                SendByte(b);
            }

            msDelay(WAIT_SEC);
            cspin.Write(GpioPinValue.High);
        }

        private async void Start()
        {
            cskpin = OpenPin(GPIO_CSK_PIN, false);
            sipin = OpenPin(GPIO_SI_PIN, false);
            sopin = OpenPin(GPIO_SO_PIN, true);
            cspin = OpenPin(GPIO_CS_PIN, false, true);

            SendCommand(0x40, 0x00, 0x00);

            Random r = new Random();
            while (true) {
                byte bits = 0x0;
                for(int i = 0; i < 4; i++) {
                    if (r.Next(0, 2) == 1) bits |= (byte)(1 << i);
                }
                SendCommand(0x40, 0x09, bits);

                await Task.Delay(500);
            }
        }

        public void Run(IBackgroundTaskInstance taskInstance)
        {
            deferral = taskInstance.GetDeferral();

            Start();
        }
    }
}

このプログラムで実行される内容は前回のLチカと全く同じなので、実行例は写真を掲載するだけにとどめておきます。

, | 2016年10月7日