LCDにカラーバーを表示する回路をClaudecodeで実装しました.このデザインをベースとして使い,画面内を移動するロゴを表示させる回路を作ります.
ハードウェアとしては,引き続きTang Primer 25KとPmod接続のLCDを使います.
●ロゴを表示
適当に用意したロゴを表示させます.LCDの画面サイズが240×320なので,ロゴは40×40としました.Claudecodeにロゴとして使う画像を渡し,LCDの画面中央に表示するよう指示したところ,画像データを保持するファイルとしてimg_rom.vというVerilogファイルが出力されました.
メインのモジュールは,カラーバーの表示に加えて,アイコンを描画するように更新されました.カラーバーを表示するだけであれば,ピクセル値は座標だけで決まります.今回はアイコンを表示するので,特定座標だったときだけアイコン・データを使うような回路になっています.
Gowin EDA上でデザインにimg_rom.vを追加し,合成します.特に修正する必要もなく,右図の通りアイコンが表示されました.

●ロゴを動かす
Claudecodeに指示したところ,すぐに正しい解釈がされたようです.

●コード
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
//
// 表示内容:
// 背景: 8色縦縞テストパターン (各40px × 8 = 320px)
// アニメーション: ウサギ画像 (40x40px) が斜め移動・壁で跳ね返る
// 0.1秒ごとに1px移動 (X+Y同時)
//
// リセット: ユーザボタン H11 (押下=Low) で非同期リセット
// ============================================================
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段同期化 + 非同期リセット生成
// ============================================================
reg btn_r1, btn_r2;
always @(posedge clk) begin
btn_r1 <= btn_rst;
btn_r2 <= btn_r1;
end
wire rst_n = btn_r2;
// ============================================================
// 画像パラメータ
// LCD : 320(col) x 240(row)
// 画像 : 40x40px
// 移動範囲: X=0..(320-40-1)=279, Y=0..(240-40-1)=199
// ============================================================
localparam [8:0] IMG_W = 9'd40;
localparam [7:0] IMG_H = 8'd40;
localparam [8:0] LCD_COLS = 9'd320;
localparam [7:0] LCD_ROWS = 8'd240;
localparam [8:0] X_MAX = 9'd279; // 320-40-1
localparam [7:0] Y_MAX = 8'd199; // 240-40-1
// ============================================================
// 画像ROM インスタンス
// ============================================================
reg [10:0] img_addr;
wire [15:0] img_data;
img_rom u_img_rom (
.addr (img_addr),
.data (img_data)
);
// ============================================================
// 初期化シーケンス ROM
// bit[8]=0 → CMD (DC=Low), bit[8]=1 → DAT (DC=High)
// ============================================================
localparam [6:0] ROM_DEPTH = 7'd68;
localparam [8:0]
ROM_00 = 9'h001, ROM_01 = 9'h011,
ROM_02 = 9'h0C0, ROM_03 = 9'h123,
ROM_04 = 9'h0C1, ROM_05 = 9'h110,
ROM_06 = 9'h0C5, ROM_07 = 9'h13E, ROM_08 = 9'h128,
ROM_09 = 9'h0C7, ROM_10 = 9'h186,
ROM_11 = 9'h036, ROM_12 = 9'h128,
ROM_13 = 9'h03A, ROM_14 = 9'h155,
ROM_15 = 9'h0B1, ROM_16 = 9'h100, ROM_17 = 9'h11B,
ROM_18 = 9'h0B6, ROM_19 = 9'h108, ROM_20 = 9'h182, ROM_21 = 9'h127,
ROM_22 = 9'h026, ROM_23 = 9'h101,
ROM_24 = 9'h0E0,
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,
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 (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 (0~319)
ROM_62 = 9'h100, ROM_63 = 9'h100,
ROM_64 = 9'h100, ROM_65 = 9'h1EF,
ROM_66 = 9'h029, ROM_67 = 9'h02C;
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) - 8色縦縞背景
// ============================================================
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 待機
// ============================================================
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)
init_start <= 1'b1;
else begin
init_start <= 1'b0;
poweron_cnt <= poweron_cnt + 1'b1;
end
end
end
// ============================================================
// SPI バイト送信器
// Mode 0: tx_cnt[5:2]=ビット番号, tx_cnt[1:0]=フェーズ
// ============================================================
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];
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: ;
2'b11: begin
lcd_sck <= 1'b0;
if (tx_cnt[5:2] == 4'd7) begin
tx_busy <= 1'b0;
lcd_cs <= 1'b1;
end
end
endcase
end
end
// ============================================================
// アニメーション: 画像位置管理
//
// img_x : 画像左端のLCD列位置 (0 .. X_MAX=279)
// img_y : 画像上端のLCD行位置 (0 .. Y_MAX=199)
// dir_x : X方向 (1'b0=+1, 1'b1=-1)
// dir_y : Y方向 (1'b0=+1, 1'b1=-1)
//
// move_pulse: 0.1秒ごとの1クロックパルス
// 50MHz × 0.1s = 5,000,000サイクル
//
// 描画開始時 (S_FRAME_START) に位置を確定し、
// 描画中は位置を変えない (テアリング防止)
// ============================================================
localparam [22:0] MOVE_PERIOD = 23'd2_000_000;
reg [22:0] move_cnt;
reg move_pulse;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
move_cnt <= 23'd0;
move_pulse <= 1'b0;
end else begin
if (move_cnt == MOVE_PERIOD - 1'b1) begin
move_cnt <= 23'd0;
move_pulse <= 1'b1;
end else begin
move_cnt <= move_cnt + 1'b1;
move_pulse <= 1'b0;
end
end
end
// 画像位置レジスタ (描画開始時にスナップショット)
reg [8:0] img_x; // 現在のX位置 (描画用、動かない)
reg [7:0] img_y; // 現在のY位置 (描画用、動かない)
reg [8:0] next_x; // 次フレームのX位置
reg [7:0] next_y; // 次フレームのY位置
reg dir_x; // 0=右方向, 1=左方向
reg dir_y; // 0=下方向, 1=上方向
// 位置更新ロジック (move_pulse ごとに next_x/next_y を更新)
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
next_x <= 9'd140; // 初期位置: 中央付近
next_y <= 8'd100;
dir_x <= 1'b0; // 初期方向: 右下
dir_y <= 1'b0;
end else if (move_pulse) begin
// X方向移動 & 跳ね返り
if (!dir_x) begin
if (next_x >= X_MAX) begin
next_x <= next_x - 9'd1;
dir_x <= 1'b1;
end else
next_x <= next_x + 9'd1;
end else begin
if (next_x == 9'd0) begin
next_x <= next_x + 9'd1;
dir_x <= 1'b0;
end else
next_x <= next_x - 9'd1;
end
// Y方向移動 & 跳ね返り
if (!dir_y) begin
if (next_y >= Y_MAX) begin
next_y <= next_y - 8'd1;
dir_y <= 1'b1;
end else
next_y <= next_y + 8'd1;
end else begin
if (next_y == 8'd0) begin
next_y <= next_y + 8'd1;
dir_y <= 1'b0;
end else
next_y <= next_y - 8'd1;
end
end
end
// ============================================================
// メインステートマシン
// ============================================================
localparam [3:0]
S_WAIT = 4'd0,
S_INIT_TX = 4'd1,
S_INIT_WAIT = 4'd2,
S_SWRST_DLY = 4'd3,
S_SLPOUT_DLY = 4'd4,
S_FRAME_START= 4'd5, // フレーム開始: 位置スナップショット
S_PIX_CALC = 4'd6, // ピクセル色決定
S_PIX_ADDR_W = 4'd7, // img_rom 読み出し待ち (1サイクル)
S_PIX_HI_TX = 4'd8,
S_PIX_HI_W = 4'd9,
S_PIX_LO_TX = 4'd10,
S_PIX_LO_W = 4'd11,
S_FRAME_END = 4'd12, // フレーム完了: 次フレーム待機
S_NEXT_FRAME = 4'd13; // Memory Write 再発行
reg [3:0] state;
reg [6:0] init_idx;
reg [23:0] dly_cnt;
reg [16:0] pix_cnt; // 0..76799
reg [8:0] col_cnt; // 0..319
reg [7:0] row_cnt; // 0..239
reg [15:0] cur_color;
// 画像領域判定 (描画中は img_x/img_y を使用)
wire in_img_col = (col_cnt >= img_x) && (col_cnt < (img_x + {1'b0, IMG_W[7:0]}));
wire in_img_row = (row_cnt >= img_y) && (row_cnt < (img_y + IMG_H));
wire in_image = in_img_col && in_img_row;
// 画像ROM アドレス (img_x/img_y 基準の相対座標)
wire [7:0] img_rel_col = col_cnt[7:0] - img_x[7:0];
wire [7:0] img_rel_row = row_cnt - img_y;
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;
row_cnt <= 8'd0;
cur_color <= 16'd0;
tx_start <= 1'b0;
tx_byte <= 8'd0;
tx_dc_reg <= 1'b0;
img_addr <= 11'd0;
img_x <= 9'd140;
img_y <= 8'd100;
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_start <= 1'b1;
state <= S_INIT_WAIT;
end
end else begin
state <= S_FRAME_START;
end
end
S_INIT_WAIT: begin
if (!tx_busy && !tx_start) begin
case (tx_byte)
8'h01: begin dly_cnt <= 24'd0; state <= S_SWRST_DLY; end
8'h11: begin 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
S_SWRST_DLY: begin
if (dly_cnt == 24'd250_000)
begin init_idx <= init_idx + 1'b1; state <= S_INIT_TX; end
else dly_cnt <= dly_cnt + 1'b1;
end
S_SLPOUT_DLY: begin
if (dly_cnt == 24'd6_000_000)
begin init_idx <= init_idx + 1'b1; state <= S_INIT_TX; end
else dly_cnt <= dly_cnt + 1'b1;
end
// ---- フレーム開始: 位置スナップショット ----
// next_x/next_y を img_x/img_y に取り込んで描画開始
S_FRAME_START: begin
img_x <= next_x;
img_y <= next_y;
pix_cnt <= 17'd0;
col_cnt <= 9'd0;
row_cnt <= 8'd0;
state <= S_PIX_CALC;
end
// ---- ピクセル色決定 ----
S_PIX_CALC: begin
if (in_image) begin
// row*40 + col (40=32+8 → シフト加算)
img_addr <= (({3'b000, img_rel_row} << 5)
+ ({3'b000, img_rel_row} << 3))
+ {3'b000, img_rel_col[7:0]};
state <= S_PIX_ADDR_W; // 1サイクル待機
end else begin
cur_color <= get_color(col_cnt);
state <= S_PIX_HI_TX;
end
end
// ---- img_rom 読み出し待ち ----
S_PIX_ADDR_W: begin
cur_color <= img_data;
state <= S_PIX_HI_TX;
end
// ---- SPI 送信 (上位バイト) ----
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
// ---- SPI 送信 (下位バイト) ----
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
// 列・行カウンタ更新
if (col_cnt == 9'd319) begin
col_cnt <= 9'd0;
row_cnt <= row_cnt + 8'd1;
end else
col_cnt <= col_cnt + 9'd1;
if (pix_cnt == 17'd76_799) begin
// フレーム完了
state <= S_FRAME_END;
end else begin
pix_cnt <= pix_cnt + 17'd1;
state <= S_PIX_CALC;
end
end
end
// ---- フレーム完了: 次フレームのタイミング待ち ----
// move_pulse が来たら次フレームを開始
S_FRAME_END: begin
if (move_pulse)
state <= S_NEXT_FRAME;
end
// ---- 次フレーム: Memory Write コマンド再発行 ----
// ILI9341 は 0x2C を送るとアドレスポインタが先頭に戻る
S_NEXT_FRAME: begin
if (!tx_busy && !tx_start) begin
tx_byte <= 8'h2C; // Memory Write
tx_dc_reg <= 1'b0; // Command
tx_start <= 1'b1;
state <= S_FRAME_START;
end
end
default: state <= S_WAIT;
endcase
end
end
endmoduleExpand
●動作確認
うまく動きました.
ピンバック: Claude CodeでGowin FPGA開発 -ILI9341編- | FPGAマガジン公式