第3章:基本周辺を自作 ― GPIO/タイマ/PWM
第2章で、SoCの骨格(CPU=Wishboneマスタ、アドレスデコーダ、メモリマップ)ができました。本章では、その骨格に最初の周辺機器(ペリフェラル)を足します。題材は、どんなマイコンにもある基本の3つ――GPIO(汎用入出力)・タイマ・PWMです。そしてこれらは、買ってくるのではなく自分で作ります。なぜ自作なのか、その判断から始めましょう。
3.1 思想 ― コアは無改造、まわりをMMIOで足す
本講座の背骨となる考え方を、改めて確認します。CPU本体(PicoRV32)には一切手を入れません。代わりに、周辺機器を「特定の番地に割り当てたWishboneスレーブ」として足し、CPUはその番地を読み書きするだけで周辺を操ります。この「番地への読み書き=周辺機器の操作」という仕組みを MMIO(メモリマップドI/O)と呼びます。
基礎編からの発展: 基礎編第6章で、番地 0x4000_0000 へ sw 命令で書き込むとLEDが点きました。あれがMMIOの最初の一歩でした。ただし基礎編では、CPUの素のメモリI/Fを直接見て応答する「メモリ係」を手書きしていました。本章では、それをWishboneスレーブという、繰り返し使える正式な型に格上げします。一度型を作れば、GPIOもタイマもPWMも、その型の上に乗せるだけ――これがSoCを効率よく育てるコツです。
「FPGAで遊ぶにはC言語ができないとダメなんでしょう?」――入口でよく聞く不安ですが、これは誤解です。FPGAの主役はハードを作るVerilog。Cは、自作したCPUに仕事を“させたい”ときに登場するごく少しの助手にすぎません。本講座でも、出てくるCは数行で読めるものだけです。まず、この講座に出る言語がそれぞれ何の係で、あなたが「書く」のか「読むだけ」なのかを一望しておきましょう。
この講座に出てくる言語 ― だれが何の係で、あなたは「書く」のか ─────────────────────────────────────────────────────────────── 言語 役割(何の係) あなたは… ─────────────────────────────────────────────────────────────── Verilog ハード(回路)そのものを作る ★書く(主役) C 自作CPUに仕事をさせる“最小限” 読めればよい(数行) Python ビルドの裏方/将来の本番ボード用 当面は触らない HTML/CSS この教材サイト 触らない ─────────────────────────────────────────────────────────────── 要点: FPGAの主役は Verilog。C は「番地を叩く数行」だけ。
では、その「数行のC」とは具体的にどれだけか。じつは本講座で読めればよいCの“形”は、たった4つです。if もループ設計も関数も配列もポインタ演算も出てきません。
- ① 番地に書く:
REG = 値;― 周辺を「操作する」(例:LEDを点ける) - ② 番地を読む:
値 = REG;― 周辺の「今」を知る(例:スイッチを読む) - ③ 印が立つまで待つ:
while ((STATUS & 1) == 0) {}― ポーリング(時間を待つ) - ④ 番地に名前を付ける:
#define― 生番地を読みやすい名前にするだけ
第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 の握手から、スレーブが満たすべき約束は明らかでした――「マスタが cyc と stb を立てて自分の番地を指してきたら、必要な処理をして 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等の仕組み)とは異なる独自の方式です(maskirq・waitirq・retirq といった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)の順です。
- GPIO: スイッチを読み、その値をLEDに出すプログラム。入力→処理→出力の往復を確認。
- PWM: DUTYレジスタに値を書き、LEDの明るさが変わるのを確認。さらにタイマと組み合わせ、DUTYを少しずつ増減させれば「呼吸するLED」になります。
- タイマ(ポーリング): 一定時間ごとにSTATUSが立つのを読み取り、LEDを点滅させる。「CPUが時間を数えている」のを目で確認。
- 記録: 3つの周辺を足したSoCの利用率と、つまずいた点を実験ログに残す。
※完全なコードは、設計合意のうえで: 本章のGPIO・タイマ・PWMは「型と考え方」を示す骨格です。これらをWishboneでつなぎ、CPUのプログラムまで含めた完全な一式(cat一発のフルコード)と構文・シミュ確認は、ご了承いただいた専用工程でまとめて用意します。勝手に進めず、確認を取ってから――本講座の流儀どおりです。
3.8 この章のまとめ
- MMIOの思想を正式化した: コアは無改造、周辺は「番地に割り当てたWishboneスレーブ」として足す。
- 自作と調達の判断軸を持った: 単純・明快・自分に密着=自作、複雑・標準・実績重視=調達。理由を言葉にできることが大事。
- スレーブの型を作った: 「cyc&stbで要求が来たら処理してackを返す」共通の器を用意し、以降はその中身を埋めるだけにした。
- 3つの基本周辺を設計した: GPIO(入出力)、タイマ(ポーリングから。割り込みはPicoRV32独自方式を現物確認のうえ慎重に)、PWM(明るさ・強さ・音にも使える応用部品)。
自分の手で周辺を作る感覚が掴めました。SoCが、外の世界とやり取りを始めています。次の第4章では、いよいよ既製IPの調達に踏み込みます。先輩ボードの弱点だった「メモリの非力さ」を、外部メモリ(SPI Flash/RAM)の実装で正面から脱します。IP調達7ステップを、今度は外部の他人のIPを相手に実演します。