プロジェクト

オープンソースツールでTang Nano 9KのLチカからPWMブリージングLEDへ

Yosys、nextpnr-himbaechel、openFPGALoaderをインストールし、三角波とガンマ補正のルックアップ、自作のPWMモジュールで、Tang Nano 9Kの6個のLEDをゆっくり明滅させます。

入門Gowinオープンソース更新 2026年11月13日
ボードSipeed Tang Nano 9K
所要時間約2時間
言語verilog
追加部品Sipeed Tang Nano 9K、USB-Cケーブル、Linux、macOS、Windowsのいずれかが動くコンピュータ

作るもの

Tang Nano 9Kは、完全なオープンソースFPGAフローが動く最も安価なボードです。このプロジェクトでは、空のディレクトリから始めて、6個のLEDが波のように順に明るくなったり暗くなったりするところまで進みます。Gowin EDAもライセンスも必要ありません。ツールをインストールし、回路図を見ながら制約ファイルを書き、Yosys、nextpnr、Gowinのパッカーを実行するMakefileを作り、三角波をガンマ風のルックアップに通して明るさを決めるPWMモジュールを書きます。一方のボタンを押している間は波が速くなり、もう一方のボタンでリセットします。

はじめてのHDLモジュールを終えていれば、Verilogの知識は十分です。新しく学ぶのはツールチェーンと、「LEDには半分点灯という状態がないので、明るさは時間でごまかすしかない」という考え方です。

必要なもの

  • Sipeed Tang Nano 9K(Gowin GW1NR-LV9QN88PC6/I5、8,640 LUT)と、給電だけでなくデータ通信に対応したUSB-Cケーブル。
  • Linux、macOS、Windowsのいずれかが動くコンピュータ。最も手間が少ないのはLinuxです。WindowsではUSBパススルーを使ったWSL2でも動きますが、ツールをネイティブにインストールするより手間がかかります。
  • オープンソースフロー: 合成にYosys、配置配線にApiculaのGowinデータベースを使うnextpnr-himbaechel、ビットストリーム生成にApiculaのgowin_pack、ボードへの書き込みにopenFPGALoader。
  • 任意: 付属のPWMセルフテストを実行するIcarus Verilogと、lintのためのVerilator。

ツールのインストール

一番簡単なのはYosysHQのoss-cad-suiteです。上記のすべてのツールをLinux、macOS、Windows向けにまとめたナイトリービルドです。YosysHQのGitHubリリースページから自分のプラットフォーム用のアーカイブをダウンロードし、恒久的に置ける場所に展開して、そのbinディレクトリをパスに追加します。

tar -xzf oss-cad-suite-linux-x64-2026*.tgz -C ~/opt
source ~/opt/oss-cad-suite/environment
yosys --version
nextpnr-himbaechel --version
openFPGALoader --version

Linuxでは、openFPGALoaderに同梱されているudevルール(99-openfpgaloader.rules)を追加するとsudoなしで書き込めます。追加したらボードを抜き差ししてください。macOSでは追加の作業は不要です。ディストリビューションのパッケージを使いたい場合、DebianとUbuntuにはyosys、nextpnr-gowin、openfpgaloaderがありますが、ナイトリービルドより古く、himbaechelアーキテクチャが含まれていないこともあります。頼る前にnextpnr-himbaechelが存在するか確認してください。

設計

FPGAのピンから駆動されるLEDは、点灯か消灯かのどちらかです。半分の明るさに見せるには、短い周期ごとにその半分の時間だけ点灯させます。周期が十分に短ければ、目はパルスを平均して、安定した暗めの光として認識します。これがパルス幅変調(PWM)で、点灯している時間の割合をデューティと呼びます。

ステップ分周器27 MHz / 105,468三角波0..255..0位相オフセットLEDごとに +40gammax² / 256pwm8ビット、105 kHzLED負論理stepleveldutyLEDごとに位相・gamma・pwmのブロックを1組(計6組)S2を押している間はステップが4倍速
27 MHzクロックから6本のLEDピンまでの信号の流れ。

pwmモジュールは、フリーランニングの8ビットカウンタと1つの比較器です。カウンタがデューティ値より小さい間、出力はHighになります。デューティ0では一度も点灯せず、デューティ255では256クロックのうち255クロック点灯します。カウンタは256クロックごとに一周するので、27 MHzではPWM周波数は27,000,000 / 256 = 105 kHzになります。

PWM周波数とちらつき

