Quantcast
Channel: Todotaniのはやり物Log
Viewing all 48 articles
Browse latest View live

Windows 10 IoT Core Build 10586を使ってみる

$
0
0

最近Raspberry Pi用のWindows 10 IoT Coreを触っています。当初はBuild 10556を使っていましたが、10586が最近リリースされました。Build 10586になってインストールの方法やデバック時の設定が変わっているため、以下の通り整理してみます。

Windows 10 IoT Coreのインストール

Windows 10が動くPCを用意します。まず、リンクのサイトにアクセスし、「Get the Windows 10 IoT Core Dashboard」をクリクしてWindows 10 IoT Core DashboardをPCにインストールします。

SetupWindowsIoT_Device.PNG

Build 10556ではISOファイルをダウンロードしてインストールを行いましたが、Build 10586ではIoT Core DashbordがイメージのダウンロードとSDカードへの書き込みを行ってくれます。Windows 10 IoT Coreのイメージを書き込むSDカードをPCにセットしてWindows 10 IoT Core Dashboardを起動すると以下の画面が表示されますので、デバイスの種類(Raspberry Pi 2)とSDカードのドライブを指定して「ダウンロードとインストール」ボタンを押します。

Windows10_IoT_Install.PNG

SDカードへの書き込みが始まると、以下の画面が表示されます。

Windows10_IoT_ImageFlush.PNG

イメージの書き込みが完了したら、SDカードをRaspberry Piにセットして起動します。Raspberry PiにはディスプレーとEthernetケーブルをつないでおきます。

初回の起動には結構時間がかかるので気長に待ちます。Build 10556では起動中に画面がブラックアウトした時間が長く、固まってしまったのかと思ったことがありましたが、Build 10586は画面がブラックアウトすることはなく処理中の表示が回っているので改善が見られます。

Windows 10 IoT Coreが立ち上がったら、ディスプレーにDHCPで割り当てられたIPアドレスが表示されているので、Webブラウザーから<IPアドレス>:8080にアクセスすると管理Web画面が表示されます(ユーザー名:Administrator、初期パスワード:p@ssw0rdでログイン)。ログインできたら、パスワードやデバイス名の変更が管理Webの画面から行えます。

Lチカプログラムの実行

お約束のLチカをやってみます。開発環境のVisual Studio 2015はこのリンクなどの手順に従ってセットアップされているとします。Build 10586のWindows 10 IoT Coreで開発を行う際は、以下の追加インストールが必要です:

  • Visual Studio 2015 Update 1のインストール:ここからプログラムをダウンロードしてインストール。Update 1をインストールしないとBuild 10586では、ターゲット(Raspberry Pi)へのプログラムのアップロードが行えませんので必ずUpdateが必要です
  • Windows IoT Core Project Templatesを最新版に更新:ここからテンプレートをダウンロード。すでに旧版がインストール済みの場合は、VS2015から一旦旧版を削除して再度インストールします

VS2015を起動したら以下の設定を行います

  • 新しいプロジェクトの作成を行い、テンプレートに「ユニバーサル」「空白のアプリ」を選択して、適当な名前をつける
  • 参照設定に「Windows IoT Extensions for the UWP」を追加

プロジェクトが作成できたら、MainPage.xaml.csに以下のコードを入力。

using System.Threading.Tasks;
using Windows.UI.Xaml.Controls;
using Windows.Devices.Gpio;

// 空白ページのアイテム テンプレートについては、http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409 を参照してください

namespace LED
{
    /// <summary>
    /// Lチカサンプル
    /// <summary>
    public sealed partial class MainPage : Page
    {
        private const int LED_PIN = 5;
        private GpioPin pin;

        public MainPage()
        {
            this.InitializeComponent();

            // GPIOの初期化メソッドを呼び出します
            InitGPIO();

            // LEDの ON / OFFのためのループ処理を呼び出します
            loop();
        }

        private void InitGPIO()
        {
            var gpio = GpioController.GetDefault();

            // GPIOコントローラーがない場合
            if (gpio == null)
            {
                pin = null;
                return;
            }
            // GPIOの5番ピンを開きます
            pin = gpio.OpenPin(LED_PIN);

            // 5番ピンをHighに設定します
            pin.Write(GpioPinValue.High);

            // 5番ピンを出力として使うよう設定します
            pin.SetDriveMode(GpioPinDriveMode.Output);
        }

        // 1秒おきでLEDをON/OFFさせるループの処理
        private async void loop()
        {
            while(true)
            {
                pin.Write(GpioPinValue.Low);
                await Task.Delay(500);
                pin.Write(GpioPinValue.High);
                await Task.Delay(500);
            }
        }
    }
}

ビルドができたら、以下の手順でデバックを開始し、Raspberry Piへプログラムをアップロードします
  • デバックセッションの接続先として「リモートコンピューター」を選択
  • Raspberry PiのIPアドレスを入力
  • 認証モードは「ユニバーサル」を選択。Build 10586では認証モードを「ユニバーサル」にしないとターゲット(Raspberry Pi)へのプログラムのアップロードが行えません
  • プロジェクトのプロパティーを開いて、デバックオプションの認証モードが「Universal」になっていることを確認(もしくはここで、Universalを選択)。

VS2015_DebugSetting.PNG

参考情報




Windows 10 IoT CoreでI2Cを使う

$
0
0

Windows 10 IoT CoreでI2Cを動かしてみました。Windows 10 IoT Coreは執筆時点の最新版であるBuild 10586を使用しています。Windows 10 IoT CoreではRaspberry Piのような組み込み系ボードを使う場合でも、アプリ作成はUniversal Windows Platform (UWP)の作法に従う必要があります。コードは参考文献のものを流用していますが、当方が初めて見る非同期処理などが使われており、最初はコードの意味がわかりませんでした。

当方、Windowsのプログラムは、VS2008の時代に.Net Frameworkを使ったWindows Formプログラムを少々かじりましたが、確かC# 2.0が出た頃でラムダ式はありましたが、非同期プログラミングの概念はまだなかったと思います。ですので、UWPの流儀はWindowsプログラミングの経験的にも初めての要素が多かったです。(注:.NET 1.0の時代からDelegateを使った非同期プログラミングの概念はあったとのコメントをいただきました。ただし大変煩雑だったようです)

非同期プログラミングは、WindowsストアアプリやiOSアプリを作っている方には当たり前の処理だと思うので今更ではありますが、サンプルコードから私が理解したことを記載します(間違っていたらごめんなさい)。シングルスレッドのmbed/Arduino用プログラムを見慣れた(というか、これしか知らない)私には結構新鮮でした。

サンプルコード

TMP102温度センサーを使用して、測定値を画面に表示します。画面はTextBlockを一つ表示するだけの単純なものです。

using System;
using System.Diagnostics;
using System.Threading;
using Windows.UI.Xaml.Controls;
using Windows.Devices.I2c;
using Windows.Devices.Enumeration;
using Windows.ApplicationModel.Core;

namespace TMP102
{
    /// <summary>
    /// TMP102を使用した温度測定
    /// </summary>
    public sealed partial class MainPage : Page
    {
        private I2cDevice TMP102;
        private Timer periodicTimer;

        public MainPage()
        {
            this.InitializeComponent();
            Unloaded += MainPage_Unload;

            InitTMP102();
        }

        private void MainPage_Unload(object sender, object args)
        {
            TMP102.Dispose();
        }

        private async void InitTMP102()
        {
            try
            {
                // Get a selector string for bus "I2C1"
                string aqs = I2cDevice.GetDeviceSelector("I2C1");

                // Find the I2C bus controller with our selector string
                var dis = await DeviceInformation.FindAllAsync(aqs);
                if (dis.Count == 0)
                {
                    Debug.WriteLine("No I2C bus found");
                    CoreApplication.Exit();
                }
                string deviceID = dis[0].Id;
                var setting = new I2cConnectionSettings(0x48);
                setting.BusSpeed = I2cBusSpeed.FastMode;

                // Create an I2cDevice with our selected bus controller and I2C settings
                TMP102 = await I2cDevice.FromIdAsync(deviceID, setting);
            }
            catch(Exception error)
            {
                Debug.Write("TMP102 Instantiation Error: " + error.Message);
                CoreApplication.Exit();
            }

            periodicTimer = new Timer(this.TimerCallback, null, 0, 1000);
        }

        // スレッドプールにキューキングされ、Timerクラスをインスタンス化した
        // スレッド(UIスレッド)とは異なるスレッドで実行される
        private void TimerCallback(object state)
        {
            byte[] readBuf = new byte[2];

            try
            {
                TMP102.Read(readBuf);
            }
            catch(Exception error)
            {
                Debug.Write("I2C Read Error: " + error.Message);
                CoreApplication.Exit();
            }

            float temperature = CalcTemperature((int)readBuf[1], (int)readBuf[0]);
            string temperatureText = String.Format("Temperature : {0:F2}℃", temperature);

            // 直接 textBlock.Text = temperatureText; と書くと例外が発生する
            // UIスレッドを操作するためには以下のように記述する
            var task = this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                textBlock.Text = temperatureText;
            });

            Debug.WriteLine("Temperature:{0:F2}℃", temperature);
        }

        private float CalcTemperature(int valh, int vall)
        {
            int res = (vall << 4 | valh >> 4);
            float temperature = (float)res * 0.0625f;

            return temperature;
        }
    }
}

async/awaitによる非同期処理

Windows 10 IoT CoreもWindowsのファミリーなので、MainPageクラスはメインスレッドで動き、UI操作などのイベントが配信されます。メインスレッド内で重たい処理を実行してしまうと、処理が完了するまでの間他のイベントを処理できなくなり、UIが固まってしまいます。そのため、メインスレッドが長時間ブロックされるのを防ぐために、重たい処理を別スレッドで実行する非同期処理を使います。Windows 10 IoT Coreのデバイス操作でも非同期処理が多用されるようです。非同期処理の概要を以下に記載します。

40行目で「await演算子」がDeviceInformation.FindAllAsync(aqs)の呼び出しに付与されています。awaitを指定することによって、このメソッドを別スレッドで非同期に実行します。メソッド内に非同期処理が含まれる場合は、メソッド名(この場合はInitTMP102())に「async修飾子」をつけます。

40行目のawait DeviceInformation.FindAllAsync(aqs);を実行すると、InitTMP102()メソッドは一旦終了し(処理を呼び出し元に戻し)、他のイベントを受け取れるようにします。DeviceInformation.FindAllAsync(aqs)の処理が終了すると、41行目以降の処理を自動的に再開します。従来の非同期プログラムスタイルだと、非同期処理終了後の後処理(41〜51行目に相当する部分)を別のブロック(終了時に呼び出されるコールバックやクロージャー)に記述したりしますが、C# 5.0では一連の処理として記述できるためコードの見通しがよく大変便利です。

周期タイマー処理

このサンプルでは、Timerクラスを使って、64行目のTimerCallback()メソッドを1000ms周期で実行します。タイマ・メソッドは.NET Frameworkが管理するスレッド・プールにキューイングされて実行されるため、TimerCallback()メソッドはTimerクラスをインスタンス化したメインスレッドとは異なるスレッドで実行されます。

TimerCallback()メソッド内で測定した温度を画面に表示していますが、ここで注意が必要です。上記に示した通り、タイマー処理は別スレッドで動いているため、メインスレッドで処理すべきUIの操作ができません。例えば、このタイマースレッド内で、textBlock.Text = temperatureText;を実行すると例外が発生します。

タイマースレッド内でUIの操作依頼を行うために、83行目のDispatcher.RunAsyncによって、クロージャーとして指定した処理ブロックをUIスレッドにキューイングしています。

メインスレッド内で動くDispatcherTimerクラスを使えば上記のDispatcher.RunAsync処理は不要で、タイマー処理の中で直接UIを操作できますが、重たい処理を記述するとUIが固まってしまうため注意が必要です。Windows 10 IoT Coreを使った組み込み系のプログラムでは複雑なUIを使うことはないため、DispatcherTimerクラスでもよいような気がします。

参考情報


Windows 10 IoT Core用のBME280ライブラリ

$
0
0

Windows 10 IoT Core用に、BME280センサーのライブラリを作ってみました。BME280は3.3V動作の温度・湿度・気圧センサーで、秋月さんSwitch Scienceさんから購入できます。

ESP-WROOM-02 ArduinoでBME280を使った際は、Embedded Adventuresさんのライブラリを使っていました。Windows 10 IoT用のライブラリに関しては、類似品であるBMP280用のライブラリならNuGetで取得することができるのですが、BME280用のライブラリは見つかりませんでした。そのため、Adafruitのforumで紹介されていたBMP280用のライブラリをベースに自作(改造)してみました。

BMP280とBME280はレジスターの構成や設定はほんとんど共通のため、湿度読み取り機能を追加し、読み取り値の補正関数をBME280用に修正しています。

またオリジナルのAdafruit版は、I2Cのからのセンサーデーター読み取りなどで非同期処理(async/await)を多用していたのですが、データ読み取りの時間は瞬時で非同期処理を入れるオーバーヘッド(非同期用の別タスクを起こす処理)の方が大きそうな気がしたため、デバイスの初期化部分以外では非同期処理を使わないように変更しました。

作成したライブラリのコードを以下に示します。GitHubでもプロジェエクトファイルを公開しました:

using System;
using System.Threading.Tasks;
using System.Diagnostics;
using Windows.Devices.Enumeration;
using Windows.Devices.I2c;

namespace Sensor.BME280
{
    public class BME280_CalibrationData
    {
        //BME280 Registers
        public ushort dig_T1 { get; set; }
        public short  dig_T2 { get; set; }
        public short  dig_T3 { get; set; }

        public ushort dig_P1 { get; set; }
        public short  dig_P2 { get; set; }
        public short  dig_P3 { get; set; }
        public short  dig_P4 { get; set; }
        public short  dig_P5 { get; set; }
        public short  dig_P6 { get; set; }
        public short  dig_P7 { get; set; }
        public short  dig_P8 { get; set; }
        public short  dig_P9 { get; set; }

        public byte   dig_H1 { get; set; }
        public short  dig_H2 { get; set; }
        public byte   dig_H3 { get; set; }
        public short  dig_H4 { get; set; }
        public short  dig_H5 { get; set; }
        public sbyte  dig_H6 { get; set; }
    }


    public class BME280
    {
        const byte BME280_Address   = 0x76;
        const byte BME280_Signature = 0x60;

        enum eRegisters : byte
        {
            BME280_REGISTER_DIG_T1 = 0x88,
            BME280_REGISTER_DIG_T2 = 0x8A,
            BME280_REGISTER_DIG_T3 = 0x8C,

            BME280_REGISTER_DIG_P1 = 0x8E,
            BME280_REGISTER_DIG_P2 = 0x90,
            BME280_REGISTER_DIG_P3 = 0x92,
            BME280_REGISTER_DIG_P4 = 0x94,
            BME280_REGISTER_DIG_P5 = 0x96,
            BME280_REGISTER_DIG_P6 = 0x98,
            BME280_REGISTER_DIG_P7 = 0x9A,
            BME280_REGISTER_DIG_P8 = 0x9C,
            BME280_REGISTER_DIG_P9 = 0x9E,

            BME280_REGISTER_DIG_H1 = 0xA1,
            BME280_REGISTER_DIG_H2 = 0xE1,
            BME280_REGISTER_DIG_H3 = 0xE3,
            BME280_REGISTER_DIG_H4_L = 0xE4,
            BME280_REGISTER_DIG_H4_H = 0xE5,
            BME280_REGISTER_DIG_H5_L = 0xE5,
            BME280_REGISTER_DIG_H5_H = 0xE6,
            BME280_REGISTER_DIG_H6 = 0xE7,

            BME280_REGISTER_CHIPID  = 0xD0,
            BME280_REGISTER_SOFTRESET = 0xE0,

            BME280_REGISTER_CONTROLHUMID = 0xF2,
            BME280_REGISTER_STATUS  = 0xF3,
            BME280_REGISTER_CONTROL = 0xF4,
            BME280_REGISTER_CONFIG  = 0xF5,

            BME280_REGISTER_PRESSUREDATA_MSB  = 0xF7,
            BME280_REGISTER_PRESSUREDATA_LSB  = 0xF8,
            BME280_REGISTER_PRESSUREDATA_XLSB = 0xF9, // bits <7:4>

            BME280_REGISTER_TEMPDATA_MSB  = 0xFA,
            BME280_REGISTER_TEMPDATA_LSB  = 0xFB,
            BME280_REGISTER_TEMPDATA_XLSB = 0xFC, // bits <7:4>

            BME280_REGISTER_HUMIDDATA_MSB = 0xFD,
            BME280_REGISTER_HUMIDDATA_LSB = 0xFE,
        };

        // Enables 2-wire I2C interface when set to ‘0’
        public enum interface_mode_e : byte
        {
            i2c = 0,
            spi = 1
        };

        // t_sb standby options - effectively the gap between automatic measurements 
        // when in "normal" mode
        public enum standbySettings_e : byte
        {
            tsb_0p5ms   = 0,
            tsb_62p5ms  = 1,
            tsb_125ms   = 2,
            tsb_250ms   = 3,
            tsb_500ms   = 4,
            tsb_1000ms  = 5,
            tsb_10ms    = 6,
            tsb_20ms    = 7
        };


        // sensor modes, it starts off in sleep mode on power on
        // forced is to take a single measurement now
        // normal takes measurements reqularly automatically
        public enum mode_e :byte
        {
            smSleep     = 0,
            smForced    = 1,
            smNormal    = 3
        };


        // Filter coefficients
        // higher numbers slow down changes, such as slamming doors
        public enum filterCoefficient_e : byte
        {
            fc_off  = 0,
            fc_2    = 1,
            fc_4    = 2,
            fc_8    = 3,
            fc_16   = 4
        };


        // Oversampling options for humidity
        // Oversampling reduces the noise from the sensor
        public enum oversampling_e : byte
        {
            osSkipped   = 0,
            os1x        = 1,
            os2x        = 2,
            os4x        = 3,
            os8x        = 4,
            os16x       = 5
        };


        //String for the friendly name of the I2C bus 
        private const string I2CControllerName = "I2C1";
        //Create an I2C device
        private I2cDevice bme280 = null;
        //Create new calibration data for the sensor
        private BME280_CalibrationData CalibrationData;

        // Value hold sensor operation parameters
        private byte int_mode = (byte)interface_mode_e.i2c;
        private byte t_sb;
        private byte mode;
        private byte filter;
        private byte osrs_p;
        private byte osrs_t;
        private byte osrs_h;

        public BME280(standbySettings_e t_sb = standbySettings_e.tsb_0p5ms, 
                      mode_e mode = mode_e.smNormal, 
                      filterCoefficient_e filter = filterCoefficient_e.fc_16,
                      oversampling_e osrs_p = oversampling_e.os16x, 
                      oversampling_e osrs_t = oversampling_e.os2x,
                      oversampling_e osrs_h = oversampling_e.os1x)
        {
            this.t_sb = (byte)t_sb;
            this.mode = (byte)mode;
            this.filter = (byte)filter;
            this.osrs_p = (byte)osrs_p;
            this.osrs_t = (byte)osrs_t;
            this.osrs_h = (byte)osrs_h;
        }


        //Method to initialize the BME280 sensor
        public async Task Initialize()
        {
            Debug.WriteLine("BME280::Initialize");

            try
            {
                //Instantiate the I2CConnectionSettings using the device address of the BME280
                I2cConnectionSettings settings = new I2cConnectionSettings(BME280_Address);
                //Set the I2C bus speed of connection to fast mode
                settings.BusSpeed = I2cBusSpeed.FastMode;
                //Use the I2CBus device selector to create an advanced query syntax string
                string aqs = I2cDevice.GetDeviceSelector(I2CControllerName);
                //Use the Windows.Devices.Enumeration.DeviceInformation class to create a collection using the advanced query syntax string
                DeviceInformationCollection dis = await DeviceInformation.FindAllAsync(aqs);
                //Instantiate the the BME280 I2C device using the device id of the I2CBus and the I2CConnectionSettings
                bme280 = await I2cDevice.FromIdAsync(dis[0].Id, settings);
                //Check if device was found
                if (bme280 == null)
                {
                    Debug.WriteLine("Device not found");
                }
            }
            catch (Exception e)
            {
                Debug.WriteLine("Exception: " + e.Message + "\n" + e.StackTrace);
                throw;
            }

            byte[] readChipID = new byte[] { (byte)eRegisters.BME280_REGISTER_CHIPID };
            byte[] ReadBuffer = new byte[] { 0xFF };

            //Read the device signature
            bme280.WriteRead(readChipID, ReadBuffer);
            Debug.WriteLine("BME280 Signature: " + ReadBuffer[0].ToString());

            //Verify the device signature
            if (ReadBuffer[0] != BME280_Signature)
            {
                Debug.WriteLine("BME280::Begin Signature Mismatch.");
                return;
            }

            //Read the coefficients table
            CalibrationData = ReadCoefficeints();

            //Set configuration registers
            WriteConfigRegister();
            WriteControlMeasurementRegister();
            WriteControlRegisterHumidity();

            //Set configuration registers again to ensure configuration of humidity
            WriteConfigRegister();
            WriteControlMeasurementRegister();
            WriteControlRegisterHumidity();

            //Dummy read temp to setup t_fine
            ReadTemperature();
        }


        //Method to write the config register (default 16)
        //000  100  00 
        // ↑  ↑   ↑I2C mode
        // ↑  ↑Filter coefficient = 16
        // ↑t_sb = 0.5ms
        private void WriteConfigRegister()
        {
            byte value = (byte)(int_mode + (filter << 2) + (t_sb << 5));
            byte[] WriteBuffer = new byte[] { (byte)eRegisters.BME280_REGISTER_CONFIG, value };
            bme280.Write(WriteBuffer);
            return;
        }

        //Method to write the control measurment register (default 87)
        //010  101  11 
        // ↑  ↑   ↑ mode
        // ↑  ↑ Pressure oversampling
        // ↑ Temperature oversampling
        private void WriteControlMeasurementRegister()
        {
            byte value = (byte)(mode + (osrs_p << 2) + (osrs_t << 5));
            byte[] WriteBuffer = new byte[] { (byte)eRegisters.BME280_REGISTER_CONTROL, value };
            bme280.Write(WriteBuffer);
            return;
        }

        //Method to write the humidity control register (default 01)
        private void WriteControlRegisterHumidity()
        {
            byte value = osrs_h;
            byte[] WriteBuffer = new byte[] { (byte)eRegisters.BME280_REGISTER_CONTROLHUMID, value };
            bme280.Write(WriteBuffer);
            return;
        }


        //Method to read a 16-bit value from a register and return it in little endian format
        private ushort ReadUInt16_LittleEndian(byte register)
        {
            ushort value = 0;
            byte[] writeBuffer = new byte[] { 0x00 };
            byte[] readBuffer = new byte[] { 0x00, 0x00 };

            writeBuffer[0] = register;

            bme280.WriteRead(writeBuffer, readBuffer);
            int h = readBuffer[1] << 8;
            int l = readBuffer[0];
            value = (ushort)(h + l);
            return value;
        }

        //Method to read an 8-bit value from a register
        private byte ReadByte(byte register)
        {
            byte value = 0;
            byte[] writeBuffer = new byte[] { 0x00 };
            byte[] readBuffer = new byte[] { 0x00 };

            writeBuffer[0] = register;

            bme280.WriteRead(writeBuffer, readBuffer);
            value = readBuffer[0];
            return value;
        }

        //Method to read the caliberation data from the registers
        private BME280_CalibrationData ReadCoefficeints()
        {
            // 16 bit calibration data is stored as Little Endian, the helper method will do the byte swap.
            CalibrationData = new BME280_CalibrationData();

            // Read temperature calibration data
            CalibrationData.dig_T1 = ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_T1);
            CalibrationData.dig_T2 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_T2);
            CalibrationData.dig_T3 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_T3);

            // Read presure calibration data
            CalibrationData.dig_P1 = ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P1);
            CalibrationData.dig_P2 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P2);
            CalibrationData.dig_P3 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P3);
            CalibrationData.dig_P4 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P4);
            CalibrationData.dig_P5 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P5);
            CalibrationData.dig_P6 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P6);
            CalibrationData.dig_P7 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P7);
            CalibrationData.dig_P8 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P8);
            CalibrationData.dig_P9 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P9);

            // Read humidity calibration data
            CalibrationData.dig_H1 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H1);
            CalibrationData.dig_H2 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_H2);
            CalibrationData.dig_H3 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H3);
            short e4 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H4_L);    // Read 0xE4
            short e5 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H4_H);    // Read 0xE5
            CalibrationData.dig_H4 = (short)((e4 << 4) + (e5 & 0x0F));
            short e6 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H5_H);    // Read 0xE6
            CalibrationData.dig_H5 = (short)((e5 >> 4) + (e6 << 4));
            CalibrationData.dig_H6 = (sbyte)ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H6);

            return CalibrationData;
        }


        //t_fine carries fine temperature as global value
        int t_fine;

        //Method to return the temperature in DegC. Resolution is 0.01 DegC. Output value of “51.23” equals 51.23 DegC.
        private double BME280_compensate_T_double(int adc_T)
        {
            double var1, var2, T;

            //The temperature is calculated using the compensation formula in the BME280 datasheet
            var1 = (adc_T / 16384.0 - CalibrationData.dig_T1 / 1024.0) * CalibrationData.dig_T2;
            var2 = ((adc_T / 131072.0 - CalibrationData.dig_T1 / 8192.0) * 
                (adc_T / 131072.0 - CalibrationData.dig_T1 / 8192.0)) * CalibrationData.dig_T3;

            t_fine = (int)(var1 + var2);

            T = (var1 + var2) / 5120.0;
            return T;
        }


        //Method to returns the pressure in Pa, in Q24.8 format (24 integer bits and 8 fractional bits).
        //Output value of “24674867” represents 24674867/256 = 96386.2 Pa = 963.862 hPa
        private long BME280_compensate_P_Int64(int adc_P)
        {
            long var1, var2, p;

            //The pressure is calculated using the compensation formula in the BME280 datasheet
            var1 = (long)t_fine - 128000;
            var2 = var1 * var1 * CalibrationData.dig_P6;
            var2 = var2 + ((var1 * CalibrationData.dig_P5) << 17);
            var2 = var2 + ((long)CalibrationData.dig_P4 << 35);
            var1 = ((var1 * var1 * CalibrationData.dig_P3) >> 8) + ((var1 * CalibrationData.dig_P2) << 12);
            var1 = (((long)1 << 47) + var1) * CalibrationData.dig_P1 >> 33;
            if (var1 == 0)
            {
                Debug.WriteLine("BME280_compensate_P_Int64 Jump out to avoid / 0");
                return 0; //Avoid exception caused by division by zero
            }
            //Perform calibration operations as per datasheet: 
            p = 1048576 - adc_P;
            p = (((p << 31) - var2) * 3125) / var1;
            var1 = ((long)CalibrationData.dig_P9 * (p >> 13) * (p >> 13)) >> 25;
            var2 = ((long)CalibrationData.dig_P8 * p) >> 19;
            p = ((p + var1 + var2) >> 8) + ((long)CalibrationData.dig_P7 << 4);
            return p;
        }


        // Returns humidity in %rH as as double. Output value of “46.332” represents 46.332 %rH
        private double BME280_compensate_H_double(int adc_H)
        {
            double var_H;

            var_H = t_fine - 76800.0;
            var_H = (adc_H - (CalibrationData.dig_H4 * 64.0 + CalibrationData.dig_H5 / 16384.0 * var_H)) *
                CalibrationData.dig_H2 / 65536.0 * (1.0 + CalibrationData.dig_H6 / 67108864.0 * var_H *
                (1.0 + CalibrationData.dig_H3 / 67108864.0 * var_H));
            var_H = var_H * (1.0 - CalibrationData.dig_H1 * var_H / 524288.0);

            if (var_H > 100.0)
            {
                Debug.WriteLine("BME280_compensate_H_double Jump out to 100%");
                var_H = 100.0;
            } else if (var_H < 0.0)
            {
                Debug.WriteLine("BME280_compensate_H_double Jump under 0%");
                var_H = 0.0;
            }

            return var_H;
        }


        public float ReadTemperature()
        {
            //Read the MSB, LSB and bits 7:4 (XLSB) of the temperature from the BME280 registers
            byte tmsb = ReadByte((byte)eRegisters.BME280_REGISTER_TEMPDATA_MSB);
            byte tlsb = ReadByte((byte)eRegisters.BME280_REGISTER_TEMPDATA_LSB);
            byte txlsb = ReadByte((byte)eRegisters.BME280_REGISTER_TEMPDATA_XLSB); // bits 7:4

            //Combine the values into a 32-bit integer
            int t = (tmsb << 12) + (tlsb << 4) + (txlsb >> 4);

            //Convert the raw value to the temperature in degC
            double temp = BME280_compensate_T_double(t);

            //Return the temperature as a float value
            return (float)temp;
        }

        public float ReadPreasure()
        {
            //Read the MSB, LSB and bits 7:4 (XLSB) of the pressure from the BME280 registers
            byte pmsb = ReadByte((byte)eRegisters.BME280_REGISTER_PRESSUREDATA_MSB);
            byte plsb = ReadByte((byte)eRegisters.BME280_REGISTER_PRESSUREDATA_LSB);
            byte pxlsb = ReadByte((byte)eRegisters.BME280_REGISTER_PRESSUREDATA_XLSB); // bits 7:4

            //Combine the values into a 32-bit integer
            int p = (pmsb << 12) + (plsb << 4) + (pxlsb >> 4);

            //Convert the raw value to the pressure in Pa
            long pres = BME280_compensate_P_Int64(p);

            //Return the pressure as a float value
            return ((float)pres) / 256;
        }

        public float ReadHumidity()
        {
            //Read the MSB and LSB of the humidity from the BME280 registers
            byte hmsb = ReadByte((byte)eRegisters.BME280_REGISTER_HUMIDDATA_MSB);
            byte hlsb = ReadByte((byte)eRegisters.BME280_REGISTER_HUMIDDATA_LSB);

            //Combine the values into a 32-bit integer
            int h = (hmsb << 8) + hlsb;

            //Convert the raw value to the humidity in %
            double humidity = BME280_compensate_H_double(h);

            //Return the humidity as a float value
            return (float)humidity;
        }

        //Method to take the sea level pressure in Hectopascals(hPa) as a parameter and calculate the altitude using current pressure.
        public float ReadAltitude(float seaLevel)
        {
            //Read the pressure first
            float pressure = ReadPreasure();
            //Convert the pressure to Hectopascals(hPa)
            pressure /= 100;

            //Calculate and return the altitude using the international barometric formula
            return 44330.0f * (1.0f - (float)Math.Pow((pressure / seaLevel), 0.1903f));
        }
    }
}

