1use std::fmt;
24use std::path::PathBuf;
25
26use crate::from_graph::FromGraphOptions;
27use crate::legalize::{ExportQuantMode, LegalizeOptions};
28use crate::tune::{OptTarget, Tune};
29
30#[derive(Debug, Clone, PartialEq, Eq, Default)]
36pub enum HwTarget {
37 #[default]
40 Generic,
41 Ecp5 {
43 device: String,
45 package: String,
47 },
48 Ice40 { device: String, package: String },
50 Xilinx7 {
54 part: String,
56 },
57}
58
59impl HwTarget {
60 pub fn generic() -> Self {
62 Self::Generic
63 }
64
65 pub fn ecp5_25k() -> Self {
67 Self::Ecp5 {
68 device: "25k".into(),
69 package: "CABGA381".into(),
70 }
71 }
72
73 pub fn ice40_up5k() -> Self {
75 Self::Ice40 {
76 device: "up5k".into(),
77 package: "sg48".into(),
78 }
79 }
80
81 pub fn parse(s: &str) -> Result<Self, String> {
84 let s = s.trim();
85 let lower = s.to_ascii_lowercase();
86 match lower.as_str() {
87 "generic" | "agnostic" | "soft" => Ok(Self::Generic),
88 "ecp5" => Ok(Self::ecp5_25k()),
89 "ice40" => Ok(Self::ice40_up5k()),
90 _ => {
91 let mut parts = s.split(':');
92 let kind = parts.next().unwrap_or("").to_ascii_lowercase();
93 match kind.as_str() {
94 "ecp5" => {
95 let device = parts.next().unwrap_or("25k").to_string();
96 let package = parts.next().unwrap_or("CABGA381").to_string();
97 Ok(Self::Ecp5 { device, package })
98 }
99 "ice40" => {
100 let device = parts.next().unwrap_or("up5k").to_string();
101 let package = parts.next().unwrap_or("sg48").to_string();
102 Ok(Self::Ice40 { device, package })
103 }
104 "xilinx7" | "xc7" => {
105 let part = parts
106 .next()
107 .ok_or_else(|| {
108 "xilinx7 requires a part, e.g. xilinx7:xc7a35tcsg324-1".to_string()
109 })?
110 .to_string();
111 Ok(Self::Xilinx7 { part })
112 }
113 _ => Err(format!(
114 "unknown HwTarget {s:?}; expected generic | ecp5[:device:pkg] | \
115 ice40[:device:pkg] | xilinx7:<part>"
116 )),
117 }
118 }
119 }
120 }
121
122 pub fn tag(&self) -> String {
124 match self {
125 Self::Generic => "generic".into(),
126 Self::Ecp5 { device, package } => format!("ecp5_{device}_{package}"),
127 Self::Ice40 { device, package } => format!("ice40_{device}_{package}"),
128 Self::Xilinx7 { part } => format!("xilinx7_{part}"),
129 }
130 }
131
132 pub fn emits_constraints(&self) -> bool {
134 !matches!(self, Self::Generic)
135 }
136}
137
138impl fmt::Display for HwTarget {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 match self {
141 Self::Generic => write!(f, "generic (target-agnostic)"),
142 Self::Ecp5 { device, package } => write!(f, "ecp5 {device} {package}"),
143 Self::Ice40 { device, package } => write!(f, "ice40 {device} {package}"),
144 Self::Xilinx7 { part } => write!(f, "xilinx7 {part}"),
145 }
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
151pub enum OutputKind {
152 #[default]
154 Argmax,
155 Logits,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct PortNames {
163 pub clk: String,
164 pub rst: String,
165 pub start: String,
166 pub done: String,
167 pub in_addr: String,
168 pub in_we: String,
169 pub in_din: String,
170 pub out_addr: String,
171 pub out_re: String,
172 pub out_dout: String,
173 pub pred: String,
174 pub in_valid: String,
175 pub in_ready: String,
176 pub in_data: String,
177 pub out_valid: String,
178 pub out_ready: String,
179 pub out_data: String,
180}
181
182impl Default for PortNames {
183 fn default() -> Self {
184 Self {
185 clk: "clk".into(),
186 rst: "rst".into(),
187 start: "start".into(),
188 done: "done".into(),
189 in_addr: "in_addr".into(),
190 in_we: "in_we".into(),
191 in_din: "in_din".into(),
192 out_addr: "out_addr".into(),
193 out_re: "out_re".into(),
194 out_dout: "out_dout".into(),
195 pred: "pred".into(),
196 in_valid: "in_valid".into(),
197 in_ready: "in_ready".into(),
198 in_data: "in_data".into(),
199 out_valid: "out_valid".into(),
200 out_ready: "out_ready".into(),
201 out_data: "out_data".into(),
202 }
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
208pub enum InputIface {
209 #[default]
211 Memory,
212 Stream {
214 beat_elems: u8,
216 },
217 MemoryAndStream { beat_elems: u8 },
219}
220
221impl InputIface {
222 pub fn parse(s: &str) -> Result<Self, String> {
223 let s = s.trim().to_ascii_lowercase();
224 match s.as_str() {
225 "memory" | "mem" | "poke" => Ok(Self::Memory),
226 "stream" => Ok(Self::Stream { beat_elems: 1 }),
227 "both" | "memory+stream" | "mem+stream" => Ok(Self::MemoryAndStream { beat_elems: 1 }),
228 other if other.starts_with("stream:") => {
229 let n: u8 = other[7..].parse().map_err(|_| {
230 format!("bad stream beat width in {other:?}; expected stream:N")
231 })?;
232 Ok(Self::Stream {
233 beat_elems: n.max(1),
234 })
235 }
236 other if other.starts_with("both:") => {
237 let n: u8 = other[5..]
238 .parse()
239 .map_err(|_| format!("bad both beat width in {other:?}; expected both:N"))?;
240 Ok(Self::MemoryAndStream {
241 beat_elems: n.max(1),
242 })
243 }
244 _ => Err(format!(
245 "unknown InputIface {s:?}; expected memory | stream | both \
246 [|stream:N|both:N]"
247 )),
248 }
249 }
250
251 pub fn wants_memory(self) -> bool {
252 matches!(self, Self::Memory | Self::MemoryAndStream { .. })
253 }
254
255 pub fn wants_stream(self) -> bool {
256 matches!(self, Self::Stream { .. } | Self::MemoryAndStream { .. })
257 }
258
259 pub fn beat_elems(self) -> u8 {
260 match self {
261 Self::Memory => 1,
262 Self::Stream { beat_elems } | Self::MemoryAndStream { beat_elems } => beat_elems.max(1),
263 }
264 }
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
269pub enum OutputIface {
270 #[default]
272 ScalarPred,
273 MemoryReadout,
275 Stream { beat_elems: u8 },
277 ScalarAndMemory,
279 MemoryAndStream { beat_elems: u8 },
281}
282
283impl OutputIface {
284 pub fn parse(s: &str) -> Result<Self, String> {
285 let s = s.trim().to_ascii_lowercase();
286 match s.as_str() {
287 "scalar" | "pred" | "argmax" => Ok(Self::ScalarPred),
288 "memory" | "mem" | "readout" => Ok(Self::MemoryReadout),
289 "stream" => Ok(Self::Stream { beat_elems: 1 }),
290 "scalar+memory" | "scalar_and_memory" | "both_scalar" => Ok(Self::ScalarAndMemory),
291 "both" | "memory+stream" | "mem+stream" => Ok(Self::MemoryAndStream { beat_elems: 1 }),
292 other if other.starts_with("stream:") => {
293 let n: u8 = other[7..].parse().map_err(|_| {
294 format!("bad stream beat width in {other:?}; expected stream:N")
295 })?;
296 Ok(Self::Stream {
297 beat_elems: n.max(1),
298 })
299 }
300 other if other.starts_with("both:") => {
301 let n: u8 = other[5..]
302 .parse()
303 .map_err(|_| format!("bad both beat width in {other:?}; expected both:N"))?;
304 Ok(Self::MemoryAndStream {
305 beat_elems: n.max(1),
306 })
307 }
308 _ => Err(format!(
309 "unknown OutputIface {s:?}; expected scalar | memory | stream | \
310 scalar+memory | both [|stream:N|both:N]"
311 )),
312 }
313 }
314
315 pub fn wants_memory(self) -> bool {
316 matches!(
317 self,
318 Self::MemoryReadout | Self::ScalarAndMemory | Self::MemoryAndStream { .. }
319 )
320 }
321
322 pub fn wants_stream(self) -> bool {
323 matches!(self, Self::Stream { .. } | Self::MemoryAndStream { .. })
324 }
325
326 pub fn wants_pred_port(self) -> bool {
328 !matches!(self, Self::MemoryReadout)
329 }
330
331 pub fn beat_elems(self) -> u8 {
332 match self {
333 Self::Stream { beat_elems } | Self::MemoryAndStream { beat_elems } => beat_elems.max(1),
334 _ => 1,
335 }
336 }
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Default)]
341pub struct GraphIoBind {
342 pub input: Option<String>,
344 pub outputs: Vec<String>,
347 pub sideband_inputs: Vec<String>,
350}
351
352impl GraphIoBind {
353 pub fn with_input(mut self, name: impl Into<String>) -> Self {
354 self.input = Some(name.into());
355 self
356 }
357
358 pub fn with_outputs<I, S>(mut self, names: I) -> Self
359 where
360 I: IntoIterator<Item = S>,
361 S: Into<String>,
362 {
363 self.outputs = names.into_iter().map(Into::into).collect();
364 self
365 }
366
367 pub fn with_sideband_inputs<I, S>(mut self, names: I) -> Self
368 where
369 I: IntoIterator<Item = S>,
370 S: Into<String>,
371 {
372 self.sideband_inputs = names.into_iter().map(Into::into).collect();
373 self
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct SidebandSpec {
384 pub name: String,
386 pub bits: u8,
388 pub signed: bool,
390 pub echo: bool,
392}
393
394impl SidebandSpec {
395 pub fn input(name: impl Into<String>, bits: u8) -> Self {
397 Self {
398 name: name.into(),
399 bits: bits.clamp(1, 64),
400 signed: false,
401 echo: true,
402 }
403 }
404
405 pub fn signed_input(name: impl Into<String>, bits: u8) -> Self {
407 Self {
408 name: name.into(),
409 bits: bits.clamp(1, 64),
410 signed: true,
411 echo: true,
412 }
413 }
414
415 pub fn with_echo(mut self, echo: bool) -> Self {
416 self.echo = echo;
417 self
418 }
419
420 pub fn parse(s: &str) -> Result<Self, String> {
422 let s = s.trim();
423 if s.is_empty() {
424 return Err("empty sideband spec".into());
425 }
426 let mut parts = s.split(':');
427 let name = parts.next().unwrap_or("").trim().to_string();
428 if name.is_empty() {
429 return Err(format!("bad sideband {s:?}; expected name[:bits[:signed]]"));
430 }
431 let bits: u8 = match parts.next() {
432 None => 8,
433 Some(b) => b
434 .parse()
435 .map_err(|_| format!("bad sideband width in {s:?}"))?,
436 };
437 let mut signed = false;
438 if let Some(flag) = parts.next() {
439 match flag.trim().to_ascii_lowercase().as_str() {
440 "s" | "signed" | "i" => signed = true,
441 "u" | "unsigned" => signed = false,
442 other => {
443 return Err(format!(
444 "bad sideband signedness {other:?}; expected signed|unsigned"
445 ));
446 }
447 }
448 }
449 Ok(Self {
450 name,
451 bits: bits.clamp(1, 64),
452 signed,
453 echo: true,
454 })
455 }
456}
457
458#[derive(Debug, Clone, PartialEq, Eq)]
460pub struct IoConfig {
461 pub names: PortNames,
462 pub input: InputIface,
463 pub output: OutputIface,
464 pub bind: GraphIoBind,
465 pub sidebands: Vec<SidebandSpec>,
467}
468
469impl Default for IoConfig {
470 fn default() -> Self {
471 Self {
472 names: PortNames::default(),
473 input: InputIface::Memory,
474 output: OutputIface::ScalarPred,
475 bind: GraphIoBind::default(),
476 sidebands: Vec::new(),
477 }
478 }
479}
480
481impl IoConfig {
482 pub fn new() -> Self {
483 Self::default()
484 }
485
486 pub fn with_names(mut self, names: PortNames) -> Self {
487 self.names = names;
488 self
489 }
490
491 pub fn with_input(mut self, iface: InputIface) -> Self {
492 self.input = iface;
493 self
494 }
495
496 pub fn with_output(mut self, iface: OutputIface) -> Self {
497 self.output = iface;
498 self
499 }
500
501 pub fn with_bind(mut self, bind: GraphIoBind) -> Self {
502 self.bind = bind;
503 self
504 }
505
506 pub fn with_sidebands(mut self, sidebands: Vec<SidebandSpec>) -> Self {
507 self.sidebands = sidebands;
508 self
509 }
510
511 pub fn sideband(mut self, spec: SidebandSpec) -> Self {
512 self.sidebands.push(spec);
513 self
514 }
515}
516
517#[derive(Debug, Clone)]
519pub struct FpgaExportConfig {
520 pub tune: Tune,
522 pub hw_target: HwTarget,
524 pub emit_synth_scripts: bool,
526 pub emit_constraints: bool,
528 pub emit_board_shell: bool,
530 pub quant_mode: ExportQuantMode,
532 pub legalize: LegalizeOptions,
534 pub output_kind: OutputKind,
536 pub io: IoConfig,
538 pub from_graph: FromGraphOptions,
540 pub out_dir: Option<PathBuf>,
542}
543
544impl Default for FpgaExportConfig {
545 fn default() -> Self {
546 Self {
547 tune: Tune::default(),
548 hw_target: HwTarget::Generic,
549 emit_synth_scripts: true,
550 emit_constraints: true,
551 emit_board_shell: true,
552 quant_mode: ExportQuantMode::Int8,
553 legalize: LegalizeOptions::default(),
554 output_kind: OutputKind::Argmax,
555 io: IoConfig::default(),
556 from_graph: FromGraphOptions::default(),
557 out_dir: None,
558 }
559 }
560}
561
562impl FpgaExportConfig {
563 pub fn new() -> Self {
564 Self::default()
565 }
566
567 pub fn for_opt_target(target: OptTarget) -> Self {
568 Self {
569 tune: Tune::for_target(target),
570 ..Self::default()
571 }
572 }
573
574 pub fn with_tune(mut self, tune: Tune) -> Self {
575 self.tune = tune;
576 self
577 }
578
579 pub fn with_hw_target(mut self, hw: HwTarget) -> Self {
580 self.hw_target = hw;
581 self
582 }
583
584 pub fn with_quant_mode(mut self, mode: ExportQuantMode) -> Self {
585 self.quant_mode = mode;
586 self.from_graph.weight_bits = mode.weight_bits();
587 self.from_graph.weight_encoding = mode.encoding();
588 self
589 }
590
591 pub fn with_legalize(mut self, opts: LegalizeOptions) -> Self {
592 self.legalize = opts;
593 self
594 }
595
596 pub fn with_output_kind(mut self, kind: OutputKind) -> Self {
597 self.output_kind = kind;
598 self.legalize.append_argmax = matches!(kind, OutputKind::Argmax);
599 if matches!(kind, OutputKind::Logits) && matches!(self.io.output, OutputIface::ScalarPred) {
601 self.io.output = OutputIface::ScalarAndMemory;
602 }
603 self
604 }
605
606 pub fn with_io(mut self, io: IoConfig) -> Self {
607 self.io = io;
608 self
609 }
610
611 pub fn with_port_names(mut self, names: PortNames) -> Self {
612 self.io.names = names;
613 self
614 }
615
616 pub fn with_input_iface(mut self, iface: InputIface) -> Self {
617 self.io.input = iface;
618 self
619 }
620
621 pub fn with_output_iface(mut self, iface: OutputIface) -> Self {
622 self.io.output = iface;
623 self
624 }
625
626 pub fn bind_input(mut self, name: impl Into<String>) -> Self {
627 self.io.bind.input = Some(name.into());
628 self
629 }
630
631 pub fn bind_outputs<I, S>(mut self, names: I) -> Self
632 where
633 I: IntoIterator<Item = S>,
634 S: Into<String>,
635 {
636 self.io.bind.outputs = names.into_iter().map(Into::into).collect();
637 self
638 }
639
640 pub fn bind_sideband_inputs<I, S>(mut self, names: I) -> Self
641 where
642 I: IntoIterator<Item = S>,
643 S: Into<String>,
644 {
645 self.io.bind.sideband_inputs = names.into_iter().map(Into::into).collect();
646 self
647 }
648
649 pub fn with_sidebands(mut self, sidebands: Vec<SidebandSpec>) -> Self {
650 self.io.sidebands = sidebands;
651 self
652 }
653
654 pub fn sideband(mut self, spec: SidebandSpec) -> Self {
655 self.io.sidebands.push(spec);
656 self
657 }
658
659 pub fn with_from_graph(mut self, opts: FromGraphOptions) -> Self {
660 self.from_graph = opts;
661 self
662 }
663
664 pub fn with_out_dir(mut self, dir: impl Into<PathBuf>) -> Self {
665 self.out_dir = Some(dir.into());
666 self
667 }
668
669 pub fn without_synth_scripts(mut self) -> Self {
670 self.emit_synth_scripts = false;
671 self
672 }
673}