← 講義ポータルへ戻る 第5回・第6回講義:周辺を自作する

第3章:基本周辺を自作 ― GPIO/タイマ/PWM

第2章で、SoCの骨格(CPU=Wishboneマスタ、アドレスデコーダ、メモリマップ)ができました。本章では、その骨格に最初の周辺機器(ペリフェラル)を足します。題材は、どんなマイコンにもある基本の3つ――GPIO(汎用入出力)・タイマ・PWMです。そしてこれらは、買ってくるのではなく自分で作ります。なぜ自作なのか、その判断から始めましょう。

3.1 思想 ― コアは無改造、まわりをMMIOで足す

本講座の背骨となる考え方を、改めて確認します。CPU本体(PicoRV32)には一切手を入れません。代わりに、周辺機器を「特定の番地に割り当てたWishboneスレーブ」として足し、CPUはその番地を読み書きするだけで周辺を操ります。この「番地への読み書き=周辺機器の操作」という仕組みを MMIO(メモリマップドI/O)と呼びます。

基礎編からの発展: 基礎編第6章で、番地 0x4000_0000sw 命令で書き込むとLEDが点きました。あれがMMIOの最初の一歩でした。ただし基礎編では、CPUの素のメモリI/Fを直接見て応答する「メモリ係」を手書きしていました。本章では、それをWishboneスレーブという、繰り返し使える正式な型に格上げします。一度型を作れば、GPIOもタイマもPWMも、その型の上に乗せるだけ――これがSoCを効率よく育てるコツです。

コラム:FPGAに「C必須」は誤解です

「FPGAで遊ぶにはC言語ができないとダメなんでしょう?」――入口でよく聞く不安ですが、これは誤解です。FPGAの主役はハードを作るVerilog。Cは、自作したCPUに仕事を“させたい”ときに登場するごく少しの助手にすぎません。本講座でも、出てくるCは数行で読めるものだけです。まず、この講座に出る言語がそれぞれ何の係で、あなたが「書く」のか「読むだけ」なのかを一望しておきましょう。

   この講座に出てくる言語 ― だれが何の係で、あなたは「書く」のか
 ───────────────────────────────────────────────────────────────
   言語         役割(何の係)                    あなたは…
 ───────────────────────────────────────────────────────────────
   Verilog      ハード(回路)そのものを作る        ★書く(主役)
   C            自作CPUに仕事をさせる“最小限”        読めればよい(数行)
   Python       ビルドの裏方/将来の本番ボード用      当面は触らない
   HTML/CSS     この教材サイト                      触らない
 ───────────────────────────────────────────────────────────────
   要点: FPGAの主役は Verilog。C は「番地を叩く数行」だけ。

では、その「数行のC」とは具体的にどれだけか。じつは本講座で読めればよいCの“形”は、たった4つです。if もループ設計も関数も配列もポインタ演算も出てきません

第3章の検証で実際に動かすCも、この4つの組み合わせだけでできています。「Cの知識が要る」のではなく、「番地を読み書きする数行が読めればよい」――これが実態です。

さらに念を押すと、そもそもCPUを載せない遊び方なら、Cは1行も要りません。 LチカもPWMも、Verilogだけで完結します。下は、CPUもCも使わずに「スイッチでLEDを点け、PWMで明るさを作る」だけの回路です(考え方の骨格・未検証)。

// ※CPUを載せない「Cゼロ」版(骨格イメージ・未検証)
// CPUもCも無し。Verilogだけで、スイッチでLEDを点け、PWMで明るさを作る。
module toy (
    input         clk,
    input  [15:0] sw,        // スイッチ
    output [15:0] led,       // LED
    output        pwm_out    // PWM出力(明るさ)
);
    // スイッチをそのままLEDへ(C不要)
    assign led = sw;

    // 高速カウンタと比較だけでPWMを作る(C不要)
    reg [7:0] cnt;
    always @(posedge clk) cnt <= cnt + 1'b1;
    assign pwm_out = (cnt < sw[7:0]);   // スイッチ下位8bitを明るさに
endmodule

※正直な線引き(断定を避けて): 「Cは絶対に不要」と言い切るのも不正確です。自作CPUに“判断”や“手順”をやらせたくなったら、上の4つの形を使う数行のCが要る場面は出てきます。要するに――FPGAそのものはVerilogだけで深く遊べる/自作CPUに仕事をさせる段で、はじめてC(しかも数行)が助手として加わる。「Cができないと無理」ではなく、「この程度のCで、自作CPUまで動かせる」。それが本講座のスタンスです。

3.2 「自作」か「調達」か ― 最初の分かれ道

序章のIP調達ワークフローは、ステップ1「要求を言語化する」から始まりました。じつは、その要求を書き出した時点で、「自分で作るか/既製IPを調達するか」の判断が始まっています。本章で扱うGPIO・タイマ・PWMは、あえて自作を選びます。理由は明快です。

自作が向くもの(本章)