ライブラリを使用したセンサーデーター読み取りアプリのサンプルは以下です:

using System;
using System.Threading;
using System.Diagnostics;
using Windows.UI.Xaml.Controls;
using Sensor.BME280;


namespace BME280_Test
{
    public sealed partial class MainPage : Page
    {
        private BME280 bme280;
        private Timer periodicTimer;

        public MainPage()
        {
            this.InitializeComponent();

            bme280 = new BME280();
            initBme280();
        }

        private async void initBme280()
        {
            await bme280.Initialize();
            periodicTimer = new Timer(this.TimerCallback, null, 0, 1000);
        }

        private void TimerCallback(object state)
        {
            var temp = bme280.ReadTemperature();
            var press = bme280.ReadPreasure() / 100;
            var humidity = bme280.ReadHumidity();
            var alt  = bme280.ReadAltitude(1013);   // 1013hPa = pressure at 0m
            Debug.WriteLine("Temp:{0:F2}℃ Humidity:{1:F2}% Press:{2:F2}hPa Alt:{3:F0}m", temp, humidity, press, alt);

            var task = this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                tempValue.Text = temp.ToString("F2") + "℃";
                humValue.Text = humidity.ToString("F2") + "%";
                pressValue.Text = press.ToString("F2") + "hPa";
            });

        }
    }
}

電源オンの初回起動時に湿度情報が読み取れない問題があり(一旦アプリを終了して再度起動すると正常になる)、回避策としてレジスタの初期化を2回行っています(ライブラリソースの226行目)。Arudino用のライブラリコードを見ると設定を2回やるような処理は行っておらず、この点は謎です。

BME280のセンサーとしての精度ですが、手持ちのAM2302に比べて温度が高め、湿度が低めに出ます。湿度は5%以上の差分があり、センサー種別によって測定値に大きなばらつきがあります。温度はその他手持ちの温度計と比較するとAM2302の方が正確なため、湿度もAM2302の方が正確ではないかと思います。

iOSからMilkcocoaへMQTT publishする

$
0
0

iOSからMQTTブローカにメッセージをpublishするサンプルを作ってみました。

MQTTブローカにはMilkcocoaというサービスを使いました。フリーのMQTTブローカとしてはtest.mosquitto.orgがありますが、以前に試した際はイマイチ接続が安定していない感じでした。そこで、無償使用も可能な商用サービスであるMilkcocoaを試してみました。Milkcocoaは10万メッセージまでは無償で利用できるため、ちょっとしたお試しは無償利用範囲内だと思います。

Milkcocoaのアカウント作成などは、本記事では割愛します。MilkcocoaにはArudino SDKも提供されており、本SDKを使えばESP-WROOM-02を使って簡単にMilkcocoaサーバーにデータをpublishすることができます。今回は、iOSデバイスからMilkcocoaにデーターのpublishを行いたかったため、iOS用のMQTTライブラリを使ってサンプルを作成しました(Arduino SDKを使うともっと簡潔に記述ができるのですが)。

iOS用MQTTライブラリの準備

iOS用のMQTTライブラリはGoogleで検索するといくつかの候補がヒットします。最近Swiftをお勉強中のため、Objective-CではなくSwiftでプログラミングできるライブラリを選びました。Swiftが使えるMQTTライブラリとしては、上位にヒットするもので以下がありました:

  • CocoaMQTT: SwiftとObjective-C(ソケット関連の処理部分)で書かれたSwift/Objective-Cネイティブなライブラリ
  • Moscapsule: APIはSwiftになっていますが、実態はmosquittoのラッパー

今回は、Swift/Objective-CネイティブなCocoaMQTTを使いました。Webサイトの指示に従って、CocoaPodを使ってライブラリをインストールし、XcodeプロジェクトにBridging-Header.hを登録します。 Bridging-Header.hはCocoaMQTTがSwiftとObjective-Cの両方を使用しているため、SwiftとObjective-C間の連携を行うために必要です。

Swiftのコード

今回作成したコードを以下に示します。iPhoneの加速度センサーを読み取り、1秒毎にMilkcocoa MQTTブローカにメッセージをpublishします。UIとしてiPhoneにも加速度センサーの読み取り値を表示します。

//
//  ViewController.swift
//  mqtt_test
//
//  Created by Todotani on 2016/01/17.
//  Copyright © 2016年 Todotani. All rights reserved.
//

import UIKit
import CoreMotion
import CocoaMQTT

class ViewController: UIViewController, CocoaMQTTDelegate{
    
    @IBOutlet var connectionState: UILabel!
    @IBOutlet var label_accel_X: UILabel!
    @IBOutlet var lable_accel_Y: UILabel!
    
    private struct MqttConstants {
        static let AppID    = "YourAppID"     // Set AppID
        static let ClientId = MqttConstants.AppID
        static let HostName = MqttConstants.AppID + ".mlkcca.com"
        static let UserName = "sdammy"
        static let PassWord = MqttConstants.AppID
        static let Topic    = MqttConstants.AppID + "/Test/push"
    }

    var mqtt: CocoaMQTT!
    var motionManager: CMMotionManager!
    var isConnected: Bool = false {
        didSet{
            if isConnected == true {
                connectionState.text = "Connected"
            } else {
                connectionState.text = "Disconnect"
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let appDelegate:AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        appDelegate.viewController = self
        
        mqtt = CocoaMQTT(clientId: MqttConstants.ClientId, host: MqttConstants.HostName)
        mqtt.username = MqttConstants.UserName
        mqtt.password = MqttConstants.PassWord
        mqtt.keepAlive = 60
        mqtt.delegate = self
        
        motionManager = CMMotionManager()
        motionManager.accelerometerUpdateInterval = 0.1
        
        mqtt.connect()
    }
    
    private var counter = 0
    
    func startUpdate() {
        motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.currentQueue()!, withHandler: {(data, error) in
            let accel_X = String(format: "%.4f", data!.acceleration.x)
            let accel_Y = String(format: "%.4f", data!.acceleration.y)
            
            self.label_accel_X.text = accel_X
            self.lable_accel_Y.text = accel_Y
            
            if ((self.counter % 10) == 0) {
                let message = "{\"params\":{\"Accel_X\":\(accel_X),\"Accel_Y\":\(accel_Y)}}"
                self.mqtt.publish(MqttConstants.Topic, withString: message)
            }
            self.counter += 1
        })
    }

    // MARK: CocoaMQTTDelegate
    
    func mqtt(mqtt: CocoaMQTT, didConnect host: String, port: Int) {
        print("didConnect \(host):\(port)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) {
        print("didConnectAck \(ack.rawValue)")
        if ack == .ACCEPT {
            mqtt.subscribe(MqttConstants.Topic, qos: .QOS0)
            mqtt.ping()
            isConnected = true
            startUpdate()
        }
    }
    
    func mqtt(mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) {
        print("didPublishMessage with message: \(message.string!)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didPublishAck id: UInt16) {
        print("didPublishAck with id: \(id)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16 ) {
        print("didReceivedMessage: \(message.string!) with id \(id)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didSubscribeTopic topic: String) {
        print("didSubscribeTopic to \(topic)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didUnsubscribeTopic topic: String) {
        print("didUnsubscribeTopic to \(topic)")
    }
    
    func mqttDidPing(mqtt: CocoaMQTT) {
        print("didPing")
    }
    
    func mqttDidReceivePong(mqtt: CocoaMQTT) {
        _console("didReceivePong")
    }
    
    func mqttDidDisconnect(mqtt: CocoaMQTT, withError err: NSError?) {
        _console("mqttDidDisconnect")
    }
    
    func _console(info: String) {
        print("Delegate: \(info)")
    }
}


//
//  AppDelegate.swift
//  mqtt_test
//
//  Created by Todotani on 2016/01/17.
//  Copyright © 2016年 Todotani. All rights reserved.
//

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    
    var viewController: ViewController!

    func applicationDidEnterBackground(application: UIApplication) {
        viewController.mqtt.disconnect()
        viewController.isConnected = false
        viewController.motionManager.stopAccelerometerUpdates()
    }

    func applicationWillEnterForeground(application: UIApplication) {
        viewController.mqtt.connect()
    }

}

注意点

  1. 20行目のAppIDには、Milkcocoa Webサイトで「新しいアプリ」を登録した際に付与されるAppIDを設定します。25行目のTopicは、「AppID/データストア名/push」の書式で記載します。今回の例では、データストア名としてTestを使っています。
  2. CocoaMQTTライブラリを使う際は、CocoaMQTTDelegateプロトコルに定義されているメソッドを実装する必要があります。今回の例ではViewControllerクラスにCocoaMQTTDelegateを適用(adopt)し、77行目以降にdelegateメッソドを記述しています。
  3. CocoaMQTTライブラリは非同期処理を使っており、connect()メッソドを呼ぶとすぐに処理が帰ってきますが、裏で接続処理が続いています。そのため、接続が完了するまではメッセージをpublishしてはいけません。接続が完了するとmqtt:didConnectAckメソッドが呼び出されるため、mqtt:didConnectAckにメッセージの送信開始を記述します。→ ソースの80行目
  4. PublishするメッセージはJSON形式で記述し、”params”をkeyにする必要があります。最初このkeyを書いておらず、送信したメッセージがサーバー側で正しく認識されないため、だいぶ悩みました。X/Y軸加速度をpublishするためのメッセージは以下のように記述します。
    {"params":{"Key_X":"X_Val", “Key_Y":"Y_Val"}}
    今回は70行目の処理で、JSONのひな形文字列内にvalue値を埋め込む汚い処理になっています。Dictionalyを使いKey/Valueを指定することで値を設定しJSONフォーマットにシリアライズする処理も考えたのですが、変換にゴミが入ってしまいうまくいかなかったので手抜きをしています。Arduino SDKはKey/Valueを指定してメッセージのデーターを生成できるのでこちらの方が汎用的です。

終わりに

Swiftが発表されてから長らく様子見だったのですが、サンプルコードも充実してきたのでiOS 9 & Swift 2.0からSwiftのお勉強を始めました。Objective-Cを使ったiOSのプログラミングも殆ど行ってはいないので、覚えた先から忘れているのですが、Objective-CよりSwiftの方がプログラミングもしやすくなってきました。

Milkcocoaはそんなに長時間の接続は試していませんが、フリーのmosquitto.orgより接続が安定しているように思います。今回のサンプルコードでは、publishしたメッセージをローカルにsubscribeしていますが配信の遅延もありません。

参考資料


HomebridgeとRaspberry Piを使ってHome Kitの実験をしてみた

$
0
0

TwitterでHomebridgeとRaspberry Piを使ったHome Kitのデモが流れていたので、私も真似をしてやってみました。これ、面白いです! Siriを使って音声でRaspberry PiにつないだLEDをOn/Offできるまでの手順を簡単に示します。

今回の実験はRaspberry Pi 2とRaspbian Wheezyを使っています。Raspbianはずいぶん前にインストールしたものですが、apt-get upgradeで最新の状態にして一連のインストールを行いました。最新のRaspbianはJessieになっていますが、Jessieでは試していません。そのため、最近RasPiを買ってRaspbianをインストールした方には以下の手順は適用できませんのでご注意下さい。

WebIOPiのインストール

今回の実験では、Raspberry PiにLED制御(Raspberry PiのGPIO制御)用のREST APIを作り、Home KitからこのREST APIを叩くことでLEDの制御を行うことにします。そのために、REST APIの提供とGPIOの制御を同時にできるWebIOPiを使用します。WebIOPiのインストールはリンクの手順に従って行いました。リンクの手順はv0.6.0を使っていますが、執筆時点で最新のv0.7.1を使っています。

WebIOPiのv0.7.1はRaspberry Pi 2に対応していないため、以下の手順でパッチを当ててからビルドします。

wget http://sourceforge.net/projects/webiopi/files/WebIOPi-0.7.1.tar.gz
tar xvzf WebIOPi-0.7.1.tar.gz
cd WebIOPi-0.7.1
wget https://raw.githubusercontent.com/doublebind/raspi/master/webiopi-pi2bplus.patch
patch -p1 -i webiopi-pi2bplus.patch
sudo ./setup.sh

WebIOPiが動くようになったら、LEDをつなぐGPIOポート(今回はGPIO 17を使用)を出力に設定しておきます。

NodeJSとHomebridgeのインストール

このリンクの手順に従って、NodeJSをインストールします。NodeJSをインストールする前にGCC 4.9をインストールする必要があり(Raspberry PiのGCCはバージョンが古くC++14に対応していないため)、Install C++14の手順に従って行います。

リンクの手順ではNodeJSは4.0.0を使っていますが、執筆時点のLTS版であるv4.2.6をインストールしました。私はRaspberry Pi 2を使っているので、ARMv7版を使いました。

続けて、AvahiとHomebridgeをインストールします。

Homebridge Pluginのインストール

HomebridgeをインストールしただけではLEDや電球の制御は行えず、そのためにはプラグインをインストールする必要があります。プラグインはリンクにあるように90近くがあり何を使ったら良いか迷いますが、Raspberry PiのREST APIを叩くことでLEDの制御ができるように、httpベースのプラグインを探しました。今回は、homebridge-httpというプラグインを使います。プラグインのインストールは次のコマンドで行います。

sudo npm install -g homebridge-http

Pluginの設定

インストールしたプラグインをhomebridgeが認識するために、/home/pi/.homebridge ディレクトリにconfig.jsonという名前のファイルを作成します。config.jsonの内容は以下の通りです。

{
    "bridge":
    {
       "name": "Homebridge",
       "username": "B8:27:EB:4D:31:D7",
       "port": 51826,
       "pin": "031-45-154"
    },

    "accessories": [
    {
        "accessory": "Http",
        "name": "部屋のランプ",
        "switchHandling": "yes",
        "http_method": "POST",
        "on_url":      "http://localhost:8000/GPIO/17/value/1",
        "off_url":     "http://localhost:8000/GPIO/17/value/0",
        "status_url":  "http://localhost:8000/GPIO/17/value",
        "service": "Light",
        "brightnessHandling": "no",
        "brightness_url":     "http://localhost/controller/1707/%b",
        "brightnesslvl_url":  "http://localhost/status/100054",
        "sendimmediately": "",
        "username" : "webiopi",
        "password" : "raspberry"                     
     }
  ]
}

設定ファイルのポイントは以下です;

  • bridgeのusernameはMACアドレスの形式ならなんでもよいみたいですが、Raspberry PiのMACアドレスにしています
  • accessoriesのon_rul/off_urlにWebIOPiのREST APIに従ってGPIO制御のurlを記述します。私はGPIO 17番を使っています
  • WebIOPiはusername/passwordを使った認証機能があるため、accessoriesにデフォルトのusername/passwordを設定しています

Homebridgeの起動

homebrigeを起動すると以下のようなメッセージが出れば起動成功です。

*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see 
*** WARNING *** The program 'node' called 'DNSServiceRegister()' which is not supported (or only supported partially) in the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see 
Loaded plugin: homebridge-http
Registering accessory 'homebridge-httpstatus.Http'
---
Loaded plugin: homebridge-lockitron
Registering accessory 'homebridge-lockitron.Lockitron'
---
Loaded config.json with 1 accessories and 0 platforms.
---
Loading 1 accessories...
[部屋のランプ] Initializing Http accessory...
Scan this code with your HomeKit App on your iOS device to pair with Homebridge:
                       
    ┌────────────┐     
    │ 031-45-154 │     
    └────────────┘     
                       
Homebridge is running on port 51826.

iPhoneにHome Kitアプリをインストール

iPhoneにHome Kitアプリをインストールします。私はアップルのHomeKit Catalogを使用しました。本アプリはソース形式で配布されているため、Xcodeを使ってビルドする必要があります。ダウンロードにはDevelopperアカウントが必要だと思います。HomeKit Catalog以外にも、App Storeに登録されたHome Kit対応のアプリがあるようですので、HomeKit Catalogがダウンロードできない場合にはApp Storeのアプリを使用可能です。

Home Kitアプリを起動してアクセサリの登録を行い、LEDを制御するための「シーン」を設定します。細かな手順は割愛しますが、アプリを触っていると使い方はわかると思います。

動作イメージ

一連の設定でSiriを使ってRaspberry PiにつないだLEDを音声でOn/Offができるようになります。操作のデモを動画に撮ってYouTubeにアップしました。

Cephストレージの構築

$
0
0

自宅のLinux環境にCephストレージを構築してみました。備忘録を兼ねて構築手順を記載します。

Cephは汎用サーバーを使った分散ストレージを構築するためのオープンソースソフトウェアです(アーキテクチャーの解説はこちら)。複数サーバーにデーターをレプリケーションする(3つのレプリケーションが推奨されています)ことで耐障害性を確保しながらペタバイトスケールの大規模ストレージが構築できます。今回は自宅のPCを使用しているので1台のサーバーで構築しており、分散ストレージにはなっていません^^;。

動作環境

  • ホストマシン:Intel NUC D34010WYKH(CPU Core i3-4010U)
  • ディスク: 起動用 256GB SSD (/dev/sdb), データー用 1TB HDD (/dev/sda)
  • メモリー: 16GB
  • NICは1ポートのみ(しかありません…)→ IPアドレス: 192.168.0.111/24
  • Linuxディストリビューション: CentOS 7.2.1511

IMG_0912.jpg

事前準備

  • ホスト名はnuc2としています。ホスト名でIPアドレス(ループバックではないアドレス)が解決できるよう /etc/hostsファイルを設定しておきます(我が家の環境では、Raspberry PiでDNSを構築しているためDNSで名前解決ができるようにしています)
  • データー用HDDをOSD(Object Storage Daemon)の数分にパーティション分割します。今回の例では2つのパーティションを作り/var/local/osd0, /var/local/osd1にマウントしています(一般的には3つのOSDにデーターをレプリケーションするのですが、所詮HDDが1台しかないので2つのOSDで済ませています)
  • ntpを設定します(chronydをインストール)

パーティションの作成は以下のイメージ(フォーマットはXFSを使っています)

# parted /dev/sda
(parted) mklabel gpt
(parted) mkpart primary xfs 0% 50%
(parted) mkpart primary xfs 51% 100%
(parted) quit

# mkfs.xfs /dev/sda1
# mkfs.xfs /dev/sda2

# mkdir /var/local/osd0  -> sda1をマウント
# mkdir /var/local/osd1  -> sda2をマウント

# lsblk
NAME            MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda               8:0    0 931.5G  0 disk 
├─sda1            8:1    0 465.8G  0 part /var/local/osd0
└─sda2            8:2    0 456.5G  0 part /var/local/osd1
sdb               8:16   0 238.5G  0 disk 
├─sdb1            8:17   0   200M  0 part /boot/efi
├─sdb2            8:18   0   500M  0 part /boot
└─sdb3            8:19   0 237.8G  0 part 
  ├─centos-root 253:0    0   180G  0 lvm  /
  ├─centos-swap 253:1    0   7.8G  0 lvm  [SWAP]
  └─centos-home 253:2    0    50G  0 lvm  /home

Cephのインストール

基本的には公式サイトのQuick Start手順に従っていますが、一部うまくいかない部分がありましたので手直しをしています。インストールしたバージョンは最新のinfernalis(9.2.0)です。当初、LTS版のhummerをインストールしようとしたのですが、yumレポジトリーにhummerを設定してもinfernalisが降ってくるので、infernalisでインストールを進めました。

以下の設定は、cephをインストールするマシン(今回の例ではhost名nuc2)で行っています。Storageノードに加えてAdminノードを立てるケースが一般的ですが、StorageノードとAdminノードを兼用しています。

1) パッケージマネージャの設定

# sudo yum install -y yum-utils && sudo yum-config-manager --add-repo https://dl.fedoraproject.org/pub/epel/7/x86_64/ && sudo yum install --nogpgcheck -y epel-release && sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7 && sudo rm /etc/yum.repos.d/dl.fedoraproject.org*

2) yumレポジトリーの設定。/etc/yum.repos.d/ceph-deploy.repoに以下を設定

[ceph-noarch]
name=Ceph no arch packages
baseurl=http://ceph.com/rpm-infernalis/el7/noarch
enabled=1
gpgcheck=1
type=rpm-md
gpgkey=https://ceph.com/git/?p=ceph.git;a=blob_plain;f=keys/release.asc

3) ceph-deployのインストール

# sudo yum update && sudo yum install ceph-deploy

4) cephインストールノードにceph用アカウントを作成

ユーザー名にcephは使ってはいけません(cephデーモンが使用するため)
# useradd -d /home/cephuser -m cephuser
# passwd cephuser
# echo "cephuser ALL = (root) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/cephuser
# chmod 0440 /etc/sudoers.d/cephuser

# useradd -d /home/cephadmin -m cephadmin
# passwd cephadmin

cephadminアカウントでもsudoができるように、visudoで以下を追加しておきます
cephadmin   ALL=(ALL) ALL

sudoresのrequirettyをコメントアウト
# sudo sed -i s'/Defaults    requiretty/#Defaults    requiretty'/g /etc/sudoers

5) SSH鍵の生成と配布

cephadminアカウントで実施 (passphraseはempty)
$ ssh-keygen
$ ssh-copy-id cephuser@nuc2

6) cephのインストール

以下はcephadminアカウントで実施します。

$ sudo set enforce 0

$ mkdir my-cluster
$ cd my-cluster

$ ceph-deploy new nuc2
NICカードを複数使っている場合はpublic側のnetworkをceph.confに記載する必要がありますが
今回はNICは1ポートのため設定は割愛できます。

以下の行をceph.confに追加(OSDを2つにするための設定。defaultは3なので設定が必要)
osd pool default size = 2

以下の行をceph.confに追加(OSDを複数サーバーに分散せず、シングルノードにインストールする際に必要)
osd crush chooseleaf type = 0

infernalisリリースからosdデーモンがrootではなく、osdユーザー権限で実行されるようになったため、
osdをマウントするディレクトリのオーナーをrootからosdに変更します(rootのままではインストールが
エラーになってしまいました)
$ sudo chown ceph:ceph /var/local/osd0
$ sudo chown ceph:ceph /var/local/osd1

firewallにcephが使用するポートを許可設定
$ sudo firewall-cmd --zone=public --add-port=6789/tcp --permanent

以下のcephインストールコマンドはsudoで行う必要はありません(マニュアルにはsudoは使うなと書いてあります)
$ ceph-deploy install nuc2
$ ceph-deploy mon create-initial

コマンドパラメーターにnode名:osdのディレクトリを指定します。osdのディレクトリに加えてジャーナル
ディスクを指定することができる(高速アクセスのためにはジャーナルディスクを分けることが推奨されて
いる)のですが今回はosdディスク内にジャーナルファイルを作ります(ジャーナルの指定は省略)
$ ceph-deploy osd prepare nuc2:/var/local/osd0 nuc2:/var/local/osd1
$ ceph-deploy osd activate nuc2:/var/local/osd0 nuc2:/var/local/osd1

$ ceph-deploy admin nuc2
$ sudo chmod +r /etc/ceph/ceph.client.admin.keyring

以下のコマンドでstatusがHELTH_OKと表示されれば正常に動作しています。

[cephadmin@nuc2 ~]$ ceph status
    cluster 29e648d2-a75e-4e74-a0d0-41c41bd1c300
     health HEALTH_OK
     monmap e1: 1 mons at {nuc2=192.168.0.111:6789/0}
            election epoch 1, quorum 0 nuc2
     osdmap e12: 2 osds: 2 up, 2 in
            flags sortbitwise
      pgmap v27: 64 pgs, 1 pools, 0 bytes data, 0 objects
            10306 MB used, 911 GB / 921 GB avail
                  64 active+clean

[cephadmin@nuc2 ~]$ ceph osd tree
ID WEIGHT  TYPE NAME     UP/DOWN REWEIGHT PRIMARY-AFFINITY 
-1 0.90009 root default                                    
-2 0.90009     host nuc2                                   
 0 0.45459         osd.0      up  1.00000          1.00000 
 1 0.44550         osd.1      up  1.00000          1.00000 

これだけではデーモンが起動しただけでストレージとしては使えないので、次回はOpenStackのバックエンドとしてcephを使えるようにしてみます。

アンイストールの方法

インストールに失敗した場合などは以下の手順でアンインストールができます(インストール中にエラーが出て何回かお世話になりました)。

$ ceps-deploy purge nuc2
$ ceph-deploy purgedata nuc2
$ ceps-deploy forgetkeys

参考情報


CephをOpenStackのバックエンドにする

$
0
0

前回の記事でCephのインストールを行いましたが、今回はCephをOpenStackのバックエンドに接続してみました。Nova ehpemeral、Cinder volume、Glande image storeにCeph RBDを使えるようにしました。基本的にはceph.comの設定手順に従っていますが、一部ドキュメントに書いていない設定が必要だったりして結構悩みました。

当方、OpenStackはまだまだ初心者で、かつ構築した環境はPC1台(Ceph) + PC1台(OpenStack)のミニマム環境ですので、参考程度にして下さい(全部の動作を確認できていないので、商用システムに適用する場合は十分な試験が必要です)。

動作環境

