1use super::psram::{
19 emit_psram_arbiter_verilog, emit_psram_cache_hit_function, emit_psram_cache_hit_wires,
20 emit_psram_cache_verilog, emit_psram_edge_verilog, emit_psram_model_verilog,
21 psram_cache_hit_ports, PsramCache, PsramEdge, EDGE_WORD_ADDR_BITS,
22};
23use super::wpe_floorplan::ArrayRole;
24use super::wprog_engine_rtl::{
25 emit_wprog_engine_soc_offchip_body, emit_wprog_engine_soc_testbench_board, MemBackend, WImage,
26};
27use std::fmt::Write as _;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct OffchipMemory {
32 pub edge: PsramEdge,
33 pub heap: Option<PsramCache>,
34 pub frames: Option<PsramCache>,
35 pub cells: Option<PsramCache>,
38}
39
40#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum OffchipUnplaceable {
43 NothingOffChip,
45 CellsNeedConfigBram { backend: MemBackend },
48 RegionsLargerThanPart { words: u64, part_words: u64 },
50 StallOutlastsByte { stall_cycles: u64, byte_cycles: u64 },
53 InitOutlastsLoad { init_cycles: u64, load_cycles: u64 },
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub struct Region {
61 pub name: &'static str,
62 pub base: u32,
63 pub bits: u32,
64}
65
66pub fn wire_bytes_before_first_part_access() -> u64 {
70 WImage::default().wire_bytes(&[]).len() as u64
71}
72
73pub fn uart_byte_cycles(clks_per_bit: u64) -> u64 {
75 10 * clks_per_bit
76}
77
78fn ceil_log2(n: usize) -> u32 {
79 n.max(1).next_power_of_two().trailing_zeros()
80}
81
82impl OffchipMemory {
83 pub fn roles(&self) -> Vec<ArrayRole> {
85 [
86 (self.heap, ArrayRole::Heap),
87 (self.frames, ArrayRole::FrameStack),
88 (self.cells, ArrayRole::ConfigImage),
89 ]
90 .into_iter()
91 .filter_map(|(c, r)| c.map(|_| r))
92 .collect()
93 }
94
95 pub fn part_words(&self) -> u64 {
97 (self.edge.part.capacity_bytes as u64 / 8).min(1 << EDGE_WORD_ADDR_BITS)
98 }
99
100 pub fn regions(&self, caps: [usize; 10]) -> Vec<Region> {
105 let [nreg, _, maxcell, _, _, _, _, heap, depth, _] = caps;
106 let mut r: Vec<(&'static str, u32)> = Vec::new();
107 if self.cells.is_some() {
108 let bits = ceil_log2(maxcell);
109 r.extend([
110 ("cell_lane0", bits),
111 ("cell_lane1", bits),
112 ("cell_lane2", bits),
113 ]);
114 }
115 if self.frames.is_some() {
116 r.push(("frames", ceil_log2(depth * nreg)));
117 }
118 if self.heap.is_some() {
119 r.push(("heap", ceil_log2(heap)));
120 }
121 r.sort_by(|a, b| b.1.cmp(&a.1));
122 let mut base = 0u32;
123 r.into_iter()
124 .map(|(name, bits)| {
125 let region = Region { name, base, bits };
126 base += 1 << bits;
127 region
128 })
129 .collect()
130 }
131
132 pub fn check(&self, caps: [usize; 10], backend: MemBackend) -> Result<(), OffchipUnplaceable> {
135 if self.roles().is_empty() {
136 return Err(OffchipUnplaceable::NothingOffChip);
137 }
138 if self.cells.is_some() && backend != MemBackend::ConfigBram {
139 return Err(OffchipUnplaceable::CellsNeedConfigBram { backend });
140 }
141 let words: u64 = self.regions(caps).iter().map(|r| 1u64 << r.bits).sum();
142 if words > self.part_words() {
143 return Err(OffchipUnplaceable::RegionsLargerThanPart {
144 words,
145 part_words: self.part_words(),
146 });
147 }
148 Ok(())
149 }
150
151 pub fn loading_stall_cycles(&self) -> u64 {
157 [self.heap, self.cells]
158 .into_iter()
159 .flatten()
160 .map(|c| c.miss_stall_cycles(&self.edge))
161 .max()
162 .unwrap_or(0)
163 }
164
165 pub fn check_uart(&self, clks_per_bit: u64) -> Result<(), OffchipUnplaceable> {
171 let byte_cycles = uart_byte_cycles(clks_per_bit);
172 let stall_cycles = self.loading_stall_cycles();
173 if stall_cycles >= byte_cycles {
174 return Err(OffchipUnplaceable::StallOutlastsByte {
175 stall_cycles,
176 byte_cycles,
177 });
178 }
179 let load_cycles = wire_bytes_before_first_part_access() * byte_cycles;
180 let init_cycles = self.edge.init_cycles();
181 if init_cycles >= load_cycles {
182 return Err(OffchipUnplaceable::InitOutlastsLoad {
183 init_cycles,
184 load_cycles,
185 });
186 }
187 Ok(())
188 }
189
190 pub fn min_clks_per_bit(&self) -> u64 {
192 let by_stall = self.loading_stall_cycles() / 10 + 1;
193 let by_init = self.edge.init_cycles() / (10 * wire_bytes_before_first_part_access()) + 1;
194 by_stall.max(by_init)
195 }
196
197 fn glue(&self, caps: [usize; 10]) -> String {
200 let aw = EDGE_WORD_ADDR_BITS;
201 let regions = self.regions(caps);
202 let addr = |name: &str, index: &str| {
203 let r = regions
204 .iter()
205 .find(|r| r.name == name)
206 .expect("a region per off-chip memory");
207 match aw - r.bits {
208 0 => format!("{index}[{}:0]", aw - 1),
209 high => format!(
210 "{{{high}'d{}, {index}[{}:0]}}",
211 r.base >> r.bits,
212 r.bits - 1
213 ),
214 }
215 };
216 struct Port<'a> {
217 inst: String,
218 cache: &'a PsramCache,
219 req_r: String,
220 ra: String,
221 rhit: String,
222 req_w: String,
223 wa: String,
224 whit: String,
225 wd: &'static str,
226 wm: &'static str,
227 rd: String,
228 }
229 let mut hit_at = String::new();
233 let mut ports: Vec<Port> = Vec::new();
234 let mut cell_wires = String::new();
235 if let Some(c) = &self.heap {
236 ports.push(Port {
237 inst: "heap_cache".into(),
238 cache: c,
239 req_r: "hq_r".into(),
240 ra: addr("heap", "heap_ra"),
241 rhit: "hq_rhit".into(),
242 req_w: "hq_w".into(),
243 wa: addr("heap", "hq_wa"),
244 whit: "hq_whit".into(),
245 wd: "hq_wd",
246 wm: "2'b11",
247 rd: "heap_rd".into(),
248 });
249 let _ = write!(
250 hit_at,
251 " function hq_hit_at(input [31:0] a); hq_hit_at = heap_cache_hit({}); endfunction\n",
252 addr("heap", "a")
253 );
254 }
255 if let Some(c) = &self.frames {
256 ports.push(Port {
257 inst: "frame_cache".into(),
258 cache: c,
259 req_r: "sq_r".into(),
260 ra: addr("frames", "sf_ra"),
261 rhit: "sq_rhit".into(),
262 req_w: "sq_w".into(),
263 wa: addr("frames", "sq_wa"),
264 whit: "sq_whit".into(),
265 wd: "sq_wd",
266 wm: "2'b11",
267 rd: "sf_rd".into(),
268 });
269 let _ = write!(
270 hit_at,
271 " function sq_hit_at(input [31:0] a); sq_hit_at = frame_cache_hit({}); endfunction\n",
272 addr("frames", "a")
273 );
274 }
275 if let Some(c) = &self.cells {
276 cell_wires.push_str(
277 " // The walk reads the current cell every cycle of the run, as the on-chip registered read does.\n\
278 \x20 wire cell_fetch = (ph == P_RUN);\n\
279 \x20 wire [31:0] cell_at = c_cellptr + ci;\n\
280 \x20 wire [63:0] cell_rd0, cell_rd1, cell_rd2;\n\
281 \x20 assign cc_kind_r = cell_rd0[31:0]; assign cc_code_r = cell_rd0[63:32];\n\
282 \x20 assign cc_op0_r = cell_rd1[31:0]; assign cc_op1_r = cell_rd1[63:32];\n\
283 \x20 assign cc_op2_r = cell_rd2[31:0]; assign cc_op3_r = cell_rd2[63:32];\n",
284 );
285 let mut lanes = Vec::new();
286 for k in 0..3 {
287 let lane = format!("cell_lane{k}");
288 lanes.push(format!("{lane}_hit({})", addr(&lane, "a")));
289 ports.push(Port {
290 inst: lane.clone(),
291 cache: c,
292 req_r: "cell_fetch".into(),
293 ra: addr(&lane, "cell_at"),
294 rhit: format!("cq_rhit[{k}]"),
295 req_w: format!("cq_w[{k}]"),
296 wa: addr(&lane, "cq_wa"),
297 whit: format!("cq_whit[{k}]"),
298 wd: "cq_wd",
299 wm: "cq_wm",
300 rd: format!("cell_rd{k}"),
301 });
302 }
303 lanes.reverse();
304 let _ = write!(
305 hit_at,
306 " function [2:0] cq_hit_at(input [31:0] a); cq_hit_at = {{{}}}; endfunction\n",
307 lanes.join(", ")
308 );
309 }
310 let n = ports.len();
311 let mut g = format!(
312 " // The off-chip memories, one cache each, sharing the PSRAM edge through the arbiter. Each lives in its\n\
313 \x20 // own naturally aligned power-of-two region of the part: its address is the region's high bits over\n\
314 \x20 // its own index.\n\
315 \x20 wire [{n1}:0] mem_busy;\n\
316 \x20 wire mstall = |mem_busy;\n\
317 \x20 wire [{n1}:0] mc_valid, mc_ready, mc_we;\n\
318 \x20 wire [{awn}:0] mc_addr; wire [{lw}:0] mc_len; wire [{dw}:0] mc_wdata;\n\
319 \x20 wire me_valid, me_ready, me_we, me_take, me_rvalid;\n\
320 \x20 wire [{am}:0] me_addr; wire [7:0] me_len; wire [63:0] me_wdata, me_rdata;\n\
321 {cell_wires}",
322 n1 = n - 1,
323 awn = n as u32 * aw - 1,
324 lw = n * 8 - 1,
325 dw = n * 64 - 1,
326 am = aw - 1,
327 );
328 for (k, p) in ports.iter().enumerate() {
329 g.push_str(&emit_psram_cache_hit_wires(&p.inst, p.cache));
330 let _ = write!(
331 g,
332 " psram_cache #(.IW({iw}), .OW({ow})) {inst} (\n\
333 \x20 .clk(clk), .rst(rst), .req_r({rq}), .ra({ra}), .req_w({wq}), .wa({wa}), .wd({wd}), .wm({wm}),\n\
334 \x20 {hits},\n\
335 \x20 .rd({rd}), .busy(mem_busy[{k}]), .e_valid(mc_valid[{k}]), .e_ready(mc_ready[{k}]), .e_we(mc_we[{k}]),\n\
336 \x20 .e_addr(mc_addr[{a} +: {aw}]), .e_len(mc_len[{l} +: 8]), .e_wdata(mc_wdata[{d} +: 64]),\n\
337 \x20 .e_wdata_take(me_take), .e_rdata(me_rdata), .e_rdata_valid(me_rvalid));\n",
338 iw = p.cache.index_bits,
339 ow = p.cache.offset_bits,
340 inst = p.inst,
341 hits = psram_cache_hit_ports(&p.inst, &p.rhit, &p.whit),
342 rq = p.req_r,
343 ra = p.ra,
344 wq = p.req_w,
345 wa = p.wa,
346 wd = p.wd,
347 wm = p.wm,
348 rd = p.rd,
349 a = k as u32 * aw,
350 l = k * 8,
351 d = k * 64,
352 );
353 }
354 for p in &ports {
355 g.push_str(&emit_psram_cache_hit_function(&p.inst, p.cache));
356 }
357 g.push_str(&hit_at);
358 let _ = write!(
359 g,
360 " psram_arbiter #(.N({n})) mem_arb (\n\
361 \x20 .clk(clk), .rst(rst), .c_valid(mc_valid), .c_ready(mc_ready), .c_we(mc_we), .c_addr(mc_addr), .c_len(mc_len),\n\
362 \x20 .c_wdata(mc_wdata), .e_valid(me_valid), .e_ready(me_ready), .e_we(me_we), .e_addr(me_addr), .e_len(me_len),\n\
363 \x20 .e_wdata(me_wdata));\n\
364 \x20 psram_edge mem_edge (\n\
365 \x20 .clk(clk), .rst(rst), .req_valid(me_valid), .req_ready(me_ready), .req_we(me_we), .req_addr(me_addr),\n\
366 \x20 .req_len(me_len), .wdata(me_wdata), .wdata_take(me_take), .rdata(me_rdata), .rdata_valid(me_rvalid),\n\
367 \x20 .ce_n(psram_ce_n), .sclk(psram_sclk), .sio_out(psram_sio_out), .sio_oe(psram_sio_oe), .sio_in(psram_sio_in));"
368 );
369 g
370 }
371}
372
373pub fn emit_wprog_engine_soc_verilog_offchip(
376 name: &str,
377 caps: [usize; 10],
378 backend: MemBackend,
379 memory: &OffchipMemory,
380) -> Result<String, OffchipUnplaceable> {
381 memory.check(caps, backend)?;
382 let body = emit_wprog_engine_soc_offchip_body(name, caps, backend, &memory.roles());
383 assert_eq!(
384 body.matches("//__OFFCHIP_GLUE__").count(),
385 1,
386 "the template carries one glue site"
387 );
388 let cache = memory
389 .heap
390 .or(memory.frames)
391 .or(memory.cells)
392 .expect("checked: something is off-chip");
393 Ok(format!(
394 "{}{}\n{}\n{}",
395 body.replace("//__OFFCHIP_GLUE__", &memory.glue(caps)),
396 emit_psram_cache_verilog(&cache),
397 emit_psram_arbiter_verilog(),
398 emit_psram_edge_verilog(&memory.edge),
399 ))
400}
401
402pub fn emit_wprog_engine_soc_offchip_testbench(
407 name: &str,
408 n_vectors: usize,
409 clks_per_bit: usize,
410 max_bytes: usize,
411 memory: &OffchipMemory,
412) -> String {
413 emit_wprog_engine_soc_offchip_testbench_clocked(
414 name,
415 n_vectors,
416 clks_per_bit,
417 max_bytes,
418 memory,
419 memory.edge.sys_period_ps,
420 )
421}
422
423pub fn emit_wprog_engine_soc_offchip_testbench_clocked(
426 name: &str,
427 n_vectors: usize,
428 clks_per_bit: usize,
429 max_bytes: usize,
430 memory: &OffchipMemory,
431 clk_period_ps: u32,
432) -> String {
433 let half_ns = clk_period_ps as f64 / 2000.0;
434 format!(
435 "{}\n{}",
436 emit_wprog_engine_soc_testbench_board(name, n_vectors, clks_per_bit, max_bytes, true)
437 .replace("__HALF_NS__", &format!("{half_ns}")),
438 emit_psram_model_verilog(&memory.edge.part),
439 )
440}