CRAFT cycle · 2.5 hours · Mon 6/8 · Week 3 open
HDL for Digital System Design · UCF ECE · Barcelona Summer 2026
| Phase | Time | Activity |
|---|---|---|
| 🌍 Contextualize | 10 min | Memory everywhere · the iCE40's 64 Kbit secret |
| ⚠️ Reframe | 15 min | Memory ≠ array · the EBR inference pattern |
| 🛠 Assemble | 70 min | ROM sequencer · inferred BRAM · RAW testbench · dual-port stretch |
| 🛡 Fortify | 45 min | Confirm SB_RAM40_4K · read-after-write coverage · 🤖 AI BRAM critique |
| 🔗 Transfer | 10 min | Re-ground in Weeks 1–2 · → D10 timing (tomorrow) |
▸ Phase 1 of 5 · ~10 min
Storage is in everything you've touched in Barcelona
Today's iCE40 has 16 Block RAMs × 4 Kbits = 64 Kbits of fast, dedicated memory. Free — if your code asks for it correctly.
Same 256×8 memory implemented as LUTs: ~2 048 SB_LUT4s. As EBR: 1 Block RAM. That's the difference between fitting on the chip and not.
▸ Phase 2 of 5 · ~15 min
Memory has physics. Code reflects it.
"reg [7:0] mem [0:255]; is just an array. I'll read and write it however I want."
Real memories have physical constraints: synchronous vs async read, port count, init mechanism. Coding patterns are recipes the synthesizer recognizes — match the recipe, get free EBR; miss it, burn LUTs.
SB_RAM40_4K. Combinational read ⇒ LUT RAM (or worse).
dout updated inside @(posedge clk)? This is the pattern you match in Ex 2.
module bram #(
parameter ADDR_WIDTH = 8,
parameter DATA_WIDTH = 8
)(
input wire clk,
input wire we,
input wire [ADDR_WIDTH-1:0] addr,
input wire [DATA_WIDTH-1:0] din,
output reg [DATA_WIDTH-1:0] dout // ← reg, registered
);
reg [DATA_WIDTH-1:0] mem [0:(1<<ADDR_WIDTH)-1];
always @(posedge clk) begin
if (we) mem[addr] <= din;
dout <= mem[addr]; // ← READ inside @(posedge clk)
end
endmodule
dout is reg · the read is sequential · Yosys can map this to SB_RAM40_4Kalways block for both ports — the standard "no read-during-write" pattern// ❌ This LOOKS efficient. It is not.
reg [7:0] mem [0:255];
assign dout = mem[addr]; // combinational read
always @(posedge clk)
if (we) mem[addr] <= din;
// ✓ Synchronous read — Yosys can map to EBR
always @(posedge clk) begin
if (we) mem[addr] <= din;
dout <= mem[addr];
end
yosys stat after writing memory code.
▸ Phase 3 of 5 · ~70 min · You build
ROM · RAM · TB · stretch
.hex, load with $readmemh, step a counter through addresses, drive LEDs & 7-seg.SB_RAM40_4K appears in yosys stat.module rom_seq #(
parameter INIT_FILE = "patterns.hex"
)(
input wire clk,
input wire [3:0] addr,
output reg [7:0] data
);
reg [7:0] mem [0:15];
initial $readmemh(INIT_FILE, mem); // synthesizable in Yosys/iCE40
always @(posedge clk) data <= mem[addr]; // registered read
endmodule
patterns.hex (one byte per line):
01 03 07 0F 1F 3F 7F FF ...
Drive addr from a slow counter — LEDs trace the pattern at human speed.
// Self-checking pattern: write N, read back, verify
task automatic check (input [7:0] addr_i, input [7:0] data_i);
begin
@(posedge clk); we <= 1; addr <= addr_i; din <= data_i;
@(posedge clk); we <= 0; // 1 cycle latency
@(posedge clk); // capture
if (dout !== data_i) begin
$display("FAIL: wrote %02h at %02h, read %02h", data_i, addr_i, dout);
fails = fails + 1;
end
tests = tests + 1;
end
endtask
initial begin
fails = 0; tests = 0;
check(8'h00, 8'hA5);
check(8'h01, 8'h5A);
check(8'hFF, 8'hC3);
check(8'h00, 8'h12); // overwrite addr 0
if (fails == 0) $display("PASS: %0d tests", tests);
else $display("FAIL: %0d/%0d", fails, tests);
$finish;
end
Note the 2-cycle latency — write on edge 1, dout valid on edge 3. This is what "synchronous read" means.
▸ Phase 4 of 5 · ~45 min · Confirm EBR · sweep · AI check
Trust the synth report, not the source
yosys -p "synth_ice40 -top bram; stat" bram.v 2>&1 \
| grep -E "SB_RAM|SB_LUT4|SB_DFF"
SB_RAM40_4K 1 SB_LUT4 ~30 SB_DFF ~8
SB_RAM40_4K 0 SB_LUT4 2048 SB_DFF 8
dout is declared wire instead of reg. Fix and re-synth.
Prompt: "Write a dual-port RAM in Verilog for the Lattice iCE40 that Yosys will infer as Block RAM."
dout outputs as reg? Many fall back to assign dout = mem[addr]; (combinational) — kills EBR inference.always @(posedge clk) per port? The iCE40 EBR has two independently clocked ports.SB_RAM40_4K directly? If so — politely refuse. We want inference, not hard instantiation. Inferred code is portable; the primitive isn't.yosys stat on the AI's code and your own. Compare. Save to portfolio.patterns.hex appear on LEDs / 7-seg at the expected cadence. Change the hex file, re-synth, see new pattern.yosys stat is the only honest check.▸ Phase 5 of 5 · ~10 min · Week 3 opens
Where we left off, and where memory takes you
Back from the catch-up day, free Friday, and weekend — here's the foundation memory builds on:
W1 Hardware thinking · data types · combinational logic · clocked logic & RTL.
D5+D6 Counters · shift registers · debouncing · self-checking + 🤖 AI testbenches. Semidynamics.
D7 FSMs · 3-block pattern · Moore & Mealy. HP Customer Center.
D8 Hierarchy · parameters · generate · first PPA width sweep. Cooking workshop.
D9 ROM, RAM & Block RAM inference. You can now describe any synchronous digital subsystem.
Tue 6/9 brings setup, hold, Fmax, and PPA — the constraints that turn working code into shippable silicon. Plus numerical architectures: adders, shift-add multipliers, fixed-point.
Bring your D8 PPA table — we'll add a timing column. Eve: Flamenco.
🔗 End of Day 9 · Week 3 is underway
You can now design any synchronous subsystem — and verify it both manually and with AI.
This week: timing, then UART — your first communication protocol.