  • ホストマシン: Intel NUC D54250WYKH(CPU Core i5-4250U)
  • ディスク: 1TB HDD
  • メモリー: 16GB
  • Linuxディストリビューション: CentOS 7.2.1511
  • OpenStackバージョン: Kilo(RDO Packstackを使ってAll-In-One構成でインストール済み)

ネットワーク構成


201602210741.jpg

構築手順

1) Cephプロトコルパケットが疎通できるようにする

デフォルトのFirewall設定があるとCephのパケットが疎通しません。Cephノード(NUC2)側は面倒
なのでfirewallをdisable。
[root@nuc1 ~]# iptables -A INPUT -i br-ex -p tcp -s 192.168.0.0/24 --dport 6789 -j ACCEPT
[root@nuc2 ~]# systemctl disable firewalls

2) Cephプールの生成

CephドキュメントではPG数を128にしていますが、当方の環境ではOSDが2つしかなく、PG数の合計が超過
してしまうので32に減らしました。PG数の値は適当なので、本来は最適化が必要です。
[cephadmin@nuc2 ~]$ ceph osd pool create volumes 32
[cephadmin@nuc2 ~]$ ceph osd pool create images 32
[cephadmin@nuc2 ~]$ ceph osd pool create backups 32
[cephadmin@nuc2 ~]$ ceph osd pool create vms 32

3) OpenStackノード(NUC1)の環境設定

[root@nuc1 ~]# mkdir /etc/ceph
[cephadmin@nuc2 ~]$ scp /etc/ceph/ceph.conf root@nuc1:/etc/ceph
[cephadmin@nuc2 ~]$ scp /etc/yum.repos.d/ceph.repo root@nuc1:/etc/yum.repos.d

[root@nuc1 ~] visudoで次の行をコメントアウト→ # Defaults    requiretty
[root@nuc1 ~]# yum install python-rbd
[root@nuc1 ~]# wget ftp://ftp.pbone.net/mirror/ftp5.gwdg.de/pub/opensuse/repositories/home:/dalgaaf:/ceph/CentOS_CentOS-6/x86_64/libleveldb1-1.9-6.1.x86_64.rpm

[root@nuc1 ~]# rpm -ivh libleveldb1-1.9-6.1.x86_64.rpm
[root@nuc1 ~]# yum install ceph
[root@nuc1 ~]# yum install libvirt

[root@nuc2 ~]# yum install qemu-img.x86_64
[root@nuc2 ~]# yum install libvirt-daemon-driver-qemu.x86_64

4) Cephの認証とアクセス権の設定

[cephadmin@nuc2 ~]$ ceph auth get-or-create client.cinder mon 'allow r' osd 'allow class-read object_prefix rbd_children, allow rwx pool=volumes, allow rwx pool=vms, allow rx pool=images'
[cephadmin@nuc2 ~]$ ceph auth get-or-create client.glance mon 'allow r' osd 'allow class-read object_prefix rbd_children, allow rwx pool=images'
[cephadmin@nuc2 ~]$ ceph auth get-or-create client.cinder-backup mon 'allow r' osd 'allow class-read object_prefix rbd_children, allow rwx pool=backups'

[cephadmin@nuc2 ~]$ ceph auth list

5) キーリングをOpenStackノードに転送

[cephadmin@nuc2 ~]$ ceph auth get-or-create client.glance | ssh root@nuc1 sudo tee /etc/ceph/ceph.client.glance.keyring
[cephadmin@nuc2 ~]$ ssh root@nuc1 sudo chown glance:glance /etc/ceph/ceph.client.glance.keyring

[cephadmin@nuc2 ~]$ ceph auth get-or-create client.cinder | ssh root@nuc1 sudo tee /etc/ceph/ceph.client.cinder.keyring
[cephadmin@nuc2 ~]$ ssh root@nuc1 sudo chown cinder:cinder /etc/ceph/ceph.client.cinder.keyring

[cephadmin@nuc2 ~]$ ceph auth get-or-create client.cinder-backup | ssh root@nuc1 sudo tee /etc/ceph/ceph.client.cinder-backup.keyring
[cephadmin@nuc2 ~]$ ssh root@nuc1 sudo chown cinder:cinder /etc/ceph/ceph.client.cinder-backup.keyring

6) OpenStackノードにlibvirtのシークレットキーを設定

[cephadmin@nuc2 ~]$ ceph auth get-key client.cinder | ssh root@nuc1 tee client.cinder.key

[root@nuc1 ~]# uuidgen
28c8fcca-5cc3-4ba6-961c-01e617474e1a

cat > secret-cinder.xml << EOF
<secret ephemeral='no' private='no'>
   <uuid>28c8fcca-5cc3-4ba6-961c-01e617474e1a</uuid>
   <usage type='ceph'>
      <name>client.cinder secret</name>
   </usage>
</secret>
EOF

[root@nuc1 ~]# virsh secret-define --file secret-cinder.xml
[root@nuc1 ~]# virsh secret-set-value --secret 28c8fcca-5cc3-4ba6-961c-01e617474e1a --base64 $(cat client.cinder.key)

[root@nuc1 ~]# virsh secret-list
[root@nuc1 ~]# virsh secret-get-value 28c8fcca-5cc3-4ba6-961c-01e617474e1a

7) OpenStackノードで /etc/glance/glance-api.conf を編集

default_store = rbd
stores = rbd
rbd_store_pool = images
rbd_store_user = glance
rbd_store_ceph_conf = /etc/ceph/ceph.conf
rbd_store_chunk_size = 8

show_image_direct_url = True

8) OpenStackノードで /etc/cinder/cinder.conf を編集

[ceph]
volume_driver = cinder.volume.drivers.rbd.RBDDriver
volume_backend_name = ceph
enabled_backends = ceph
rbd_pool = volumes
rbd_ceph_conf = /etc/ceph/ceph.conf
rbd_flatten_volume_from_snapshot = false
rbd_max_clone_depth = 5
rbd_store_chunk_size = 4
rados_connect_timeout = -1
glance_api_version = 2

rbd_user = cinder
rbd_secret_uuid = 28c8fcca-5cc3-4ba6-961c-01e617474e1a

backup_driver = cinder.backup.drivers.ceph
backup_ceph_conf = /etc/ceph/ceph.conf
backup_ceph_user = cinder-backup
backup_ceph_chunk_size = 134217728
backup_ceph_pool = backups
backup_ceph_stripe_unit = 0
backup_ceph_stripe_count = 0
restore_discard_excess_bytes = true

#enabled_backends=lvm —> コメントアウト

[lvm] —> 以下をコメントアウト
#iscsi_helper=lioadm
#volume_group=cinder-volumes
#iscsi_ip_address=192.168.0.110
#volume_driver=cinder.volume.drivers.lvm.LVMVolumeDriver
#volumes_dir=/var/lib/cinder/volumes
#iscsi_protocol=iscsi
#volume_backend_name=lvm

9) OpenStackノードで /etc/nova/nova.conf を編集

[libvirt]
images_type = rbd
images_rbd_pool = vms
images_rbd_ceph_conf = /etc/ceph/ceph.conf

ユーザーがcinderになっていますが、client.cinderにvms(nova ephmeral用のプール)への
アクセス権を設定しているためこの指定で問題ありません。
rbd_user = cinder
rbd_secret_uuid = 28c8fcca-5cc3-4ba6-961c-01e617474e1a
disk_cachemodes="network=writeback"

inject_password = false
inject_key = false
inject_partition = -2

cephのドキュメントにはKiloでは次の設定を追加するように記載がありますが、CentOS 7のqemu
バージョンではこの設定があるとVM起動時にNo valid hostのエラーが発生します。
#hw_disk_discard = unmap  ==> コメントアウトが必要

10) OpenStackノードでのCephの設定

/etc/ceph/ceph.conf に以下を追記
[client]
rbd cache = true
rbd cache write through until flush = true
admin socket = /var/run/ceph/guests/$cluster-$type.$id.$pid.$cctid.asok
log file = /var/log/qemu/qemu-guest-$pid.log
rbd concurrent management ops = 20

[client.cinder]
    keyring=/etc/ceph/ceph.client.cinder.keyring

[client.cinder-backup]
    keyring=/etc/ceph/ceph.client.cinder-backup.keyring

[client.glance]
    keyring=/etc/ceph/ceph.client.glance.keyring

以下のコマンドを実行
[root@nuc1 ~]# mkdir -p /var/run/ceph/guests/ /var/log/qemu/
[root@nuc1 ~]# groupadd libvirtd
[root@nuc1 ~]# chown qemu:libvirtd /var/run/ceph/guests /var/log/qemu/

11) 各プロセスを再起動

sudo systemctl restart openstack-glance-api
sudo systemctl restart openstack-nova-compute
sudo systemctl restart openstack-cinder-volume
sudo systemctl restart openstack-cinder-backup

12) Cinderでcephを使えるようにする設定(これは必要ないかもしれません)

[Openstack admin]# cinder type-create ceph
[Openstack admin]# cinder type-key ceph set volume_backend_name=ceph

ハマったところ

当初以下の設定が抜けており、CinderのVolumeからVMをブートできるのですが、ブート後Cinder volumeの状態がVMからdetach(切り離し)になりNovaとCinderでvolumeの状態がアンマッチになってしまいました。この状態になるとNovaでVMを削除できなくなり、mysqlコマンドで強制的にDBを削除する羽目になりました。

volume_backend_name = ceph
enabled_backends = ceph

NovaのephemeralをcephにするとNo valid hostのエラーになり起動できない問題にずいぶん悩みました。社内の有識者から、"hw_disk_discard = umap”の設定をコメントアウトすると良いと教えてもらいやっと解決。ドキュメント通りの設定では上手くいかない罠でした。

Glanceイメージが削除できなくなる。当初の設定誤りでCinder volumeから起動したVMを削除できなくなった際にDBを強制的にクリアしたのですが、cephのimageファイルはロックされたままになったようでcephのコマンドでも削除ができず、仕方なくceph poolを削除して再設定しました。

OpenStackはDB間のリンクが結構複雑で相互のアンマッチが起きるとリカバリーができなくなる危険があることが分かりました。本番システムで使う場合はしっかりと試験をしないと危険ですね。

Cephのパフォーマンス

当方の環境では、HDD 1台を 2パーティションに分割してOSD x 2の構成で運用しているため書き込み速度は遅いです。ddを使って書き込み速度を図ると、普通に書き込んだ場合に比べて約半分の速度しか出ていません。これは同一HDDに対してレプリカを書き込むためにライトが2回発生しているためだと思います。OSD用のサーバーを複数設置しないと本来のパフォーマンスは出ないのだと思います。

参考情報


mbed TY51822r3でmbed OSを使う

$
0
0

スッッチサイエンスさんからBLEつきのmbedボード、mbed TY51822r3を買いました。mbed TY51822r3はまだオフィシャルにmbed OSのサポート対象になっていないのですが、mbed OS対応ボードのNordic nRF51-DKと互換性があります(差分は動作クロックが32MHzとなっており、Nordic nRF51-DK の16MHzと異なること)。差分がクロック周波数だけならきっとmbed OSが動いてBLEで遊べるだろうと思い、ポッチてみた次第です。

ということで、mbed TY51822r3でmbed OSを動かしてみました。最初はNordic nRF51-DK をターゲットボードに指定して、ダウンロードしたSDKのSystemInit関数を書き換えて32MHzクロックを有効にすれば良いのかと思っていたのですが、スイッチサイエンスさんの @ytsuboi さんがご自身のGitHubでmbed TY51822r3用のターゲット定義情報や32MHzクロックに対応したSDKをすでに公開されていることを教えていただき、このリソースを活用させていただきました。

mbed OSで開発を行う際には、最初にターゲットにするボードを指定して定義ファイルをダウンロードする必要があります。ターゲットとするボードがmbed OSのオフィシャルサイトに登録されている場合は、そのボード名を指定すればよいのですが(例えば、Nordic nRF51-DK の場合はnrf51dk-gcc)、プライベートなGitHubに登録されている定義情報をurlやgitリポジトリーを指定してダウンロードする方法がわかりませんでした(色々と試したのですがうまくいかず)。そのため、mbed TY51822r3用の定義ファイルを一旦ローカルにダウンロードして、ローカルレボジトリとしてターゲット指定する方法でビルドしています。このあたりについては、もっとうまいやり方があればコメント下さい。

以下、mbed TY51822r3でmbed OS対応のアプリをビルドする手順を示します。

ビルドはOS Xで行っています。
mbed OSのフィルを格納するディレクトリを~/mbedOSとします。

$ cd ~/mbedOS

# mbed TY51822r3用の定義ファイルをダウンロード
$ git clone https://github.com/ytsuboi/target-ty51822r3-gcc

# mbed TY51822r3用のSDKをダウンロード
$ git clone https://github.com/ytsuboi/nrf51-sdk

$ mkdir ty51822-blink                  # 作成するプロジェクトのフォルダーを作成
$ cd ty51822-blink 
$ yotta init                           # プロジェクトの初期化

$ cd ~/mbedOS/target-ty51822r3-gcc/    # DLしたターゲットファイルのフォルダーに移動
$ yotta link-target                    # ローカルリンクを設定
$ cd ~/mbedOS/ty51822-blink/           # プロジェクトフォルダーに戻る
$ yotta link-target ty51822r3-gcc    # ローカルリンクのターゲット名として、ty51822r3-gccを登録
$ yotta target ty51822r3-gcc           # ローカルリンクty51822r3-gccをターゲトに指定

$ yotta target                         # 関連ファイルDL後にターゲットが設定されていることを確認
ty51822r3-gcc 1.0.0 -> /Users/kenshi/mbedOS/target-ty51822r3-gcc
nordic-nrf51822-gcc 1.0.0
mbed-gcc 1.2.2

$ yotta install mbed-drivers           # ドライバーのダウンロード
$ cd ~/mbedOS/nrf51-sdk/script         # ローカルにDLしたSDKファイルのフォルダーに移動

# スクリプトを実行してSDKをプロジェクトフォルダーにコピー
# command line: pick_nrf51_files.py  <SDKフォルダーのパス> <プロジェクトフォルダーのパス>
$ python pick_nrf51_files.py ~/mbedOS/nrf51-sdk/ ~/mbedOS/ty51822-blink/

$ cd ~mbedOS/ty51822-blink/            # プロジェクフォルダーに移動
<add source/app.cpp>                   # ソースファイルを書く 
$ yotta build                          # ビルド

# 出来上がったhexファイルをmbedにコピーして実行
$ cp build/ty51822r3-gcc/source/ty51822-blink.hex /Volumes/MBED/

サンプルーコード(普通のLチカです…)

#include "mbed-drivers/mbed.h"

static void blinky1(void) {
    static DigitalOut led1(LED1);
    led1 = !led1;
    printf("LED1 = %d \r\n",led1.read());
}

void app_start(int, char**) {
    minar::Scheduler::postCallback(blinky1).period(minar::milliseconds(100));
}

無事mbed OSでLチカが動きました。mbed TY51822r3はGPIOのドライブ能力が0.5mAと小さく、LEDを直接ドライブすることができないため、FETを使ってスイッチングする必要があります。


FPGA始めました

$
0
0

最近FPGAにハマっています。きっかけは、写真の「ディジタル回路設計とコンピュータアーキテクチャー ARM版」を買って、この本の解説に従って、FPGAボードのDE0-CVを使って最小限のARM命令が動くCPUコアをFPGAで作るようになって、FPGAのお手軽さと面白さに目覚めました。自分で部品をハンダ付け・配線して実際のハードを作るのは時間もなく(ましてやカスタム基盤を設計する根性もなく)、FPGAで回路を合成してシミュレーションすると、なんとなく回路設計したような気持ちになれ、ソフトを書くのとはまた違った面白さがあります。

IMG_0977.jpg

今回買ったFPGAボードはAlteraのCyclone Vを搭載したDE0-CVをマルツオンラインで購入しました。DE0-CVにした理由は、AlteraとXilixのどちらにしようかと思ったのですが、「ディジタル回路設計とコンピュータアーキテクチャー」でAlteraの開発環境やDE2-115ボードが紹介されていたこと、DE2-115は高価で手が出ないので、安価ですが7SEG-LEDなど表示系が比較的充実したDE0-CVを購入しました。

最初は「ディジタル回路設計とコンピュータアーキテクチャー」に掲載されているSystem Verilogのコードをそのまま打ち込んだだけですが、入力ミスを見つけるために、初めて使うAlteraの開発環境Quartus Primeと格闘しながら論理ミュレーションを動かすなどして、実際にARM命令が動くCPUが作れた時は結構感動しました。この本は、FPGAで実際に動くCPUを作ってみるためには絶好の名著だと思いますので、興味のある方は是非買ってみて下さい。Amazonのレビューに、私の書評も掲載しています。


その後コードを拡張して、演習問題のパイプライン化は自力で動かすことができました。また、パイプライン化に加えて、MOV/CMP/EOR(XOR)/ROR/LSR/ASR/LSL命令を追加しています。

現在はDE0-CVに搭載されているSD-RAMにアクセスできるようにしようとしているのですが、FPGA内臓メモリーはノーウェイトでアクセスできることに対してSD-RAMはノーウェイトではアクセスできないため、パイプラインの制御に改造が必要でまだうまくいっていません(LDR命令でメモリーから読みだすまでのパイプラーンストールに加えて、読みだした値をレジスタファイルにライトバックするあたりのパイプラインストールの追加考慮が必要なようです)。SD-RAMアクセスの単体試験はQsysで合成した出来合いのIPを使うことでできているのですが。

DE0-CVを使ったミニARMのコードなどは、オリジナル書籍発売元やARM社の版権もあると思うので、私が作ったHDLコードを掲載するのはやめておきます。

ZYBOでPmodCLP(Parallel Interface LCD)を動かす

$
0
0

FPGAハマりの勢いで、Alteraに加えて、Xilinxにも手を出してしまいました。

今回買ったのは、XilinxのSoC (ARMコア)付きFPGA Zynqを搭載したZYBOを秋月さんで購入しました。SoC付きのFPGAを試すのなら、Altera系のDE1-SoCが自然な流れですが、HDLでなくC/C++を使った高位合成(HLS)が無償で使えるXilixの開発環境(Vivado)も試したくなって、Xilinxに手を出してしまいました。下の写真の上段がDE0-CVで、下段が今回買ったZYBOです。

IMG_1084.jpg

高位合成のお試しは、下の写真にある「FPGAマガジンNo.14」を参考してやろうと思っているのですが、その前にZYBOにLCDを繋いでみました。

IMG_1147.jpg

ZYBOは写真で見て分かる通り、DE0-CVに比べて7SEG-LEDがないなど、表示系デバイスが少ないです。その代わりに、ZYBOにはPmodと呼ばれる拡張コネクターが6個付いており、ZYBOの発売元であるDigilent社から各種のPmod拡張モジュールが販売されています。回路のデバッグを行う際など表示系があると便利だと思い、LEDが8個搭載されたPmod8LD(写真右側)とParallel接続のLCD PmodCLP(写真左側)を購入しました。(FedExを使った送料込みで、$50超となり、ちょっと高かったですが)。

IMG_1148.jpg

Pmodの種別

ZYBOのPmodコネクタにはPS(ARMコア)につながっているPmod MIOと、PL(FPGA部)につながっているStandard Pmod, Hi-Speed Pmod, XADC Pmodがあります。PmodCLPはPmodコネクタ2つを占有するのですが、基板下側に4つ並んでいるPmodは左側1つがStandard Pmodで残りの3つはHi-Speed Pmodです。マニュアルによると、Hi-Speed Pmodは隣接する2つの信号ピンをペアで差動出力として使用することによって高速伝送ができるコネクターで、信号ペアを別々に使うとクロストークが発生すると記載があります。

しかしながら、ZYBOの構成ではどうしても1つはHi-Speed Pmodを使う必要があるので、買ってからこの制約に気がついて、果たしてLCDが動くのか気になっていたのでまずはこちらを試してみました。結果はちゃんと動いています。

今回は、制御信号4pinはStandard-Pmodコネクター(JE)を使用して、データーバス8pinをHi-Speed Pmodコネクター(JD)につなぎました。PmodCLPの回路図を見ると、データーバスには200Ωの抵抗が直列で入っていること(これは短絡対策の保護も兼ねていると思いますが)、差動出力で使う信号ペア(例えば、J1のpin-1とpin-2)をダイオードで2.5V電源にクランプしており、この辺りがHi-speed Pmodコネクターに繋いだ時のクロストーク対策になっているのかしらと思ったりしています(データーバスはFPGAの3.3V出力に接続するのでダイオードを使った過電圧保護は不要だと思うのですが、何のために入っているのか)。

PmodCLP.png

ZynqのIP作成

Zynq(SoC付きFPGA)では まずARMコアありきで、ARMコアに回路ブロックを接続します。Vivadoではライブラリ化された回路ブロック(IP)をZinqに接続していきます。このあたりは、AlteraのQsysと同様の考え方だと思うのですが、QsysよりVivadoのIP Integratorの方が配線やアドレス設定を自動化してくれる度合いが高く使い易い感じがしました。またQsysではTopレベルのHDLは手で書く必要がありますが、Vivadoでは自動生成してくれます。

IPを作成する手順ですが、Digilentが提供している、Vivado用のBorad Fileをインストールしておくと、オンボードのLED/SWはGPIOのメニューに表示されるようになります。ZYBOボードを指定することでZYBOが使用しているZynq FPGAのタイプも自動的に選択してくれるためこのBorad Fileはインストールしておくと便利です。また、Digilentが提供しているPmod-Libraryがあるのですが、PmodCLPとPmod8LDにはライブラリがありません。

そのため、Zynq FPGAにARMコアとGPIOをVivadoのIP Integratorを使って配置して、GPIOにPmodCLPのデータバスと制御信号を接続します。

  • IP IntegratorのCreate Block Designを選択、新規Designを作成。Design名はsystemとしました(IP Integratorの使い方はGetting Started with Zynqに詳細な解説があります)
  • Add IPを使って、Zynq-7 Processing SystemとAXI GPIOを配置して接続します
  • GPIOは「Enable Dual Channel」をチェックして、GPIO2も有効にし、GPIOをデーター用の8-bit入出力(All input/ All output双方をチェックしない)、GPIO2を制御用の4-bitの出力ポートに設定します。
  • GPIOポートの名称を「LCD-DB」、GPIO2のポート名を「LCD-CTL」と設定します。

Vivado_IP.PNG


Vovado-GPIO.PNG

制約ファイルの作成

次に、FPGAのピンとPmodのピン(GPIOの端子)を関連付ける制約ファイルを作成します。この手順は、このブログを参考にさせていただきました。概要は以下の通りです。

  • IP作成ができたら、HDL Wrappperを作成。SourcesウインドウのIP SourcesタブからBlock Design名(今回の例ではsystem)を右クリック、「Create HDL Wrapper...」を選択します。
  • 次に制約ファイルを作成。SourcesウィンドウのHierarchyタブにおいて、Constraints > constrs_1上にて右クリック「Add Source」を選択、Add or create constraints選択して、PmodLCD.xdcの名称で制約ファイルを作成します。
  • Run Implementationを実行した後、Open Implementation Designを選択してOKをクリック、画面下のI/O Portsタブを選択して、GPIOポートのPackage Pin列にFPGAのpin番号を割り付けます。今回は、LCD-DBポート(lcd_db_tri_io[0]〜[7])にPmod JDコネクターに対応したFPGAの端子を割り当て、LDC-CTLポート(lcd_ctl_tri_o[0]〜[3])にはPmod-JEコネクターの下段pin(JE7〜JE10に対応するFPGAのpin)を以下のように割り当てました。
  • 加えて、I/O StdをLVCMOS33に変更します。
  • 割り当てができたら、CTRL-Sでファイルを保存します。

Pmod-xdc.PNG

一度割り当てを作ってしまえば、次からは制約ファイル(PmodLCD.xdc)インポートすることでFPGAのPin割り当てを自動的に行うことができます。

Bitstream(FPGA configデータ)の生成とSDKへのExport

  • Generate Btstramを実行後、File > Export > Export Hardware…から、Include bitstreamチェックしてOKクリック。
  • File > Launch SDKを選択して、EclipseベースのSDKでARMコア用のLCD制御ソフトを作成します。

LCD制御ソフトの作成

DigilentのPmodCLP用「Library and MPLAB Example」にサンプルコードがあるのですが、中身を見るとArduino用のLCDライブラリが入っています。このままでは使えないので、空のC++プロジェクトを作って、コードを以下のようにZynqのベアメタルARM用に書き換えました。

/************************************************************************/
/*                                                                                                                                              */
/*      LCDP.h  --      Declaration for LCDP library                                            */
/*                                                                                                                                              */
/************************************************************************/
/*      Author:         Cristian Fatu                                                                                   */
/*      Copyright 2011, Digilent Inc.                                                                           */
/************************************************************************/
/*  File Description:                                                                                                   */
/*              This file declares LCDP library functions and constants involved*/
/*                                                                                                                                              */
/************************************************************************/
/*  Revision History:                                                                                                   */
/*                                                                                                                                              */
/*      12/10/2011(CristianF): created                                                                          */
/*  21/08/2016(Todotani): adapted fro ZYBO PmodCLP                      */
/*                                                                                                                                              */
/************************************************************************/
#if !defined(LCDP_H)
#define LCDP_H


/* ------------------------------------------------------------ */
/*                              Include File Definitions                                                */
/* ------------------------------------------------------------ */
#include 
#include "xparameters.h"
#include "xgpio.h"
#include "sleep.h"


/* ------------------------------------------------------------ */
/*                                      Definitions                                                                     */
/* ------------------------------------------------------------ */

/* ------------------------------------------------------------ */
/*                                      Errors Definitions                                                      */
/* ------------------------------------------------------------ */