周波数は、初心者が思う以上に重要です。およそ60 Hzを下回ると、ほとんどの人はちらつきを直接感じます。約60 Hzから数百Hzの間では、じっと見ている分には安定して見えますが、目やボードを動かすと点の軌跡が見え、スマートフォンのカメラには縞模様が映ります。数kHzを超えれば、どれも見えなくなります。PWM周波数を高くする代償は分解能です。クロックが固定なら、デューティのビット数を増やすほど周期は長くなります。27 MHzなら、105 kHzで8ビット、または6.6 kHzで12ビットを確保でき、どちらも目で検知できる範囲をはるかに上回ります。モジュールのtick入力を使えば、プリスケーラでカウンタを遅くして、ちらつきを自分の目で確かめることもできます。

三角波とガンマルックアップ

三角波は0から255まで数えてから折り返します。105,468クロックごとのステップパルスで1つ進むので、1回の明滅には512ステップ、約2 sかかります。このレベルをそのままデューティに入れると、LEDは早い段階で最大の明るさに達してそのまま止まっているように見えます。目の応答が線形ではなく対数に近いためです。レベルを2乗して上位バイトを残す(通常の2.2ではなく、2.0という粗いガンマ)と、知覚される明るさの変化がずっと滑らかになります。この乗算は実質的にルックアップです。入力256通り、出力256通りで、合成ツールはDSPブロックか数個のLUTを選びます。

各LEDにはレベルに位相オフセットを加えた値を与えるので、6つの出力は同じ波を時間方向にずらしたものになり、光がボードの上を流れていくように見えます。

コード

Makefileはtop.vだけを読むので、すべてを1つのファイルにまとめています。pwmモジュールとtriangleモジュールは同じファイルに置けるほど小さく、あとで別ファイルに移すこともできます。

// Breathing LEDs on the Tang Nano 9K: a triangle wave feeds a gamma lookup, which feeds six PWM channels.