単純で、仕様が明快で、自分のメモリマップに密着しているもの。GPIO・タイマ・PWMがこれ。作るのも理解するのも速く、Wishboneスレーブの型を学ぶ教材として最適。探して合わせる手間より、書いたほうが早い。

調達が向くもの(第4・5章)

規格が複雑で、細かい例外処理が多く、実績がものを言うもの。UART・SPI・I2C・USBがこれ。自作するとバグの温床になりやすいので、枯れた既製IPを調達し、現物確認して使うほうが堅実。

※これは絶対のルールではなく、判断の目安です: 同じUARTでも「学ぶために自作する」選択はあり得ますし、タイマでも「高機能なものが既にあるから調達する」選択はあり得ます。大事なのは、「なぜ自作(または調達)を選んだか」を、要求に照らして自分で言葉にできること。アーキテクトの仕事は、この判断にこそあります。

3.3 Wishboneスレーブの「型」をつくる

周辺機器を足す前に、すべての周辺に共通する器(スレーブの型)を用意します。第2章で読んだ picorv32_wb の握手から、スレーブが満たすべき約束は明らかでした――「マスタが cycstb を立てて自分の番地を指してきたら、必要な処理をして ack を返す」。これだけです。

// ※Wishboneスレーブの最小の型(骨格イメージ・未検証)
// マスタが cyc&stb で要求 → このスレーブが選ばれていれば処理して ack を返す
module wb_slave_template (
    input  wire        clk,
    input  wire        rst,        // 押す=1(正論理。SoC全体で揃える)
    // Wishbone スレーブ側の口
    input  wire        wb_cyc_i,
    input  wire        wb_stb_i,   // このスレーブが選ばれている時だけ1(デコーダが渡す)
    input  wire        wb_we_i,    // 1=書き込み, 0=読み出し
    input  wire [3:0]  wb_sel_i,   // バイト選択
    input  wire [31:0] wb_adr_i,   // 番地(必要な下位ビットだけ使う)
    input  wire [31:0] wb_dat_i,   // 書き込みデータ
    output reg  [31:0] wb_dat_o,   // 読み出しデータ
    output reg         wb_ack_o    // 1で「受理・完了」
);
    // 1取引につき ack を1クロックだけ返す素直な作り
    wire access = wb_cyc_i & wb_stb_i;

    always @(posedge clk) begin
        if (rst) begin
            wb_ack_o <= 1'b0;
        end else begin
            wb_ack_o <= access & ~wb_ack_o;   // 要求中かつ未ack なら、今クロックで ack
            if (access & ~wb_ack_o) begin
                if (wb_we_i) begin
                    // ここで「書き込み」の処理(レジスタ更新など)
                end else begin
                    // ここで「読み出し」の処理(wb_dat_o に値をのせる)
                end
            end
        end
    end
endmodule

※これは型の骨格です(未検証): 実際のWishboneは、待ち(ウェイト)の扱いや、複数バイトの選択、未割り当て番地への応答など、詰めるべき細部があります。本章のコードは「考え方の型」を示すスケッチであり、完全な実装と検証は、第2章と同じく設計合意のうえでの専用ビルド工程でまとめて行います。以降のGPIO・タイマ・PWMも、この型の中身を埋める形で示します。

3.4 自作GPIO ― スイッチを読み、LEDを点ける

最初の周辺は GPIO(General Purpose Input/Output)。いちばん基本の入出力です。Basys 3 のスイッチ(入力)とLED(出力)を、CPUから扱えるようにします。MMIOの番地を2つ用意するイメージです。

   GPIO の MMIO レジスタ(番地は第2章メモリマップの GPIO 領域内に配置)
 ───────────────────────────────────────────────────────────────
   +0  OUT  (書き込み)この値が16個のLEDに出る
   +4  IN   (読み出し)16個のスイッチの状態が読める
   ───────────────────────────────────────────────────────────────
   CPUの動き: OUT番地に書く → LEDが光る / IN番地を読む → スイッチが分かる
// ※自作GPIOの中身(骨格イメージ・未検証。スレーブの型に中身を入れたもの)
// 書き込み時:下位番地 +0 なら OUTレジスタを更新 → LEDへ
// 読み出し時:下位番地 +4 なら スイッチ入力を返す
if (wb_we_i) begin
    if (wb_adr_i[3:2] == 2'd0) led_reg <= wb_dat_i[15:0];   // +0: OUT → LED
end else begin
    case (wb_adr_i[3:2])
        2'd0: wb_dat_o <= {16'b0, led_reg};                 // +0: OUT読み返し
        2'd1: wb_dat_o <= {16'b0, sw_in};                   // +4: IN(スイッチ)
        default: wb_dat_o <= 32'b0;
    endcase
end
assign led = led_reg;   // 16個のLEDへ

これで、CPUのプログラムから「LEDを点ける/スイッチを読む」ができるようになります。基礎編第6章では1ビットのLEDだけでしたが、ここでは16ビットまとめて、しかも入力も扱えます。SoCが、外の世界と双方向にやり取りする第一歩です。