/* ------------------------------------------------------------ */
/*              Command codes Definitions                                                               */
/* ------------------------------------------------------------ */
#define LCDP_CMD_LcdFcnInit     0x38    // function set command, (8-bit interface, 2 lines, and 5x8 dots)
#define LCDP_CMD_LcdCtlInit     0x08    // display control set command
#define LCDP_CMD_LcdClear               0x01    // clear display command
#define LCDP_CMD_LcdRetHome             0x02    // return home command
#define LCDP_CMD_LcdDisplayShift 0x18   // shift display command
#define LCDP_CMD_LcdCursorShift  0x10   // shift cursor command
#define LCDP_CMD_LcdSetDdramPos 0x80    // set DDRAM position command
#define LCDP_CMD_LcdSetCgramPos 0x40    // set CGRAM position command

#define LCDP_MSK_BStatus        0x80            // bit busy
#define LCDP_MSK_ShiftRL        0x04            // shift direction mask
#define LCDP_OPT_DisplayOn      0x4             // Set Display On option
#define LCDP_OPT_CursorOn       0x2             // Set Cursor On option
#define LCDP_OPT_BlinkOn        0x1             // Set Blink On option


/* ------------------------------------------------------------ */
/*                              Parameters Definitions                                                  */
/* ------------------------------------------------------------ */
#define LCDP_DISP_SetOptionDisplayOn    0x4 // Set Display On option
#define LCDP_DISP_SetOptionCursorOn     0x2 // Set Cursor On option
#define LCDP_DISP_SetBlinkOn                    0x1 // Set Blink On option
/* ------------------------------------------------------------ */
/*                                      Class Declarations                                                      */
/* ------------------------------------------------------------ */
#define mskLCDPDat07            0x000000FF

#define LCDP_ERR_SUCCESS                                0               // The action completed successfully 
#define LCDP_ERR_UCHAR_POSITION_INVALID 0x20    // The user character position is not correct
#define LCDP_ERR_ARG_ROW_RANGE                  0x80    // The row index is not valid
#define LCDP_ERR_ARG_COL_RANGE                  0x40    // The column index is not valid 

#define LCDP_NO_ROWS    2
#define LCDP_NO_COLS    40
#define LCDP_NO_UCHARS  8

/* ------------------------------------------------------------ */
/*                                      Control Singnals                                                        */
/* ------------------------------------------------------------ */
#define RS                              0b0001          // Register Select: High for Data Transfer, Low for Instruction Transfer
#define RW                              0b0010          // Read/Write signal: High for Read mode, Low for Write mode
#define EN                              0b0100          // Read/Write Enable: High for Read, falling edge writes data
#define BL                              0b1000

class LCDP {
private:
        XGpio PmodLCD;
        uint8_t control;
        
        uint8_t m_bDisplayMode;

        uint8_t ReadByte();
        void WriteByte(uint8_t bData);

        void WaitUntilNotBusy();
        void WriteCommand(uint8_t bCmd);
        void WriteDataByte(uint8_t bData);

        void SetWriteCgramPosition(uint8_t bAdr);
        void SetWriteDdramPosition(uint8_t bAdr);
        uint8_t ReadStatus();
public:

        LCDP();

        void begin();

        void DisplayClear();
        void ReturnHome();
        void SetDisplay(bool fDisplayOn);
        void SetCursor(bool fCursorOn);
        void SetBlink(bool fBlinkOn);   
        void SetBacklight(bool fBacklightOn);
        uint8_t SetPos(uint8_t idxLine, uint8_t idxCol);        
        uint8_t WriteStringAtPos(char *szLn, uint8_t idxLine, uint8_t idxCol);
        void DisplayShift(bool fRight);
        void CursorShift(bool fRight);
        uint8_t DefineUserChar(uint8_t *pBytes, uint8_t bCharNo);
        uint8_t WriteUserCharsAtPos(uint8_t* rgCharPos, uint8_t bNoChars, uint8_t idxLine, uint8_t idxCol) ;
        };

#endif
/************************************************************************/
/*                                                                                                                                              */
/*      LCDP.cpp                --      Definition for LCDP library                             */
/*                                                                                                                                              */
/************************************************************************/
/*      Author:         Cristian Fatu                                                                                   */
/*      Copyright 2011, Digilent Inc.                                                                           */
/************************************************************************/
/*  File Description:                                                                                                   */
/*              This file defines functions for LCDP                                                    */
/*                                                                                                                                              */
/************************************************************************/
/*  Revision History:                                                                                                   */
/*                                                                                                                                              */
/*      12/10/2011(CristianF): created                                                                          */
/*  21/08/2016(Todotani): adapted fro ZYBO PmodCLP                      */
/*                                                                                                                                              */
/************************************************************************/


/* ------------------------------------------------------------ */
/*                              Include File Definitions                                                */
/* ------------------------------------------------------------ */

#include "LCDP.h"


/* ------------------------------------------------------------ */
/*                              Procedure Definitions                                                   */
/* ------------------------------------------------------------ */


/* ------------------------------------------------------------ */
/**        Description:
**                      Class constructor. Performs variables initialization tasks
**
**
*/
LCDP::LCDP()
{
        m_bDisplayMode = 0;
}

/* ------------------------------------------------------------ */
/*        LCDP::begin
**
**        Synopsis:
**                              
**        Description:
**                              This function saves the pins corresponding to the CLP and performs the required LCDP initialization tasks.
**
*/
void LCDP::begin()
{
        // Initialize Zynq GPIO
        XGpio_Initialize(&PmodLCD, XPAR_AXI_GPIO_0_DEVICE_ID);
        XGpio_SetDataDirection(&PmodLCD, 2, 0x0);           // Set control port output

        // set control flag 0
        control = 0;

        // perform initialization sequence, according to datasheet
        //      wait 20 ms
        usleep(20*000);
        // Set function
        WriteCommand(LCDP_CMD_LcdFcnInit);
        // Wait 37 us
        usleep(37);

        // display on, no cursor, no blinking
        SetDisplay(true);
        SetBacklight(true);

        // Wait 37 us
        usleep(37);
        // Display Clear
        DisplayClear();
        // Wait 1.52 ms
        usleep(1520);
}


/* ------------------------------------------------------------ */
/*        ReadByte
**
**        Return Values:
**                uint8_t - the byte that was read
**
**        Description:
**                              The function implements a CLP read sequence. 
**                              The function is used to read data (RS set before calling this function) or to read status (RS cleared before calling this function).
**                              The function implements two approaches of handling data pins.
**
*/
uint8_t LCDP::ReadByte()
{
        uint8_t bData = 0;

        // Set data port input
        XGpio_SetDataDirection(&PmodLCD, 1, 0xFF);

        // Set RW
        control |=  RW;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        // Set EN
        control |=  EN;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        bData = XGpio_DiscreteRead(&PmodLCD, 1);

        // Clear EN
        control &= ~EN;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        // Clear  RW
        control &=  ~RW;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        return bData;
}

/* ------------------------------------------------------------ */
/*        WriteByte
**
**        Parameters:
**                              uint8_t bData - data to be written to display
**
**        Return Values:
**                void 
**
**        Description:
**                              The function implements a CLP write sequence. 
**                              The function is used to write data (RS set before calling this function) or to write commands (RS cleared before calling this function).
**                              When writing data it writes in DDRAM or CGRAM according to the last set write position.
*/
void LCDP::WriteByte(uint8_t bData)
{
        // Set data port output
        XGpio_SetDataDirection(&PmodLCD, 1, 0x00);

        // Clear RW
        control &= ~RW;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        // Set Enalbe
        control |= EN;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        XGpio_DiscreteWrite(&PmodLCD, 1, bData);
        
        // Clear Enable
        control &= ~EN;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        // Set Read
        control |= RW;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
}

/* ------------------------------------------------------------ */
/*        ReadStatus
**
**        Return Values:
**                uint8_t - the byte that was read.
**
**        Description:
**                              Reads the status of the CLP. It clears the RS and calls ReadByte() function.
**
*/
uint8_t LCDP::ReadStatus()
{
        uint8_t bStatus;
        // clear RS
        control &= ~RS;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        
        // read status byte
        bStatus = ReadByte();
        return bStatus;
}

/* ------------------------------------------------------------ */
/*        WaitUntilNotBusy
**
**        Synopsis:
**                              WaitUntilNotBusy()
**
**        Return Values:
**                void 
**
**        Description:
**                              Waits until the status of the CLP is not busy. This function relies on ReadStatus().
**
*/
void LCDP::WaitUntilNotBusy()
{
        uint8_t bStatus;
        bStatus = ReadStatus();
        while (bStatus & LCDP_MSK_BStatus)
        {
                usleep(10);
                bStatus = ReadStatus();
        }
}

/* ------------------------------------------------------------ */
/*        WriteCommand
**
**        Synopsis:
**                              WriteCommand(cmdLcdClear);
**        Parameters:
**                              uint8_t bCmd    - the command code byte
**
**        Description:
**                              Writes the specified byte as command. When the device is ready it clears the RS and writes byte.
**
*/
void LCDP::WriteCommand(uint8_t bCmd)
{
        // wait until LCD is not busy
        WaitUntilNotBusy();

        // Clear RS
        control &= ~RS;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        // Write command byte
        WriteByte(bCmd);
}


/* ------------------------------------------------------------ */
/*        WriteDataByte
**
**        Synopsis:
**                              WriteDataByte(pBytes[idx]);
**        Parameters:
**                              uint8_t bData           - the data byte
**
**        Description:
**                              Writes the specified byte as data. When the device is ready it sets the RS and writes byte.
**
*/
void LCDP::WriteDataByte(uint8_t bData)
{
        // wait until LCD is not busy
        WaitUntilNotBusy();

        // Set RS
        control |= RS;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        // Write command byte
        WriteByte(bData);
}

/* ------------------------------------------------------------ */
/*        SetWriteCgramPosition
**
**        Synopsis:
**                              SetWriteCgramPosition(bAdr);
**        Parameters:
**                              uint8_t bAdr    - the write location. The position in CGRAM where the next data writes will put bytes.
**
**        Description:
**                              Sets the CGRAM write position. This is the location where the next data write will be performed.
**                              Be aware that writing to a location auto-increments the write location.
**
*/
void LCDP::SetWriteCgramPosition(uint8_t bAdr)
{
        uint8_t bCmd = LCDP_CMD_LcdSetCgramPos | bAdr;
        WriteCommand(bCmd);
}

/* ------------------------------------------------------------ */
/*        SetWriteDdramPosition
**
**        Synopsis:
**                              SetWriteDdramPosition(bAddrOffset);
**        Parameters:
**                              uint8_t bAdr - the write location. The position in DDRAM where the next data writes will put bytes.
**                                      0x00-0x27 refer to the first row
**                                      0x40-0x67 refer to the second row
**
**        Description:
**                              Sets the DDRAM write position. This is the location where the next data write will be performed.
**                              Be aware that writing to a location auto-increments the write location.
**
*/
void LCDP::SetWriteDdramPosition(uint8_t bAdr)
{
        uint8_t bCmd = LCDP_CMD_LcdSetDdramPos | bAdr;
        WriteCommand(bCmd);
}


/* ------------------------------------------------------------ */
/*        DisplayClear
**
**        Synopsis:
**                              DisplayClear();
**
**        Description:
**                              Clears the display and returns the cursor home (upper left corner).
**
*/
void LCDP::DisplayClear()
{
        WriteCommand(LCDP_CMD_LcdClear);
}


/* ------------------------------------------------------------ */
/*        ReturnHome
**
**        Description:
**                              Returns the cursor home (upper left corner).
**
*/
void LCDP::ReturnHome()
{
        WriteCommand(LCDP_CMD_LcdRetHome);
}

/* ------------------------------------------------------------ */
/*        SetDisplay
**
**        Synopsis:
**                              SetDisplay(true);
**        Parameters:
**                              bool fDisplayOn - Display option
**                                              - true in order to set the display ON   
**                                              - false in order to set the display OFF
**
**        Description:
**                              Sets the display option. If true, display is on, if false, the display is off.
**
*/
void LCDP::SetDisplay(bool fDisplayOn)
{
        if(fDisplayOn)
        {
                m_bDisplayMode |= LCDP_OPT_DisplayOn;
        }
        else
        {
                m_bDisplayMode &= ~LCDP_OPT_DisplayOn;              
        }
        WriteCommand(LCDP_CMD_LcdCtlInit | m_bDisplayMode);
}

/* ------------------------------------------------------------ */
/*        SetCursor
**
**        Synopsis:
**                              SetCursor(true);
**        Parameters:
**                              bool fCursorOn - Cursor option
**                                              - true in order to set the Cursor ON    
**                                              - false in order to set the Cursor OFF
**
**        Description:
**                              Sets the cursor option. If true, Cursor is on, if false, the Cursor is off.
**
*/
void LCDP::SetCursor(bool fCursorOn)
{
        if(fCursorOn)
        {
                m_bDisplayMode |= LCDP_OPT_CursorOn;
        }
        else
        {
                m_bDisplayMode &= ~LCDP_OPT_CursorOn;               
        }
                
        WriteCommand(LCDP_CMD_LcdCtlInit | m_bDisplayMode);
}

/* ------------------------------------------------------------ */
/*        SetBlink
**
**        Synopsis:
**                              SetBlink(true);
**        Parameters:
**                              bool fBlinkOn - Blink option
**                                              - true in order to set the Blink ON     
**                                              - false in order to set the Blink OFF
**
**        Description:
**                              Sets the Blink option. If true, Blink is on, if false, the Blink is off.
**
*/
void LCDP::SetBlink(bool fBlinkOn)
{
        if(fBlinkOn)
        {
                m_bDisplayMode |= LCDP_OPT_BlinkOn;
        }
        else
        {
                m_bDisplayMode &= ~LCDP_OPT_BlinkOn;                
        }
                
        WriteCommand(LCDP_CMD_LcdCtlInit | m_bDisplayMode);
}

/* ------------------------------------------------------------ */
/*        SetBacklight
**
**        Synopsis:
**                              SetBacklight(fBackLight);
**        Parameters:
**                              bool fBl - Backlight option
**                                              - true in order to set the backlight ON 
**                                              - false in order to set the backlight OFF
**
**        Description:
**                              This function turns the backlight on or off, according to the user's selection.
**                              Note that there are CLP Pmods that do not have backlight functionality. Using this function for this type of modules will have no effect.
**
*/
void LCDP::SetBacklight(bool fBacklightOn)
{
        if (fBacklightOn)
                control |= BL;
        else
                control &= ~BL;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
}

/* ------------------------------------------------------------ */
/*        SetPos
**
**        Synopsis:
**                              SetPos(0, 3);
**        Parameters:
**                              uint8_t idxLine - the line where the position will be set
**                              uint8_t idxCol  - the column where the position will be set
**
**
**        Return Value:
**                uint8_t 
**                                      - LCDP_ERR_SUCCESS (0)  - The action completed successfully 
**                                      - a combination (ORed) of the following errors:
**                                              - LCDP_ERR_ARG_ROW_RANGE (0x80) - The row index is not valid
**                                              - LCDP_ERR_ARG_COL_RANGE (0x40) - The column index is not valid 
**        Description:
**                              This function sets the corresponding LCD position. This is used for write position and cursor position.
**                              If position set is invalid (outside the display), errors are returned.
**
**
*/
uint8_t LCDP::SetPos(uint8_t idxLine, uint8_t idxCol)
{
        uint8_t bResult = LCDP_ERR_SUCCESS;

        if (idxLine < 0 || idxLine >= LCDP_NO_ROWS)
        {
                bResult |= LCDP_ERR_ARG_ROW_RANGE;
        }
        if (idxCol < 0 || idxCol >= LCDP_NO_COLS)
        {
                bResult |= LCDP_ERR_ARG_COL_RANGE;
        }
        if (bResult == LCDP_ERR_SUCCESS)
        {
                // Set write position
                uint8_t bAddrOffset = (idxLine == 0 ? 0: 0x40) + idxCol;
                SetWriteDdramPosition(bAddrOffset);
        }       
        return bResult;
        
}

/* ------------------------------------------------------------ */
/*        WriteStringAtPos
**
**        Synopsis:
**                              WriteStringAtPos(szInfo1, 0, 0);
**        Parameters:
**                              char *szLn      - string to be written to LCD
**                              uint8_t idxLine - the line where the string will be displayed
**                              uint8_t idxCol  - the column line where the string will be displayed
**
**
**        Return Value:
**                uint8_t 
**                                      - LCDP_ERR_SUCCESS (0)  - The action completed successfully 
**                                      - a combination (ORed) of the following errors:
**                                              - LCDP_ERR_ARG_ROW_RANGE (0x80) - The row index is not valid
**                                              - LCDP_ERR_ARG_COL_RANGE (0x40) - The column index is not valid 
**                                              
**
**        Errors:
**                              See Return Value
**
**        Description:
**                              The function writes the specified string at the specified position (line and column). 
**                              It sets the corresponding write position and then writes data bytes when the device is ready.
**                              Strings that span over the end of line are trimmed so they fit in the line.
**                              If position is invalid (outside the display), errors are returned.
**
*/
uint8_t LCDP::WriteStringAtPos(char *szLn, uint8_t idxLine, uint8_t idxCol)
{
        uint8_t bResult = SetPos(idxLine, idxCol);
        if (bResult == LCDP_ERR_SUCCESS)
        {
                // Strings that span over the end of line are trimmed so they fit in the line.
                int len = strlen(szLn);
                if(len + idxCol > LCDP_NO_COLS)
                {
                        len = LCDP_NO_COLS - idxCol;
                        szLn[len] = 0; // crop the string at this position  
                }

                uint8_t bIdx = 0;
                while(bIdx < len)
                {
                        WriteDataByte(szLn[bIdx]);
                        bIdx++;
                }
        }       
        return bResult;
        
}


/* ------------------------------------------------------------ */
/*        DisplayShift
**
**        Synopsis:
**                              DisplayShift(fBtn1Process);
**        Parameters:
**                              bool fRight - parameter indicating the direction of the display shift
**                                              - true in order to shift the display right
**                                              - false in order to shift the display left
**
**        Description:
**                              This function shifts the display one position right or left, depending on the fRight parameter.
**
**
*/
void LCDP::DisplayShift(bool fRight)
{
        uint8_t bCmd = LCDP_CMD_LcdDisplayShift | (fRight != false ? LCDP_MSK_ShiftRL: 0);
        WriteCommand(bCmd);
}


/* ------------------------------------------------------------ */
/*        CursorShift
**
**        Synopsis:
**                              CursorShift(fBtn1Process);
**        Parameters:
**                              bool fRight - parameter indicating the direction of the cursor shift
**                                              - true in order to shift the cursor right
**                                              - false in order to shift the cursor left
**
**        Description:
**                              This function shifts the cursor one position right or left, depending on the fRight parameter.
**
*/
void LCDP::CursorShift(bool fRight)
{
        uint8_t bCmd = LCDP_CMD_LcdCursorShift | (fRight != false ? LCDP_MSK_ShiftRL: 0);
        WriteCommand(bCmd);
}

/* ------------------------------------------------------------ */
/*        DefineUserChar
**
**        Synopsis:
**                              MyLCDP.DefineUserChar(defChar2, 2);
**        Parameters:
**                              uint8_t *pBytes - pointer to the string that contains the 8 bytes definition of the character. 
**                              uint8_t bCharNo - the position of the user character to be saved in the memory 
**
**
**        Return Values:
**                uint8_t 
**                                      - LCDP_ERR_SUCCESS (0)                          - The action completed successfully 
**                                      - LCDP_ERR_UCHAR_POSITION_INVALID       (0x20) - The user character position is not within 0 - 7 
**
**        Description:
**                              This function writes the specified number of bytes to CGRAM starting at the specified position.
**                              It sets the corresponding write position and then writes data bytes when the device is ready.
**                              If the user character position is not within 0 - 7 range, error is returned. 
**
*/
uint8_t LCDP::DefineUserChar(uint8_t *pBytes, uint8_t bCharNo)
{
        uint8_t bResult = LCDP_ERR_SUCCESS;

        if (bCharNo >=0 || bCharNo < LCDP_NO_UCHARS)
        {       
                uint8_t bAdr = bCharNo << 3; // multiply by 8
                // Set write position to CGRAM
                SetWriteCgramPosition(bAdr);
                uint8_t len = 8;
                // Write the string of bytes that define the character to CGRAM
                uint8_t bIdx = 0;
                while(bIdx < len)
                {
                        WriteDataByte(pBytes[bIdx]);
                        bIdx++;
                }
                bResult = LCDP_ERR_SUCCESS;
        }
        else
        {
                bResult = LCDP_ERR_UCHAR_POSITION_INVALID;
        }       
        return bResult; 
}

/* ------------------------------------------------------------ */
/*        WriteUserCharsAtPos
**
**        Synopsis:
**                              WriteUserCharsAtPos(szInfo1, 0, 0);
**        Parameters:
**                              rgCharPos - an array containing the index (position) of the user characters to be displayed 
**                              bNoChars - an array containing the index (position) of the user characters to be displayed
**                              uint8_t idxLine - line where the string will be displayed
**                              uint8_t idxCol  - the starting position of the string within the line

**
**        Return Value:
**                uint8_t 
**                                      - LCDP_ERR_SUCCESS (0)  - The action completed successfully 
**                                      - a combination (ORed) of the following errors:
**                                              - LCDP_ERR_ARG_ROW_RANGE (0x80) - The row index is not valid
**                                              - LCDP_ERR_ARG_COL_RANGE (0x40) - The column index is not valid 
**                                              - LCDP_ERR_UCHAR_POSITION_INVALID       (0x20) - The user character position is not within the accepted range (0 ? 7)
**
**      Description:
**              This function displays one or more user defined characters at the specified positions on the LCD. 
**              If the position set or the user character position is not correct, errors are returned.
**              
**
**
-----------------------------------------------------------------------*/
uint8_t LCDP::WriteUserCharsAtPos(uint8_t* rgCharPos, uint8_t bNoChars, uint8_t idxLine, uint8_t idxCol) 
{
        uint8_t bResult = SetPos(idxLine, idxCol);
        if (bResult == LCDP_ERR_SUCCESS)
        {
                // validate the user character positions to be between 0 and 7
                uint8_t bIdx = 0;
                while(bIdx < bNoChars)
                {
                        if (rgCharPos[bIdx] < 0 || rgCharPos[bIdx] >= LCDP_NO_UCHARS)
                        {
                                bResult = LCDP_ERR_UCHAR_POSITION_INVALID;
                                bIdx = bNoChars; // force out, no need to continue
                        }                       
                        bIdx++;
                }
                if (bResult == LCDP_ERR_SUCCESS)
                {
                        //set the write position of the cursor to the wanted line/column for displaying custom chars
                        uint8_t bAddrOffset = (idxLine == 0 ? 0: 0x40) + idxCol;
                        SetWriteDdramPosition(bAddrOffset);
                        //send the position of the user character to be displayed
                        uint8_t bIdx = 0;
                        while(bIdx < bNoChars)
                        {               
                                WriteDataByte(rgCharPos[bIdx]);
                                bIdx++;
                        }
                }
        }
        return bResult;
}

LCDに表示を行う簡単なmain.cppを以下のように作成

#include "LCDP.h"
#include "sleep.h"
#include 

int main()
{
        LCDP lcd;
        char buff[16];
        char message[16] = "Hello ZYBO";

        lcd.begin();
        int i = 0;

        while(1) {
                lcd.WriteStringAtPos(message, 0, 0);
                sprintf(buff, "%d", i);
                lcd.WriteStringAtPos(buff, 1, 0);
                i++;
                usleep(50*1000);
        }

        return 0;
}

プログラムの実行

ビルドを行い以下の手順でFPGAのconfigとプログラムの実行を行います。

  • ZYBOのProgramming Mode JumperをJTAGにセット
  • Xilix Tools → Program FPGAを実行。LD10のグリーンLED(DONE)が点灯すればFPGAのconfigが完了です
  • Run → Run As → Launch on Hardware (GDB)
以下のとおりプログラムが起動します。


JTAGインターフェース経由で起動した場合、電源を切ったりリセットを行うと、FPGAのconfig・PSのプログラムとも消えてしまいます。configやプログラムを永続化したい場合は、QSPIメモリーにbootイメージを書き込む必要があります。この辺りは別途書きます。
参考情報

ZYBOのPSでI2Cを動かしてみた

$
0
0

ZYBOのPS(ARM Core部分)のI2Cを動かしてみました。本当は、OV7670カメラモジュールを使って画像の取り込みをやってみたかったのですが、Amazonで買ったOV7670モジュールがどうも不良品のようで、SCCB(I2Cのサブセットのカメラモジュール制御プロトコル)を使ってカメラモジュールとどうしても通信ができず、その過程で分かったIC2の使い方を書いています。

カメラモージュールの実験は、代替え品をaitendoさんに注文したので、商品が届いたら出直しです。Amazonのもaitendoさんのもカメラモジュール自体は同じものを使っていると思いますが、回路構成が若干異なり、aitendoの製品の方が使いやすと思います。理由は、Amazonで販売しているモジュールはパワーオンリセットやI2C信号線のプルアップがないためです。ペリフェラル側にプルアップがあると、ブレッドボードなどを経由してプルアップの配線をする必要がないので構成がスッキリします。

プルアップはFPGA内蔵のプルアップ機能を使う手もありますが、外付け抵抗を使ったプルアップの方が安定して動くと思います。ちなみに、今回の実験で使ったTMP102温度センサーは外付けプルアップ抵抗が必要で、FPGAのプルアップではクロックを10KHzに落としても正常に動作しませんでした。FPGAの内蔵プルアップはweak pull-upなので高速動作には使えないというようなフォーラムの書き込みがありました。

Update:FPGA内蔵のプルアップで動作しなかったのは、200Ωの保護抵抗が直列に入っているStandard Pmod (JE)にI2Cセンサーをつないだ時の場合でした。試しに保護抵抗が入っていない、Hi-Speed Pmod (JD)にFPGA内蔵のプルアップでつないでみたたらクロック100KHzでも動作しました。条件がよければFPGA内蔵のプルアップでも動作しますが、10KΩ程度の外部抵抗を使った方がより安定していると思われます。

ZYNQ PSのI2Cを使う方法

ZYBOでZYNQ PSのI2Cを使う方法は、以下の2通りのやり方があります:

  • PL(FPGAブロック)を介して、ZYBOのPmodコネクタ(JB〜JE)に接続する
  • PS(CPUブロック)直結のPmod MIO(JF)に接続する

ここでは、それぞれの場合について試してみます。開発環境は執筆時点で最新の、Vivado 2016.2を使用しています。

PL(FPGAブロック)を介して、ZYBOのPmodコネクタ(JB〜JE)に接続する方法

Vivadoで新規プロジェクトを作成して、IP Integrator(IPI)で新規のデザインを作成します(今回はsystemというデザイン名にしています)。図のようにZYNQ 7 Processing Sysemのみをインスタンス化してクロックの接続を手動で行います。

Block Design

ZYNQ 7 Processing Systemのアイコンを右クリックして、”Customize Block..”メニューを開きます。

Customize Block

MIO Configurationをクリックして、IO Peripheralsのプルダウンを開き、I2C0にチェックを入れます。また、IOに「EMIO」を指定します。EMIOを指定することによって、CPUコアのI2C信号がFPGAのPL部分を通って外部に接続できるようになります。