// One PWM channel. The output is high while the free-running counter is below the duty value.
module pwm #(
    parameter integer BITS = 8
) (
    input  wire            clk,
    input  wire            rst,
    input  wire            tick,      // advance the counter on this (1 = every clock)
    input  wire [BITS-1:0] duty,      // 0 = off, 255 = almost fully on
    output reg             out
);
    reg [BITS-1:0] count;
    always @(posedge clk) begin
        if (rst) begin
            count <= {BITS{1'b0}};
            out   <= 1'b0;
        end else begin
            if (tick) count <= count + {{(BITS-1){1'b0}}, 1'b1};
            out <= (count < duty);
        end
    end
endmodule

// Triangle wave: counts 0..255 then 255..0, one step per 'step' pulse.
module triangle (
    input  wire       clk,
    input  wire       rst,
    input  wire       step,
    output reg  [7:0] level
);
    reg rising;
    always @(posedge clk) begin
        if (rst) begin
            level  <= 8'd0;
            rising <= 1'b1;
        end else if (step) begin
            if (rising) begin
                if (level == 8'd255) rising <= 1'b0;
                else level <= level + 8'd1;
            end else begin
                if (level == 8'd0) rising <= 1'b1;
                else level <= level - 8'd1;
            end
        end
    end
endmodule

module top (
    input  wire       clk,     // 27 MHz crystal on pin 52
    input  wire       s1_n,    // button S1, active low
    input  wire       s2_n,    // button S2, active low
    output wire [5:0] led_n    // six LEDs, active low
);
    localparam integer CLK_HZ = 27_000_000;

    // Synchronise the buttons and derive an active-high reset from S1.
    reg [1:0] s1_sync, s2_sync;
    always @(posedge clk) begin
        s1_sync <= {s1_sync[0], ~s1_n};
        s2_sync <= {s2_sync[0], ~s2_n};
    end
    wire rst  = s1_sync[1];
    wire fast = s2_sync[1];   // hold S2 to breathe four times faster

    // Step pulse for the triangle wave. 512 steps per breath, about 2 s per breath.
    localparam integer STEP_SLOW = CLK_HZ / 256;   // 105,468 clocks per step
    localparam integer STEP_FAST = STEP_SLOW / 4;
    reg [16:0] step_cnt;
    reg        step;
    wire [16:0] step_limit = fast ? STEP_FAST[16:0] : STEP_SLOW[16:0];
    always @(posedge clk) begin
        step <= 1'b0;
        if (rst || step_cnt >= step_limit - 17'd1) begin
            step_cnt <= 17'd0;
            step     <= ~rst;
        end else begin
            step_cnt <= step_cnt + 17'd1;
        end
    end

    wire [7:0] level;
    triangle u_tri (.clk(clk), .rst(rst), .step(step), .level(level));

    // Gamma-ish correction: perceived brightness is roughly the square root of duty,
    // so square the level to make the ramp look linear. 255 * 255 >> 8 = 254.
    function [7:0] gamma(input [7:0] x);
        /* verilator lint_off UNUSEDSIGNAL */
        reg [15:0] sq;   // the low byte is deliberately dropped
        begin
            sq    = x * x;
            gamma = sq[15:8];
        end
        /* verilator lint_on UNUSEDSIGNAL */
    endfunction

    // Each LED gets the wave with a phase offset so the glow appears to travel.
    wire [7:0] lvl [0:5];
    assign lvl[0] = level;
    assign lvl[1] = level + 8'd40;
    assign lvl[2] = level + 8'd80;
    assign lvl[3] = level + 8'd120;
    assign lvl[4] = level + 8'd160;
    assign lvl[5] = level + 8'd200;

    genvar i;
    generate
        for (i = 0; i < 6; i = i + 1) begin : g_led
            wire       pwm_out;
            reg  [7:0] duty;
            always @(posedge clk) duty <= gamma(lvl[i]);
            pwm #(.BITS(8)) u_pwm (
                .clk(clk), .rst(rst), .tick(1'b1), .duty(duty), .out(pwm_out)
            );
            assign led_n[i] = ~pwm_out;   // LEDs light when the pin is low
        end
    endgenerate
endmodule

PWMセルフテスト

短いテストベンチで、いくつかのデューティ値について、256クロックの1周期のうち出力がHighになるクロック数を数えます。ビルドには不要ですが、実行してもコストはかかりません。

`timescale 1ns / 1ps
// Checks that a pwm channel is high for exactly 'duty' clocks out of every 256.
module tb_pwm;
    reg        clk = 1'b0;
    reg        rst = 1'b1;
    reg  [7:0] duty = 8'd0;
    wire       out;
    always #18.5 clk = ~clk;   // roughly 27 MHz

    pwm #(.BITS(8)) dut (.clk(clk), .rst(rst), .tick(1'b1), .duty(duty), .out(out));

    integer highs, k, errors = 0;
    task measure(input [7:0] d);
        begin
            duty = d;
            @(posedge clk); @(posedge clk);
            highs = 0;
            for (k = 0; k < 256; k = k + 1) begin
                @(posedge clk);
                if (out) highs = highs + 1;
            end
            if (highs !== d) begin
                errors = errors + 1;
                $display("FAIL: duty %0d gave %0d high clocks", d, highs);
            end else begin
                $display("ok:   duty %0d -> %0d/256 high", d, highs);
            end
        end
    endtask

    initial begin
        repeat (3) @(posedge clk);
        rst = 1'b0;
        measure(8'd0);
        measure(8'd1);
        measure(8'd64);
        measure(8'd128);
        measure(8'd255);
        if (errors == 0) $display("PASS");
        else $display("FAIL: %0d errors", errors);
        $finish;
    end
endmodule

制約

Gowinフローの物理制約はCSTファイルに書きます。お使いのボードリビジョンのSipeedの回路図と照らして、すべてのピンを確認してください。LEDは3.3 Vバンクにあります。9Kでは2つのボタンが1.8 Vバンクにあるため、I/Oタイプが異なります。ボタンも6個のLEDもすべて負論理です。

// Tang Nano 9K physical constraints. Check every pin against the Sipeed schematic.
IO_LOC "clk" 52;
IO_PORT "clk" IO_TYPE=LVCMOS33 PULL_MODE=NONE;

IO_LOC "s1_n" 4;
IO_PORT "s1_n" IO_TYPE=LVCMOS18 PULL_MODE=UP;
IO_LOC "s2_n" 3;
IO_PORT "s2_n" IO_TYPE=LVCMOS18 PULL_MODE=UP;

IO_LOC "led_n[0]" 10;
IO_LOC "led_n[1]" 11;
IO_LOC "led_n[2]" 13;
IO_LOC "led_n[3]" 14;
IO_LOC "led_n[4]" 15;
IO_LOC "led_n[5]" 16;
IO_PORT "led_n[0]" IO_TYPE=LVCMOS33 DRIVE=8;
IO_PORT "led_n[1]" IO_TYPE=LVCMOS33 DRIVE=8;
IO_PORT "led_n[2]" IO_TYPE=LVCMOS33 DRIVE=8;
IO_PORT "led_n[3]" IO_TYPE=LVCMOS33 DRIVE=8;
IO_PORT "led_n[4]" IO_TYPE=LVCMOS33 DRIVE=8;
IO_PORT "led_n[5]" IO_TYPE=LVCMOS33 DRIVE=8;

Makefile

# Open-source flow for the Tang Nano 9K
TOP     = top
DEVICE  = GW1NR-LV9QN88PC6/I5
FAMILY  = GW1N-9C
BOARD   = tangnano9k

all: $(TOP).fs

$(TOP).json: $(TOP).v
	yosys -p "read_verilog $(TOP).v; synth_gowin -top $(TOP) -json $(TOP).json"

pnr.json: $(TOP).json $(TOP).cst
	nextpnr-himbaechel --json $(TOP).json --write pnr.json \
	    --device $(DEVICE) --vopt family=$(FAMILY) --vopt cst=$(TOP).cst

$(TOP).fs: pnr.json
	gowin_pack -d $(FAMILY) -o $(TOP).fs pnr.json

load: $(TOP).fs
	openFPGALoader -b $(BOARD) $(TOP).fs

flash: $(TOP).fs
	openFPGALoader -b $(BOARD) -f $(TOP).fs

sim:
	iverilog -g2012 -o sim.vvp tb_pwm.v top.v && vvp sim.vvp

lint:
	verilator --lint-only -Wall -Wno-DECLFILENAME --top-module top top.v

clean:
	rm -f $(TOP).json pnr.json $(TOP).fs sim.vvp

.PHONY: all load flash sim lint clean

ビルドと書き込み

3つのファイルを1つのディレクトリに置いた状態で、次を実行します。

make            # synthesise, place, route, pack: produces top.fs
make load       # write the bitstream into SRAM (lost at power-off)
make flash      # or write it to the on-board flash so it survives a power cycle

各ステップを手で実行して、何をしているか確かめることもできます。

yosys -p "read_verilog top.v; synth_gowin -top top -json top.json"
nextpnr-himbaechel --json top.json --write pnr.json \
    --device GW1NR-LV9QN88PC6/I5 --vopt family=GW1N-9C --vopt cst=top.cst
gowin_pack -d GW1N-9C -o top.fs pnr.json
openFPGALoader -b tangnano9k top.fs

Yosysは最後にリソースの集計を表示します。LUTは200個未満、フリップフロップは数個のはずです。nextpnrは達成した最大クロック周波数を報告し、27 MHzを大きく上回るはずです。openFPGALoaderがデバイスを見つけられないと言う場合は、ケーブルがデータ通信に対応しているか、Linuxではudevルールがインストールされているかを確認してください。

セルフテストとlintを実行するには次のようにします。

make sim
make lint

シミュレーションはok:の行を5つとPASSを表示します。

テスト

書き込み直後から、6個のLEDが列の端から端へ流れる波のように明滅し、2 s前後で繰り返します。S2を押している間は波が4倍速くなります。S1を押している間はすべてのLEDが消え、離すと波が最初から再開します。

スマートフォンのカメラをLEDに向けてみてください。105 kHzはどんなカメラの露光レートよりはるかに高いので、縞模様は見えないはずです。次に、pwmインスタンスのtick入力を1'b1からstepに変えて再ビルドしてください。PWMカウンタは三角波の1ステップごとに1つ進むようになるので、PWM周期は256ステップ、約1 sになり、「ゆっくりした明滅」は明らかな遅い点滅に変わります。PWM周波数が低すぎるとこう見えます。

試してみること

  • tickがNクロックごとに立つようにプリスケーラレジスタを追加し、ボードを動かしてもちらつきが見えなくなる最低のPWM周波数を実験で見つけてください。
  • 2乗のルックアップを、短いPythonスクリプトで生成してcase文に格納した本来の2.2のガンマテーブルに置き換えてください。変化の滑らかさを目で比べてみましょう。
  • 分解能を12ビットに上げて、8ビットPWMでは届かない最も暗い段階を観察してください。
  • 同じモジュールから、ボード上のRGB LCDのバックライトや、MOSFET経由の外部LEDストリップを駆動してください。
  • 同じ設計をGowin EDAでビルドして、LUT数と報告される最大周波数をオープンソースの結果と比べてください。

まとめ

  • オープンソースのGowinフローは、Yosys、Apiculaを使うnextpnr-himbaechel、gowin_pack、openFPGALoaderで構成され、oss-cad-suiteにすべてが入っています。
  • CSTファイルにはピン位置とI/O規格を書きます。必ず回路図と照らして確認してください。
  • PWMは時間から明るさを作ります。目に見えるちらつきやカメラの縞模様を避けるため、周波数はkHz台に保ちます。
  • 知覚される明るさはデューティに対して線形ではないので、比較器に渡す前にレベルを2乗します。
  • 小さなモジュール1つを異なる入力で6回インスタンス化するのが、FPGA流の設計のスケールのさせ方です。