3.5 自作タイマ ― 時間を数える、そして割り込みへ

次はタイマ。「一定時間ごとに何かをする」ための部品です。仕組みは、基礎編で何度も使ったカウンタそのもの。設定した値まで数えたら「時間になった」という印(フラグ)を立てます。

   タイマの MMIO レジスタ(イメージ)
 ───────────────────────────────────────────────────────────────
   +0  RELOAD   (書き込み)数える上限。例:1秒ぶんのクロック数
   +4  STATUS   (読み出し)時間になったら1が立つ(読むとクリア等)
   ───────────────────────────────────────────────────────────────
   動き: 0から RELOAD まで数える → STATUS のフラグを立てる → また0から

このフラグの使い方には、2つの段階があります。本講座は、やさしい順に進みます。

段階1:ポーリング(やさしい)

CPUがSTATUSを繰り返し読みに行き、フラグが立ったら処理する。仕組みが単純で、まず確実に動かせます。本章ではこちらから始めます。

段階2:割り込み(IRQ・進んだ方法)

フラグが立った瞬間、CPUに「今すぐこっちを見て」と知らせ、自動で処理へ飛ばす。効率的ですが、扱いに作法が要ります(下の注記)。

※PicoRV32の割り込みは「独自方式」です(重要・正直な注記): 第1章の分解で見たとおり、PicoRV32の割り込みは、標準的なRISC-Vの割り込み(mtvec等の仕組み)とは異なる独自の方式です(maskirqwaitirqretirq といったPicoRV32独自の命令と、専用のqレジスタを使います)。そのため、ENABLE_IRQ を有効にして割り込みを使うには、PicoRV32独自の作法に沿ったコードが必要で、標準的なRISC-Vの割り込み解説がそのままは当てはまりません。本講座では、まずポーリングで確実に動かし、割り込みは「PicoRV32の流儀を現物(公式README)で確認してから」慎重に扱います。ここはAIの一般論を鵜呑みにせず、現物で固めるべき箇所です。

3.6 自作PWM ― 明るさ・強さを操る

3つめは PWM(パルス幅変調)。基礎編第5章で「LEDの明るさを16段階に変える」のに使ったあの仕組みを、Wishboneスレーブとして部品化します。これは後の章で大きな意味を持ちます――PWMは、LEDの明るさだけでなく、モーターの回転の強さ(第9章)や、音の出力(第8章)にも使う、応用の広い部品だからです。

   PWM の MMIO レジスタ(イメージ)
 ───────────────────────────────────────────────────────────────
   +0  DUTY  (書き込み)点いている時間の割合(0=消灯〜最大=最も強い)
   ───────────────────────────────────────────────────────────────
   仕組み: 速く点滅させ、DUTY の割合だけ点灯 → 明るさ/強さに見える
// ※自作PWMの中身(骨格イメージ・未検証。基礎編第5章のPWMをスレーブ化)
// 書き込みで DUTY を設定。内部カウンタが DUTY 未満の間だけ出力ON
reg [7:0] duty;
reg [7:0] cnt;
always @(posedge clk) cnt <= cnt + 1'b1;       // 高速に巡回する“ものさし”
assign pwm_out = (cnt < duty);                  // 点灯割合 = duty / 256

// Wishbone 書き込みで duty を更新(スレーブの型の中で)
if (wb_we_i & access & ~wb_ack_o) duty <= wb_dat_i[7:0];

3.7 【確認】プログラムで「調光」と「定周期」を動かす

3つの自作周辺がそろいました。これらをSoCに組み込み、CPUのプログラムから操って確かめます。検証は、序章の段階どおり、シミュレーション→FPGA(Basys 3)の順です。

  1. GPIO: スイッチを読み、その値をLEDに出すプログラム。入力→処理→出力の往復を確認。
  2. PWM: DUTYレジスタに値を書き、LEDの明るさが変わるのを確認。さらにタイマと組み合わせ、DUTYを少しずつ増減させれば「呼吸するLED」になります。
  3. タイマ(ポーリング): 一定時間ごとにSTATUSが立つのを読み取り、LEDを点滅させる。「CPUが時間を数えている」のを目で確認。
  4. 記録: 3つの周辺を足したSoCの利用率と、つまずいた点を実験ログに残す。

※完全なコードは、設計合意のうえで: 本章のGPIO・タイマ・PWMは「型と考え方」を示す骨格です。これらをWishboneでつなぎ、CPUのプログラムまで含めた完全な一式(cat一発のフルコード)と構文・シミュ確認は、ご了承いただいた専用工程でまとめて用意します。勝手に進めず、確認を取ってから――本講座の流儀どおりです。

3.8 この章のまとめ

自分の手で周辺を作る感覚が掴めました。SoCが、外の世界とやり取りを始めています。次の第4章では、いよいよ既製IPの調達に踏み込みます。先輩ボードの弱点だった「メモリの非力さ」を、外部メモリ(SPI Flash/RAM)の実装で正面から脱します。IP調達7ステップを、今度は外部の他人のIPを相手に実演します。