I2C EMIO

ブロックデザインに戻って、ZYNQ 7のIIC_0を右クリックして”Make External”メニューを選択します。この操作によって、I2Cの信号をFPGAから外部に出力できるようになります。外部出力を作る方法には、”Create Interface Port.."などオプション指定ができる方法もありますが、今回はMake Externalで問題ありませんでした。

I2C Make External

次に、Block DesignのSource画面に移って、デザイン名(今回の場合はsystem)を右クリックし、”Create HDL Wrapper..”を選択します。

CreateHDL Wrapper

以下のダイアログボックスが表示されるので、”Let Vivado manage wrapper and auto-update”を選択してOKをクリック。

Let Vivado Manage Wrapper

Flow Navigatorから「Run Implementation」を実行。Implementationが終了すると以下のダイアログボックスが表示されるので、”Open Implemented Design”を選択。

Implementation Complete

画面下に表示される「IO Ports」タブを開くとIIC_0ポートのピンアサイン画面が表示されます。ここに信号を接続したいFPGAのピン番号を入力します。

IO Port

ZYBOのReference Manualを参照して、今回は信号をPmod JE(Standard Pmod)のJE1とJE2に接続します。それぞれに対応するFPGAのピン番号V12とW16を入力、出力電圧をLVCMOS33(3.3V)に指定します。

Assign PL Pin

CTL-Sキーを押すと、Constraintsを保存するダイアログボックスが表示されるのでOKをクリック。

Save Constraints

ファイル名を指定して保存すると、SourcesにConstraints(制約)ファイルが追加されています。

Constraints Added

続けて、Flow Navigatorから「Generate Bitstream」を実行。実行が完了すると以下のダイアログボックスが表示されるので、”Open Implementation Design”を指定してOKをクリック。

Genarate BitStream

File Menu → Export → Export Hardware..を選択。Include bitstreamをチェックしてOKをクリック。

Expot Hardware

File Menu → Launch SDKを選択。SDKが立ち上がります。

SDK Launched

SDKのFile Menu → New → Application Projectを選択。New Projectの設定画面にプロジェクト名(今回はI2C Test)を入力し、Nextをクリック。

New Project

Hello World Templateを選択します(このテンプレートにはprint文を使ってUARTにデバッグ情報を出力するために必要なファイルが含まれているため)。

Select Hello World

プロジェクトが生成されたら、helloworld.cをリネーム。今回は、i2c_test.cにしています(これは好みですが)。

Rename helloworld c

テンプレートが自動生成したソースを全部削除して、以下のコードを入力。 

 
#include "platform.h"
#include "xparameters.h"
#include "sleep.h"
#include "xiicps.h"
#include "stdio.h"


// I2C parameters
#define IIC_SCLK_RATE		100000	// clock 100KHz
#define TMP102_ADDRESS		0x48	// 7bit address
#define IIC_DEVICE_ID		XPAR_XIICPS_0_DEVICE_ID

XIicPs Iic;

int Init()
{
	int Status;
	XIicPs_Config *Config;	/**< configuration information for the device */

	Config = XIicPs_LookupConfig(IIC_DEVICE_ID);
	if(Config == NULL){
		printf("Error: XIicPs_LookupConfig()\n");
		return XST_FAILURE;
	}

	Status = XIicPs_CfgInitialize(&Iic, Config, Config->BaseAddress);
	if(Status != XST_SUCCESS){
		printf("Error: XIicPs_CfgInitialize()\n");
		return XST_FAILURE;
	}

	Status = XIicPs_SelfTest(&Iic);
	if(Status != XST_SUCCESS){
		printf("Error: XIicPs_SelfTest()\n");
		return XST_FAILURE;
	}

	XIicPs_SetSClk(&Iic, IIC_SCLK_RATE);
	printf("I2C configuration done.\n");

	return XST_SUCCESS;
}

int i2c_write(XIicPs *Iic, u8 command, u16 i2c_adder)
{
	int Status;
	u8 buffer[4];
	buffer[0] = command;

	Status = XIicPs_MasterSendPolled(Iic, buffer, 1, i2c_adder);

	if(Status != XST_SUCCESS){
		return XST_FAILURE;
	}

	// Wait until bus is idle to start another transfer.
	while(XIicPs_BusIsBusy(Iic)){
		/* NOP */
	}

	return XST_SUCCESS;
}


int i2c_read(XIicPs *Iic, u8* buff, u32 len, u16 i2c_adder)
{
	int Status;

	Status = XIicPs_MasterRecvPolled(Iic, buff, len, i2c_adder);

	if (Status == XST_SUCCESS)
		return XST_SUCCESS;
	else
		return -1;
}


int main()
{
	init_platform();
	Init();

	u8    buff[4];
	u16   rawdata;
	float temp;

	while(1) {
		i2c_write(&Iic, 0, TMP102_ADDRESS);
		i2c_read(&Iic, buff, 2, TMP102_ADDRESS);

		rawdata = ((int8_t)buff[0] << 4) | ((u8)buff[1] >> 4);
		temp = (float) ((float)rawdata * 0.0625);
		printf("Tmep: %2.1f\n", temp);
		usleep(1000*1000);		// sleep 1sec (1000 x 1000us)
	}

	cleanup_platform();
	return 0;
}

ファイルをセーブすると自動的にビルドが実行されます。次に、ツールバーのProgram FPGAボタンをクリックしてFPGAのコンフィグデーター(Bitstream)をJTAGインタフェース経由で転送します(ZYBOのJP5ジャンパーピンをJTAGに設定しておくこと)。

Program FPGA

続いて、デバッガーを起動してPS(ARM CPUコア)のプログラムを転送しますが、ちょっとコツがあります。私の環境では、Debgug Configurationをいきなり作って起動しようとするとエラーが出てプログラムの起動に失敗することが多いです。そのため、Project ExplorerのI2C_Testプロジェクトを右クリックし、Debug As → Launc on Hardware (GDB)を選択してまずプログラムを起動します。無事プログラムが起動するとmainの最初の行でプログラムがブレークします。

Debug Started

この段階ではDebug Configurationをしていないため、"STDIO not connected”の警告がConsoleに出力され、print文の実行結果は表示されません。ここで一旦、ツルーバーのTerminateボタンを押してプログラムを終了します。デバッグPerspectiveから一旦C/C++ Perspectiveに戻ってProject Explorer → I2C_Testプロジェクトを右クリック → Debug As → Debug Configurations..を選択。STDIO ConnectionにCOMポート番号と通信速度(115200)を設定してDebugをクリック。ツールバーのResumeボタンをクリックするとプログラムが動き出します。

Debug Configurations

TMP102から読み取った温度がConsole画面に表示されています。

Program Run

今回使っているI2CドラバーライブラリのサンプルやドキュメントはSDKのインストールフォルダーの中(C:\Xilinx\SDK\2016.2\data\embeddedsw\XilinxProcessorIPLib\drivers)にあるので、コードの中身の説明は割愛します。

以上が、I2Cの信号をFPGA(PL)経由で取り出す方法です。FPGA内の配線を見ると、I2CのSDAなどI/Oの信号はinとoutの2本の独立した信号としてCPUから出ており、Tri-state Bufferを介してI/Oの信号として外部ピンに接続されています。なんだが回りくどいことをやってるんですね。

SDA iobuf

このIOBUFはVivadoのIPをWrapperの中でインスタンス化して接続していることが以下のHDLコードから分かります。

Wrapper IOBUF

PS(CPUブロック)直結のPmod MIO(JF)に接続する方法

SDKを終了し、VivadoのBlock Designを開き、IIC_0ポートを削除します。

DeleteI 2C Port

Customize Block..を開き、I2C0の接続先を「MIO 10..11」に変更します。

Assign I2C MIO

OKをクリックし、Run Implementationを実行します。実行後のI/O Portsタブを見ると、I2Cの信号が消えていることが分かります。これはI2Cの信号がFPGAブロック(PL)を経由しなくなったことを意味します。

No I2C In PL

Generate Bitstreamを実行し、終了後、Export Hardwareを実行して再度SDKを立ち上げます。SDKのProject ExplorerからI2C_Test_bspを右クリックして、Re-generate BSP Sourcesを実行。さらに、Project Menu → Clean..を実行してビルド環境をクリーンアップします。

Clean Build

センサーのI2CのピンをJFコネクタのMIO-10 (JF2: SCL)、MIO-11 (JF3: SDA)に繋ぎ変えてデバックを実行するとプログラムが起動します。今回はCPUから直接I2Cに出力しています。デバッグでプログラムの起動に失敗する場合は、Debgug Configurationに入って既存のデバッグエントリを一旦削除してから、最初の手順ででデバッグを際実行する(先ずはDebug Configを作らずにDebug Asから起動する)とうまくいくと思います。

Delete Debug Target

ということで、PL経由・PS直結のどちらでもI2Cが動くことが確認できました。

参考情報

オシロ(RIGOL DS1054Z)を購入しました

$
0
0

最近FPGA(特にZYBO)をいじるようになって、ちょっとした信号波形の観測がしたいケースが増えてきました。そこで、とうとうオシロスコープを買ってしまいました。以前は日曜大工の電子工作にオシロスコープはもったいないと思って購入を躊躇していたのですが、いざ買って使ってみるとやはり便利です。

購入した機種はRIGOL DS1054Z(中国製)でAmazonから購入しました。同価格帯(5万円台)で買える、Tektronix TBS1052Bとどちらにしようかと少し悩んだのですが、4ch vs 2chでRIGOLの方がch数が多いこと、その他測定機能もRIGOLの方が充実しているように見えたのと、Amazonや他のBlogのレビューを見ても、RIGOLの評価は概ね良かったので、ブランド的にはTextronixですがコスパを取ってRIGOLにしました。

IMG 1175

オシロスコープは学生時代にアナログオシロをちょっと触ったことがある程度で、ディジタルオシロは初めてだったので、まだ使いこなせていませんが、個人用の電子工作用途にはこれで十分だと思います。やっぱり実際の信号波形が見えると動作確認の効率が飛躍的に上がるので、買って良かったと思っています。

マニュアル・表示ともに日本語にも対応しており、マニュアルの日本語が不自然なこともありません(ただ、英語版に比べて更新がやや遅いようですが)。DS1054ZはオプションでI2Cのプロトコル解析やデコードにも対応しているのですが(買った状態では使用時間限定で動かすことができます)、マニュアルを見ただけではさっぱり使いたかが分かりませんでした。Webで検索するとYou-Tubeに実際に操作している画像がアップされており、それを見て使い方が分かりました。英語が主体になりますが、サポート情報も比較的充実しているのでその点も良いと思います。

OV7670 25V PU

今の所気になる点は、ファンの動作音がややうるさいこと(You Tubeには自分でファンを静音型のもの交換している画像もありました)、画面表示をUSBメモリーに保存できるのですが本体にRTCがないらしく、ファイルのタイムスタンプが2015年の固定の日付になってしまい、母艦のMacにコピーした際に時間順にソートできないことです(コンソールから毎回touchコマンドでタイムスタンプを更新する必要があり面倒)。

とは言え、機能的には十分満足しています。まだまだ使いこなせていないので、色々触ってみたいと思っています。

ZYBOでOV7670カメラモジュールの画像を表示する

$
0
0

前回のポストでZYBOのZYNQ PS (ARMコア)を使ってOV7670カメラモジュールをI2C(SCCB)経由初期化できるようになりましたが、ようやく次のステップとしていた画像の取り込みとVGAモニターへの出力ができました。使ったカメラモジュールはaitendoさんから購入したOV7670です。

色々紆余曲折(最初の構想からの見直し)やFPGAを使った同期回路設計のスキル不足でかなり苦戦して、たかだか100行ちょっとのRTLコードを動かすのに1ヶ月近くかかってしまいました。まだまだ未熟ですが、(自己流ですが)少しノウハウがたまったので、顛末を書いてみます。

使用した開発環境は、最初はVivado 2016.2でしたが、最終的にVivado  2016.3にアップグレードしています。

当初の構想

最初は、既存のXILINX Video-OutとVTC IPを使って、画像の取り込み部分だけを作るのが近道かと思い(自分で記述する部分を最小にした方が確実に動かせると思っていた)この方向で情報収集を行いました。

Video Out IPに画像を流し込むためには、カメラ入力側にAXI4-Stream Masterインタフェースを作りこむ必要があります。今にして思うと、いきなりAIX4を作るというのも無謀な試みでした。You TubeにAXI-Stram Masterを作るチュートリアルビデオがあり、(英語ですが平易な語りなのでなんとか理解できますが、量は結構多い)これを参考にカメラデータの取り込みとAXI4-Stram出力のIPを作ってみました。単体のテストベンチではAIX4の制御信号を出力できているように見えていたのですが、Video OutやVTCと結合していきなり実機で動かそうとしても全く画像が表示されず。

当初、OV7670の解像度がVGA(640 x 480)なのでVGAモニターにスルーでデーターを流せると思っていたのですが、オシロで出力波形を観測すると垂直同期(リフレッシュ周期)が24Hz〜30Hz, 水平同期が12.5KHz位で、VGAのリフレッシュレート60Hz, 水平同期31KHzに対して速度が大きく異なるため、そもそもスルーで出力できないということが判明(考えてみれば、3000円以下のカメラモジュールで60Hzのリフレッシュレートを期待するのが間違っている)。Video OutやVTC IPもブラックボックスで中身が分からないため、問題の切り分けができず、表示部分も自分で作った方がよいと方針転換。

AXI4-Stramの実装は結局使い物にはなりませんでしたが、チュートリアルで学んだことはVivadoを使った開発フローを含めて結構有意義でした。

VGA画面表示も自分で作る

カメラモジュールとVGAの動作速度が異なるため、速度差を吸収するために、VRAM(Dual Port RAM)を間に入れて、カメラ入力に同期して画像データをVRAMに書き込み、VGAのPixelレートで画像データーを読み出せるようにする必要があります。

VGAのフル解像度を使うためには、必要なメモリー容量的にZYBOのDDR3-SDRAMを使う必要がありますが、いきなりDDR3-SDRAMを使うのはハードルが高いので、先ずはFPGA内に作れるBRAM(Block RAM)を使うことにしました。ZYBOに搭載されているZYNQ XC7Z010-1CLG400CのBRAM容量の制約から、VGAのフル解像度を使うのは諦めて、QVGA(320 x 240)で画像を取り込んで、VGA画面(640 x 480)の中央に320 x 240の画像を表示できるようにすることを目標に再設定。

このblogにあるVHDLの実装を参考に(というかほとんどパクっていますが…)Verilogに焼き直して、画像取り込みとVGA出力のIPを作成。参考にしたBlogはRGB444(12bitカラー)の画像データーをBRAMに取り込む際に、下位1bitを抜いた形でメモリーに格納していますが(BRAMの容量制約のために、19bit中の上位18bitをアドレスに使っている)、このようなデーターの間引きを行うとどういう形で画像データーが表示できるのかイメージができなかったのと、全部パクリでなく、多少オリジナルにしたかったため、RGB 565(16 bitカラー)のQVGAデーターを扱えるように改造しました。

再度カメラ画像取り込みとVGA表示のIPを作って単体でテストベンチを動かすと一応動いているように見えるので、BRAMやカメラモジュールと結合して実機で動かすとやはり画像が出力されず(画面が真っ暗)。オシロでVGA出力の信号を観測すると、VSYNCとHSYNCは正常に出力されているので、部分的には動作しているがどこが悪のか切り分けができず(後で、BRAMの読み出し側クロックを配線していなかったという、初歩的なチョンボだったことが分かるのですが..)。

モジュール単位のテストベンチだけでなく、全体を一気通貫で確認できるシミュレーション環境が必要と思い、以下のようにCPUブロックを除いた、カメラ画像取り込み→ BRAM → VGA出力のIPをつないだBlock Designを作ってシミュレーション環境にしました。

Test Blcok Design

Test Benchを作る際は、Create HDL Wrapperが生成するBlock Designのインスタンスを雛形にして、試験信号生成の部分を追加記述しています。まず、以下の手順でWrapperファイルを生成。

Create HDL Wrapper

