FPGAからLCDを動かします.デザインは,Claudecodeに書かせます.
使うFPGAボードはTang Primer 25K(Sipeed)です.このボードにはGowin FPGAが載っています.LCDはPmodを2ポート使って接続するモジュールを利用します.と言っても1ポートはタッチパネル用なので,今回は使っていません.
ILI9341はSPIで制御します.
適当に指示してデザインを作らせたところ,当初 ILI9341の初期化用パラメータは,initial ブロックに記述されていたので,修正させました.
合成および実機での動作確認後の様子を右図に示します.
初期化用パラメータの液晶方向に関わる部分として,次の2点を手動で修正しました.
・ 0x36 ( Mem Access Ctrl CMD ) のデータを修正(28 → E8)
・ 0h2A ( Column Addr Set CMD (0~239)) と 0h2B ( Row Addr Set CMD (0~319)) のデータを入れ替え(319 ←→ 239)

Claudecodeが出してきた,ILI9341にカラーバーを描かせるVerilogコード
Verilog
// ============================================================
// lcd_ctrl.v - ILI9341 SPI LCD Controller
// Tang Primer 25K + PMOD-TFTLCD v1.1
//
// Display : 320 x 240, RGB565 (16-bit color)
// Interface: 4-wire SPI (CS, DC, MOSI, SCK)
// SPI clock: 50MHz / 4 = 12.5MHz
//
// RST : ボード側VCC固定 → SW Reset(0x01)コマンドで代替
// BLK : ボード側VCC固定 → FPGA未接続
// リセット: ユーザボタン H11 (押下=Low) で非同期リセット
//
// ★ initial文廃止: 初期化ROMをlocalparam + case文(関数)で実装
// ★ 全レジスタを always @(posedge clk or negedge rst_n) で統一
//
// 8色縦縞テストパターン (各40px × 8 = 320px):
// 省略
// ============================================================
module lcd_ctrl (
input wire clk, // 50 MHz
input wire btn_rst, // ユーザボタン H11 (押下=Low, 非同期リセット)
output reg lcd_cs, // G5 Chip Select (Low有効)
output reg lcd_dc, // K11 Data/Command (RS)
output reg lcd_mosi, // G8 SPI MOSI
output reg lcd_sck // J5 SPI Clock
);
// ============================================================
// ユーザボタン 2段同期化 + リセット生成
// ボタン押下=Low → rst_n=0 でリセット
// ============================================================
reg btn_r1, btn_r2;
always @(posedge clk) begin
btn_r1 <= btn_rst;
btn_r2 <= btn_r1;
end
wire rst_n = btn_r2;
// ============================================================
// 初期化シーケンス ROM (initial文不使用)
//
// localparam で全エントリを定数として定義し、
// rom_read() 関数の case 文でアドレスに応じて返す。
// 合成ツールはこれをROM/MUXとして実装する。
//
// bit[8]=0 → コマンド送信 (DC=Low)
// bit[8]=1 → データ送信 (DC=High)
//
// 特殊コマンド後の待機 (ステートマシン側で対応):
// 0x01 (SW Reset) → 5ms 待機
// 0x11 (Sleep Out) → 120ms 待機
// ============================================================
localparam [6:0] ROM_DEPTH = 7'd68;
//localparam [6:0] ROM_DEPTH = 7'd105;
localparam [8:0]
ROM_00 = 9'h001, // SW Reset CMD
ROM_01 = 9'h011, // Sleep Out CMD
ROM_02 = 9'h0C0, // Power Ctrl 1 CMD
ROM_03 = 9'h123, // DAT 0x23
ROM_04 = 9'h0C1, // Power Ctrl 2 CMD
ROM_05 = 9'h110, // DAT 0x10
ROM_06 = 9'h0C5, // VCOM Ctrl 1 CMD
ROM_07 = 9'h13E, // DAT 0x3E
ROM_08 = 9'h128, // DAT 0x28
ROM_09 = 9'h0C7, // VCOM Ctrl 2 CMD
ROM_10 = 9'h186, // DAT 0x86
ROM_11 = 9'h036, // Mem Access Ctrl CMD
ROM_12 = 9'h1E8, // DAT 0x28 (MV=1, BGR=1, 320x240) !
ROM_13 = 9'h03A, // Pixel Format CMD
ROM_14 = 9'h155, // DAT 0x55 (16bit RGB565)
ROM_15 = 9'h0B1, // Frame Rate CMD
ROM_16 = 9'h100, // DAT 0x00
ROM_17 = 9'h118, // DAT 0x1B !
ROM_18 = 9'h0B6, // Disp Func Ctrl CMD
ROM_19 = 9'h108, // DAT 0x08
ROM_20 = 9'h182, // DAT 0x82
ROM_21 = 9'h127, // DAT 0x27
ROM_22 = 9'h026, // Gamma Set CMD
ROM_23 = 9'h101, // DAT 0x01
ROM_24 = 9'h0E0, // Pos Gamma CMD (以下15バイト)
ROM_25 = 9'h10F, ROM_26 = 9'h131, ROM_27 = 9'h12B,
ROM_28 = 9'h10C, ROM_29 = 9'h10E, ROM_30 = 9'h108,
ROM_31 = 9'h14E, ROM_32 = 9'h1F1, ROM_33 = 9'h137,
ROM_34 = 9'h107, ROM_35 = 9'h110, ROM_36 = 9'h103,
ROM_37 = 9'h10E, ROM_38 = 9'h109, ROM_39 = 9'h100,
ROM_40 = 9'h0E1, // Neg Gamma CMD (以下15バイト)
ROM_41 = 9'h100, ROM_42 = 9'h10E, ROM_43 = 9'h114,
ROM_44 = 9'h103, ROM_45 = 9'h111, ROM_46 = 9'h107,
ROM_47 = 9'h131, ROM_48 = 9'h1C1, ROM_49 = 9'h148,
ROM_50 = 9'h108, ROM_51 = 9'h10F, ROM_52 = 9'h10C,
ROM_53 = 9'h131, ROM_54 = 9'h136, ROM_55 = 9'h10F,
ROM_56 = 9'h02A, // Column Addr Set CMD (0~239)
ROM_57 = 9'h100, ROM_58 = 9'h100,
ROM_59 = 9'h101, ROM_60 = 9'h13F,
ROM_61 = 9'h02B, // Row Addr Set CMD (0~319)
ROM_62 = 9'h100, ROM_63 = 9'h100,
ROM_64 = 9'h100, ROM_65 = 9'h1EF,
ROM_66 = 9'h029, // Display ON CMD
ROM_67 = 9'h02C; // Memory Write CMD (ピクセルデータ開始)
// ROMアクセス関数 (合成時はMUX/LUTに展開される)
function [8:0] rom_read;
input [6:0] addr;
case (addr)
7'd0 : rom_read=ROM_00; 7'd1 : rom_read=ROM_01;
7'd2 : rom_read=ROM_02; 7'd3 : rom_read=ROM_03;
7'd4 : rom_read=ROM_04; 7'd5 : rom_read=ROM_05;
7'd6 : rom_read=ROM_06; 7'd7 : rom_read=ROM_07;
7'd8 : rom_read=ROM_08; 7'd9 : rom_read=ROM_09;
7'd10: rom_read=ROM_10; 7'd11: rom_read=ROM_11;
7'd12: rom_read=ROM_12; 7'd13: rom_read=ROM_13;
7'd14: rom_read=ROM_14; 7'd15: rom_read=ROM_15;
7'd16: rom_read=ROM_16; 7'd17: rom_read=ROM_17;
7'd18: rom_read=ROM_18; 7'd19: rom_read=ROM_19;
7'd20: rom_read=ROM_20; 7'd21: rom_read=ROM_21;
7'd22: rom_read=ROM_22; 7'd23: rom_read=ROM_23;
7'd24: rom_read=ROM_24; 7'd25: rom_read=ROM_25;
7'd26: rom_read=ROM_26; 7'd27: rom_read=ROM_27;
7'd28: rom_read=ROM_28; 7'd29: rom_read=ROM_29;
7'd30: rom_read=ROM_30; 7'd31: rom_read=ROM_31;
7'd32: rom_read=ROM_32; 7'd33: rom_read=ROM_33;
7'd34: rom_read=ROM_34; 7'd35: rom_read=ROM_35;
7'd36: rom_read=ROM_36; 7'd37: rom_read=ROM_37;
7'd38: rom_read=ROM_38; 7'd39: rom_read=ROM_39;
7'd40: rom_read=ROM_40; 7'd41: rom_read=ROM_41;
7'd42: rom_read=ROM_42; 7'd43: rom_read=ROM_43;
7'd44: rom_read=ROM_44; 7'd45: rom_read=ROM_45;
7'd46: rom_read=ROM_46; 7'd47: rom_read=ROM_47;
7'd48: rom_read=ROM_48; 7'd49: rom_read=ROM_49;
7'd50: rom_read=ROM_50; 7'd51: rom_read=ROM_51;
7'd52: rom_read=ROM_52; 7'd53: rom_read=ROM_53;
7'd54: rom_read=ROM_54; 7'd55: rom_read=ROM_55;
7'd56: rom_read=ROM_56; 7'd57: rom_read=ROM_57;
7'd58: rom_read=ROM_58; 7'd59: rom_read=ROM_59;
7'd60: rom_read=ROM_60; 7'd61: rom_read=ROM_61;
7'd62: rom_read=ROM_62; 7'd63: rom_read=ROM_63;
7'd64: rom_read=ROM_64; 7'd65: rom_read=ROM_65;
7'd66: rom_read=ROM_66; 7'd67: rom_read=ROM_67;
default: rom_read = 9'h000;
endcase
endfunction
// ============================================================
// カラーパレット関数 (RGB565)
// ============================================================
function [15:0] get_color;
input [8:0] col;
begin
if (col < 9'd40) get_color = 16'hFFFF; // 白
else if (col < 9'd80) get_color = 16'hFFE0; // 黄
else if (col < 9'd120) get_color = 16'h07FF; // シアン
else if (col < 9'd160) get_color = 16'h07E0; // 緑
else if (col < 9'd200) get_color = 16'hF81F; // マゼンタ
else if (col < 9'd240) get_color = 16'hF800; // 赤
else if (col < 9'd280) get_color = 16'h001F; // 青
else get_color = 16'h0000; // 黒
end
endfunction
// ============================================================
// 電源ON後 150ms 待機
// リセット解除後にカウント開始し、完了で init_start をアサート
// 150ms @ 50MHz = 7,500,000 サイクル
// ============================================================
localparam [23:0] POWERON_WAIT = 24'd7_500_000;
reg [23:0] poweron_cnt;
reg init_start;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
poweron_cnt <= 24'd0;
init_start <= 1'b0;
end else begin
if (poweron_cnt == POWERON_WAIT) begin
init_start <= 1'b1; // 1クロックパルスで通知
end else begin
init_start <= 1'b0;
poweron_cnt <= poweron_cnt + 1'b1;
end
end
end
// ============================================================
// SPI バイト送信器
// Mode 0 (CPOL=0, CPHA=0): 立ち上がりエッジでサンプル
// 1ビット=4クロック、1バイト=32クロック (2.56us @ 50MHz)
//
// tx_cnt[5:2] : ビット番号 (0=MSB, 7=LSB)
// tx_cnt[1:0] : フェーズ
// 00: MOSI セットアップ
// 01: SCK 立ち上がり (ILI9341 がサンプル)
// 10: SCK High 保持
// 11: SCK 立ち下がり、最終ビットなら送信完了
// ============================================================
reg [7:0] tx_byte;
reg tx_dc_reg;
reg tx_start;
reg tx_busy;
reg [5:0] tx_cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
lcd_cs <= 1'b1;
lcd_sck <= 1'b0;
lcd_mosi <= 1'b0;
lcd_dc <= 1'b0;
tx_busy <= 1'b0;
tx_cnt <= 6'd0;
end else if (tx_start && !tx_busy) begin
// 送信開始
tx_busy <= 1'b1;
tx_cnt <= 6'd0;
lcd_cs <= 1'b0;
lcd_dc <= tx_dc_reg;
lcd_sck <= 1'b0;
lcd_mosi <= tx_byte[7]; // MSB 先行
end else if (tx_busy) begin
tx_cnt <= tx_cnt + 1'b1;
case (tx_cnt[1:0])
2'b00: lcd_mosi <= tx_byte[7 - tx_cnt[5:2]]; // データ出力
2'b01: lcd_sck <= 1'b1; // 立ち上がり
2'b10: ; // High 保持
2'b11: begin
lcd_sck <= 1'b0;
if (tx_cnt[5:2] == 4'd7) begin // 8ビット完了
tx_busy <= 1'b0;
lcd_cs <= 1'b1;
end
end
endcase
end
end
// ============================================================
// メインステートマシン
// ============================================================
localparam [3:0]
S_WAIT = 4'd0, // 電源ON待機 (init_start 待ち)
S_INIT_TX = 4'd1, // 初期化ROMエントリ送信要求
S_INIT_WAIT = 4'd2, // SPI 送信完了待ち
S_SWRST_DLY = 4'd3, // SW Reset 後 5ms 待機
S_SLPOUT_DLY = 4'd4, // Sleep Out 後 120ms 待機
S_PIX_CALC = 4'd5, // ピクセル色決定
S_PIX_HI_TX = 4'd6, // 上位バイト送信要求
S_PIX_HI_W = 4'd7, // 上位バイト完了待ち
S_PIX_LO_TX = 4'd8, // 下位バイト送信要求
S_PIX_LO_W = 4'd9, // 下位バイト完了待ち
S_DONE = 4'd10; // 表示完了・ボタンリセット待ち
reg [3:0] state;
reg [6:0] init_idx;
reg [23:0] dly_cnt;
reg [16:0] pix_cnt; // 0 .. 76799 (320×240-1)
reg [8:0] col_cnt; // 0 .. 319
reg [15:0] cur_color;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_WAIT;
init_idx <= 7'd0;
dly_cnt <= 24'd0;
pix_cnt <= 17'd0;
col_cnt <= 9'd0;
cur_color <= 16'd0;
tx_start <= 1'b0;
tx_byte <= 8'd0;
tx_dc_reg <= 1'b0;
end else begin
tx_start <= 1'b0; // デフォルト: 非アサート
case (state)
// ---- 電源ON 150ms 待機 ----
S_WAIT: begin
if (init_start) begin
init_idx <= 7'd0;
state <= S_INIT_TX;
end
end
// ---- 初期化シーケンス送信 ----
S_INIT_TX: begin
if (init_idx < ROM_DEPTH) begin
if (!tx_busy && !tx_start) begin
{tx_dc_reg, tx_byte} <= rom_read(init_idx);
// tx_dc_reg <= rom_read(init_idx)[8];
tx_start <= 1'b1;
state <= S_INIT_WAIT;
end
end else begin
// 全エントリ送信完了 → ピクセル転送開始
pix_cnt <= 17'd0;
col_cnt <= 9'd0;
state <= S_PIX_CALC;
end
end
// ---- SPI 完了待ち & ディレイ判定 ----
S_INIT_WAIT: begin
if (!tx_busy && !tx_start) begin
case (tx_byte)
8'h01: begin // SW Reset → 5ms 待機
dly_cnt <= 24'd0;
state <= S_SWRST_DLY;
end
8'h11: begin // Sleep Out → 120ms 待機
dly_cnt <= 24'd0;
state <= S_SLPOUT_DLY;
end
default: begin
init_idx <= init_idx + 1'b1;
state <= S_INIT_TX;
end
endcase
end
end
// ---- SW Reset 後 5ms 待機 ----
S_SWRST_DLY: begin
if (dly_cnt == 24'd250_000) begin // 5ms @ 50MHz
init_idx <= init_idx + 1'b1;
state <= S_INIT_TX;
end else
dly_cnt <= dly_cnt + 1'b1;
end
// ---- Sleep Out 後 120ms 待機 ----
S_SLPOUT_DLY: begin
if (dly_cnt == 24'd6_000_000) begin // 120ms @ 50MHz
init_idx <= init_idx + 1'b1;
state <= S_INIT_TX;
end else
dly_cnt <= dly_cnt + 1'b1;
end
// ---- ピクセル転送 ----
S_PIX_CALC: begin
cur_color <= get_color(col_cnt);
state <= S_PIX_HI_TX;
end
S_PIX_HI_TX: begin
if (!tx_busy && !tx_start) begin
tx_byte <= cur_color[15:8];
tx_dc_reg <= 1'b1;
tx_start <= 1'b1;
state <= S_PIX_HI_W;
end
end
S_PIX_HI_W: begin
if (!tx_busy && !tx_start)
state <= S_PIX_LO_TX;
end
S_PIX_LO_TX: begin
if (!tx_busy && !tx_start) begin
tx_byte <= cur_color[7:0];
tx_dc_reg <= 1'b1;
tx_start <= 1'b1;
state <= S_PIX_LO_W;
end
end
S_PIX_LO_W: begin
if (!tx_busy && !tx_start) begin
col_cnt <= (col_cnt == 9'd319) ? 9'd0 : col_cnt + 9'b1;
if (pix_cnt == 17'd76_799)
state <= S_DONE;
else begin
pix_cnt <= pix_cnt + 17'b1;
state <= S_PIX_CALC;
end
end
end
// ---- 完了: ボタンリセット待ち ----
S_DONE: begin
// パターンはLCD GRAMに保持
// ユーザボタン押下 → rst_n=0 → 再スタート
end
default: state <= S_WAIT;
endcase
end
end
endmoduleExpand
トップモジュールについては,PLLのIPは手動で追加しましたが,それ以外の部分は ほぼそのまま利用しました.
Claudecodeが出してきたトップモジュール
Verilog
// ============================================================
// 省略
//
// Hardware Connection:
// Port0 Pmod5 (G5) → CS (Chip Select, active low)
// Port0 Pmod6 (G8) → MOSI (SPI Data)
// Port0 Pmod8 (J5) → CLK (SPI Clock)
// Port1 Pmod3 (K11) → RS (Data/Command = DC)
//
// RST : ボード側でVCCに固定 (FPGA未接続)
// BLK : ボード側でVCCに固定 (FPGA未接続)
// ============================================================
module lcd_test_top (
input wire clk, // 50 MHz system clock
output wire lcd_cs, // SPI Chip Select (active low) → G5
output wire lcd_mosi, // SPI MOSI → G8
output wire lcd_sck, // SPI Clock → J5
output wire lcd_dc, // Data/Command (RS) → K11
input wire USER_KEY
);
wire clkout0;
lcd_ctrl u_lcd_ctrl (
.clk (clkout0),
.lcd_cs (lcd_cs),
.lcd_mosi (lcd_mosi),
.lcd_sck (lcd_sck),
.lcd_dc (lcd_dc),
.btn_rst (USER_KEY)
);
Gowin_PLL your_instance_name(
.clkout0(clkout0), //output clkout0
.clkin(clk) //input clkin
);
endmoduleClaudecode上でピン番号を指定し,生成させた物理制約ファイル.ほぼそのまま利用した.
Claudecodeが出してきた物理制約ファイル
Verilog
// ============================================================
// lcd_test.cst - Physical Constraints
// Tang Primer 25K Dock (GW5A-LV25MG121) + PMOD-TFTLCD v1.1
// GOWIN EDA format
//
// PMOD-TFTLCD v1.1 接続:
//
// Port0 Pmod5 (G5) → lcd_cs (Chip Select, Low有効)
// Port0 Pmod6 (G8) → lcd_mosi (SPI MOSI)
// Port0 Pmod8 (J5) → lcd_sck (SPI Clock)
// Port1 Pmod3 (K11) → lcd_dc (RS: Data/Command)
//
// RST : ボード側でVCCに固定 → FPGA未接続
// BLK : ボード側でVCCに固定 → FPGA未接続
// ============================================================
// ---- システムクロック (50MHz) ----
// ---- Port0 Pmod5: CS (Chip Select) ----
IO_LOC "lcd_cs" A10;
IO_PORT "lcd_cs" PULL_MODE=UP IO_TYPE=LVCMOS33 DRIVE=8;
// ---- Port0 Pmod6: MOSI (SPI Data) ----
IO_LOC "lcd_mosi" E10;
IO_PORT "lcd_mosi" PULL_MODE=NONE IO_TYPE=LVCMOS33 DRIVE=8;
// ---- Port0 Pmod8: SCK (SPI Clock) ----
IO_LOC "lcd_sck" K5;
IO_PORT "lcd_sck" PULL_MODE=NONE IO_TYPE=LVCMOS33 DRIVE=8;
// ---- Port1 Pmod3: DC/RS (Data/Command) ----
IO_LOC "lcd_dc" K11;
IO_PORT "lcd_dc" PULL_MODE=NONE IO_TYPE=LVCMOS33 DRIVE=8;
IO_LOC "USER_KEY" H11;
IO_PORT "USER_KEY" PULL_MODE=NONE DRIVE=OFF BANK_VCCIO=3.3;
IO_LOC "clk" E2;
IO_PORT "clk" PULL_MODE=NONE DRIVE=OFF BANK_VCCIO=3.3;無事,動いたっぽい. ♪(*’▽’) v
