diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b7a3b20 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +DIRU=/home/alex/Desktop/Hacking/libre-chip/fayalite-wip/target/blinky-out +NEXTPNR_DENSITY:=--25k + +all: + cp $(DIRU)/*.pcf /tmp + RUST_BACKTRACE=full cargo run --example blinky yosys-nextpnr-ecp5 \ + --nextpnr /home/alex/.guix-profile/bin/nextpnr-ecp5 \ + --platform orangecrab-85k -o target/blinky-out \ + --ecppack /home/alex/.guix-profile/bin/ecppack \ + --placeholder-dir /tmp/anyPathBuf/orangecrab_r0.2.1.pcf + ls -1 $(DIRU)/*.bit +ls: + ls -1 $(DIRU) +clean: + rm $(DIRU)/* +nextpnr: + cd $(DIRU) && nextpnr-ecp5 --json blinky.json --textcfg blinky.nextpnr.out $(NEXTPNR_DENSITY) \ + --package CSFBGA285 --lpf orangecrab_r0.2.1.pcf --lpf-allow-unconstrained +pack: + cd $(DIRU) && ecppack --compress --freq 38.8 --input blinky.nextpnr.out --bit blinky.nextpnr.bit + cd $(DIRU) && file *.bit \ No newline at end of file diff --git a/crates/fayalite/src/vendor.rs b/crates/fayalite/src/vendor.rs index cdf302d..e7aff58 100644 --- a/crates/fayalite/src/vendor.rs +++ b/crates/fayalite/src/vendor.rs @@ -2,11 +2,14 @@ // See Notices.txt for copyright information pub mod xilinx; +pub mod lattice; pub(crate) fn built_in_job_kinds() -> impl IntoIterator { - xilinx::built_in_job_kinds() + xilinx::built_in_job_kinds(); + lattice::built_in_job_kinds() } pub(crate) fn built_in_platforms() -> impl IntoIterator { - xilinx::built_in_platforms() + xilinx::built_in_platforms(); + lattice::built_in_platforms() } diff --git a/crates/fayalite/src/vendor/lattice.rs b/crates/fayalite/src/vendor/lattice.rs new file mode 100644 index 0000000..a9ad5a6 --- /dev/null +++ b/crates/fayalite/src/vendor/lattice.rs @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// See Notices.txt for copyright information + +use crate::{ + annotations::make_annotation_enum, + build::{GlobalParams, ToArgs, WriteArgs}, + intern::Interned, + prelude::{DynPlatform, Platform}, +}; +use clap::ValueEnum; +use ordered_float::NotNan; +use serde::{Deserialize, Serialize}; +use std::fmt; + +pub mod orangecrab; +pub mod primitives; +pub mod yosys_nextpnr; + +#[derive(Clone, PartialEq, Eq, Hash, Debug, clap::Args)] +pub struct LatticeArgs { + #[arg(long)] + pub device: Option, +} + +impl LatticeArgs { + pub fn require_device( + &self, + platform: Option<&DynPlatform>, + global_params: &GlobalParams, + ) -> clap::error::Result { + if let Some(device) = self.device { + return Ok(device); + } + if let Some(device) = + platform.and_then(|platform| platform.aspects().get_single_by_type::().copied()) + { + return Ok(device); + } + Err(global_params.clap_error( + clap::error::ErrorKind::MissingRequiredArgument, + "missing --device option", + )) + } +} + +impl ToArgs for LatticeArgs { + fn to_args(&self, args: &mut (impl WriteArgs + ?Sized)) { + if let Some(device) = self.device { + args.write_long_option_eq("device", device.as_str()); + } + } +} + +macro_rules! make_device_enum { + ($vis:vis enum $Device:ident { + $( + #[ + name = $name:literal, + lattice_part = $lattice_part:literal, + lattice_device = $lattice_device:literal, + lattice_family = $lattice_family:literal, + ] + $variant:ident, + )* + }) => { + #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, ValueEnum)] + $vis enum $Device { + $( + #[value(name = $name, alias = $lattice_part)] + $variant, + )* + } + + impl $Device { + $vis fn as_str(self) -> &'static str { + match self { + $(Self::$variant => $name,)* + } + } + $vis fn lattice_part(self) -> &'static str { + match self { + $(Self::$variant => $lattice_part,)* + } + } + $vis fn lattice_device(self) -> &'static str { + match self { + $(Self::$variant => $lattice_device,)* + } + } + $vis fn lattice_family(self) -> &'static str { + match self { + $(Self::$variant => $lattice_family,)* + } + } + } + + struct DeviceVisitor; + + impl<'de> serde::de::Visitor<'de> for DeviceVisitor { + type Value = $Device; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("a Lattice device string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + match $Device::from_str(v, false) { + Ok(v) => Ok(v), + Err(_) => Err(E::invalid_value(serde::de::Unexpected::Str(v), &self)), + } + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + match str::from_utf8(v).ok().and_then(|v| $Device::from_str(v, false).ok()) { + Some(v) => Ok(v), + None => Err(E::invalid_value(serde::de::Unexpected::Bytes(v), &self)), + } + } + } + + impl<'de> Deserialize<'de> for $Device { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_string(DeviceVisitor) + } + } + + impl Serialize for $Device { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.as_str().serialize(serializer) + } + } + }; +} + +//ECP5 variants +make_device_enum! { + pub enum Device { + #[ + name = "placeholder25k", + lattice_part = "fixme", + lattice_device = "fixme", + lattice_family = "fixme", + ] + Placeholder25k, + #[ + name = "placeholder85k", + lattice_part = "fixme", + lattice_device = "fimxe", + lattice_family = "fixme", + ] + Placeholder85k, + } +} + +//rest looks good + +impl fmt::Display for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +pub(crate) fn built_in_job_kinds() -> impl IntoIterator { + orangecrab::built_in_job_kinds() + .into_iter() + .chain(yosys_nextpnr::built_in_job_kinds()) +} + +pub(crate) fn built_in_platforms() -> impl IntoIterator { + orangecrab::built_in_platforms() + .into_iter() + .chain(yosys_nextpnr::built_in_platforms()) +} + +//first step yosys -p "read_verilog $<; synth_ecp5 -json $@" diff --git a/crates/fayalite/src/vendor/lattice/orangecrab.rs b/crates/fayalite/src/vendor/lattice/orangecrab.rs new file mode 100644 index 0000000..b35554b --- /dev/null +++ b/crates/fayalite/src/vendor/lattice/orangecrab.rs @@ -0,0 +1,396 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// See Notices.txt for copyright information + +use crate::{ + intern::{Intern, Interned}, + module::{instance_with_loc, reg_builder_with_loc, wire_with_loc}, + platform::{ + DynPlatform, Peripheral, PeripheralRef, Peripherals, PeripheralsBuilderFactory, + PeripheralsBuilderFinished, Platform, PlatformAspectSet, + peripherals::{ClockInput, Led, RgbLed, Uart}, + }, + prelude::*, + vendor::lattice::{ + Device, + primitives, + }, +}; +use ordered_float::NotNan; +use std::sync::OnceLock; + +//keep unchanged +macro_rules! orangecrab_platform { + ( + $vis:vis enum $OrangeCrabPlatform:ident { + $(#[name = $name:literal, device = $device:ident] + $Variant:ident,)* + } + ) => { + #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] + #[non_exhaustive] + $vis enum $OrangeCrabPlatform { + $($Variant,)* + } + + impl $OrangeCrabPlatform { + $vis const VARIANTS: &'static [Self] = &[$(Self::$Variant,)*]; + $vis fn device(self) -> Device { + match self { + $(Self::$Variant => Device::$device,)* + } + } + $vis const fn as_str(self) -> &'static str { + match self { + $(Self::$Variant => $name,)* + } + } + fn get_aspects(self) -> &'static PlatformAspectSet { + match self { + $(Self::$Variant => { + static ASPECTS_SET: OnceLock = OnceLock::new(); + ASPECTS_SET.get_or_init(|| self.make_aspects()) + })* + } + } + } + }; +} + +//untested +orangecrab_platform! { + pub enum OrangeCrabPlatform { + #[name = "orangecrab-25k", device = Placeholder25k] + OrangeCrab_25k, + #[name = "orangecrab-85k", device = Placeholder85k] + OrangeCrab_85k, + } +} + +//FIXME +#[derive(Debug)] +pub struct ArtyA7Peripherals { + clk100_div_pow2: [Peripheral; 4], + rst: Peripheral, + rst_sync: Peripheral, + ld0: Peripheral, + ld1: Peripheral, + ld2: Peripheral, + ld3: Peripheral, + ld4: Peripheral, + ld5: Peripheral, + ld6: Peripheral, + ld7: Peripheral, + uart: Peripheral, + // TODO: add rest of peripherals when we need them +} + +impl Peripherals for ArtyA7Peripherals { + fn append_peripherals<'a>(&'a self, peripherals: &mut Vec>) { + let Self { + clk100_div_pow2, + rst, + rst_sync, + ld0, + ld1, + ld2, + ld3, + ld4, + ld5, + ld6, + ld7, + uart, + } = self; + clk100_div_pow2.append_peripherals(peripherals); + rst.append_peripherals(peripherals); + rst_sync.append_peripherals(peripherals); + ld0.append_peripherals(peripherals); + ld1.append_peripherals(peripherals); + ld2.append_peripherals(peripherals); + ld3.append_peripherals(peripherals); + ld4.append_peripherals(peripherals); + ld5.append_peripherals(peripherals); + ld6.append_peripherals(peripherals); + ld7.append_peripherals(peripherals); + uart.append_peripherals(peripherals); + } +} + +impl OrangeCrabPlatform { + fn make_aspects(self) -> PlatformAspectSet { + let mut retval = PlatformAspectSet::new(); + retval.insert_new(self.device()); + retval + } +} + +#[hdl_module(extern)] +fn reset_sync() { + #[hdl] + let clk: Clock = m.input(); + #[hdl] + let inp: Bool = m.input(); + #[hdl] + let out: SyncReset = m.output(); + m.annotate_module(BlackBoxInlineAnnotation { + path: "fayalite_orangecrab_reset_sync.v".intern(), + text: r#"module __fayalite_orangecrab_reset_sync(input clk, input inp, output out); + wire reset_0_out; + always @(posedge clk) begin + reset_0_out <= inp; + outp <= reset_0_out; + end +endmodule +"# + .intern(), + }); + m.verilog_name("__fayalite_orangecrab_reset_sync"); +} + +impl Platform for OrangeCrabPlatform { + type Peripherals = ArtyA7Peripherals; + + fn name(&self) -> Interned { + self.as_str().intern() + } + + fn new_peripherals<'builder>( + &self, + builder_factory: PeripheralsBuilderFactory<'builder>, + ) -> (Self::Peripherals, PeripheralsBuilderFinished<'builder>) { + let mut builder = builder_factory.builder(); + + let clk100_div_pow2 = std::array::from_fn(|log2_divisor| { + let divisor = 1u64 << log2_divisor; + let name = if divisor != 1 { + format!("clk100_div_{divisor}") + } else { + "clk100".into() + }; + builder.input_peripheral(name, ClockInput::new(100e6 / divisor as f64)) + }); + builder.add_conflicts(Vec::from_iter(clk100_div_pow2.iter().map(|v| v.id()))); + ( + ArtyA7Peripherals { + clk100_div_pow2, + rst: builder.input_peripheral("rst", Reset), + rst_sync: builder.input_peripheral("rst_sync", SyncReset), + ld0: builder.output_peripheral("ld0", RgbLed), + ld1: builder.output_peripheral("ld1", RgbLed), + ld2: builder.output_peripheral("ld2", RgbLed), + ld3: builder.output_peripheral("ld3", RgbLed), + ld4: builder.output_peripheral("ld4", Led), + ld5: builder.output_peripheral("ld5", Led), + ld6: builder.output_peripheral("ld6", Led), + ld7: builder.output_peripheral("ld7", Led), + uart: builder.output_peripheral("uart", Uart), + }, + builder.finish(), + ) + } + + fn source_location(&self) -> SourceLocation { + SourceLocation::builtin() + } + + fn add_peripherals_in_wrapper_module(&self, m: &ModuleBuilder, peripherals: Self::Peripherals) { + let ArtyA7Peripherals { + clk100_div_pow2, + rst, + rst_sync, + ld0, + ld1, + ld2, + ld3, + ld4, + ld5, + ld6, + ld7, + uart, + } = peripherals; + let make_buffered_input = |name: &str, location: &str, io_standard: &str, invert: bool| { + let pin = m.input_with_loc(name, SourceLocation::builtin(), Bool); + /* fixme annotate( + pin, + XdcLocationAnnotation { + location: location.intern(), + }, + ); */ + /* fixme annotate( + pin, + XdcIOStandardAnnotation { + value: io_standard.intern(), + }, + ); */ + //let buf = instance_with_loc( + // &format!("{name}_buf"), + // //primitives::IBUF(), + // SourceLocation::builtin(), + //); + //connect(buf.I, pin); + //if invert { !buf.O } else { buf.O } + if invert { !pin } else { pin } + }; + let make_buffered_output = |name: &str, location: &str, io_standard: &str| { + let pin = m.output_with_loc(name, SourceLocation::builtin(), Bool); + /* fixme annotate( + pin, + XdcLocationAnnotation { + location: location.intern(), + }, + ); + annotate( + pin, + XdcIOStandardAnnotation { + value: io_standard.intern(), + }, + ); */ + //let buf = instance_with_loc( + // &format!("{name}_buf"), + // primitives::OBUFT(), + // SourceLocation::builtin(), + //); + //connect(pin, buf.O); + //connect(buf.T, false); + //buf.I + pin + }; + let mut frequency = clk100_div_pow2[0].ty().frequency(); + let mut log2_divisor = 0; + let mut clk = None; + for (cur_log2_divisor, p) in clk100_div_pow2.into_iter().enumerate() { + let Some(p) = p.into_used() else { + continue; + }; + debug_assert!( + clk.is_none(), + "conflict-handling logic should ensure at most one clock is used", + ); + frequency = p.ty().frequency(); + clk = Some(p); + log2_divisor = cur_log2_divisor; + } + let clk100_buf = make_buffered_input("clk100", "E3", "LVCMOS33", false); + //let startup = instance_with_loc( + // "startup", + // primitives::STARTUPE2_default_inputs(), + // SourceLocation::builtin(), + //); + //let clk_global_buf = instance_with_loc( + // "clk_global_buf", + // primitives::BUFGCE(), + // SourceLocation::builtin(), + //); + //connect(clk_global_buf.CE, startup.EOS); + let mut clk_global_buf_in = clk100_buf.to_clock(); + for prev_log2_divisor in 0..log2_divisor { + let prev_divisor = 1u64 << prev_log2_divisor; + let clk_in = wire_with_loc( + &format!("clk_div_{prev_divisor}"), + SourceLocation::builtin(), + Clock, + ); + connect(clk_in, clk_global_buf_in); + /* fixme + annotate( + clk_in, + XdcCreateClockAnnotation { + period: NotNan::new(1e9 / (100e6 / prev_divisor as f64)) + .expect("known to be valid"), + }, + ); */ + annotate(clk_in, DontTouchAnnotation); + let cd = wire_with_loc( + &format!("clk_div_{prev_divisor}_in"), + SourceLocation::builtin(), + ClockDomain[AsyncReset], + ); + connect(cd.clk, clk_in); + //FIXME connect(cd.rst, (!startup.EOS).to_async_reset()); + let divider = reg_builder_with_loc("divider", SourceLocation::builtin()) + .clock_domain(cd) + .reset(false) + .build(); + connect(divider, !divider); + clk_global_buf_in = divider.to_clock(); + } + //connect(clk_global_buf.I, clk_global_buf_in); + let clk_out = wire_with_loc("clk_out", SourceLocation::builtin(), Clock); + //connect(clk_out, clk_global_buf.O); + connect(clk_out, clk_global_buf_in); + /* fixme annotate( + clk_out, + XdcCreateClockAnnotation { + period: NotNan::new(1e9 / frequency).expect("known to be valid"), + }, + ); */ + annotate(clk_out, DontTouchAnnotation); + if let Some(clk) = clk { + connect(clk.instance_io_field().clk, clk_out); + } + //undo 1 + let rst_value = { + let rst_buf = make_buffered_input("rst", "C2", "LVCMOS33", true); + let rst_sync = instance_with_loc("rst_sync", reset_sync(), SourceLocation::builtin()); + connect(rst_sync.clk, clk_out); + connect(rst_sync.inp, rst_buf/* | !startup.EOS*/); //FIXME + rst_sync.out + }; + if let Some(rst) = rst.into_used() { + connect(rst.instance_io_field(), rst_value.to_reset()); + } + if let Some(rst_sync) = rst_sync.into_used() { + connect(rst_sync.instance_io_field(), rst_value); + } + let rgb_leds = [ + (ld0, ("G6", "F6", "E1")), + (ld1, ("G3", "J4", "G4")), + (ld2, ("J3", "J2", "H4")), + (ld3, ("K1", "H6", "K2")), + ]; + for (rgb_led, (r_loc, g_loc, b_loc)) in rgb_leds { + let r = make_buffered_output(&format!("{}_r", rgb_led.name()), r_loc, "LVCMOS33"); + let g = make_buffered_output(&format!("{}_g", rgb_led.name()), g_loc, "LVCMOS33"); + let b = make_buffered_output(&format!("{}_b", rgb_led.name()), b_loc, "LVCMOS33"); + if let Some(rgb_led) = rgb_led.into_used() { + connect(r, rgb_led.instance_io_field().r); + connect(g, rgb_led.instance_io_field().g); + connect(b, rgb_led.instance_io_field().b); + } else { + connect(r, false); + connect(g, false); + connect(b, false); + } + } + let leds = [(ld4, "H5"), (ld5, "J5"), (ld6, "T9"), (ld7, "T10")]; + for (led, loc) in leds { + let o = make_buffered_output(&led.name(), loc, "LVCMOS33"); + if let Some(led) = led.into_used() { + connect(o, led.instance_io_field().on); + } else { + connect(o, false); + } + } + let uart_tx = make_buffered_output("uart_tx", "D10", "LVCMOS33"); + let uart_rx = make_buffered_input("uart_rx", "A9", "LVCMOS33", false); + if let Some(uart) = uart.into_used() { + connect(uart_tx, uart.instance_io_field().tx); + connect(uart.instance_io_field().rx, uart_rx); + } else { + connect(uart_tx, true); // idle + } + } + + fn aspects(&self) -> PlatformAspectSet { + self.get_aspects().clone() + } +} + +pub(crate) fn built_in_job_kinds() -> impl IntoIterator { + [] +} + +pub(crate) fn built_in_platforms() -> impl IntoIterator { + OrangeCrabPlatform::VARIANTS + .iter() + .map(|&v| DynPlatform::new(v)) +} diff --git a/crates/fayalite/src/vendor/lattice/primitives.rs b/crates/fayalite/src/vendor/lattice/primitives.rs new file mode 100644 index 0000000..f5f07bb --- /dev/null +++ b/crates/fayalite/src/vendor/lattice/primitives.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// See Notices.txt for copyright information + +#![allow(non_snake_case)] + +use crate::prelude::*; + +//#[hdl_module(extern)] +//pub fn IBUF() { +// m.verilog_name("IBUF"); +// #[hdl] +// let O: Bool = m.output(); +// #[hdl] +// let I: Bool = m.input(); +//} + +//#[hdl_module(extern)] +//pub fn OBUFT() { +// m.verilog_name("OBUFT"); +// #[hdl] +// let O: Bool = m.output(); +// #[hdl] +// let I: Bool = m.input(); +// #[hdl] +// let T: Bool = m.input(); +//} + +//#[hdl_module(extern)] +//pub fn BUFGCE() { +// m.verilog_name("BUFGCE"); +// #[hdl] +// let O: Clock = m.output(); +// #[hdl] +// let CE: Bool = m.input(); +// #[hdl] +// let I: Clock = m.input(); +//} + +#[hdl_module(extern)] +pub fn FIXME_PLACEHOLDER() { + m.verilog_name("FIXME_PLACEHOLDER"); + #[hdl] + let CFGCLK: Clock = m.output(); + #[hdl] + let CFGMCLK: Clock = m.output(); + #[hdl] + let EOS: Bool = m.output(); + #[hdl] + let PREQ: Bool = m.output(); +} diff --git a/crates/fayalite/src/vendor/lattice/yosys_nextpnr.rs b/crates/fayalite/src/vendor/lattice/yosys_nextpnr.rs new file mode 100644 index 0000000..6164609 --- /dev/null +++ b/crates/fayalite/src/vendor/lattice/yosys_nextpnr.rs @@ -0,0 +1,896 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// See Notices.txt for copyright information + +use crate::{ + annotations::{Annotation, TargetedAnnotation}, + build::{ + BaseJob, CommandParams, DynJobKind, GetJobPositionDependencies, GlobalParams, + JobAndDependencies, JobArgsAndDependencies, JobDependencies, JobItem, JobItemName, JobKind, + JobKindAndDependencies, ToArgs, WriteArgs, + external::{ + ExternalCommand, ExternalCommandJob, ExternalCommandJobKind, ExternalProgramTrait, + }, + verilog::{UnadjustedVerilog, VerilogDialect, VerilogJob, VerilogJobKind}, + }, + bundle::{Bundle, BundleType}, + expr::target::{Target, TargetBase}, + firrtl::{ScalarizedModuleABI, ScalarizedModuleABIAnnotations, ScalarizedModuleABIPort}, + intern::{Intern, InternSlice, Interned}, + module::{ + NameId, ScopedNameId, TargetName, + transform::visit::{Visit, Visitor}, + }, + prelude::*, + source_location::SourceLocation, + util::{HashSet, job_server::AcquiredJob}, + vendor::lattice::{ + Device, + /* fixme LatticeAnnotation,*/ LatticeArgs, + }, +}; +use eyre::Context; +use serde::{Deserialize, Serialize}; +use std::{ + convert::Infallible, + ffi::{OsStr, OsString}, + fmt::{self, Write}, + ops::ControlFlow, + path::{Path, PathBuf}, +}; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Default)] +pub struct YosysNextpnrWriteYsFileJobKind; + +#[derive(Clone, PartialEq, Eq, Hash, Debug, clap::Args)] +pub struct YosysNextpnrWriteYsFileArgs {} + +impl ToArgs for YosysNextpnrWriteYsFileArgs { + fn to_args(&self, _args: &mut (impl WriteArgs + ?Sized)) { + let Self {} = self; + } +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)] +pub struct YosysNextpnrWriteYsFile { + main_verilog_file: Interned, + ys_file: Interned, + json_file: Interned, + json_file_name: Interned, +} + +impl YosysNextpnrWriteYsFile { + pub fn main_verilog_file(&self) -> Interned { + self.main_verilog_file + } + pub fn ys_file(&self) -> Interned { + self.ys_file + } + pub fn json_file(&self) -> Interned { + self.json_file + } + pub fn json_file_name(&self) -> Interned { + self.json_file_name + } + fn write_ys( + &self, + output: &mut OsString, + additional_files: &[Interned], + main_module_name_id: NameId, + ) -> eyre::Result<()> { + let Self { + main_verilog_file, + ys_file: _, + json_file: _, + json_file_name, + } = self; + for verilog_file in VerilogJob::all_verilog_files(*main_verilog_file, additional_files)? { + output.push("read_verilog -sv \""); + output.push(verilog_file); + output.push("\"\n"); + } + let circuit_name = crate::firrtl::get_circuit_name(main_module_name_id); + writeln!( + output, + "synth_ecp5 -top {circuit_name}" + ) + .expect("writing to OsString can't fail"); + output.push("write_json \""); + output.push(json_file_name); + output.push("\"\n"); + Ok(()) + } +} + +impl JobKind for YosysNextpnrWriteYsFileJobKind { + type Args = YosysNextpnrWriteYsFileArgs; + type Job = YosysNextpnrWriteYsFile; + type Dependencies = JobKindAndDependencies; + + fn dependencies(self) -> Self::Dependencies { + Default::default() + } + + fn args_to_jobs( + mut args: JobArgsAndDependencies, + params: &JobParams, + global_params: &GlobalParams, + ) -> eyre::Result> { + args.dependencies + .dependencies + .args + .args + .additional_args + .verilog_dialect + .get_or_insert(VerilogDialect::Yosys); + args.args_to_jobs_simple(params, global_params, |_kind, args, dependencies| { + let YosysNextpnrWriteYsFileArgs {} = args; + let base_job = dependencies.get_job::(); + let verilog_job = dependencies.get_job::(); + let json_file = base_job.file_with_ext("json"); + Ok(YosysNextpnrWriteYsFile { + main_verilog_file: verilog_job.main_verilog_file(), + ys_file: base_job.file_with_ext("ys"), + json_file, + json_file_name: json_file + .interned_file_name() + .expect("known to have file name"), + }) + }) + } + + fn inputs(self, _job: &Self::Job) -> Interned<[JobItemName]> { + [JobItemName::DynamicPaths { + source_job_name: VerilogJobKind.name(), + }] + .intern_slice() + } + + fn outputs(self, job: &Self::Job) -> Interned<[JobItemName]> { + [JobItemName::Path { path: job.ys_file }].intern_slice() + } + + fn name(self) -> Interned { + "yosys-nextpnr-ecp5-write-ys-file".intern() + } + + fn external_command_params(self, _job: &Self::Job) -> Option { + None + } + + fn run( + self, + job: &Self::Job, + inputs: &[JobItem], + params: &JobParams, + _global_params: &GlobalParams, + _acquired_job: &mut AcquiredJob, + ) -> eyre::Result> { + assert!(inputs.iter().map(JobItem::name).eq(self.inputs(job))); + let [additional_files] = inputs else { + unreachable!(); + }; + let additional_files = VerilogJob::unwrap_additional_files(additional_files); + let mut contents = OsString::new(); + job.write_ys( + &mut contents, + additional_files, + params.main_module().name_id(), + )?; + let path = job.ys_file; + std::fs::write(path, contents.as_encoded_bytes()) + .wrap_err_with(|| format!("writing {path:?} failed"))?; + Ok(vec![JobItem::Path { path }]) + } + + fn subcommand_hidden(self) -> bool { + true + } +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug, clap::Args)] +pub struct YosysNextpnrSynthArgs {} + +impl ToArgs for YosysNextpnrSynthArgs { + fn to_args(&self, _args: &mut (impl WriteArgs + ?Sized)) { + let Self {} = self; + } +} + +#[derive(Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub struct YosysNextpnrSynth { + #[serde(flatten)] + write_ys_file: YosysNextpnrWriteYsFile, + ys_file_name: Interned, +} + +impl fmt::Debug for YosysNextpnrSynth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + write_ys_file: + YosysNextpnrWriteYsFile { + main_verilog_file, + ys_file, + json_file, + json_file_name, + }, + ys_file_name, + } = self; + f.debug_struct("YosysNextpnrSynth") + .field("main_verilog_file", main_verilog_file) + .field("ys_file", ys_file) + .field("ys_file_name", ys_file_name) + .field("json_file", json_file) + .field("json_file_name", json_file_name) + .finish() + } +} + +impl YosysNextpnrSynth { + pub fn main_verilog_file(&self) -> Interned { + self.write_ys_file.main_verilog_file() + } + pub fn ys_file(&self) -> Interned { + self.write_ys_file.ys_file() + } + pub fn ys_file_name(&self) -> Interned { + self.ys_file_name + } + pub fn json_file(&self) -> Interned { + self.write_ys_file.json_file() + } + pub fn json_file_name(&self) -> Interned { + self.write_ys_file.json_file_name() + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] +pub struct Yosys; + +impl ExternalProgramTrait for Yosys { + fn default_program_name() -> Interned { + "yosys".intern() //must be yosys + } +} + +impl ExternalCommand for YosysNextpnrSynth { + type AdditionalArgs = YosysNextpnrSynthArgs; + type AdditionalJobData = Self; + type BaseJobPosition = GetJobPositionDependencies< + GetJobPositionDependencies< + GetJobPositionDependencies<::BaseJobPosition>, + >, + >; + type Dependencies = JobKindAndDependencies; + type ExternalProgram = Yosys; + + fn dependencies() -> Self::Dependencies { + Default::default() + } + + fn args_to_jobs( + args: JobArgsAndDependencies>, + params: &JobParams, + global_params: &GlobalParams, + ) -> eyre::Result<( + Self::AdditionalJobData, + ::JobsAndKinds, + )> { + args.args_to_jobs_external_simple(params, global_params, |args, dependencies| { + let YosysNextpnrSynthArgs {} = args.additional_args; + Ok(Self { + write_ys_file: dependencies.job.job.clone(), + ys_file_name: dependencies + .job + .job + .ys_file() + .interned_file_name() + .expect("known to have file name"), + }) + }) + } + + fn inputs(job: &ExternalCommandJob) -> Interned<[JobItemName]> { + [ + JobItemName::Path { + path: job.additional_job_data().ys_file(), + }, + JobItemName::Path { + path: job.additional_job_data().main_verilog_file(), + }, + JobItemName::DynamicPaths { + source_job_name: VerilogJobKind.name(), + }, + ] + .intern_slice() + } + + fn output_paths(job: &ExternalCommandJob) -> Interned<[Interned]> { + [job.additional_job_data().json_file()].intern_slice() + } + + fn command_line_args(job: &ExternalCommandJob, args: &mut W) { + args.write_arg("-s"); + args.write_interned_arg(job.additional_job_data().ys_file_name()); + } + + fn current_dir(job: &ExternalCommandJob) -> Option> { + Some(job.output_dir()) + } + + fn job_kind_name() -> Interned { + "yosys-nextpnr-ecp5-synth".intern() + } + + fn subcommand_hidden() -> bool { + true + } +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Default)] +pub struct YosysNextpnrWritePcfFileJobKind; + +#[derive(Clone, PartialEq, Eq, Hash, Debug, clap::Args)] +pub struct YosysNextpnrWritePcfFileArgs {} + +impl ToArgs for YosysNextpnrWritePcfFileArgs { + fn to_args(&self, _args: &mut (impl WriteArgs + ?Sized)) { + let Self {} = self; + } +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)] +pub struct YosysNextpnrWritePcfFile { + firrtl_export_options: crate::firrtl::ExportOptions, + output_dir: Interned, + pcf_file: Interned, +} + +struct WritePcfContentsError(eyre::Report); + +impl From for WritePcfContentsError { + fn from(v: eyre::Report) -> Self { + Self(v) + } +} + +impl From for WritePcfContentsError { + fn from(_v: fmt::Error) -> Self { + unreachable!("String write can't fail") + } +} + +fn tcl_escape(s: impl AsRef) -> String { + let s = s.as_ref(); + if !s.contains(|ch: char| !ch.is_alphanumeric() && ch != '_') { + return s.into(); + } + let mut retval = String::with_capacity(s.len().saturating_add(2)); + retval.push('"'); + for ch in s.chars() { + if let '$' | '\\' | '[' = ch { + retval.push('\\'); + } + retval.push(ch); + } + retval.push('"'); + retval +} + +#[derive(Copy, Clone, Debug)] +enum AnnotationTarget { + None, + Module(Module), + Mem(Mem), + Target(Interned), +} + +impl AnnotationTarget { + fn source_location(self) -> SourceLocation { + match self { + AnnotationTarget::None => unreachable!(), + AnnotationTarget::Module(module) => module.source_location(), + AnnotationTarget::Mem(mem) => mem.source_location(), + AnnotationTarget::Target(target) => target.base().source_location(), + } + } +} + +struct PcfFileWriter { //TODO + output: W, + module_depth: usize, + annotation_target: AnnotationTarget, + dont_touch_targets: HashSet>, + required_dont_touch_targets: HashSet>, +} + +impl PcfFileWriter { + fn run(output: W, top_module: Module) -> Result<(), WritePcfContentsError> { + let mut this = Self { + output, + module_depth: 0, + annotation_target: AnnotationTarget::None, + dont_touch_targets: HashSet::default(), + required_dont_touch_targets: HashSet::default(), + }; + top_module.visit(&mut this)?; + let Self { + output: _, + module_depth: _, + annotation_target: _, + dont_touch_targets, + required_dont_touch_targets, + } = this; + for &target in required_dont_touch_targets.difference(&dont_touch_targets) { + return Err(eyre::eyre!( + "a DontTouchAnnotation is required since the target is also annotated with a LatticeAnnotation:\ntarget: {target:?}\nat: {}", + target.base().source_location(), + ).into()); + } + Ok(()) + } + fn default_visit_with>( + &mut self, + module_depth: usize, + annotation_target: AnnotationTarget, + v: &T, + ) -> Result<(), WritePcfContentsError> { + let Self { + output: _, + module_depth: old_module_depth, + annotation_target: old_annotation_target, + dont_touch_targets: _, + required_dont_touch_targets: _, + } = *self; + self.module_depth = module_depth; + self.annotation_target = annotation_target; + let retval = v.default_visit(self); + self.module_depth = old_module_depth; + self.annotation_target = old_annotation_target; + retval + } +} + +impl Visitor for PcfFileWriter { + type Error = WritePcfContentsError; + + fn visit_targeted_annotation(&mut self, v: &TargetedAnnotation) -> Result<(), Self::Error> { + self.default_visit_with(self.module_depth, AnnotationTarget::Target(v.target()), v) + } + + fn visit_module(&mut self, v: &Module) -> Result<(), Self::Error> { + self.default_visit_with( + self.module_depth + 1, + AnnotationTarget::Module(v.canonical()), + v, + ) + } + + fn visit_mem( + &mut self, + v: &Mem, + ) -> Result<(), Self::Error> + where + Element: Visit, + { + self.default_visit_with( + self.module_depth + 1, + AnnotationTarget::Mem(v.canonical()), + v, + ) + } + + fn visit_dont_touch_annotation(&mut self, _v: &DontTouchAnnotation) -> Result<(), Self::Error> { + if let AnnotationTarget::Target(target) = self.annotation_target { + self.dont_touch_targets.insert(target); + } + Ok(()) + } + + + /* FIXME fn visit_lattice_annotation(&mut self, v: &LatticeAnnotation) -> Result<(), Self::Error> */ +} + +impl YosysNextpnrWritePcfFile { + fn write_pcf_contents_for_port_and_annotations( + &self, + output: &mut impl fmt::Write, + port: &ScalarizedModuleABIPort, + annotations: ScalarizedModuleABIAnnotations<'_>, + ) -> Result<(), WritePcfContentsError> { + /* fixme for annotation in annotations .. */ + Ok(()) + } + fn write_pcf_contents( + &self, + output: &mut String, + top_module: &Module, + ) -> eyre::Result<()> { + let scalarized_module_abi = + ScalarizedModuleABI::new(top_module, self.firrtl_export_options) + .map_err(eyre::Report::from)?; + match scalarized_module_abi.for_each_port_and_annotations(|port, annotations| { + match self.write_pcf_contents_for_port_and_annotations(output, port, annotations) { + Ok(()) => ControlFlow::Continue(()), + Err(e) => ControlFlow::Break(e), + } + }) { + ControlFlow::Continue(()) => {} + ControlFlow::Break(e) => return Err(e.0), + } + PcfFileWriter::run(output, *top_module).map_err(|e| e.0) + } +} + +impl JobKind for YosysNextpnrWritePcfFileJobKind { + type Args = YosysNextpnrWritePcfFileArgs; + type Job = YosysNextpnrWritePcfFile; + type Dependencies = JobKindAndDependencies>; + + fn dependencies(self) -> Self::Dependencies { + Default::default() + } + + fn args_to_jobs( + args: JobArgsAndDependencies, + params: &JobParams, + global_params: &GlobalParams, + ) -> eyre::Result> { + let firrtl_export_options = args + .dependencies + .dependencies + .dependencies + .dependencies + .dependencies + .args + .args + .export_options; + args.args_to_jobs_simple(params, global_params, |_kind, args, dependencies| { + let YosysNextpnrWritePcfFileArgs {} = args; + let base_job = dependencies.get_job::(); + Ok(YosysNextpnrWritePcfFile { + firrtl_export_options, + output_dir: base_job.output_dir(), + pcf_file: base_job.file_with_ext("pcf"), + }) + }) + } + + fn inputs(self, job: &Self::Job) -> Interned<[JobItemName]> { + [JobItemName::Path { + path: job.output_dir, + }] + .intern_slice() + } + + fn outputs(self, job: &Self::Job) -> Interned<[JobItemName]> { + [JobItemName::Path { path: job.pcf_file }].intern_slice() + } + + fn name(self) -> Interned { + "yosys-nextpnr-ecp5-write-pcf-file".intern() + } + + fn external_command_params(self, _job: &Self::Job) -> Option { + None + } + + fn run( + self, + job: &Self::Job, + inputs: &[JobItem], + params: &JobParams, + _global_params: &GlobalParams, + _acquired_job: &mut AcquiredJob, + ) -> eyre::Result> { + assert!(inputs.iter().map(JobItem::name).eq(self.inputs(job))); + let mut pcf = String::new(); + job.write_pcf_contents(&mut pcf, params.main_module())?; + std::fs::write(job.pcf_file, pcf)?; + Ok(vec![JobItem::Path { path: job.pcf_file }]) + } + + fn subcommand_hidden(self) -> bool { + true + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] +pub struct NextpnrLattice; + +impl ExternalProgramTrait for NextpnrLattice { + fn default_program_name() -> Interned { + "nextpnr".intern() + } +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug, clap::Args)] +pub struct YosysNextpnrRunNextpnrArgs { + #[command(flatten)] + pub common: LatticeArgs, + #[arg(long, default_value_t = 0)] + pub nextpnr_lattice_seed: i32, +} + +impl ToArgs for YosysNextpnrRunNextpnrArgs { + fn to_args(&self, args: &mut (impl WriteArgs + ?Sized)) { + let Self { + common, + nextpnr_lattice_seed, + } = self; + common.to_args(args); + args.write_display_arg(format_args!("--nextpnr-lattice-seed={nextpnr_lattice_seed}")); + } +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)] +pub struct YosysNextpnrRunNextpnr { + device: Device, + nextpnr_lattice_seed: i32, + pcf_file: Interned, + json_file: Interned, + json_file_name: Interned, + routed_json_file: Interned, + routed_json_file_name: Interned, + textcfg_file: Interned, + textcfg_file_name: Interned, +} + +impl ExternalCommand for YosysNextpnrRunNextpnr { + type AdditionalArgs = YosysNextpnrRunNextpnrArgs; + type AdditionalJobData = Self; + type BaseJobPosition = GetJobPositionDependencies< + GetJobPositionDependencies<::BaseJobPosition>, + >; + type Dependencies = JobKindAndDependencies; + type ExternalProgram = NextpnrLattice; + + fn dependencies() -> Self::Dependencies { + Default::default() + } + + fn args_to_jobs( + args: JobArgsAndDependencies>, + params: &JobParams, + global_params: &GlobalParams, + ) -> eyre::Result<( + Self::AdditionalJobData, + ::JobsAndKinds, + )> { + args.args_to_jobs_external_simple(params, global_params, |args, dependencies| { + let YosysNextpnrRunNextpnrArgs { + common, + nextpnr_lattice_seed, + } = args.additional_args; + let base_job = dependencies.get_job::(); + let write_pcf_file = dependencies.get_job::(); + let synth = dependencies.get_job::, _>(); + let routed_json_file = base_job.file_with_ext("routed.json"); + let textcfg_file = base_job.file_with_ext("config"); //file must exist + Ok(Self { + device: common.require_device(base_job.platform(), global_params)?, + nextpnr_lattice_seed, + pcf_file: write_pcf_file.pcf_file, + json_file: synth.additional_job_data().json_file(), + json_file_name: synth.additional_job_data().json_file_name(), + routed_json_file: routed_json_file, + routed_json_file_name: routed_json_file + .interned_file_name() + .expect("known to have file name"), + textcfg_file:textcfg_file, + textcfg_file_name: textcfg_file + .interned_file_name() + .expect("known to have file name"), + }) + }) + } + + fn inputs(job: &ExternalCommandJob) -> Interned<[JobItemName]> { + [ + JobItemName::Path { + path: job.additional_job_data().json_file, + }, + JobItemName::Path { + path: job.additional_job_data().pcf_file, + }, + ] + .intern_slice() + } + + fn output_paths(job: &ExternalCommandJob) -> Interned<[Interned]> { + [ + job.additional_job_data().routed_json_file, + ] + .intern_slice() + } + + fn command_line_args(job: &ExternalCommandJob, args: &mut W) { + let job_data @ YosysNextpnrRunNextpnr { + nextpnr_lattice_seed, + json_file_name, + routed_json_file_name, + textcfg_file_name, + .. + } = job.additional_job_data(); + args.write_long_option_eq("json", json_file_name); + args.write_long_option_eq("textcfg",textcfg_file_name); + args.write_arg("--25k"); + args.write_long_option_eq("package","CSFBGA285"); + args.write_long_option_eq("lpf","/tmp/orangecrab_r0.2.1.pcf"); + args.write_arg("--lpf-allow-unconstrained"); + //???args.write_display_arg(format_args!("--seed={nextpnr_lattice_seed}")); + + } + + fn current_dir(job: &ExternalCommandJob) -> Option> { + Some(job.output_dir()) + } + + fn job_kind_name() -> Interned { + "yosys-nextpnr-ecp5-run-nextpnr".intern() + } + + fn subcommand_hidden() -> bool { + true + } +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug, clap::Args)] +pub struct YosysNextpnrArgs { + #[arg(long, env = "DB_DIR", value_hint = clap::ValueHint::DirPath)] + pub pcf1: PathBuf, +} + +impl ToArgs for YosysNextpnrArgs { + fn to_args(&self, args: &mut (impl WriteArgs + ?Sized)) { + let Self { pcf1 } = self; + args.write_long_option_eq("pcf1", pcf1); + } +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)] +pub struct YosysNextpnr { + pcf1: Interned, + device: Device, + frames_file: Interned, + frames_file_name: Interned, + bit_file: Interned, + bit_file_name: Interned, +} + + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] +pub struct Ecppack; + +impl ExternalProgramTrait for Ecppack { + fn default_program_name() -> Interned { + "ecppack".intern() + } +} + +//begin +#[derive(Clone, PartialEq, Eq, Hash, Debug, clap::Args)] +pub struct YosysNextpnrRunEcpPackArgs { + #[arg(long, env = "DB_DIR", value_hint = clap::ValueHint::DirPath)] + pub placeholder_dir: PathBuf, +} + +impl ToArgs for YosysNextpnrRunEcpPackArgs { + fn to_args(&self, args: &mut (impl WriteArgs + ?Sized)) { + let Self { placeholder_dir } = self; + args.write_long_option_eq("placeholder-dir", placeholder_dir); + } +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)] +pub struct YosysNextpnrRunEcpPack { + placeholder_dir: Interned, + device: Device, + routed_json_file: Interned, + routed_json_file_name: Interned, + textcfg_file: Interned, + textcfg_file_name: Interned, + frames_file: Interned, + frames_file_name: Interned, + bit_file: Interned, + bit_file_name: Interned, +} + +impl ExternalCommand for YosysNextpnrRunEcpPack { + type AdditionalArgs = YosysNextpnrRunEcpPackArgs; + type AdditionalJobData = Self; + type BaseJobPosition = GetJobPositionDependencies< + ::BaseJobPosition, + >; + type Dependencies = JobKindAndDependencies>; + type ExternalProgram = Ecppack; + + fn dependencies() -> Self::Dependencies { + Default::default() + } + + fn args_to_jobs( + args: JobArgsAndDependencies>, + params: &JobParams, + global_params: &GlobalParams, + ) -> eyre::Result<( + Self::AdditionalJobData, + ::JobsAndKinds, + )> { + args.args_to_jobs_external_simple(params, global_params, |args, dependencies| { + let YosysNextpnrRunEcpPackArgs { placeholder_dir } = args.additional_args; + let base_job = dependencies.get_job::(); + let frames_file = base_job.file_with_ext("frames"); + let bit_file = base_job.file_with_ext("bit"); + Ok(Self { + placeholder_dir: placeholder_dir.intern_deref(), + device: dependencies.job.job.additional_job_data().device, + //fixme glue code + routed_json_file: dependencies.job.job.additional_job_data().routed_json_file, + routed_json_file_name: dependencies.job.job.additional_job_data().routed_json_file_name, + textcfg_file: dependencies.job.job.additional_job_data().textcfg_file, + textcfg_file_name: dependencies.job.job.additional_job_data().textcfg_file_name, + frames_file, + frames_file_name: frames_file + .interned_file_name() + .expect("known to have file name"), + bit_file, + bit_file_name: bit_file + .interned_file_name() + .expect("known to have file name"), + }) + }) + } + + fn inputs(job: &ExternalCommandJob) -> Interned<[JobItemName]> { + [JobItemName::Path { + path: job.additional_job_data().routed_json_file, + }] + .intern_slice() + } + + fn output_paths(job: &ExternalCommandJob) -> Interned<[Interned]> { + [ + job.additional_job_data().bit_file, + ] + .intern_slice() + } + + fn command_line_args(job: &ExternalCommandJob, args: &mut W) { + let job_data @ YosysNextpnrRunEcpPack { + placeholder_dir, + device, + routed_json_file_name, + textcfg_file_name, + frames_file_name, + bit_file_name, + .. + } = job.additional_job_data(); + args.write_arg("--compress"); + args.write_long_option_eq("freq", "38.8"); //FIXME do not hardcode + args.write_long_option_eq("input",textcfg_file_name); + args.write_long_option_eq("bit",bit_file_name); + } + + fn current_dir(job: &ExternalCommandJob) -> Option> { + Some(job.output_dir()) + } + + fn job_kind_name() -> Interned { + "yosys-nextpnr-ecp5".intern() + } +} +//end + +pub(crate) fn built_in_job_kinds() -> impl IntoIterator { + [ + DynJobKind::new(YosysNextpnrWriteYsFileJobKind), //working + DynJobKind::new(ExternalCommandJobKind::::new()), //working + DynJobKind::new(YosysNextpnrWritePcfFileJobKind), //TODO + DynJobKind::new(ExternalCommandJobKind::::new()), //working + DynJobKind::new(ExternalCommandJobKind::::new()), //working + ] +} + +pub(crate) fn built_in_platforms() -> impl IntoIterator { + [] +} diff --git a/nextpnr.txt b/nextpnr.txt new file mode 100644 index 0000000..85d5678 --- /dev/null +++ b/nextpnr.txt @@ -0,0 +1,5 @@ +nextpnr-ecp5 for orangecrab +nextpnr-ice40 for other one* + +modified: Makefile +modified: crates/fayalite/src/vendor/lattice/yosys_nextpnr.rs diff --git a/tools/firrtl.tar.gz b/tools/firrtl.tar.gz new file mode 100644 index 0000000..1676917 Binary files /dev/null and b/tools/firrtl.tar.gz differ