生成されたWrapperファイルをベースに以下のテストベンチを記述

 
//Copyright 1986-2016 Xilinx, Inc. All Rights Reserved.
//--------------------------------------------------------------------------------
//Tool Version: Vivado v.2016.3 (win64) Build 1682563 Mon Oct 10 19:07:27 MDT 2016
//Date        : Tue Oct 18 21:28:14 2016
//Host        : iMac2-Win running 64-bit major release  (build 9200)
//Command     : generate_target ov7670_tb_wrapper.bd
//Design      : ov7670_tb_wrapper
//Purpose     : IP block netlist
//--------------------------------------------------------------------------------
`timescale 1 ns / 1 ps

module ov7670_tb_wrapper();

  reg  clka;
  reg  clk25;
  reg  href;
  reg  vsync;
  reg  pclk;
  reg  resetN;
  wire [4:0]vo_b_data;
  wire [5:0]vo_g_data;
  wire [4:0]vo_r_data;
  wire vo_hsync;
  wire vo_vsync;
  reg  [17:0] count;
  reg  [7:0]  test_data;
  reg  [7:0]  test_vector[307200:0];

  // Instantiate device to be tested
  ov7670_tb ov7670_tb_i(
         // Instantiate ov7670_camera
        .pclk(pclk),
        .href(href),
        .data(test_data),
        // Instantiate VGA
        .clk25(clk25),
        .resetN(resetN),
        .vo_b_data(vo_b_data),
        .vo_g_data(vo_g_data),
        .vo_hsync(vo_hsync),
        .vo_r_data(vo_r_data),
        .vo_vsync(vo_vsync),
        .vsync(vsync)
        );


  // Initialize test
  initial
  begin
    resetN <= 0;
    count  <= 17'd0;
    vsync  <= 0;
    href   <= 0;
    #100; resetN <= 1;
  end

  // Generae vsync and href
  // 1t_LINE = (320 + 144) x pclk(84ns)
  initial
  begin
    $readmemh("testdata.txt", test_vector);
    #400; vsync <= 1;
    #400; vsync <= 0;
    #960;
    repeat (240)
      begin
        @(negedge pclk);
        href <= 1; #26880;  // 320 pclk per line
        href <= 0; #1096;   // 144 pclk
      end
    
    #2000;
    
    $display("Vsync");
    #1000; vsync <= 1;
    #400;  vsync <= 0;
    #960;
    repeat (240)
      begin
        @(negedge pclk);
        href <= 1; #26880;  // 320 pclk per line
        href <= 0; #1096;   // 144 pclk
      end

    #400;
    $finish;
  end


  // Generate clcok  12MHz camera pclk in => 84ns
  always
  begin
    pclk <= 1;
    if (href)
      begin
        test_data <= test_vector[count];
        count <= count + 1;
      end
    #42;
    pclk  <= 0;
    #42;
  end

  always
  begin
    clk25 <= 1; #20; clk25 <= 0; #20;   //  25MHz VGA pixel clock
  end

endmodule

試験用のダミーデータを読み込んで、2画面分の出力を行なっています。シミュレーション波形を見ると、hrefが有効になった後、2バイトのデーターを取り込んで、最初のweでBRAMのアドレス0x00000にデーターが書き込めています。タイミング的には動いている模様。

Simulation

BRAMの設定は以下のように、Stand Alone/ Simple Dual Port RAMにしています。

BRAM Setting-1

BRAMのOperating ModeはWrite Firstを選択。BRAMへの書き込みと読み出しクロックは非同期ですが、書き込みと読み出しが競合する可能性がある場合は、Write Fisrtを推奨するとマニュアルに書いてあったのでそうしています。確かにテストベンチのシミュレーションでもcollisonの警告が出ていたりしますが、書き込みと読み出しの競合は避けられないので仕方ありません。

BRAM Setting-2

実機で動作させる

以下のようにPSコアとClock生成を組み込んだBlock Designを作成して実機で動作確認。OV7670の初期化はARMコアのソフト処理で行なっています。

Final Desing

またもや、画面が真っ黒、VSYNCとHSYNCは出ている、、
しばらく悩みましたが、VGA出力側のクロックをBRAMに配線し忘れていたという、超初歩的なミスが発覚。

配線を修正して動かしてみたら、やっと画像が表示されたが、こんな感じでえらく不鮮明。カメラの設定はちゃんとRGBにしているし(YUVとかになっているわけではない)。しばらく、カメラの設定が間違っていないかデーターシートとにらめっこをしながら確認しても、特におかしな箇所は見つからず。 

Camera Image NG

しばらく悩んだのですが、カメラモジュールを壊したのではないかと結論づけて、2個目を購入。2個目が届いて入れ替えたら、やっと動きました!!

今回作ったVerilogコード

<カメラ画像取り込み> 

/* This is a bit tricky href starts a pixel transfer that takes 3 cycles for first pixel
   then 2 cycles after 2nd pixcel

        Input | state after clock tick
         href | wr_hold   data_in           data_out     we address address_next
   cycle -1 x | xx      xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxx  x  xxxx      xxx
    cycle 0 1 | 00      xxxxxxxxRRRRRGGG xxxxxxxxxxxxxxxx  x  xxxx      addr
    cycle 1 1 | 01      RRRRRGGGGGBBBBB  xxxxxxxxRRRRRGGG  x  addr      addr
    cycle 2 1 | 10      GGGBBBBBxxxxxxxx RRRRRGGGGGGBBBBB  1  addr      addr+1
*/

module ov7670_camera(
  input  wire pclk,
  input  wire vsync,
  input  wire href,
  input  wire [7:0] data,
  output wire [16:0] bram_addr,
  output reg [15:0] data_out,
  output reg  we
);

  reg [16:0] address;      // address with one clcok cyle delayed
  reg [16:0] address_next;
  reg [15:0] data_in;
  reg [1:0]  wr_hold;

  assign bram_addr = address;

  always @(posedge pclk)
    begin
     if (vsync)
       begin
         address <= 17'd0;
         address_next <= 17'd0;
         wr_hold <= 2'd0;
       end
     else
       begin
         data_out <= data_in;
         address <= address_next;
         we       <= wr_hold[1];
         wr_hold  <= {wr_hold[0],  (href & ~wr_hold[0])};
         data_in  <= {data_in[7:0], data};
         if (wr_hold[1] == 1)
           begin
             address_next <= address_next + 1;
           end
       end
    end
endmodule

<VGA画面表示> 

 module qvga_to_vga(
  input  wire clk25,
  input  wire resetN,
  output reg  [4:0] vo_r_data,
  output reg  [5:0] vo_g_data,
  output reg  [4:0] vo_b_data,
  output wire vo_hsync,
  output wire vo_vsync,
  output reg [16:0] frame_addr,
  input  wire [15:0] frame_pixel
  );
  
  parameter hRez = 640, hStartSync = 640+16, hEndSync = 640+16+96, hMaxCount = 800;
  parameter vRez = 480, vStartSync = 480+10, vEndSync = 480+10+2,  vMaxCount = 525;
  parameter hDispStart = 160, hDispEnd = 160+320;
  parameter vDispStart = 120, vDispEnd = 120+240;
  
  reg [9:0] hCounter;
  reg [9:0] vCounter;
  reg blank;

  assign vo_hsync = ((hCounter > hStartSync) && (hCounter <= hEndSync))? 0: 1;
  assign vo_vsync = ((vCounter >= vStartSync) && (vCounter < vEndSync))? 0: 1;

  always @(posedge clk25)
    begin
      if (resetN == 0)
        begin
          hCounter <= 10'd0;
          vCounter <= 10'd0;
          frame_addr <= 17'd0;
          blank <= 1;
        end
      else if (hCounter == (hMaxCount - 1) )
        begin
          hCounter <= 10'd0;
          if (vCounter == (vMaxCount - 1))
            begin
              vCounter <= 10'd0;
            end
          else
            begin
              vCounter <= vCounter + 1;
            end
        end
      else
        begin
          hCounter = hCounter + 1;
        end
        
      if (vCounter >= vDispEnd)
        begin
          frame_addr <= 0;
        end
      else if ( ((vCounter >= vDispStart) && (vCounter < vDispEnd)) && ((hCounter >= hDispStart) && (hCounter < hDispEnd)) )
        begin
          blank <= 0;
        end
      else
        begin
          blank <= 1;
        end

      if (blank == 0)
          begin
            vo_r_data <= frame_pixel[15:11];
            vo_g_data <= frame_pixel[10:5];
            vo_b_data <= frame_pixel[4:0];
            frame_addr <= frame_addr + 1;
          end
        else
          begin
            vo_r_data <= 5'd0;
            vo_g_data <= 6'd0;
            vo_b_data <= 5'd0;
          end
    end
      
endmodule

ZYNQ PSを使ったカメラモジュールの初期化部分はコードが長いのでGitHubに置いておきました(初期化パラメーターはmbedの作例を流用)。

ZYBO OV7670

その他

実は、画像は表示できているのですが、画面が鏡に映った形で左右逆になるのと画面が倒立してしいます(aitendoのシルク印刷部分を上にすると画像が倒立する)。そのため、MVEPレジスタのMirror ImageをOn及びVFlipをOnにして反転を回避しています。この点はちょっと謎です。

カメラから読み出したデーターは、カメラのPCLK(Pixel Clock)をBRAMのクロックとして使っています。BRAMのWE信号もPCLKに同期して生成しているため、BRAMクロックの立ち上がりとWEの立ち上がりが同じタイミングになり、WEのSetup Timeマージンが取れていなのではないかと思い、BRAMのクロックを100MHzのFPGAクロックにしてみました(BRAMクロックの立ち上がりエッジでWEを確実に補足するため)。そうすると、逆に若干画像にノイズが乗るような現象が見えたので、BRAMの書き込み側はPCLKに戻しています。

Next Step

ZYBO + OV7670でI2Cを動かすのにもえらく時間がかかりましたが、最初AIX4で遠回りをしたことや、カメラモジュールの不調もありましたが、画像が出せるまで1ヶ月近くかかってしまいました。まだまだ、未熟で、先人の方には数週遅れで同じことをやっていますが、次はVGAのフル解像度データーをDDR3-SDRAMをVRAMとして取り込めるようにしたいと思っています。

その次は、HLSを使って画像処理などのフィルターを挿入できるようにしてみたいと思っています。

参考資料

Vivado Integrated Logic Analyzer(ILA)の使い方

$
0
0

ちょっと小ネタですが、VivadoのIntegrated Logic Analyzer(ILA)を使ったFPGA内の信号観測の方法について記載します。

ILAの使い方は、以下のWebにも詳しい使い方が書いてあるのですが、最新版のVivadoではもっと簡単に設定ができることが分かったのでその内容を記載します(執筆時点ではVivado 2016.3で動作確認しています)。

1. Debug対象信号の指定

マウスの右クリックからMark Debugを選択します。選択した信号にDebugマークがつきます。信号を選択し終わったら、Run connection automationでILAを接続します。以下のように、Debugマークがついた信号がILAに接続されます。

Block Design with ILA

2. 論理合成

Mark DebugをつけたBlock Designで”Run Synthesis”を実行

3. Implementation実行とBitstream生成

通常の手順で”Run Implementation”と”Generate Bitstream”を実行します。

4. デバッグ画面の表示

Hardware Managerを開き、”Open target”をクリック。

Hardware Mnager

Auto Connectをクリック。ターゲットが認識されるので、”Program device”でbitstreamをダウンロードします

Bitstream downloadˇ

ZynqデバイスのFCLK_CLK0など、PSからのクロックをロジックアナライザに使用している場合やPSのプログラムで周辺デバイスの初期化などを行なっている場合はは、Xilinx SDKでプログラムを実行してPSを起動します。

その後、”Refresh device”を実行します。デバッグ画面が表示されます。 Waveformウインドウにデバッグ対象にした信号の一覧が表示されています。

Debug Screen

5. デバッグ

トリガーにしたい信号をWaveformウインドウからTrigger Setupウインドウにドラッグアンドドロップし、トリガー条件を設定します。

Trigger Setup

”Run Trigger for this ILA core”ボタンを押すとトリガー条件を検出時に信号が波形表示されます。

Wave From Display

以前のように制約ファイル(xdcファイル)にデバッグ用のエントリを追加する必要もありません。デバッグが終わったらデバッグ対象の信号を右クリックして”Clear Debug”を実行し、ILAを削除すれば終わりです。

非常にお手軽に信号観測ができます。

ZYBOでOV7670カメラモジュールのVGA画像を表示する

$
0
0

前回行った、OV7670カメラモジュールを使ったQVGA画像の表示を拡張して、VGA画像を表示できるようにしてみました。まだ不完全な部分がありますが、やったことを書いてみます。

全体構想

VGA(640 x 480) x 16bitカラーの画像を扱うためには、600KBのVRAM容量が必要となり、QVGAのようにFPGAのBRAMには格納できないため、ZYBOのPS側に搭載されているDDR3 RAMをVRAMとして使用する必要があります。今回はPL(FPGA部)からDDR3 RAMへのアクセスを行うことがテーマでした。

やり方は以下の3通りが考えられますが、③のAXI-HPを使ったメモリアクセスを使いました。

  1. AXIGPポート経由でアクセス(32bit幅)
  2. ZYNQ DMAエンジンを使ってDMA転送
  3. AXIHPポート経由でアクセス(32/64bit幅)

AXIHPポートを使ったDDR3メモリアクセスのイメージを下記に示します。データー転送側がAXI Masterデバイスとなります。

AXIHP

DDR3アクセスのために、AXI Masterプロトコルを喋るモジュールを作る必要がありますが、HLSで作るのが一番楽だと考え、以下のような構成としました。(インタフェースポートの属性にm_axiを指定するだけであとはHLSがAXI Masterの処理を行なってくれるため)

System_Design

Cameraからのデータ受信モジュールとVGA IFはクロックに従ったデーターの送受信が必要なため、HLSではなくVerilogで作成しています(前回作ったモジュールを拡張)。Verilog - HLS間のインタフェースはAXIS(Stream)を使うことも考えたのですが、どのタイミングでAXISのデーターの切れ目(tvalid)をHLSに伝えればよいかがよく分からなかったため、ピクセル座標をap_hs(ハンドシェーク)で渡すインタフェースとしました。当初はap_hsもそれほど苦労せずに作れるだろうと思ったのですが、ap_hsを動かすのに思いっきりハマりました。

今回の構成では、カメラからのデータの取り込みはOV7670のPCLK(24MHz)に同期する必要がありますが、メモリアクセス部分はFPGA PLに供給されている100MHzのシステムクロック(FCLK)で動作するため、クロックドメインまたがり(Clock Domain Crossing: CDC)の問題に直面し、このせいで(だと思うのですが)当初Cameraデーター受信モジュールが期待通りに動いてくれずかなり悩みました。あまりスマートでないように思うのですが、カメラ側の24MHzクロックをFCLKに同期化することで対処しています。

各モジュールのコード

HLSで書いたMemory Write / Memory Readジュールのコードは以下の通りです。

<Memory Write> 

#include <ap_int.h>
#include <stdint.h>

#define MAXLINE		480
#define MAXCOL		640

void MemWrite(ap_uint line, ap_uint col, uint16_t data, uint64_t *VRAM)
{
// depth 1280byte of lineBuff size is too big (got SIGSEGV fault in C/RTL co-sim),
// but 160 words(64bit) is too small
#pragma HLS INTERFACE m_axi depth=640 port=VRAM offset=direct bundle=VRAMW
#pragma HLS INTERFACE ap_hs port=col
#pragma HLS INTERFACE ap_hs port=line
#pragma HLS INTERFACE ap_hs port=data
#pragma HLS INTERFACE ap_ctrl_hs port=return

	static uint64_t lineBuff[2][MAXCOL/4];
// Force to make 2 line buffers in different BRAM
#pragma HLS ARRAY_PARTITION variable=lineBuff factor=2 dim=1
#pragma HLS RESOURCE variable=lineBuff core=RAM_1P_BRAM
	static uint64_t strData = 0;
	static ap_uint<2> mod = 0;
	ap_uint<1> cur;
	uint64_t inData;


// Data reception block
	cur    = line & 0x0001;
	inData = data;

	switch(mod) {
		case 0: strData = data;
				break;
		case 1: strData = strData | inData << 16;
				break;
		case 2: strData = strData | inData << 32;
				break;
		case 3: strData = strData | inData << 48;
				lineBuff[cur][col/4] = strData;
				break;
	}
	mod++;

	if (col == (MAXCOL - 1)) {
		// Data transfer block
		uint64_t* destPtr = &VRAM[line*MAXCOL/4];
		memcpy(destPtr, lineBuff[cur], MAXCOL*2);
		mod = 0;
		return;
	}

	return;
}

<Memory Read>

#include <ap_int.h>
#include <stdint.h>

#define MAXLINE		480
#define MAXCOL		640

uint16_t MemRead(ap_uint vga_line, ap_uint vga_col, uint64_t* VRAM)
{

#pragma HLS INTERFACE m_axi depth=640 port=VRAM offset=direct bundle=VRAMR
#pragma HLS INTERFACE ap_hs port=vga_line
#pragma HLS INTERFACE ap_hs port=vga_col
#pragma HLS INTERFACE ap_ctrl_hs port=return

	static uint64_t lineBuff[2][MAXCOL/4];
// Force to make 2 line buffers in different BRAM
#pragma HLS ARRAY_PARTITION variable=lineBuff factor=2 dim=1
#pragma HLS RESOURCE variable=lineBuff core=RAM_1P_BRAM

	static uint64_t rdData = 0;
	static ap_uint<2> mod = 0;
	ap_uint<1> cur;
	uint16_t pixcelData;

	cur = vga_line & 0x0001;

	// Transfer data during blanking period
	if (vga_col == 0) {
		uint64_t* srcPtr = &VRAM[vga_line*MAXCOL/4];
		memcpy(lineBuff[cur], srcPtr, MAXCOL*2);
		mod = 0;
	}

	switch(mod) {
		case 0: rdData = lineBuff[cur][vga_col/4];
				pixcelData = (uint16_t)rdData;
				break;
		case 1: pixcelData = (uint16_t)(rdData >> 16);
				break;
		case 2: pixcelData = (uint16_t)(rdData >> 32);
				break;
		case 3: pixcelData = (uint16_t)(rdData >> 48);
				break;
	}
	mod++;

	return pixcelData;
}

内容的には単純な処理で、MemoryWriteはCameraモジュールからCol/Lineの座標データと16bitカラーのデーターを受信し、4ピクセル分のデータを64bit長の配列に格納します。1 Line分(640 pixel)のデータを受信すると水平同期のブランキング期間を使ってデータをDDR3メモリーにバースト転送します。データー転送の時間を短くするためAXIバスは64bit長で動かしています(そのため、ピクセルデーターを64bit単位にパッキングしています)。VRAM portに64bit符号なし整数に対するポインターを(uint64_t *)指定して、インタフェースモードにm_axiを指定すると64bitモードで動いてくれました。

VRAMメモリのオフセット値はdirectを使ってBlock Designのconstantで指定しています(今回は、80_0000Hを使用)。

MemoryReadも同様で、水平同期のブランキング期間を最初に持ってきており、1 Line分のデータをブランキング期間にDDRメモリから読み込んで、VGA IFが要求するpixcel座標に対応したデーターを渡すようにしています。

HLSの合成結果を以下に示します。MemWriteは最小2クロックサイクルで動作し、DDRメモリーにデーター転送を行う際は169クロックかかることが分かります。MemReadは同様な処理を行なっているのですがLatencyが1クロックサイクル多く、1処理に3クロックサイクルを要しています。

MemWrite

MemRead

次に、OV7670カメラモジュールIF、VGA IFのVerilogコードを示します。以前のHDLコードは参考にしたブログのコードそのままなのですが、複数のレジスターを一つのalwaysブロックの中で操作していました。参考書として参照した「FPGAボードで学ぶ組込みシステム開発入門[Altera編]」ではレジスタ毎に独立したalwaysブロックを使うスタイルの記述になっていたため、参考書のスタイルに改めました。生成される回路は異なると思いますが、alwaysブロックで複数レジスタを操作しないのがベストプラクティスな書き方なのかがまだよく分かっていません。

<OV7670カメラモジュールIF>

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:  Kenshi Kamiya
//
// Create Date: 2016/10/11 15:00:13
// Design Name: ov7670_camera
// Module Name: ov7670_camera
// Project Name: ov7670 camera data capture and output data wht ap_hs
// Target Devices: Zybo
// Tool Versions: Vivado 2016.3
//
// Revision: 01
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////

/* This is a bit tricky href starts a pixel transfer that takes 3 cycles for first pixel
   then 2 cycles after 2nd pixcel

        Input | state after clock tick
         href | wr_hold   data_in           data_out     we   col     col_next
   cycle -1 x | xx      xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxx  x  xxxx      xxx
    cycle 0 1 | 00      xxxxxxxxRRRRRGGG xxxxxxxxxxxxxxxx  x  xxxx      col
    cycle 1 0 | 01      RRRRRGGGGGBBBBB  xxxxxxxxRRRRRGGG  x  col       col
    cycle 2 x | 10      GGGBBBBBxxxxxxxx RRRRRGGGGGGBBBBB  1  col       col+1
*/

module ov7670_camera_hs(
  input  wire clk,
  input  wire resetN,
  input  wire pclk,
  input  wire vsync,
  input  wire href,
  input  wire [7:0] data,
  output reg  [9:0] line,
  output reg  [9:0] col,
  output reg  [15:0] data_out,
  output wire  ap_start,
  input  wire ap_idle,
  input  wire ap_done,
  output wire  col_vld,
  output wire  line_vld,
  output wire  data_vld,
  input  wire col_ack,
  input  wire line_ack,
  input  wire data_ack
);

  reg [9:0] col_next;
  reg [9:0] line_next;
  reg [7:0]  data_s;
  reg [15:0] data_in;
  reg [1:0]  wr_hold;
  reg        we;
  reg [2:0]  state;
  reg [3:0] control;
  // Registers for synchronizer
  reg       pclk_d, pclk_s;
  reg       href_d, href_s;
  
  parameter IDLE=0, AP_START=1, OUT_VLD=2, OUT_ACK=3, AP_DONE=4, WAITE_NEXT=5;
  parameter MAXCOL=640, MAXLINE=480;
  
  // Synchronizer for pclk
  always @(posedge clk)
    begin
      pclk_d <= pclk;
      pclk_s <= pclk_d;
    end

  // Synchronizer for href
  always @(posedge clk)
    begin
      href_d <= href;
      href_s <= href_d;
    end
  
  // Synchronizer for input data
  always @(posedge clk)
    begin
      data_s <= data;
    end
  
  // FSM for ap_hs protocol
  always @(posedge clk)
    if (resetN == 0)
      state <= IDLE;
    else
      begin
        case(state)
          IDLE:     begin
                      if (we == 1)
                        begin
                          state <= AP_START;
                        end
                      else
                        begin
                          state <= IDLE;
                        end
                    end
          AP_START: begin
                      state <= OUT_VLD;
                    end
          OUT_VLD:  begin
                      state <= OUT_ACK;
                    end
          OUT_ACK:  begin
                      // ap_ack retun immediatry, so no chcke ap_ack
                      // To catch when ap_done return one clclk after ap_ack
                      if (ap_done == 1)
                        begin
                          state <= WAITE_NEXT;
                        end
                      else
                        begin
                          state <= AP_DONE;
                        end
                    end
          AP_DONE:  begin
                      if (ap_done == 1)
                        begin
                          state <= WAITE_NEXT;
                        end
                      else
                        begin
                          state <= AP_DONE;
                        end
                    end
          WAITE_NEXT: begin
                        // Wait to end current WE cycle
                        if(we == 1)
                          begin
                            state <= WAITE_NEXT;
                          end
                        else
                          begin
                            state <= IDLE;
                          end
                      end
          default:    begin
                          state <= IDLE;
                      end
        endcase
      end

  // Generate control signal
  always @(*)
    case(state)
      IDLE:     control = 4'b0000;
      AP_START: control = 4'b1000;
      OUT_VLD:  control = 4'b1111;
      OUT_ACK:  control = 4'b0000;
      AP_DONE:  control = 4'b0000;
      WAITE_NEXT: control = 4'b0000;
      default:  control = 4'b0000;
    endcase

  assign {ap_start, col_vld, line_vld, data_vld} = control;
  

  //  Camera data capture FSM
  always @(posedge pclk_s)
    begin
      if (vsync)
        wr_hold  <= 2'd0;
      else
        begin
          we <= wr_hold[1];
          wr_hold  <= {wr_hold[0],  (href_s & ~wr_hold[0])};
          data_out <= data_in;
          data_in  <= {data_in[7:0], data_s};
        end
    end

  always @(posedge pclk_s)
    begin
      if (vsync)
        begin
          col      <= 10'd0;
          col_next <= 10'd0;
        end
      else
        begin
          col <= col_next;
          if (wr_hold[1] == 1)
            begin
              col_next <= col_next + 1;
              if (col_next == MAXCOL - 1)
                 col_next  <= 10'd0;
            end
        end
    end

  always @(posedge pclk_s)
    begin
      if (vsync)
        begin
          line     <= 10'd0;
          line_next<= 10'd0;
        end
      else
        begin
          line <= line_next;
          if (wr_hold[1] == 1 && col_next == MAXCOL - 1)
              begin
                line_next <= line_next + 1;
                if (line_next == MAXLINE - 1)
                    line_next <= 10'd0;
              end
          end
        end

endmodule

<VGA IF> 

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer: Kenshi Kamiya
//
// Create Date: 2016/11/06 16:33:59
// Design Name: VGA Driver with ap_hs protocol
// Module Name: vga_hs
// Project Name: OV7670_VGA
// Target Devices: Zybo
// Tool Versions: Vivado 2016.3
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////

module vga_hs(
  input wire  clk,
  input  wire clk25,
  input  wire resetN,
  output reg  [4:0] vo_r_data,
  output reg  [5:0] vo_g_data,
  output reg  [4:0] vo_b_data,
  output reg  vo_hsync,
  output reg  vo_vsync,
  output reg [9:0] vga_line,
  output reg [9:0] vga_col,
  input  wire [15:0] frame_pixel,
  output wire ap_start,
  input  wire ap_idle,
  input  wire ap_done,
  output wire line_vld,
  output wire col_vld,
  input  wire line_ack,
  input  wire col_ack
  );
  
  parameter hRez = 640, hFrontPorch = 16, hSyncPluse = 96, hBackPorch = 48, hMaxCount = 800;
  parameter vRez = 480, vFrontPorch = 10, vSyncPluse = 2,  vBackPorch = 33, vMaxCount = 525;
  parameter hDispStart = hFrontPorch + hSyncPluse + hBackPorch;  // 160
  parameter vDispStart = vFrontPorch + vSyncPluse + vBackPorch;  // 45
  
  parameter IDLE=0, AP_START=1, OUT_VLD=2, AP_DONE=3, WAITE=4;
  
  reg  [9:0] hCounter;
  reg  [9:0] vCounter;
  reg  [9:0] cur_hCounter;
  reg   blank;
  reg  [2:0]  state;
  reg  [15:0] pixelData;
  reg  [2:0] control;
  wire cke;
  
  assign cke   = ~blank & clk25;

  // hCounter
  always @(posedge clk25)
    begin
      if (resetN == 0)
        hCounter <= 10'd0;
      else if (hCounter == (hMaxCount - 1))
        hCounter <= 10'd0;
      else
        hCounter <= hCounter + 1;
     end

  //vCounter
  always @(posedge clk25)
    begin
      if (resetN == 0)
        vCounter <= 10'd0;
      else if (hCounter == (hMaxCount - 1))
        if (vCounter == (vMaxCount - 1))
          vCounter <= 10'd0;
        else
          vCounter <= vCounter + 1;
    end

  // FSM for ap_hs protocol
  always @(posedge clk)
    if (resetN == 0)
      state <= 0;
    else
      case(state)
        IDLE:     begin   // State 0
                    if ( (cke && vga_col >= 1) || (vCounter >= vDispStart && hCounter == 11'd10))
                      state = AP_START;
                    else
                      state = IDLE;
                  end
        AP_START: begin   // State 1
                    state = OUT_VLD;
                  end
        OUT_VLD:  begin   // State 2
                    state = AP_DONE;
                  end
        AP_DONE:  begin   // State 3
                    // ap_ack retun immediately, so no chcke ap_ack
                    if (ap_done && cke)
                      state <= AP_START;    // Sthort cut to execute next cycle
                    else if (ap_done )
                      state = IDLE;
                    else if (ap_done && hCounter < hDispStart)
                      state = WAITE;
                    else
                      state= AP_DONE;
                  end
        WAITE:    begin   // State 4
                    // Waite to finish hBlanking in case read first block at line start
                    if (hCounter < hDispStart)
                      state = WAITE;
                    else
                      state = IDLE;
                  end
        default:  begin
                    state = IDLE;
                  end
      endcase

  always @(*)
    case(state)
      IDLE:     control = 3'b000;
      AP_START: control = 3'b100;
      OUT_VLD:  control = 3'b111;
      AP_DONE:  control = 3'b000;
      WAITE:    control = 3'b000;
      default   control = 4'b000;
    endcase

  assign  {ap_start, line_vld, col_vld} = control;
  
  // Generate HSYNC
  always @(posedge clk25)
  begin
    if (resetN == 0)
      vo_hsync <= 1;
    else if ((hCounter > hFrontPorch) && (hCounter <= hFrontPorch + hSyncPluse))
      vo_hsync <= 0;
    else
      vo_hsync <= 1;
  end

  // Generate VSYNC
  always @(posedge clk25)
  begin
    if (resetN == 0)
      vo_vsync <= 1;
    else if ((vCounter > vFrontPorch) && (vCounter <= vFrontPorch + vSyncPluse))
      vo_vsync <= 0;
    else
      vo_vsync <= 1;
  end
  
  // Generate blank signal
  always @(posedge clk25)
  begin
    if (resetN == 0)
      blank <= 1;
    else if ((vCounter >= vDispStart) && (vCounter < vMaxCount ) && (hCounter >= hDispStart) && (hCounter < hMaxCount) )
      blank <= 0;
    else
      blank <= 1;
  end

  // VGA column address
  always @(posedge clk25)
    begin
      if (resetN == 0)
        vga_col  <= 10'd0;
      else if (blank == 0)
       if (vga_col == hRez - 1)
          vga_col  <= 10'd0;
        else
          vga_col <= vga_col + 1;
  end

  // VGA line address
  always @(posedge clk25)
    begin
      if (resetN == 0)
        vga_line <= 10'd0;
      else if (blank == 0)
        if (vga_col == (hRez - 1))
          if (vga_line == (vRez -1))
            vga_line <= 10'd0;
          else
            vga_line <= vga_line +1;
  end

  // Generate color signal
  always @(posedge clk25)
    begin
      if (blank == 0)
        begin
          vo_r_data <= pixelData[15:11];
          vo_g_data <= pixelData[10:5];
          vo_b_data <= pixelData[4:0];
        end
      else
        begin
          vo_r_data <= 5'd0;
          vo_g_data <= 6'd0;
          vo_b_data <= 5'd0;
        end
  end

  always @(posedge ap_done)
    begin
      pixelData <= frame_pixel;
    end

endmodule

動作確認(シミュレーション)

Verilogで作成したCamera IFとHLSで作成したMemWriteが正しく動作するかの検証を行いました。特にap_hsプロトコルが期待通りに動くかの検証が必要でした。ただ、ov7670_camera_hsとMemWriteをBlock Designで繋いだだけではダメで、MemWriteをAXI-Slaveに繋いでやらないと動作してくれません。AXIのBFMが有償なのでシミュレーションができないとTwitterでぼやいていたら、@marsee101 さんからご自身で作成されたAXI Slave BFMのありかを教えていたき、そのBFMを使って以下のようなシミュレーション環境を作りました。

MemWrite_TsetBed

axi_slave_bfmが @marsee さんから拝借したAXI-SlaveのBFMです。おかげで、VerilogとHLSモジュールの連携をシミュレーションすることができて大変助かりました。Block DesignでMemWrite (HLS) とaxi_bfm_slaveを作成してauto connectを実行すると、自動的にAXI-InterconnectやProsessor System Resetモジュールもインスタンス化され接続してくれました。

当初のコードでは、ov7670_camera_hsの164行目に相当する、OV7670からのデーター受信処理で、OV7670からのpclkをそのまま使ってpclkに同期したwe信号(ov7670_camera_hs 170行目)がHighになることをトリガーにして、ap_hsをスタートして(ov7670_camera_hs 94行目)MemWrite (HLS)にデーター転送を行おうとしました。このコードでは、シミュレーションではBehavioral Simulation、Post-Synthesis Simulation共に正常に動作しているように見えたのですが、実機ではどうしてもweがHighになった際にap_startパルスが出ず、MemWriteを起動できない問題に遭遇しました。

この問題の解決にかなり時間を要したのですが、we信号はpclkに同期して生成しているが、ap_hsプロトコルはpclkとは非同期の100MHz FCLKに同期して動かしているため、クロックドメインの違いによって問題が発生しているのではないかと考え、pclkを67行目に示すシンクロナイザーを通してFCLKに同期化してやると正常に動作するようになりました。

クロックドメインまたがり(CDC: Clock Domain Crossing)の処理として、入力クロックをシステムクロックで打ち直すのが正しい処理なのか自信がないのですが、まずは動いているのでよしとしています。Post-Synthesis Timing Simulationを行なった際の波形を以下に示します。

MemWrite Simulation-1

href信号が有効になると画像データーの取り込み処理を始めて、weがHighになった次のシステムクロック(clk)の立ち上がりエッジでap_startを出し、3クロックサイクル後にap_doneが返っています。ap_startの信号を組み合わせ回路で生成しているため、信号にグリッチが出ています。レジスターを通すことでグリッチをなくすことができるのですが、ap_startの立ち上がりが1クロック遅延します。Camera IFではタイミングに余裕があるためレジスターを入れてもよいのですが、後で説明するVGA IF回路ではタイミングの余裕がなく、制御信号生成にレジスターを入れることができなかったため、処理ロジックを共通化する意味で、信号のグリッチには目をつぶっています(動作には問題なさそうですので)。

また、これまでFSMを書く際は、状態遷移(next_stateに遷移する条件)を組み合わせ回路(always @*)で記述して、状態を進める処理を以下のような順序回路で記述していました: 

always @(posedge clk)
  begin
    if (resetN == 0)
      cur_state <= IDLE;
    else
      cur_state <= next_state;
  end 

このような組み合わせ回路と状態を進める順序回路の組み合わせでFSMを記述すると、weを検出した際のap_start起動に1クロックの遅延が発生します。VGA IF回路ではap_startの遅延を最小限にしたかったため、今回は状態遷移自体を順序回路のスタイルで記述しています。

640 column分のデータを受信すると(colアドレス = 0x27Fになると)MemWrite (HLS)がDDRメモリーにデーター転送処理を始めるためap_doneが直ぐに返らずに、169クロック後にap_doneが返ってきます。

MemWrite Simulation-2

MemWrite Simulation 3

VGA IFに関しても同様の試験環境を作ってシミュレーションを行なっています。

MemRead TestBed

OV7670 Camera IFの場合は24MHzサイクルの2クロック毎にMemWriteモジュールにデーターを転送できればよいため比較的タイミングに余裕があったのですが、VGA IFでは25MHzのVGA pixcelクロック毎にMemReadモジュールから画像データーをもらう必要があるため、タイミングマージンがギリギリになりました。以下にPost-Synthesis Timing Simulationを行なった際の波形を示します。VGA IFではvga_hsの59行目でvga pixcelクロック(25MHz)と有効画素範囲(ブランキング期間以外)のANDを取ってこのcke信号がHighになることをトリガーにしてMemReadモジュールに対してap_startを出しています。

MemRead Simulation

ap_startを最短で検出できるよう、VGA clockをMMCMで生成する際に-30度位相シフトして、VGA clk(clk25)がHighになった直後のシステムクロック(clk)の立ち上がりでap_startが出せるようにしています。シミュレーションでは次のVGAクロックサイクルが始まる直前ギリギリにap_doneが返っていますが、やはりpixelデーター毎にハンドシェークを行うap_hsを使うのはタイミング的に厳しいので、このようなケースではAXIS(ストリーミング)を使った方がよいように思われます。

全体の構成

全モジュールを結合した最終的なBlock Designを以下に示します。

System Block Design

Cameraモジュールでap_startが出ない問題にかなり悩まされましたが、なんとか動くようになりました。

残課題

以下の課題が残っており、まだ完全とは言えません。

1) 画像の左側にノイズが出る

OV7670カメラの水平方向の有効画素範囲の設定の問題だと思うのですが、該当レジスタ値(HSTART, HSTOP, HREF)を変更して表示ウインドウを右にシフトさせようとしたのですが、デフォルト値以外の値に設定すると、href信号の出力パターン(パルス幅)が大きく変わってしまい画像が取り込めなくなるため、現状未解決です。

Lift side noize

2) タイミング制約の問題

ov7670_camera_hsモジュールで、システムクロックに同期化したpclk_sを使って生成している信号やレジスタが、合成後unconstrainedになっています。unconstrainedの警告を消すために、試しにpclk-sにクロック制約を入れると、今度は合成時にhold/setupマージン不足(negative slack)の状態になるクロックパスが多数発生してしまうため、クロック制約は外しています。タイミング制約の使い方やタイミング条件の正しい設計はもっと勉強をしないといかんです。

クロック制約:create_clock -period 41.666 -name pclk_s -waveform {0.000 20.833} [get_pins ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s_reg/Q]

Unconstrained

3) Unknown 1-bit CDC  circuitry

クロック載せ替えで問題が出ていたので、Report CDCを行うと、1-bit unknown CDC circuitryのパスが多数検出されます。

Unkonown 1bit CDC

Unsafeではないのですが、1-bit unknown CDCが出ている回路を表示してみると、以下のように、システムクロックに同期したリセット信号と、同じくシステムクロックに同期したVGAクロックが同じFFに入力されている2つの信号の関係性で、unknown 1-bit CDCが発生しているように見えます。試しに、destination側のレジスタに”ASYNC_REG”プロパティーを設定したりしたのですが変化がありませんでした。クロック載せ替えの最適化もまだよく分かっていない部分です。

Unkonown CDC 2

参考資料


Vivado Constraints Wizardによるクロック・入出力制約の作成

$
0
0

前回のポストで、ZYBOを使って、OV7670カメラのVGA画像が取り込めるようになりましたが、制約条件が未設定でした。VivadoのConstraints Wizardを使うことによってクロックと入出力の制約条件を設定して、Unconstraintsの箇所を消すことができました。制約条件を設定することによって画質が向上したため、回路を正しく動作させるためには制約条件の設定は必須なんだと分かりました。

以下に、Constraints Wizardを使った制約条件の設定方法を記載します。

操作手順

Constraints WizardはVivadoのFlow Navigatorから起動でき、SynthesisとImplementationの両方から起動できます。一旦合成・配置を行なった後だと、どちらから起動しても作成される制約ファイルの内容は同じになるようです。

Constraints Wizard

Wizardを起動します。カメラのPCLKをFCLKで同期化したpclk_sと、HLSとのインタフェースに使っているap_doneが、Generated Clock(ユーザー定義のクロック)の候補として表示されました。ソースクロック(FCLK)の分周率を指定する形式となっており、実際のクロック周波数とは同じになりませんが、ここでは4を指定しました。pclk_sは24MHzなので、100MHz ÷ 4がそれに近い値ということで。

当初はpclk_sの制約条件を”create_clock”コマンドで作成しようとしたのですが、create_clockはプライマリークロックの定義となっているため、create_clockを使うとImplematation時にタイミング違反が発生してしまいます。このようなケースでは、”create_generated_clock”を使うのが正しいのだということが分かりました。

Constraints Wizard-2

Constraints Wizard-3

次に入力遅延を設定します。入力遅延はリファレンスクロック(ここではFCLK)の立ち上がりに対する遅延ということなので、これも実態(PCLKの立ち上がりに対して、実際のデーターはセットアップ時間前に有効になっている)とは異なりますが、1〜2 nsの遅延を指定。

Constraints Wizard input delay

続けて、出力遅延を設定。こちらは、25MHzのVGAクロックの立ち上がりに対するセットアップ・ホールド遅延の設定になります。

Constraints Wizard output delay

次に、非同期クロックドメインを指定する画面が表示されます。下段に3つのエントリが表示されていますが、Non-recommendedということなのでチェックはしていません。この項目にチェックを入れると、"set_clock_groups -asynchronous “の制約が生成されるのですが、オブジェクトに指定されるクロック名が認識できない旨のCritical Waringが合成時に出てしまいました。

20161211 Constraints Wizard Async Clock

最後にWizardを完了。

Constraints Wizard done

以下のエントリが制約ファイルに追加されました。 

create_generated_clock -name ov7670_vram_sys_i/MemRead_0/inst/ap_done -source [get_pins {ov7670_vram_sys_i/processing_system7_0/inst/PS7_i/FCLKCLK[0]}] -divide_by 4 [get_pins {ov7670_vram_sys_i/MemRead_0/inst/ap_CS_fsm_reg[10]/Q}]
create_generated_clock -name ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s -source [get_pins {ov7670_vram_sys_i/processing_system7_0/inst/PS7_i/FCLKCLK[0]}] -divide_by 4 [get_pins ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s_reg/Q]
create_clock -period 40.000 -name VIRTUAL_ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s -waveform {0.000 20.000}
create_clock -period 40.000 -name VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0 -waveform {-3.333 16.667}
set_input_delay -clock [get_clocks clk_fpga_0] -min -add_delay 2.000 [get_ports {data[*]}]
set_input_delay -clock [get_clocks clk_fpga_0] -max -add_delay 3.000 [get_ports {data[*]}]
set_input_delay -clock [get_clocks clk_fpga_0] -min -add_delay 2.000 [get_ports href]
set_input_delay -clock [get_clocks clk_fpga_0] -max -add_delay 3.000 [get_ports href]
set_input_delay -clock [get_clocks clk_fpga_0] -min -add_delay 2.000 [get_ports pclk]
set_input_delay -clock [get_clocks clk_fpga_0] -max -add_delay 3.000 [get_ports pclk]
set_input_delay -clock [get_clocks VIRTUAL_ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s] -min -add_delay 2.000 [get_ports vsync]
set_input_delay -clock [get_clocks VIRTUAL_ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s] -max -add_delay 3.000 [get_ports vsync]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports {vo_b_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports {vo_b_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports {vo_g_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports {vo_g_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports {vo_r_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports {vo_r_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports vo_hsync]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports vo_hsync]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports vo_vsync]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports vo_vsync]

合成・インプリメンテーションを実行すると、Unconstainted Pathがなくなりました。

Timing Summary

Clock Interactionを表示するとまだUnsafeなクロック乗せ変えパスが表示されています。WizardのAsynchronous CDCをチェックして赤がついているクロック間を非同期にするとUnsafeの表示を消すことができるのですが、先に示したように、Wizardが生成する制約エントリがうまく認識されないのでこのままとしています。 

Constraints Wizard clock interaction

制約条件を設定することによって、タイミング条件はかなり改善されたようです。入力側の信号がFPGAのFCLKに同期しているわけではないため(実態はカメラ側のPCLKに同期)、本当にこれでよいのかはまだよくわかっていないのですが。

結果

制約条件を加えて作成したBitstreamを使ってカメラを立ち上げると、以前に比べて画質がかなり向上しました。制約条件を入れる前は画像にジャギーが目立っていたのですが、制約条件を設定した後はジャギーがなくなりました。

<制約条件設定前の画像>

Constraints Wizard before

<制約条件設定後の画像>

Constraints Wizard after

制約条件が未設定の状態では回路が正しく動いていなかったことが分かりました。FPGAを使った回路の設計では、制約条件の設定やタイミング・クロージャーは必須事項なのだとよく分かりました。

参考資料

PYNQ-Z1のOverlay読み込みとPythonからのFPGA PLの制御

$
0
0

PYNQ-Z1はZYBOとよく似たZYNQ SoCを使ったFPGAボードですが、Pythonを使ってLinuxからFPGAのリソースにアクセスできることが特徴です。PYNQではFPGAのConfiguration Data (bitsteam) をOverlayと呼んでおり、標準でPYNQのI/Oやビデオ関係の処理ができるOverlayが提供されているのですが、ドキュメントを読んでいると、カスタムOverlayも作成できるとあります。

カスタムOverlayを作ってそれを動的にダウンロードできれば、FPGAのReconfigurableな利点を生かして、必要に応じてI/Oピンの割り当てを変更したり、さらには必要に応じてハードで処理する機能(画像処理など)を組み込めるという、RaspberryPiなどのマイコンボードでは絶対に真似ねできない、高い拡張性とPythonによる容易なプログラミングができるということになります。

本当にカスタムOverlayが動的にダウンロードできるのか(Linuxを動かしながらFPGAを動的にconfig変更できるのか)Lチカで実験してみました。

OverlayダウンロードとPythonからの制御の仕組みを調べる

PYNQはSMBのサーバーとして動いているため、WindowsのFile ExplorerやmacOSのFinderで/home/xilinx配下のディレクトリーやファイルにアクセスが可能です。

QYNQ Folder

pl.pyファイルの中にあるbitstreamクラスがOverlayのダウンロードに関係していそうです。ファイルの中身を以下に示します。 

class Bitstream(PL):
    """This class instantiates a programmable logic bitstream.
    
    Attributes
    ----------
    bitfile_name : str
        The absolute path of the bitstream.
    timestamp : str
        Timestamp when loading the bitstream. Format:
        year, month, day, hour, minute, second, microsecond
        
    """
    
    def __init__(self, bitfile_name):
        """Return a new Bitstream object.
        
        Users can either specify an absolute path to the bitstream file
        (e.g. '/home/xilinx/src/pynq/bitstream/base.bit'),
        or only a relative path.
        (e.g. 'base.bit').
        
        Note
        ----
        self.bitstream always stores the absolute path of the bitstream.
        
        Parameters
        ----------
        bitfile_name : str
            The bitstream absolute path or name as a string.
            
        """
        super().__init__()
        
        if not isinstance(bitfile_name, str):
            raise TypeError("Bitstream name has to be a string.")
        
        if os.path.isfile(bitfile_name):
            self.bitfile_name = bitfile_name
        elif os.path.isfile(general_const.BS_SEARCH_PATH + bitfile_name):
            self.bitfile_name = general_const.BS_SEARCH_PATH + bitfile_name
        else:
            raise IOError('Bitstream file {} does not exist.'\
                            .format(bitfile_name))
            
        self.timestamp = ''

    def download(self):
        """The method to download the bitstream onto PL.
        
        Note
        ----
        The class variables held by the singleton PL will also be updated.

        Parameters
        ----------
        None

        Returns
        -------
        None
            
        """
        # Compose bitfile name, open bitfile
        with open(self.bitfile_name, 'rb') as f:
            buf = f.read()
        
        # Set is_partial_bitfile device attribute to 0
        with open(general_const.BS_IS_PARTIAL, 'w') as fd:
            fd.write('0')
        
        # Write bitfile to xdevcfg device
        with open(general_const.BS_XDEVCFG, 'wb') as f:
            f.write(buf)
        
        t = datetime.now()
        self.timestamp = "{}/{}/{} {}:{}:{} +{}".format(t.year,t.month,t.day,\
                                t.hour,t.minute,t.second,t.microsecond)
        
        # Update PL information
        PL._client_request()
        PL._bitfile_name = self.bitfile_name
        PL._timestamp = self.timestamp
        PL._ip_dict = {}
        PL._gpio_dict = {}
        PL._server_update()

64〜65行目でbitsteamのデーターを読み込んで、72〜73行目で「general_const.BS_XDEVCFG」ファイルに書き込んでいます。general_const.BS_XDEVCFGの実態は、"/dev/xdevcfg"というデバイスファイルです。どうやらこのデバイスファイルに書き込みを行うことによってOverlay (bitstream) のダウンロードができるようです。

FPGAの制御(FPGAのレジスタへの書き込み)はmmio.pyのMMIOクラスを使って、/dev/memデバイスファイル経由でメモリー空間にマップされたFPGAのレジスタにアクセスしているようです。Pythonのコードは以下になります。 

class MMIO:
    """ This class exposes API for MMIO read and write.

    Attributes
    ----------
    virt_base : int
        The address of the page for the MMIO base address.
    virt_offset : int
        The offset of the MMIO base address from the virt_base.
    base_addr : int
        The base address, not necessarily page aligned.
    length : int
        The length in bytes of the address range.
    debug : bool
        Turn on debug mode if it is True.
    mmap_file : file
        Underlying file object for MMIO mapping
    mem : mmap
        An mmap object created when mapping files to memory.
    array : numpy.ndarray
        A numpy view of the mapped range for efficient assignment

    """

    def __init__(self, base_addr, length=4, debug=False):
        """Return a new MMIO object.

        Parameters
        ----------
        base_addr : int
            The base address of the MMIO.
        length : int
            The length in bytes; default is 4.
        debug : bool
            Turn on debug mode if it is True; default is False.

        """
        if base_addr < 0 or length < 0:
            raise ValueError("Negative offset or negative length.")

        euid = os.geteuid()
        if euid != 0:
            raise EnvironmentError('Root permissions required.')

        # Align the base address with the pages
        self.virt_base = base_addr & ~(mmap.PAGESIZE - 1)

        # Calculate base address offset w.r.t the base address
        self.virt_offset = base_addr - self.virt_base

        # Storing the base address and length
        self.base_addr = base_addr
        self.length = length

        self.debug = debug
        self._debug('MMIO(address, size) = ({0:x}, {1:x} bytes).',
                    self.base_addr, self.length)

        # Open file and mmap
        self.mmap_file = os.open(general_const.MMIO_FILE_NAME,
                                 os.O_RDWR | os.O_SYNC)

        self.mem = mmap.mmap(self.mmap_file, (self.length + self.virt_offset),
                             mmap.MAP_SHARED,
                             mmap.PROT_READ | mmap.PROT_WRITE,
                             offset=self.virt_base)

        self.array = np.frombuffer(self.mem, np.uint32,
                                   length >> 2, self.virt_offset)

    def __del__(self):
        """Destructor to ensure mmap file is closed
        """
        os.close(self.mmap_file)

    def read(self, offset=0, length=4):
        """The method to read data from MMIO.

        Parameters
        ----------
        offset : int
            The read offset from the MMIO base address.
        length : int
            The length of the data in bytes.

        Returns
        -------
        list
            A list of data read out from MMIO

        """
        if not length == 4:
            raise ValueError("MMIO currently only supports 4-byte reads.")
        if offset < 0 or length < 0:
            raise ValueError("Negative offset or negative length.")
        idx = offset >> 2
        if idx << 2 != offset:
            raise MemoryError('Read operation unaligned.')

        self._debug('Reading {0} bytes from offset {1:x}',
                    length, offset)

        # Read data out
        return int(self.array[idx])

    def write(self, offset, data):
        """The method to write data to MMIO.

        Parameters
        ----------
        offset : int
            The write offset from the MMIO base address.
        data : int / bytes
            The integer(s) to be written into MMIO.

        Returns
        -------
        None

        """
        if offset < 0:
            raise ValueError("Negative offset.")

        idx = offset >> 2
        if idx << 2 != offset:
            raise MemoryError('Write operation not aligned.')

        if type(data) is int:
            self._debug('Writing 4 bytes to offset {0:x}: {1:x}',
                        offset, data)
            self.array[idx] = np.uint32(data)
        elif type(data) is bytes:
            length = len(data)
            num_words = length >> 2
            if num_words << 2 != length:
                raise MemoryError('Need an integer number of words')
            buf = np.frombuffer(data, np.uint32, num_words, 0)
            self.array[offset:offset + num_words] = buf
        else:
            raise ValueError("Data type must be int or bytes.")

60行目でopenしている、general_const.MMIO_FILE_NAMEの実態が/dev/memで、63〜66行目でmmapを使ってFPGAのレジスタをマッピングしています。

試験用のbitstreamを作る

試験用に、ZYNQ PSとGPIOのみの最小構成のシステムを作り、GPIOにLED0〜LED3を接続しました。

MyLED Block Design

ZYNQ PLは、Diligent社PYNQサイトのZynq Presetからダウンロードした”pynq_revC.tcl”を使ってPLの設定を行います(Apply Configuration..でtclファイルを指定する)。さらに、ZYNQの構成でMAXI-GP0ポートを有効にします。次にAXI-GPIOを起こして接続したものが上記のブロックデザインになります。

合成・インプリ・ビットストリームの生成を行い、bitstreamをled.bitというファイル名で、PYNQに書き込みます。

動作確認

まずは、標準のOverlayを使ってRGB LEDをLチカするPyhonコードを動かしてみます。Jupyter Notebookは以下の通りです。

Color LED notebook

RGB LEDのLチカが動きました。

次に、一旦動いているNotebookをshutdownします(これを行わずに、Overlayを再ダウンロードするとLinuxが固まる時があります)。

Stop RGB LED

次に、先ほど作ったbitsteamをOverlayとしてダウンロードし、LEDを点滅させるコードを書いてみました。bitstreamクラスやMMIOクラスの低レベル処理をそのまま使っています。

MyLED blink2

Runすると、LED0〜LED3が点滅しました!!

このコードでは、GPIO0のアドレス、0x41200000に対して直接書き込みを行うことによってLEDの点滅を行なっています。非常に低レベルな処理で記述していますが、本来はこれを上位のクラスでラッピングして使いやすいオブジェクトとして見せることになります(今回は、上位クラスの作成はサボっています)。

後書き

今回の実験の成果を3月4日のPYNQ祭りでLTします。Lチカだけではつまらないので、もう少し機能を追加した内容で発表できればと思っています(2月は仕事でドタバタしそうなので、どこまでできるかなのですが)。前段の普通の発表や、@cobac さんのLTと被りそうな気がしますがその際はご容赦を...

PYNQで動的にFPGAをコンフィグして使用することが可能であることが分かりました。これは結構画期的だと思います。Arduinoやmbedの様にオープンソース(ハード?)のOverlayがライブラリとして流通するようになると面白いと思います。

PYNQ-Z1のOverlay読み込みとPythonからのFPGA PLの制御(2)

$
0
0

前回の「PYNQ-Z1のOverlay読み込みとPythonからのFPGA PLの制御」の続編です。前回のポストでは、Overlayのダウンロードに"/dev/xdevcfg"というデバイスファイルを叩いたり、LEDの点滅のためにmmapを直接叩いたりと非常に低レベルな処理になっていました。今回は、PYNQらしく、PythonのOverlayクラスを使ってOverlayをダウンロードし、LEDの制御もMyLEDクラスを使って、より抽象化した処理で動くようにしてみました。

Overlayクラスを使えるようにする

Overlayクラスを使ってOverlayをダウンロードするためには、カスタムOverlayのbitstreamを作った際のVivado tclファイルが必要になります。まず、カスタムOverlayのVivadoプロジェクトを開いて、Block Designを表示します。

MyLED BD

「File > Export > Block Design..」を実行すると、Block Designをtclファイルとしてexportしてくれます。ここでは、led.tclというファイル名で保存しました。

 BD export

生成した、tclファイルをPYNQのカスタムbitstreamと同じディレクトリーに保存します(/home/xilinx/pynq/bitstream)

2017 02 04 Overlay tcl

次に、以下のPyhonコードを実行すると、Overlayクラスのインスタンス作成性時に、led.tclがパースされて、bitstreamに含まれるI/Oとそのアドレスが抽出されます。この例では、axi_gpio_0が0x41200000で識別されていることが分かります。

Instansiate Ovlerlay

次に、axi_gpio_0に値を書き込むことによってLEDを制御するコードを書いて、/home/xilinx/pynq/boardにmyled.pyというファイル名で保存します。14行目のコードで、PL.ip_dictアトリビュートから、GPIOのアドレスを抽出して、mmioクラスのコンストのラクターに渡しています。これで、GPIOのアドレスの紐付けができました。

from pynq import MMIO
from pynq import PL

LEDS_OFFSET0 = 0

class MyLED(object):
    """This class controls the onboard LEDs vi axi_gpio_0. """
    _mmio = None
    _leds_value = 0

    def __init__(self):
        """Create a new MyLED object. """
        if MyLED._mmio is None:
            MyLED._mmio = MMIO(int(PL.ip_dict["SEG_axi_gpio_0_Reg"][0],16),16)
        MyLED._mmio.write(LEDS_OFFSET0, 0x0)

    def set(self, value):
        """Turn on a LED.
        
        Parameters
        ----------
        Value = GPIO out data
        
        Returns
        -------
        None
        
        """
        MyLED._mmio.write(LEDS_OFFSET0, value)

/home/xilinx/pynq/board/__init__.pyに最後の一行を追加しておきます。

from .led import LED
from .rgbled import RGBLED
from .switch import Switch
from .button import Button
from .myled import MyLED

 これでMyLEDクラスが使えるようになりました。

Jupyter Notebookの作成

以下のように、Overlayをダウンロードして、MyLEDクラスを使ってLEDを点灯させます。

Jupyter Notebook

 これで以下のように、カスタムOverlayを使ってLチカができます。前回のコードよりだいぶ簡潔で分かりやすなりました。

後書き

ということで、思ったより簡単にカスタムOverlayをPYNQのお作法に従って動かすことができました。PYNQにもZYBOのように安価なSDSoCのボード限定ライセンスが提供されればソフト屋さんもFPGAを使ったハードウェアオフロードが簡単にできるようになるのではと思います。PYNQのSDSoC、来ないのかな。

 

FPGA始めました

$
0
0

最近FPGAにハマっています。きっかけは、写真の「ディジタル回路設計とコンピュータアーキテクチャー ARM版」を買って、この本の解説に従って、FPGAボードのDE0-CVを使って最小限のARM命令が動くCPUコアをFPGAで作るようになって、FPGAのお手軽さと面白さに目覚めました。自分で部品をハンダ付け・配線して実際のハードを作るのは時間もなく(ましてやカスタム基盤を設計する根性もなく)、FPGAで回路を合成してシミュレーションすると、なんとなく回路設計したような気持ちになれ、ソフトを書くのとはまた違った面白さがあります。

IMG_0977.jpg

今回買ったFPGAボードはAlteraのCyclone Vを搭載したDE0-CVをマルツオンラインで購入しました。DE0-CVにした理由は、AlteraとXilixのどちらにしようかと思ったのですが、「ディジタル回路設計とコンピュータアーキテクチャー」でAlteraの開発環境やDE2-115ボードが紹介されていたこと、DE2-115は高価で手が出ないので、安価ですが7SEG-LEDなど表示系が比較的充実したDE0-CVを購入しました。

最初は「ディジタル回路設計とコンピュータアーキテクチャー」に掲載されているSystem Verilogのコードをそのまま打ち込んだだけですが、入力ミスを見つけるために、初めて使うAlteraの開発環境Quartus Primeと格闘しながら論理ミュレーションを動かすなどして、実際にARM命令が動くCPUが作れた時は結構感動しました。この本は、FPGAで実際に動くCPUを作ってみるためには絶好の名著だと思いますので、興味のある方は是非買ってみて下さい。Amazonのレビューに、私の書評も掲載しています。

その後コードを拡張して、演習問題のパイプライン化は自力で動かすことができました。また、パイプライン化に加えて、MOV/CMP/EOR(XOR)/ROR/LSR/ASR/LSL命令を追加しています。

現在はDE0-CVに搭載されているSD-RAMにアクセスできるようにしようとしているのですが、FPGA内臓メモリーはノーウェイトでアクセスできることに対してSD-RAMはノーウェイトではアクセスできないため、パイプラインの制御に改造が必要でまだうまくいっていません(LDR命令でメモリーから読みだすまでのパイプラーンストールに加えて、読みだした値をレジスタファイルにライトバックするあたりのパイプラインストールの追加考慮が必要なようです)。SD-RAMアクセスの単体試験はQsysで合成した出来合いのIPを使うことでできているのですが。

DE0-CVを使ったミニARMのコードなどは、オリジナル書籍発売元やARM社の版権もあると思うので、私が作ったHDLコードを掲載するのはやめておきます。

ZYBOでPmodCLP(Parallel Interface LCD)を動かす

$
0
0

FPGAハマりの勢いで、Alteraに加えて、Xilinxにも手を出してしまいました。

今回買ったのは、XilinxのSoC (ARMコア)付きFPGA Zynqを搭載したZYBOを秋月さんで購入しました。SoC付きのFPGAを試すのなら、Altera系のDE1-SoCが自然な流れですが、HDLでなくC/C++を使った高位合成(HLS)が無償で使えるXilixの開発環境(Vivado)も試したくなって、Xilinxに手を出してしまいました。下の写真の上段がDE0-CVで、下段が今回買ったZYBOです。

IMG_1084.jpg

高位合成のお試しは、下の写真にある「FPGAマガジンNo.14」を参考してやろうと思っているのですが、その前にZYBOにLCDを繋いでみました。

IMG_1147.jpg

ZYBOは写真で見て分かる通り、DE0-CVに比べて7SEG-LEDがないなど、表示系デバイスが少ないです。その代わりに、ZYBOにはPmodと呼ばれる拡張コネクターが6個付いており、ZYBOの発売元であるDigilent社から各種のPmod拡張モジュールが販売されています。回路のデバッグを行う際など表示系があると便利だと思い、LEDが8個搭載されたPmod8LD(写真右側)とParallel接続のLCD PmodCLP(写真左側)を購入しました。(FedExを使った送料込みで、$50超となり、ちょっと高かったですが)。

IMG_1148.jpg

Pmodの種別

ZYBOのPmodコネクタにはPS(ARMコア)につながっているPmod MIOと、PL(FPGA部)につながっているStandard Pmod, Hi-Speed Pmod, XADC Pmodがあります。PmodCLPはPmodコネクタ2つを占有するのですが、基板下側に4つ並んでいるPmodは左側1つがStandard Pmodで残りの3つはHi-Speed Pmodです。マニュアルによると、Hi-Speed Pmodは隣接する2つの信号ピンをペアで差動出力として使用することによって高速伝送ができるコネクターで、信号ペアを別々に使うとクロストークが発生すると記載があります。

しかしながら、ZYBOの構成ではどうしても1つはHi-Speed Pmodを使う必要があるので、買ってからこの制約に気がついて、果たしてLCDが動くのか気になっていたのでまずはこちらを試してみました。結果はちゃんと動いています。

今回は、制御信号4pinはStandard-Pmodコネクター(JE)を使用して、データーバス8pinをHi-Speed Pmodコネクター(JD)につなぎました。PmodCLPの回路図を見ると、データーバスには200Ωの抵抗が直列で入っていること(これは短絡対策の保護も兼ねていると思いますが)、差動出力で使う信号ペア(例えば、J1のpin-1とpin-2)をダイオードで2.5V電源にクランプしており、この辺りがHi-speed Pmodコネクターに繋いだ時のクロストーク対策になっているのかしらと思ったりしています(データーバスはFPGAの3.3V出力に接続するのでダイオードを使った過電圧保護は不要だと思うのですが、何のために入っているのか)。

PmodCLP.png

ZynqのIP作成

Zynq(SoC付きFPGA)では まずARMコアありきで、ARMコアに回路ブロックを接続します。Vivadoではライブラリ化された回路ブロック(IP)をZinqに接続していきます。このあたりは、AlteraのQsysと同様の考え方だと思うのですが、QsysよりVivadoのIP Integratorの方が配線やアドレス設定を自動化してくれる度合いが高く使い易い感じがしました。またQsysではTopレベルのHDLは手で書く必要がありますが、Vivadoでは自動生成してくれます。

IPを作成する手順ですが、Digilentが提供している、Vivado用のBorad Fileをインストールしておくと、オンボードのLED/SWはGPIOのメニューに表示されるようになります。ZYBOボードを指定することでZYBOが使用しているZynq FPGAのタイプも自動的に選択してくれるためこのBorad Fileはインストールしておくと便利です。また、Digilentが提供しているPmod-Libraryがあるのですが、PmodCLPとPmod8LDにはライブラリがありません。

そのため、Zynq FPGAにARMコアとGPIOをVivadoのIP Integratorを使って配置して、GPIOにPmodCLPのデータバスと制御信号を接続します。

  • IP IntegratorのCreate Block Designを選択、新規Designを作成。Design名はsystemとしました(IP Integratorの使い方はGetting Started with Zynqに詳細な解説があります)
  • Add IPを使って、Zynq-7 Processing SystemとAXI GPIOを配置して接続します
  • GPIOは「Enable Dual Channel」をチェックして、GPIO2も有効にし、GPIOをデーター用の8-bit入出力(All input/ All output双方をチェックしない)、GPIO2を制御用の4-bitの出力ポートに設定します。
  • GPIOポートの名称を「LCD-DB」、GPIO2のポート名を「LCD-CTL」と設定します。

Vivado_IP.PNG


Vovado-GPIO.PNG

制約ファイルの作成

次に、FPGAのピンとPmodのピン(GPIOの端子)を関連付ける制約ファイルを作成します。この手順は、このブログを参考にさせていただきました。概要は以下の通りです。

  • IP作成ができたら、HDL Wrappperを作成。SourcesウインドウのIP SourcesタブからBlock Design名(今回の例ではsystem)を右クリック、「Create HDL Wrapper...」を選択します。
  • 次に制約ファイルを作成。SourcesウィンドウのHierarchyタブにおいて、Constraints > constrs_1上にて右クリック「Add Source」を選択、Add or create constraints選択して、PmodLCD.xdcの名称で制約ファイルを作成します。
  • Run Implementationを実行した後、Open Implementation Designを選択してOKをクリック、画面下のI/O Portsタブを選択して、GPIOポートのPackage Pin列にFPGAのpin番号を割り付けます。今回は、LCD-DBポート(lcd_db_tri_io[0]〜[7])にPmod JDコネクターに対応したFPGAの端子を割り当て、LDC-CTLポート(lcd_ctl_tri_o[0]〜[3])にはPmod-JEコネクターの下段pin(JE7〜JE10に対応するFPGAのpin)を以下のように割り当てました。
  • 加えて、I/O StdをLVCMOS33に変更します。
  • 割り当てができたら、CTRL-Sでファイルを保存します。

Pmod-xdc.PNG

一度割り当てを作ってしまえば、次からは制約ファイル(PmodLCD.xdc)インポートすることでFPGAのPin割り当てを自動的に行うことができます。

Bitstream(FPGA configデータ)の生成とSDKへのExport

  • Generate Btstramを実行後、File > Export > Export Hardware…から、Include bitstreamチェックしてOKクリック。
  • File > Launch SDKを選択して、EclipseベースのSDKでARMコア用のLCD制御ソフトを作成します。

LCD制御ソフトの作成

DigilentのPmodCLP用「Library and MPLAB Example」にサンプルコードがあるのですが、中身を見るとArduino用のLCDライブラリが入っています。このままでは使えないので、空のC++プロジェクトを作って、コードを以下のようにZynqのベアメタルARM用に書き換えました。

/************************************************************************/
/*                                                                                                                                              */
/*      LCDP.h  --      Declaration for LCDP library                                            */
/*                                                                                                                                              */
/************************************************************************/
/*      Author:         Cristian Fatu                                                                                   */
/*      Copyright 2011, Digilent Inc.                                                                           */
/************************************************************************/
/*  File Description:                                                                                                   */
/*              This file declares LCDP library functions and constants involved*/
/*                                                                                                                                              */
/************************************************************************/
/*  Revision History:                                                                                                   */
/*                                                                                                                                              */
/*      12/10/2011(CristianF): created                                                                          */
/*  21/08/2016(Todotani): adapted fro ZYBO PmodCLP                      */
/*                                                                                                                                              */
/************************************************************************/
#if !defined(LCDP_H)
#define LCDP_H


/* ------------------------------------------------------------ */
/*                              Include File Definitions                                                */
/* ------------------------------------------------------------ */
#include <inttypes.h>
#include "xparameters.h"
#include "xgpio.h"
#include "sleep.h"


/* ------------------------------------------------------------ */
/*                                      Definitions                                                                     */
/* ------------------------------------------------------------ */

/* ------------------------------------------------------------ */
/*                                      Errors Definitions                                                      */
/* ------------------------------------------------------------ */

/* ------------------------------------------------------------ */
/*              Command codes Definitions                                                               */
/* ------------------------------------------------------------ */
#define LCDP_CMD_LcdFcnInit     0x38    // function set command, (8-bit interface, 2 lines, and 5x8 dots)
#define LCDP_CMD_LcdCtlInit     0x08    // display control set command
#define LCDP_CMD_LcdClear               0x01    // clear display command
#define LCDP_CMD_LcdRetHome             0x02    // return home command
#define LCDP_CMD_LcdDisplayShift 0x18   // shift display command
#define LCDP_CMD_LcdCursorShift  0x10   // shift cursor command
#define LCDP_CMD_LcdSetDdramPos 0x80    // set DDRAM position command
#define LCDP_CMD_LcdSetCgramPos 0x40    // set CGRAM position command

#define LCDP_MSK_BStatus        0x80            // bit busy
#define LCDP_MSK_ShiftRL        0x04            // shift direction mask
#define LCDP_OPT_DisplayOn      0x4             // Set Display On option
#define LCDP_OPT_CursorOn       0x2             // Set Cursor On option
#define LCDP_OPT_BlinkOn        0x1             // Set Blink On option


/* ------------------------------------------------------------ */
/*                              Parameters Definitions                                                  */
/* ------------------------------------------------------------ */
#define LCDP_DISP_SetOptionDisplayOn    0x4 // Set Display On option
#define LCDP_DISP_SetOptionCursorOn     0x2 // Set Cursor On option
#define LCDP_DISP_SetBlinkOn                    0x1 // Set Blink On option
/* ------------------------------------------------------------ */
/*                                      Class Declarations                                                      */
/* ------------------------------------------------------------ */
#define mskLCDPDat07            0x000000FF

#define LCDP_ERR_SUCCESS                                0               // The action completed successfully
#define LCDP_ERR_UCHAR_POSITION_INVALID 0x20    // The user character position is not correct
#define LCDP_ERR_ARG_ROW_RANGE                  0x80    // The row index is not valid
#define LCDP_ERR_ARG_COL_RANGE                  0x40    // The column index is not valid

#define LCDP_NO_ROWS    2
#define LCDP_NO_COLS    40
#define LCDP_NO_UCHARS  8

/* ------------------------------------------------------------ */
/*                                      Control Singnals                                                        */
/* ------------------------------------------------------------ */
#define RS                              0b0001          // Register Select: High for Data Transfer, Low for Instruction Transfer
#define RW                              0b0010          // Read/Write signal: High for Read mode, Low for Write mode
#define EN                              0b0100          // Read/Write Enable: High for Read, falling edge writes data
#define BL                              0b1000

class LCDP {
private:
        XGpio PmodLCD;
        uint8_t control;
        
        uint8_t m_bDisplayMode;

        uint8_t ReadByte();
        void WriteByte(uint8_t bData);

        void WaitUntilNotBusy();
        void WriteCommand(uint8_t bCmd);
        void WriteDataByte(uint8_t bData);

        void SetWriteCgramPosition(uint8_t bAdr);
        void SetWriteDdramPosition(uint8_t bAdr);
        uint8_t ReadStatus();
public:

        LCDP();

        void begin();

        void DisplayClear();
        void ReturnHome();
        void SetDisplay(bool fDisplayOn);
        void SetCursor(bool fCursorOn);
        void SetBlink(bool fBlinkOn);
        void SetBacklight(bool fBacklightOn);
        uint8_t SetPos(uint8_t idxLine, uint8_t idxCol);
        uint8_t WriteStringAtPos(char *szLn, uint8_t idxLine, uint8_t idxCol);
        void DisplayShift(bool fRight);
        void CursorShift(bool fRight);
        uint8_t DefineUserChar(uint8_t *pBytes, uint8_t bCharNo);
        uint8_t WriteUserCharsAtPos(uint8_t* rgCharPos, uint8_t bNoChars, uint8_t idxLine, uint8_t idxCol) ;
        };

#endif


/************************************************************************/
/*                                                                                                                                              */
/*      LCDP.cpp                --      Definition for LCDP library                             */
/*                                                                                                                                              */
/************************************************************************/
/*      Author:         Cristian Fatu                                                                                   */
/*      Copyright 2011, Digilent Inc.                                                                           */
/************************************************************************/
/*  File Description:                                                                                                   */
/*              This file defines functions for LCDP                                                    */
/*                                                                                                                                              */
/************************************************************************/
/*  Revision History:                                                                                                   */
/*                                                                                                                                              */
/*      12/10/2011(CristianF): created                                                                          */
/*  21/08/2016(Todotani): adapted fro ZYBO PmodCLP                      */
/*                                                                                                                                              */
/************************************************************************/


/* ------------------------------------------------------------ */
/*                              Include File Definitions                                                */
/* ------------------------------------------------------------ */

#include "LCDP.h"


/* ------------------------------------------------------------ */
/*                              Procedure Definitions                                                   */
/* ------------------------------------------------------------ */


/* ------------------------------------------------------------ */
/**        Description:
**                      Class constructor. Performs variables initialization tasks
**
**
*/
LCDP::LCDP()
{
        m_bDisplayMode = 0;
}

/* ------------------------------------------------------------ */
/*        LCDP::begin
**
**        Synopsis:
**
**        Description:
**                              This function saves the pins corresponding to the CLP and performs the required LCDP initialization tasks.
**
*/
void LCDP::begin()
{
        // Initialize Zynq GPIO
        XGpio_Initialize(&PmodLCD, XPAR_AXI_GPIO_0_DEVICE_ID);
        XGpio_SetDataDirection(&PmodLCD, 2, 0x0);           // Set control port output

        // set control flag 0
        control = 0;

        // perform initialization sequence, according to datasheet
        //      wait 20 ms
        usleep(20*000);
        // Set function
        WriteCommand(LCDP_CMD_LcdFcnInit);
        // Wait 37 us
        usleep(37);

        // display on, no cursor, no blinking
        SetDisplay(true);
        SetBacklight(true);

        // Wait 37 us
        usleep(37);
        // Display Clear
        DisplayClear();
        // Wait 1.52 ms
        usleep(1520);
}


/* ------------------------------------------------------------ */
/*        ReadByte
**
**        Return Values:
**                uint8_t - the byte that was read
**
**        Description:
**                              The function implements a CLP read sequence.
**                              The function is used to read data (RS set before calling this function) or to read status (RS cleared before calling this function).
**                              The function implements two approaches of handling data pins.
**
*/
uint8_t LCDP::ReadByte()
{
        uint8_t bData = 0;

        // Set data port input
        XGpio_SetDataDirection(&PmodLCD, 1, 0xFF);

        // Set RW
        control |=  RW;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        // Set EN
        control |=  EN;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        bData = XGpio_DiscreteRead(&PmodLCD, 1);

        // Clear EN
        control &= ~EN;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        // Clear  RW
        control &=  ~RW;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        return bData;
}


/* ------------------------------------------------------------ */
/*        WriteByte
**
**        Parameters:
**                              uint8_t bData - data to be written to display
**
**        Return Values:
**                void
**
**        Description:
**                              The function implements a CLP write sequence.
**                              The function is used to write data (RS set before calling this function) or to write commands (RS cleared before calling this function).
**                              When writing data it writes in DDRAM or CGRAM according to the last set write position.
*/
void LCDP::WriteByte(uint8_t bData)
{
        // Set data port output
        XGpio_SetDataDirection(&PmodLCD, 1, 0x00);

        // Clear RW
        control &= ~RW;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        // Set Enalbe
        control |= EN;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        XGpio_DiscreteWrite(&PmodLCD, 1, bData);
        
        // Clear Enable
        control &= ~EN;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        // Set Read
        control |= RW;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
}

/* ------------------------------------------------------------ */
/*        ReadStatus
**
**        Return Values:
**                uint8_t - the byte that was read.
**
**        Description:
**                              Reads the status of the CLP. It clears the RS and calls ReadByte() function.
**
*/
uint8_t LCDP::ReadStatus()
{
        uint8_t bStatus;
        // clear RS
        control &= ~RS;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        
        // read status byte
        bStatus = ReadByte();
        return bStatus;
}

/* ------------------------------------------------------------ */
/*        WaitUntilNotBusy
**
**        Synopsis:
**                              WaitUntilNotBusy()
**
**        Return Values:
**                void
**
**        Description:
**                              Waits until the status of the CLP is not busy. This function relies on ReadStatus().
**
*/
void LCDP::WaitUntilNotBusy()
{
        uint8_t bStatus;
        bStatus = ReadStatus();
        while (bStatus & LCDP_MSK_BStatus)
        {
                usleep(10);
                bStatus = ReadStatus();
        }
}

/* ------------------------------------------------------------ */
/*        WriteCommand
**
**        Synopsis:
**                              WriteCommand(cmdLcdClear);
**        Parameters:
**                              uint8_t bCmd    - the command code byte
**
**        Description:
**                              Writes the specified byte as command. When the device is ready it clears the RS and writes byte.
**
*/
void LCDP::WriteCommand(uint8_t bCmd)
{
        // wait until LCD is not busy
        WaitUntilNotBusy();

        // Clear RS
        control &= ~RS;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        // Write command byte
        WriteByte(bCmd);
}


/* ------------------------------------------------------------ */
/*        WriteDataByte
**
**        Synopsis:
**                              WriteDataByte(pBytes[idx]);
**        Parameters:
**                              uint8_t bData           - the data byte
**
**        Description:
**                              Writes the specified byte as data. When the device is ready it sets the RS and writes byte.
**
*/
void LCDP::WriteDataByte(uint8_t bData)
{
        // wait until LCD is not busy
        WaitUntilNotBusy();

        // Set RS
        control |= RS;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        // Write command byte
        WriteByte(bData);
}

/* ------------------------------------------------------------ */
/*        SetWriteCgramPosition
**
**        Synopsis:
**                              SetWriteCgramPosition(bAdr);
**        Parameters:
**                              uint8_t bAdr    - the write location. The position in CGRAM where the next data writes will put bytes.
**
**        Description:
**                              Sets the CGRAM write position. This is the location where the next data write will be performed.
**                              Be aware that writing to a location auto-increments the write location.
**
*/
void LCDP::SetWriteCgramPosition(uint8_t bAdr)
{
        uint8_t bCmd = LCDP_CMD_LcdSetCgramPos | bAdr;
        WriteCommand(bCmd);
}

/* ------------------------------------------------------------ */
/*        SetWriteDdramPosition
**
**        Synopsis:
**                              SetWriteDdramPosition(bAddrOffset);
**        Parameters:
**                              uint8_t bAdr - the write location. The position in DDRAM where the next data writes will put bytes.
**                                      0x00-0x27 refer to the first row
**                                      0x40-0x67 refer to the second row
**
**        Description:
**                              Sets the DDRAM write position. This is the location where the next data write will be performed.
**                              Be aware that writing to a location auto-increments the write location.
**
*/
void LCDP::SetWriteDdramPosition(uint8_t bAdr)
{
        uint8_t bCmd = LCDP_CMD_LcdSetDdramPos | bAdr;
        WriteCommand(bCmd);
}


/* ------------------------------------------------------------ */
/*        DisplayClear
**
**        Synopsis:
**                              DisplayClear();
**
**        Description:
**                              Clears the display and returns the cursor home (upper left corner).
**
*/
void LCDP::DisplayClear()
{
        WriteCommand(LCDP_CMD_LcdClear);
}


/* ------------------------------------------------------------ */
/*        ReturnHome
**
**        Description:
**                              Returns the cursor home (upper left corner).
**
*/
void LCDP::ReturnHome()
{
        WriteCommand(LCDP_CMD_LcdRetHome);
}

/* ------------------------------------------------------------ */
/*        SetDisplay
**
**        Synopsis:
**                              SetDisplay(true);
**        Parameters:
**                              bool fDisplayOn - Display option
**                                              - true in order to set the display ON
**                                              - false in order to set the display OFF
**
**        Description:
**                              Sets the display option. If true, display is on, if false, the display is off.
**
*/
void LCDP::SetDisplay(bool fDisplayOn)
{
        if(fDisplayOn)
        {
                m_bDisplayMode |= LCDP_OPT_DisplayOn;
        }
        else
        {
                m_bDisplayMode &= ~LCDP_OPT_DisplayOn;
        }
        WriteCommand(LCDP_CMD_LcdCtlInit | m_bDisplayMode);
}

/* ------------------------------------------------------------ */
/*        SetCursor
**
**        Synopsis:
**                              SetCursor(true);
**        Parameters:
**                              bool fCursorOn - Cursor option
**                                              - true in order to set the Cursor ON
**                                              - false in order to set the Cursor OFF
**
**        Description:
**                              Sets the cursor option. If true, Cursor is on, if false, the Cursor is off.
**
*/
void LCDP::SetCursor(bool fCursorOn)
{
        if(fCursorOn)
        {
                m_bDisplayMode |= LCDP_OPT_CursorOn;
        }
        else
        {
                m_bDisplayMode &= ~LCDP_OPT_CursorOn;
        }
                
        WriteCommand(LCDP_CMD_LcdCtlInit | m_bDisplayMode);
}

/* ------------------------------------------------------------ */
/*        SetBlink
**
**        Synopsis:
**                              SetBlink(true);
**        Parameters:
**                              bool fBlinkOn - Blink option
**                                              - true in order to set the Blink ON
**                                              - false in order to set the Blink OFF
**
**        Description:
**                              Sets the Blink option. If true, Blink is on, if false, the Blink is off.
**
*/
void LCDP::SetBlink(bool fBlinkOn)
{
        if(fBlinkOn)
        {
                m_bDisplayMode |= LCDP_OPT_BlinkOn;
        }
        else
        {
                m_bDisplayMode &= ~LCDP_OPT_BlinkOn;
        }
                
        WriteCommand(LCDP_CMD_LcdCtlInit | m_bDisplayMode);
}

/* ------------------------------------------------------------ */
/*        SetBacklight
**
**        Synopsis:
**                              SetBacklight(fBackLight);
**        Parameters:
**                              bool fBl - Backlight option
**                                              - true in order to set the backlight ON
**                                              - false in order to set the backlight OFF
**
**        Description:
**                              This function turns the backlight on or off, according to the user's selection.
**                              Note that there are CLP Pmods that do not have backlight functionality. Using this function for this type of modules will have no effect.
**
*/
void LCDP::SetBacklight(bool fBacklightOn)
{
        if (fBacklightOn)
                control |= BL;
        else
                control &= ~BL;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
}

/* ------------------------------------------------------------ */
/*        SetPos
**
**        Synopsis:
**                              SetPos(0, 3);
**        Parameters:
**                              uint8_t idxLine - the line where the position will be set
**                              uint8_t idxCol  - the column where the position will be set
**
**
**        Return Value:
**                uint8_t
**                                      - LCDP_ERR_SUCCESS (0)  - The action completed successfully
**                                      - a combination (ORed) of the following errors:
**                                              - LCDP_ERR_ARG_ROW_RANGE (0x80) - The row index is not valid
**                                              - LCDP_ERR_ARG_COL_RANGE (0x40) - The column index is not valid
**        Description:
**                              This function sets the corresponding LCD position. This is used for write position and cursor position.
**                              If position set is invalid (outside the display), errors are returned.
**
**
*/
uint8_t LCDP::SetPos(uint8_t idxLine, uint8_t idxCol)
{
        uint8_t bResult = LCDP_ERR_SUCCESS;

        if (idxLine < 0 || idxLine >= LCDP_NO_ROWS)
        {
                bResult |= LCDP_ERR_ARG_ROW_RANGE;
        }
        if (idxCol < 0 || idxCol >= LCDP_NO_COLS)
        {
                bResult |= LCDP_ERR_ARG_COL_RANGE;
        }
        if (bResult == LCDP_ERR_SUCCESS)
        {
                // Set write position
                uint8_t bAddrOffset = (idxLine == 0 ? 0: 0x40) + idxCol;
                SetWriteDdramPosition(bAddrOffset);
        }
        return bResult;
        
}

/* ------------------------------------------------------------ */
/*        WriteStringAtPos
**
**        Synopsis:
**                              WriteStringAtPos(szInfo1, 0, 0);
**        Parameters:
**                              char *szLn      - string to be written to LCD
**                              uint8_t idxLine - the line where the string will be displayed
**                              uint8_t idxCol  - the column line where the string will be displayed
**
**
**        Return Value:
**                uint8_t
**                                      - LCDP_ERR_SUCCESS (0)  - The action completed successfully
**                                      - a combination (ORed) of the following errors:
**                                              - LCDP_ERR_ARG_ROW_RANGE (0x80) - The row index is not valid
**                                              - LCDP_ERR_ARG_COL_RANGE (0x40) - The column index is not valid
**
**
**        Errors:
**                              See Return Value
**
**        Description:
**                              The function writes the specified string at the specified position (line and column).
**                              It sets the corresponding write position and then writes data bytes when the device is ready.
**                              Strings that span over the end of line are trimmed so they fit in the line.
**                              If position is invalid (outside the display), errors are returned.
**
*/
uint8_t LCDP::WriteStringAtPos(char *szLn, uint8_t idxLine, uint8_t idxCol)
{
        uint8_t bResult = SetPos(idxLine, idxCol);
        if (bResult == LCDP_ERR_SUCCESS)
        {
                // Strings that span over the end of line are trimmed so they fit in the line.
                int len = strlen(szLn);
                if(len + idxCol > LCDP_NO_COLS)
                {
                        len = LCDP_NO_COLS - idxCol;
                        szLn[len] = 0; // crop the string at this position
                }

                uint8_t bIdx = 0;
                while(bIdx < len)
                {
                        WriteDataByte(szLn[bIdx]);
                        bIdx++;
                }
        }
        return bResult;
        
}


/* ------------------------------------------------------------ */
/*        DisplayShift
**
**        Synopsis:
**                              DisplayShift(fBtn1Process);
**        Parameters:
**                              bool fRight - parameter indicating the direction of the display shift
**                                              - true in order to shift the display right
**                                              - false in order to shift the display left
**
**        Description:
**                              This function shifts the display one position right or left, depending on the fRight parameter.
**
**
*/
void LCDP::DisplayShift(bool fRight)
{
        uint8_t bCmd = LCDP_CMD_LcdDisplayShift | (fRight != false ? LCDP_MSK_ShiftRL: 0);
        WriteCommand(bCmd);
}


/* ------------------------------------------------------------ */
/*        CursorShift
**
**        Synopsis:
**                              CursorShift(fBtn1Process);
**        Parameters:
**                              bool fRight - parameter indicating the direction of the cursor shift
**                                              - true in order to shift the cursor right
**                                              - false in order to shift the cursor left
**
**        Description:
**                              This function shifts the cursor one position right or left, depending on the fRight parameter.
**
*/
void LCDP::CursorShift(bool fRight)
{
        uint8_t bCmd = LCDP_CMD_LcdCursorShift | (fRight != false ? LCDP_MSK_ShiftRL: 0);
        WriteCommand(bCmd);
}

/* ------------------------------------------------------------ */
/*        DefineUserChar
**
**        Synopsis:
**                              MyLCDP.DefineUserChar(defChar2, 2);
**        Parameters:
**                              uint8_t *pBytes - pointer to the string that contains the 8 bytes definition of the character.
**                              uint8_t bCharNo - the position of the user character to be saved in the memory
**
**
**        Return Values:
**                uint8_t
**                                      - LCDP_ERR_SUCCESS (0)                          - The action completed successfully
**                                      - LCDP_ERR_UCHAR_POSITION_INVALID       (0x20) - The user character position is not within 0 - 7
**
**        Description:
**                              This function writes the specified number of bytes to CGRAM starting at the specified position.
**                              It sets the corresponding write position and then writes data bytes when the device is ready.
**                              If the user character position is not within 0 - 7 range, error is returned.
**
*/
uint8_t LCDP::DefineUserChar(uint8_t *pBytes, uint8_t bCharNo)
{
        uint8_t bResult = LCDP_ERR_SUCCESS;

        if (bCharNo >=0 || bCharNo < LCDP_NO_UCHARS)
        {
                uint8_t bAdr = bCharNo << 3; // multiply by 8
                // Set write position to CGRAM
                SetWriteCgramPosition(bAdr);
                uint8_t len = 8;
                // Write the string of bytes that define the character to CGRAM
                uint8_t bIdx = 0;
                while(bIdx < len)
                {
                        WriteDataByte(pBytes[bIdx]);
                        bIdx++;
                }
                bResult = LCDP_ERR_SUCCESS;
        }
        else
        {
                bResult = LCDP_ERR_UCHAR_POSITION_INVALID;
        }
        return bResult;
}

/* ------------------------------------------------------------ */
/*        WriteUserCharsAtPos
**
**        Synopsis:
**                              WriteUserCharsAtPos(szInfo1, 0, 0);
**        Parameters:
**                              rgCharPos - an array containing the index (position) of the user characters to be displayed
**                              bNoChars - an array containing the index (position) of the user characters to be displayed
**                              uint8_t idxLine - line where the string will be displayed
**                              uint8_t idxCol  - the starting position of the string within the line

**
**        Return Value:
**                uint8_t
**                                      - LCDP_ERR_SUCCESS (0)  - The action completed successfully
**                                      - a combination (ORed) of the following errors:
**                                              - LCDP_ERR_ARG_ROW_RANGE (0x80) - The row index is not valid
**                                              - LCDP_ERR_ARG_COL_RANGE (0x40) - The column index is not valid
**                                              - LCDP_ERR_UCHAR_POSITION_INVALID       (0x20) - The user character position is not within the accepted range (0 ? 7)
**
**      Description:
**              This function displays one or more user defined characters at the specified positions on the LCD.
**              If the position set or the user character position is not correct, errors are returned.
**
**
**
-----------------------------------------------------------------------*/
uint8_t LCDP::WriteUserCharsAtPos(uint8_t* rgCharPos, uint8_t bNoChars, uint8_t idxLine, uint8_t idxCol)
{
        uint8_t bResult = SetPos(idxLine, idxCol);
        if (bResult == LCDP_ERR_SUCCESS)
        {
                // validate the user character positions to be between 0 and 7
                uint8_t bIdx = 0;
                while(bIdx < bNoChars)
                {
                        if (rgCharPos[bIdx] < 0 || rgCharPos[bIdx] >= LCDP_NO_UCHARS)
                        {
                                bResult = LCDP_ERR_UCHAR_POSITION_INVALID;
                                bIdx = bNoChars; // force out, no need to continue
                        }
                        bIdx++;
                }
                if (bResult == LCDP_ERR_SUCCESS)
                {
                        //set the write position of the cursor to the wanted line/column for displaying custom chars
                        uint8_t bAddrOffset = (idxLine == 0 ? 0: 0x40) + idxCol;
                        SetWriteDdramPosition(bAddrOffset);
                        //send the position of the user character to be displayed
                        uint8_t bIdx = 0;
                        while(bIdx < bNoChars)
                        {
                                WriteDataByte(rgCharPos[bIdx]);
                                bIdx++;
                        }
                }
        }
        return bResult;
}

LCDに表示を行う簡単なmain.cppを以下のように作成

#include "LCDP.h"
#include "sleep.h"
#include <stdio.h>

int main()
{
        LCDP lcd;
        char buff[16];
        char message[16] = "Hello ZYBO";

        lcd.begin();
        int i = 0;

        while(1) {
                lcd.WriteStringAtPos(message, 0, 0);
                sprintf(buff, "%d", i);
                lcd.WriteStringAtPos(buff, 1, 0);
                i++;
                usleep(50*1000);
        }

        return 0;
}

プログラムの実行

ビルドを行い以下の手順でFPGAのconfigとプログラムの実行を行います。

  • ZYBOのProgramming Mode JumperをJTAGにセット
  • Xilix Tools → Program FPGAを実行。LD10のグリーンLED(DONE)が点灯すればFPGAのconfigが完了です
  • Run → Run As → Launch on Hardware (GDB)

以下のとおりプログラムが起動します。


JTAGインターフェース経由で起動した場合、電源を切ったりリセットを行うと、FPGAのconfig・PSのプログラムとも消えてしまいます。configやプログラムを永続化したい場合は、QSPIメモリーにbootイメージを書き込む必要があります。この辺りは別途書きます。
参考情報

 

Viewing all 48 articles
Browse latest View live