use crate::codegen::Artifact;
use crate::export_config::{FpgaExportConfig, HwTarget};
use crate::model::Model;
pub fn collect_synth_artifacts(model: &Model, cfg: &FpgaExportConfig) -> Vec<Artifact> {
let mut out = Vec::new();
if cfg.emit_synth_scripts {
out.push(Artifact {
rel_path: "synth.sh".into(),
content: emit_synth_sh(&cfg.hw_target),
});
out.push(Artifact {
rel_path: "EXPORT.md".into(),
content: emit_export_md(model, cfg),
});
}
if cfg.emit_constraints && cfg.hw_target.emits_constraints() {
if let Some((path, body)) = emit_constraints(&cfg.hw_target) {
out.push(Artifact {
rel_path: path,
content: body,
});
}
}
out
}
fn emit_synth_sh(hw: &HwTarget) -> String {
let mut s = String::from(
"#!/usr/bin/env bash\nset -euo pipefail\n# Generated by rlx-fpga — do not hand-edit.\ncd \"$(dirname \"$0\")\"\n\n",
);
match hw {
HwTarget::Generic => {
s.push_str(
r#"# Target-agnostic: Yosys generic synth to a JSON netlist.
# Soft ports only — wrap `top` with a board shell for pin mapping.
yosys -p 'read_verilog -sv top.sv layers/*.sv primitives/*.sv; hierarchy -top top; synth -top top; write_json out.json'
echo "wrote out.json (generic). Bind pins in a board wrapper before P&R."
"#,
);
}
HwTarget::Ecp5 { device, package } => {
s.push_str(&format!(
r#"yosys -p 'read_verilog -sv top.sv layers/*.sv primitives/*.sv; synth_ecp5 -top top -json out.json'
nextpnr-ecp5 --json out.json --textcfg out.config --{device} --package {package}
ecppack out.config out.bit
echo "wrote out.bit"
"#
));
}
HwTarget::Ice40 { device, package } => {
s.push_str(&format!(
r#"yosys -p 'read_verilog -sv top.sv layers/*.sv primitives/*.sv; synth_ice40 -top top -json out.json'
nextpnr-ice40 --json out.json --pcf constraints.pcf --{device} --package {package} --asc out.asc
icepack out.asc out.bin
echo "wrote out.bin"
"#
));
}
HwTarget::Xilinx7 { part } => {
s.push_str(&format!(
r#"# Experimental open XC7 flow — requires nextpnr-xilinx / prjxray tooling.
yosys -p 'read_verilog -sv top.sv layers/*.sv primitives/*.sv; synth_xilinx -flatten -top top; write_json out.json'
echo "wrote out.json for part {part}. Run nextpnr-xilinx / vendor tools next."
"#
));
}
}
s
}
fn emit_export_md(model: &Model, cfg: &FpgaExportConfig) -> String {
format!(
r#"# RLX FPGA export — `{name}`
| Field | Value |
|-------|-------|
| Model | `{name}` |
| Input length | {input_len} |
| Layers | {n_layers} |
| Tune | `{tune}` |
| Hardware target | {hw} |
| RTL | **target-agnostic** SystemVerilog (`top.sv` soft ports) |
## Ports (`top`)
| Port | Dir | Role |
|------|-----|------|
{ports}
## Soft I/O
| Field | Value |
|-------|-------|
| Input iface | `{in_iface:?}` |
| Output iface | `{out_iface:?}` |
| Bind input | `{bind_in}` |
| Bind outputs | `{bind_out}` |
## Build
```sh
bash synth.sh
```
Generic target produces `out.json` only. Board targets (`ecp5`, `ice40`, …)
run place-and-route when the open toolchain is on `PATH`.
## Parity
Bit-exact reference: `rlx_fpga::reference` (Rust) ↔ emitted Verilog.
Do not diff against f32 Cortex-M requant paths.
"#,
name = model.name,
input_len = model.input_len,
n_layers = model.layers.len(),
tune = cfg.tune,
hw = cfg.hw_target,
ports = crate::codegen::io_ports::port_table_md(&cfg.io, model),
in_iface = cfg.io.input,
out_iface = cfg.io.output,
bind_in = cfg.io.bind.input.as_deref().unwrap_or("(first Op::Input)"),
bind_out = if cfg.io.bind.outputs.is_empty() {
"(graph.outputs[0])".into()
} else {
cfg.io.bind.outputs.join(", ")
},
)
}
fn emit_constraints(hw: &HwTarget) -> Option<(String, String)> {
match hw {
HwTarget::Generic => None,
HwTarget::Ecp5 { .. } => Some((
"constraints.lpf".into(),
r#"# Stub Lattice ECP5 constraints — map soft ports to board pins.
# BLOCK RESETPATHS;
# BLOCK ASYNCPATHS;
# LOCATE COMP "clk" SITE "P6";
# IOBUF PORT "clk" IO_TYPE=LVCMOS33;
# LOCATE COMP "rst" SITE "...";
# LOCATE COMP "start" SITE "...";
# LOCATE COMP "done" SITE "...";
"#
.into(),
)),
HwTarget::Ice40 { .. } => Some((
"constraints.pcf".into(),
r#"# Stub iCE40 PCF — map soft ports to board pins.
# set_io clk 35
# set_io rst 37
# set_io start 34
# set_io done 32
"#
.into(),
)),
HwTarget::Xilinx7 { .. } => Some((
"constraints.xdc".into(),
r#"# Stub Xilinx XDC — map soft ports to board pins.
# set_property PACKAGE_PIN <PIN> [get_ports clk]
# set_property IOSTANDARD LVCMOS33 [get_ports clk]
"#
.into(),
)),
}
}