ソフト屋のための SpinalHDL FPGA 設計入門(その1)

(更新

FPGA design tutorial by SpinalHDL for embedded software engineers.

先日、SpinalHDL というハードウェア記述言語を簡単に紹介しました。最終的な目標は RISC-V を使って FPGA 上に SoC を設計実装することですが、目標が高すぎるのでハードルを下げます。まずは、VexRiscv を実装しているハードウェア記述言語 SpinalHDL を(少し)勉強し、簡単な論理回路を FPGA(具体的には TinyFPGA BX)上で動作させてみたいと思います。

少しだけ前書き(tl;dr;)

私は組込ソフトウェアの設計が専門で、HDL も FPGA も門外漢です。なぜいま FPGA か、というのは先日書きましたが、基本的な考えとしては、設計や実装には常に適切なツール、言語を利用したい、ということがあります。たとえば、最近流行りの computer vision であれば OpenCV をベースにするのは既に常識となりつつありますし、信号処理のシミュレーションであれば MATLAB(少し古い)、GNU Octave(これも少し古い)、NumPy + SciPy を使うのは当然の選択肢です。アナログ回路シミュレーションであれば SPICE 系のシミュレータでしょうか。いま、これらを一から C 言語などで実装する人はいません。(背景となる原理を理解するためであったとしても、C 言語での設計は適切とは言えないでしょう。)

話を戻しまして、ソフトウェアによる設計というのは物事(アルゴリズム)をシーケンシャルに書き下すには適していますが、パイプライン処理や、並列処理を設計するには最適のやり方ではありません。また、特殊なハードウェアインターフェイスを実現しようとする場合、ソフトウェアによる bit-banging 等では効率が悪いことが多いのです。HDL によるハードウェア設計を理解していれば、全てをソフトウェアで設計するよりも効率的な実装ができるはずです。(と私は思います。)

お題は PWM による LED ホワホワ点滅にしよう

さて、FPGA で何を設計するか考えます。単純な LED 点滅では芸がありませんので、もう少し複雑で、それでもあまり複雑でない 🙂 テーマを取り上げます。最近、電子機器でよく見かける「LED がホワホワと明るくなったり暗くなったりする、アレ」を設計したいと思います。

余談ですが、実は TinyFPGA BX のブートローダー動作時は LED がホワホワしているので、動作しても、ブートローダーの動作と違いが分かりにくいです。後で「しまった」と思ったのですが、まあ、しようがないですね。

最初にソフト屋の視点で設計してみる

LED ホワホワは PWM で設計するのが常套手段ですが、ソフト屋の私としては、すぐに論理回路図が頭に思い浮かびません。どうしてもシーケンシャルな考えになってしまいます。まずは擬似的な C 言語で書いてみましょう。

const int PWM_PERIOD = 1 << 8;
const int MAX = PWM_PERIOD - 1;

volatile extern int pin_led;
int ct_isr_pwm = 0;
volatile int pwm_width = 0;

void isr_pwm(void)  // PWM 割込ハンドラ
{
    if (ct_isr_pwm < pwm_width)
        pin_led = 1;
    else
        pin_led = 0;

    if (++ ct_isr_pwm > MAX)
        ct_isr_pwm = 0;
}

void pwm_led(void)  // LED ホワホワルーチン
{
    extern void init_isr(void (*)(void));
    extern void delay_ms(int);
    pwm_width = 0;

    init_isr(&isr_pwm);

    for (;;) {
        while (++ pwm_width < MAX)
            delay_ms(1);
        while (-- pwm_width > 0)
            delay_ms(1);
    }
}

こんな感じでしょうか、試してませんけど。

まずはブロック図を書いてみる

ハード設計的な視点に立つには、まずはブロック図を描くのが良さそうです。頑張って描いてみました。素人っぽくてすみません。

まず基本となるのは、右上の PWM モジュールですね。SpinalHDL 的には「Pwm クラス」というところでしょうか。

PWM モジュールには、クロック clk とパルス幅 width の入力があります。clk が入る度にカウンタがインクリメントします。ビット幅いっぱいになったらラウンドアップして 0 に戻ります。カウンタ値が width より小さいとき、出力は True となり、そうでないときは False となります。

次にそれ以外の部分です。LED を明るくしていくときと暗くしていくときの状態を管理する必要があるので、FSM(ステートマシン)を置きます。状態が Counting-Up のときは、入力クロックに従って Up-Down カウンタをインクリメントしていきます。状態 Counting-Down のときは、デクリメントします。Up-Down カウンタの値に従って、FSM の状態を遷移させます。

なお、TinyFPGA BX のクロック入力 16MHz をそのまま Up-Down カウンタのクロックに使うと速すぎるので、クロック分周器(Clock Divider)を入れて、LED 明滅の速度を遅めます。

こんなところでしょうか。

さっそく SpinalHDL に書き下してみる

PWM モジュール

それでは最初に、PWM モジュール(Pwm クラス)を設計してみましょう。

なお、必要なツールは既にインストール済みと仮定します。前回までのブログを参考にインストールしてください。必要になるのは、

  • SBT(Scala 言語のためのビルドツール)
  • Yosys(論理合成ツール。後で必要になる)
  • IceStorm(後で必要になる)
  • nextpnr(placing & routing ツール。後で必要になる)
  • Verilator(Verilog シミュレーションツール。テストベンチで必要になる)
  • GTKwave(シミュレーション結果を論理値の波形で閲覧するビューア)

Pwm クラスを SpinalHDL で書いてみます。えいっ。

// package com.flogics.spinal.pwmled

import spinal.core._

class Pwm(size: Int) extends Component {
  val io = new Bundle {
    val width = in UInt(size bits)
    val output = out Bool
  }

  val counter = Reg(UInt(size bits)) init (0)

  counter := counter + 1
  io.output := counter < io.width
}

構文の説明は、SpinalHDL のサイトを御覧ください。

最初の 1行目はコメントアウトされてますが、Java や Scala の流儀で、クラスをパッケージの名前空間に押し込むためのものです。今は package は使ってないです。

val io という部分が PWM モジュールの入出力部分です。なお、Spinal HDL ではクロックは自動的に定義されるので書く必要はありません。あとで、クロックドメイン(ClockDomain)という概念が出てきます。Pwm() のカッコ内にある size: Int は Pwm クラスをインスタンス化するときに与えるパラメタですね。この size は回路合成後は固定になり、回路動作中に動的に変化することはありません。

val counter = Reg(…) はレジスタの定義です。これは内部状態になります。カウンタの幅は size ビットとなります。init (0) なので、カウンタ初期値はゼロになります。

次の行は D-FF による順序回路の定義ですね。クロックが入る度に counter が 1 インクリメントします。その次は組み合わせ回路の定義ですね。counter < io.width の場合は io.output が True に、そうでない場合は False になります。

なお、Verilog や VHDL に慣れた方には不自然な記述と思われるかも知れませんが(特に := によるアサインメント)、ソフト屋の私には案外とっつきやすい構文です。実際の動きについてはシミュレーションで追えそうですし、不明な点があれば出力の Verilog ファイルを覗くか、最悪でも友人の HDL 屋さんに相談すればアドバイスを貰えるでしょう。 🙂

Verilog に変換してみる

ここで終わってしまうと、HDL 設計屋さんに「おいおい」と言われそうですので、Verilog HDL に変換した結果を示しておきます。size には 10 を与えています。reset というのは(上記で触れませんでしたが)リセット信号ですね。クロックドメインの定義の際に、リセット信号の扱いも定義できます。後は、HDL さんには自明でしょうかね。

module Pwm (
      input  [9:0] io_width,
      output  io_output,
      input   clk,
      input   reset);
  reg [9:0] counter;
  assign io_output = (counter < io_width);
  always @ (posedge clk or posedge reset) begin
    if (reset) begin
      counter <= (10'b0000000000);
    end else begin
      counter <= (counter + (10'b0000000001));
    end
  end

endmodule

今日はここまで

今日はここまでにしましょう。次回は、これをテストベンチにかけてシミュレーションし、思った通りの動作になっているか確認しましょう。なお、最終的なファイル群(プロジェクト)については、最終回に GitHub にアップロードしようと思いますので、御参考ください。(後記: アップロードしました。)