Docker for MacでXilinx FPGA用の開発環境を作る

# TL;DR
- Xilinx ISEがインストールされたdockerイメージを作成しました。
- Mac上から自動でビルドする環境を整備し、テンプレートとして公開しました。

# はじめに

FPGAを販売するXilinx社では、ISE（最近のデバイス用にはVivado）と呼ばれる自社製の開発環境を用意しています。FPGAのコンフィグレーションに必要なBit stream file（`*.bit`）はこのISEを使って生成できますが、逆に言えばXilinx社謹製のISEを使わなければ生成できません。そして、ISEは対応OSがWindowsとLinuxのみなんですよね...見事にMacがハブれれてしまいました。メインで使っている端末がMacということもあり、MacからFPGAの開発ができると嬉しいな...と思いしばらくいじっていたので、備忘録を兼ねて記事にします。

## 以前の開発環境

一年ほど前にFPGAボードの開発をしていた際には、次のような環境で開発していました。

- ターゲットデバイス
    - [Papilio pro (Gadget Factory製)](https://papilio.cc/index.php?n=Papilio.PapilioPro)
- 開発&シミュレーション
    - Mac端末
    - 言語: VHDL
    - エディタ: Visual Studio Code
    - テスト用コンパイラ: ghdl
- 実装
    - Windows端末
    - エディタ&コンパイラ: Xilinx ISE

状況としてはMacの方でVHDLコードを書き、ghdlでコンパイルして挙動を確認します。だいたいいい感じまでできたらソースコードをDropboxなりGithubなりにアップロードして、Windows端末にダウンロードし、Xilinx ISEで作成したプロジェクトにソースコードを加えて、実装用のBit stream fileを生成していました。
なぜこのような面倒な手順を踏んでいたかというと、Windows端末が研究室の端末で、自宅に持ち帰りが難しかったこと、加えてXilinx ISE上でのソースコードの編集がお世辞にもやりやすいとは言えなかった（補完やSyntax Highlightが貧弱だった）ため、ソースの編集はVS Code上でやりたかったという事情があります。Mac上でVMを使うことも考えたのですが、Mac上のストレージの容量が少なく、VMを立てたくもない状況でした。
そんなこんなで、このしち面倒臭い環境でしばらく開発をしており、そのときの目的が達成された後には、もう金輪際FPGAに関わることもないだろうと思っていたわけです。ただ、幸か不幸かまたFPGAに触る機会ができたので、この際Macで実装までできるようにしてやろうと思いまして、環境構築に取り組みました。

# Mac上でのFPGA開発環境の構築

あらすじは次の通りです。

1. Docker for MacでXilinx ISEインストール済みのdockerイメージを作成
2. Pythonのinvokeパッケージを使ってコンパイル、テスト環境の構築

以下の手順では次のソフトウェアがインストールされていることを前提にしています。

- Docker for Mac
- Python 3.x
- XQuartz
- invoke (Python package)
- ghdl

docker、XQuartz、Python3、ghdlはHomebrewから、invokeはAnaconda経由で入手しました。invokeはpipでも入手できるようです。

## 1. Docker for MacでXilinx ISEインストール済みのdockerイメージを作成

~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~
以下の手順でISE Webpack editionをインストールしたものはDocker Hub上で`seekworser/ise_webpack`で公開しているため、Webpack版であれば

```bash
docker pull seekworser/ise_webpack
```
で事足ります。容量が30 GB以上あるので注意してください。
~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~/~


dockerを使ってXilinx Vivadoをインストールしている方がいらっしゃったので、その記事を参考にしました。
[Xilinx の開発ツールを Docker コンテナに閉じ込める](https://blog.myon.info/entry/2018/09/15/install-xilinx-tools-into-docker-container/)

この方はVivado HLx 2018.2: All OS installer Single-File Downloadをダウンロードしてきて、インストールまで含めてDockerfileで自動化しているのですが、ISEではSingle file Downloadが存在しないため、泣く泣くインストールは手動で行います。
そこで、まずfull installer for Linuxをダウンロードしてきて、次のようなDockerfileを作成しました。

```dockerfile:Dockerfile
FROM ubuntu

RUN \
  sed -i -e "s%http://[^ ]\+%http://ftp.jaist.ac.jp/pub/Linux/ubuntu/%g" /etc/apt/sources.list && \
  apt update && \
  apt upgrade -y && \
  apt -y --no-install-recommends install \
  ca-certificates curl sudo xorg dbus dbus-x11 ubuntu-gnome-default-settings gtk2-engines \
  ttf-ubuntu-font-family fonts-ubuntu-font-family-console fonts-droid-fallback lxappearance && \
  apt-get autoclean && \
  apt-get autoremove && \
  rm -rf /var/lib/apt/lists/* && \
  echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers

COPY Xilinx_ISE_DS_Lin_14.7_1015_1.tar /

RUN mkdir /ise-installer &&\
  tar --strip-components 1 -C /ise-installer \
  -xvf Xilinx_ISE_DS_Lin_14.7_1015_1.tar &&\
  rm /Xilinx_ISE_DS_Lin_14.7_1015_1.tar
```

前半は先のリンクの方から丸パクリで、X11環境のインストールその他です。後半でインストール用のフォルダを`/ise_installer`下に展開しておきます。（ファイル名はバージョンに合わせて変えてください）

ここからGUIインストーラを立ち上げ、ISEをインストールします。Docker for MacでX11 GUIを立ち上げる際には次を参考にしました。
[Mac+dockerでx11アプリケーションを起動する](http://raphine.hatenablog.com/entry/2018/08/14/004634)

先ほどのDockerfile、ダウンロードしたインストーラを全て同じフォルダに入れ、さらにhost側でXQuartzを立ち上げた状態で以下のコマンドを実行します。

```bash
docker build --no-cache --rm -t ise-installer .
xhost + 127.0.0.1
docker run -e DISPLAY=docker.for.mac.localhost:0 --name ise-installed ise-installer /ise-installer/xsetup
```

その後XQuartzでインストーラが起動するため、指示にしたがってインストールを行います。インストール後に、

```bash
(host)$ docker commit ise-installed ise-installed
(host)$ docker run -i --name ise ise-installed /bin/bash
(container)$ rm -rf /ise-installer
(container)$ exit
(host)$ docker commit ise ise
(host)$ docker container rm ise-installer ise-installed ise
(host)$ docker image rm ise-installer ise-installed
```
を順に実行し、ISEのインストールされたイメージ`ise`を作成します。

## 2. Pythonのinvokeパッケージを使ってコンパイル、テスト環境の構築

先ほど作成したimageと、Pythonのinvokeパッケージを使用して、コンフィギュレーション用のBit stream fileを作成する環境を作成します。[seekworser/ise_project_template](https://github.com/seekworser/ise_project_template)でPapilio pro用に作成したものは公開しています。
ディレクトリ構成は次のようになっています。

```:ise_project_template
.
├── Readme.md
├── log/
├── out/
├── test/
├── src/
│   ├── project.yaml
│   ├── sample.vhd
│   ├── sample_ucf.ucf
│   └── test_sample.vhd
├── sample.bit
└── tasks.py
```

logファイルの出力、中間ファイルの出力、ソースコード保管、test用のファイルの出力用のディレクトリとinvoke用のPythonスクリプトが置いてあります。

`sample.vhd`がメインのVHDLファイルです。

```vhdl:sample.vhd
entity sample is
    -- write ports
end sample;
architecture behavior of sample is
    -- write behavior
end behavior;
```
また、`sample_ucf.ucf`はFPGAの制約ファイルです。
プロジェクトが変わるごとに`Makefile`もとい`tasks.py`をいじるのは嫌だったので、`project.yaml`にコンパイル用のパラメータを格納しておくようにします。ついでに使うFPGAの情報も`part:`以下に書いておきます。

```yaml:project.yaml
src_files:
  - file_name: sample.vhd
    language: vhdl
    library: work
    entity_names:
      - sample 

test_files:
  - file_name: test_sample.vhd
    entity_names:
      - test_sample
      - test_sample2

ucf_file: sample_ucf.ucf

top_module: sample

part:
  device: xc6slx9
  package: tqg144
  speed: -2
```

`tasks.py`ではこの情報を読み取って、ビルドやテストなどを行うようコードを書いておきます。例えば次のようなイメージです。

```python:tasks.py（一部）
import yaml
import invoke

def get_project_parameter(*keys):
    param = yaml.load(open(PROJECT_YAML_FILE_NAME, "r+"))
    for item in keys:
        param = param[item]
    return param

@invoke.task
def bitgen(c):
    c.run(
        "docker run --rm -i -v $(PWD):/project seekworser/ise_webpack:latest sh <<_EOT_\n"
        "cd /project\n"
        "{ise_base:s}bitgen -w {outdir:s}{top_module:s}.ncd {top_module:s}.bit {outdir:s}{top_module:s}.pcf\n"
        "mv *.drc {logdir:s}{top_module:s}_drc.log\n"
        "mv *.bgn {logdir:s}{top_module:s}_bitgen.log\n"
        "rm -rf *.xwbt _xmsgs xilinx_device_details.xml *.xrpt\n"
        "_EOT_".format(
            ise_base=ISE_BASE,
            top_module=get_project_parameter(TOP_MODULE_KEY),
            outdir=OUTDIR,
            logdir=LOGDIR,
        )
    )
    return

# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ 
# some other tasks
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ 

ISE_BASE = "/opt/Xilinx/14.7/ISE_DS/ISE/bin/lin64/"
OUTDIR = "./out/"
SRCDIR = "./src/"
LOGDIR = "./log/"
PROJECT_YAML_FILE_NAME = SRCDIR + "project.yaml"
TOP_MODULE_KEY = "top_module"
```

# 最終的にどうなったか

以上の結果として

```bash
invoke builde
```
のコマンド一発でMacからBit stream fileが生成できるようになりました。ISEの使いにくいエディタを使う必要もなくなったため、開発がよりスムーズに進むようになりました。