From d060d2d1ea94eb68493fe32e21b108eea63d91a4 Mon Sep 17 00:00:00 2001 From: elle Date: Mon, 18 Aug 2025 20:01:34 +0000 Subject: [PATCH 1/2] fixup: add missing `Text` constructor arg --- rust-hdl/src/docs/vcd2svg/display_metrics.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rust-hdl/src/docs/vcd2svg/display_metrics.rs b/rust-hdl/src/docs/vcd2svg/display_metrics.rs index eb524a58..9e28c5ec 100644 --- a/rust-hdl/src/docs/vcd2svg/display_metrics.rs +++ b/rust-hdl/src/docs/vcd2svg/display_metrics.rs @@ -196,7 +196,7 @@ impl DisplayMetrics { if (x0 - label_width / 2) >= self.signal_width && (x0 + label_width / 2) <= self.canvas_width { - let txt = Text::new() + let txt = Text::new("") .add(svg::node::Text::new(value)) .set("x", x0) .set( @@ -218,7 +218,7 @@ impl DisplayMetrics { } pub(crate) fn signal_label(&self, index: usize, signal: &str) -> Text { - Text::new() + Text::new("") .add(svg::node::Text::new(signal)) .set("x", self.shim) .set("y", self.signal_baseline(index) - self.shim) @@ -307,7 +307,7 @@ impl DisplayMetrics { data_low = data_low.line_to((x1 + shim, y1)); data_high = data_high.line_to((x1 + shim, flip(y1))); doc = doc.add( - Text::new() + Text::new("") .add(svg::node::Text::new(value.label.to_string())) .set("x", x1 + 2.0 * shim) .set( From d337dcfa2dcf7ba4e7b3028c99b19a1defb77fa2 Mon Sep 17 00:00:00 2001 From: elle Date: Mon, 18 Aug 2025 20:01:40 +0000 Subject: [PATCH 2/2] bsp: add support for the Orange Crab Adds initial board support for the Orange Crab. Based on the `rust-hdl-bsp-alchitry-cu` BSP crate. --- Cargo.toml | 1 + rust-hdl-bsp-orange-crab/Cargo.toml | 13 + rust-hdl-bsp-orange-crab/src/lib.rs | 2 + rust-hdl-bsp-orange-crab/src/pins.rs | 290 ++++++++++++++++++ rust-hdl-bsp-orange-crab/src/synth.rs | 128 ++++++++ .../tests/bsp_orange_crab_pulser.rs | 49 +++ .../tests/bsp_orange_crab_pwm.rs | 65 ++++ 7 files changed, 548 insertions(+) create mode 100644 rust-hdl-bsp-orange-crab/Cargo.toml create mode 100644 rust-hdl-bsp-orange-crab/src/lib.rs create mode 100644 rust-hdl-bsp-orange-crab/src/pins.rs create mode 100644 rust-hdl-bsp-orange-crab/src/synth.rs create mode 100644 rust-hdl-bsp-orange-crab/tests/bsp_orange_crab_pulser.rs create mode 100644 rust-hdl-bsp-orange-crab/tests/bsp_orange_crab_pwm.rs diff --git a/Cargo.toml b/Cargo.toml index fac0987c..0b7e478a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "rust-hdl-bsp-alchitry-cu", "rust-hdl-bsp-ok-xem6010", "rust-hdl-bsp-ok-xem7010", + "rust-hdl-bsp-orange-crab", "rust-hdl-x", "rust-hdl-x-widgets", "rust-hdl-x-macro", diff --git a/rust-hdl-bsp-orange-crab/Cargo.toml b/rust-hdl-bsp-orange-crab/Cargo.toml new file mode 100644 index 00000000..15395486 --- /dev/null +++ b/rust-hdl-bsp-orange-crab/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rust-hdl-bsp-orange-crab" +version = "0.46.0" +edition = "2021" +license = "MIT" +description = "Support crate for RustHDL - provides Board Support Package for the Orange Crab board" +homepage = "https://github.com/samitbasu/rust-hdl" +repository = "https://github.com/samitbasu/rust-hdl" +keywords = ["fpga", "verilog", "hardware"] +authors = ["Elle Rhumsaa "] + +[dependencies] +rust-hdl = { version = "0.46.0", path = "../rust-hdl", features = ["fpga"] } diff --git a/rust-hdl-bsp-orange-crab/src/lib.rs b/rust-hdl-bsp-orange-crab/src/lib.rs new file mode 100644 index 00000000..5fe82f97 --- /dev/null +++ b/rust-hdl-bsp-orange-crab/src/lib.rs @@ -0,0 +1,2 @@ +pub mod pins; +pub mod synth; diff --git a/rust-hdl-bsp-orange-crab/src/pins.rs b/rust-hdl-bsp-orange-crab/src/pins.rs new file mode 100644 index 00000000..96515260 --- /dev/null +++ b/rust-hdl-bsp-orange-crab/src/pins.rs @@ -0,0 +1,290 @@ +use rust_hdl::core::prelude::*; + +/// Represents the clock speed for the FPGA: 48MHz. +pub const CLOCK_SPEED: u64 = 48_000_000; + +/// Represents the GPIO pins available on the Orange Crab board. +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Pin { + /// Reset + Rst, + /// Supply voltage power 3.3V + P3V3, + /// Reference voltage + Aref, + /// Ground + Gnd0, + /// GPIO A0 + GpioA0, + /// GPIO A1 + GpioA1, + /// GPIO A2 + GpioA2, + /// GPIO A3 + GpioA3, + /// GPIO A4 + GpioA4, + /// GPIO A4 + GpioA5, + /// SCK + Sck, + /// MOSI + Mosi, + /// MISO + Miso, + /// GPIO 0 + Gpio0, + /// GPIO 1 + Gpio1, + /// Ground + Gnd1, + /// Battery voltage + Vbat, + /// Enable + Enable, + /// USB voltage + Vusb, + /// GPIO 13 + Gpio13, + /// GPIO 12 + Gpio12, + /// GPIO 11 + Gpio11, + /// GPIO 10 + Gpio10, + /// GPIO 9 + Gpio9, + /// GPIO 6 + Gpio6, + /// GPIO 5 + Gpio5, + /// SCL + Scl, + /// SDA + Sda, + /// System clock + Clock, + /// RGB LED: red + LedRed, + /// RGB LED: green + LedGreen, + /// RGB LED: blue + LedBlue, + /// Button + Btn, + /// Reset (inverse) + Rstn, + /// USB positive + UsbP, + /// USB negative + UsbN, + /// USB pu + UsbPu, + /// QSPI Chip Select (inverse) + QspiCsn, + /// QSPI Clock + QspiClock, + /// QSPI DQ[0] + QspiDq0, + /// QSPI DQ[1] + QspiDq1, + /// QSPI DQ[2] + QspiDq2, + /// QSPI DQ[3] + QspiDq3, + /// SD DAT[0] + SdDat0, + /// SD DAT[1] + SdDat1, + /// SD DAT[2] + SdDat2, + /// SD DAT[3] + SdDat3, + /// SD Command + SdCmd, + /// SD Clock + SdClk, + /// SD Card Detect + SdCd, + /// Analog Mux[0] + AnaMux0, + /// Analog Mux[1] + AnaMux1, + /// Analog Mux[2] + AnaMux2, + /// Analog Mux[3] + AnaMux3, + /// Analog Sense Low + AnaSenseL, + /// Analog Sense High + AnaSenseH, + /// Analog Control[0] + AnaCtrl0, + /// Analog Control[1] + AnaCtrl1, +} + +impl Pin { + /// Creates a new [Pin]. + pub const fn new() -> Self { + Self::Rst + } + + /// Gets the pin name. + pub const fn pin(&self) -> &'static str { + match self { + Self::Rst => "PROGRAM", + Self::P3V3 => "3V3", + Self::Aref => "AREF", + Self::Gnd0 => "GND", + Self::GpioA0 => "PR38B", + Self::GpioA1 => "PR38D", + Self::GpioA2 => "PR38C", + Self::GpioA3 => "PR26A", + Self::GpioA4 => "PR26B", + Self::GpioA5 => "PB13B", + Self::Sck => "PR13A", + Self::Mosi => "PB4B", + Self::Miso => "PB4A", + Self::Gpio0 => "PB6A", + Self::Gpio1 => "PB6B", + Self::Gnd1 => "GND", + Self::Vbat => "VBAT", + Self::Enable => "EN", + Self::Vusb => "VUSB", + Self::Gpio13 => "PR35A", + Self::Gpio12 => "PR29D", + Self::Gpio11 => "PT32B", + Self::Gpio10 => "PT67A", + Self::Gpio9 => "PT67B", + Self::Gpio6 => "PT33A", + Self::Gpio5 => "PT29A", + Self::Scl => "PT33B", + Self::Sda => "PT29B", + Self::Clock => "clk48", + Self::LedRed => "rgb_led0_r", + Self::LedGreen => "rgb_led0_g", + Self::LedBlue => "rgb_led0_b", + Self::Btn => "BTN", + Self::Rstn => "RSTn", + Self::UsbP => "USB_p", + Self::UsbN => "USB_n", + Self::UsbPu => "USB_pu", + Self::QspiCsn => "QSPI_CSn", + Self::QspiClock => "QSPI_CLK", + Self::QspiDq0 => "QSPI_DQ[0]", + Self::QspiDq1 => "QSPI_DQ[1]", + Self::QspiDq2 => "QSPI_DQ[2]", + Self::QspiDq3 => "QSPI_DQ[3]", + Self::SdDat0 => "SD_DAT[0]", + Self::SdDat1 => "SD_DAT[1]", + Self::SdDat2 => "SD_DAT[2]", + Self::SdDat3 => "SD_DAT[3]", + Self::SdCmd => "SD_CMD", + Self::SdClk => "SD_CLK", + Self::SdCd => "SD_CD", + Self::AnaMux0 => "ANA_MUX[0]", + Self::AnaMux1 => "ANA_MUX[1]", + Self::AnaMux2 => "ANA_MUX[2]", + Self::AnaMux3 => "ANA_MUX[3]", + Self::AnaSenseL => "ANA_SENSE_L", + Self::AnaSenseH => "ANA_SENSE_H", + Self::AnaCtrl0 => "ANA_CTRL[0]", + Self::AnaCtrl1 => "ANA_CTRL[1]", + } + } + + /// Gets the pad name. + pub const fn pad(&self) -> &'static str { + match self { + Self::Rst => "T15", + Self::P3V3 => "3V3", + Self::Aref => "AREF", + Self::Gnd0 => "GND", + Self::GpioA0 => "L4", + Self::GpioA1 => "N3", + Self::GpioA2 => "N4", + Self::GpioA3 => "H4", + Self::GpioA4 => "G4", + Self::GpioA5 => "T17", + Self::Sck => "R17", + Self::Mosi => "N16", + Self::Miso => "N15", + Self::Gpio0 => "N17", + Self::Gpio1 => "M18", + Self::Gnd1 => "GND", + Self::Vbat => "VBAT", + Self::Enable => "EN", + Self::Vusb => "VUSB", + Self::Gpio13 => "J2", + Self::Gpio12 => "H2", + Self::Gpio11 => "A8", + Self::Gpio10 => "B8", + Self::Gpio9 => "C8", + Self::Gpio6 => "B9", + Self::Gpio5 => "B10", + Self::Scl => "C9", + Self::Sda => "C10", + Self::Clock => "A9", + Self::LedRed => "K4", + Self::LedGreen => "M3", + Self::LedBlue => "J3", + Self::Btn => "J17", + Self::Rstn => "V17", + Self::UsbP => "N1", + Self::UsbN => "M2", + Self::UsbPu => "N2", + Self::QspiCsn => "U17", + Self::QspiClock => "U16", + Self::QspiDq0 => "U18", + Self::QspiDq1 => "T18", + Self::QspiDq2 => "R18", + Self::QspiDq3 => "N18", + Self::SdDat0 => "J1", + Self::SdDat1 => "K3", + Self::SdDat2 => "L3", + Self::SdDat3 => "M1", + Self::SdCmd => "K2", + Self::SdClk => "K1", + Self::SdCd => "L1", + Self::AnaMux0 => "F4", + Self::AnaMux1 => "F3", + Self::AnaMux2 => "F2", + Self::AnaMux3 => "H1", + Self::AnaSenseL => "G3", + Self::AnaSenseH => "H3", + Self::AnaCtrl0 => "G1", + Self::AnaCtrl1 => "F1", + } + } + + /// Gets the [Signal] for the clock. + pub fn clock() -> Signal { + let mut x = Signal::::default(); + x.add_location(0, Self::Clock.pad()); + x.connect(); + x + } + + /// Gets the [Signal] for the LEDs. + pub fn leds() -> Signal> { + let mut x = Signal::::default(); + + [Self::LedRed, Self::LedGreen, Self::LedBlue] + .into_iter() + .map(|l| l.pad()) + .enumerate() + .for_each(|(ndx, pad)| x.add_location(ndx, pad)); + + x + } +} + +impl Default for Pin { + fn default() -> Self { + Self::new() + } +} + +// TODO: add DDR3 pins diff --git a/rust-hdl-bsp-orange-crab/src/synth.rs b/rust-hdl-bsp-orange-crab/src/synth.rs new file mode 100644 index 00000000..68697554 --- /dev/null +++ b/rust-hdl-bsp-orange-crab/src/synth.rs @@ -0,0 +1,128 @@ +use rust_hdl::core::check_error::check_all; +use rust_hdl::core::prelude::*; +use rust_hdl::fpga::toolchains::ecp5::generate_lpf; +use std::fs::{create_dir_all, remove_dir_all, File}; +use std::io::Write; +use std::path::PathBuf; +use std::process::{Command, Output}; +use std::str::FromStr; + +/// Represents the field density of the Orange Crab FPGA. +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Density { + /// Represents the 25F variant. + _25F, + /// Represents the 85F variant. + _85F, +} + +impl Density { + /// Creates a new [Density]. + pub const fn new() -> Self { + Self::_25F + } + + /// Gets the [Density] string representation. + pub const fn as_str(&self) -> &'static str { + match self { + Self::_25F => "25F", + Self::_85F => "85F", + } + } + + /// Gets the [Density] argument string representation. + pub const fn as_arg(&self) -> &'static str { + match self { + Self::_25F => "--25k", + Self::_85F => "--85k", + } + } +} + +impl Default for Density { + fn default() -> Self { + Self::new() + } +} + +impl std::fmt::Display for Density { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +fn save_stdout(output: Output, dir: &PathBuf, basename: &str) -> Result<(), std::io::Error> { + let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8(output.stderr).unwrap(); + let mut out_file = File::create(dir.clone().join(format!("{}.out", basename)))?; + write!(out_file, "{}", stdout)?; + let mut err_file = File::create(dir.clone().join(format!("{}.err", basename)))?; + write!(err_file, "{}", stderr)?; + Ok(()) +} + +/// Generates the bitstream from the RustHDL definition. +pub fn generate_bitstream(mut uut: U, prefix: &str, density: Density) { + uut.connect_all(); + check_all(&uut).unwrap(); // TODO - Change from panic to return an error + let verilog_text = generate_verilog(&uut); + let pcf_text = generate_lpf(&uut); + + let dir = PathBuf::from_str(prefix).unwrap(); + let _ = remove_dir_all(&dir); + let _ = create_dir_all(&dir); + + let mut v_file = File::create(dir.join("top.v")).unwrap(); + write!(v_file, "{}", verilog_text).unwrap(); + + let pcf_filename = "top.pcf".to_string(); + let mut pcf_file = File::create(dir.join(pcf_filename)).unwrap(); + write!(pcf_file, "{}", pcf_text).unwrap(); + + let output = Command::new("yosys") + .current_dir(dir.clone()) + .args(["-p", "read_verilog top.v; synth_ecp5 -json top.json"]) + .output() + .unwrap(); + save_stdout(output, &dir, "yosys_synth").unwrap(); + + let output = Command::new("nextpnr-ecp5") + .current_dir(dir.clone()) + .args([ + "--package", + "CSFBGA285", + density.as_arg(), + "--textcfg", + "top.asc", + "--lpf", + "top.pcf", + "--json", + "top.json", + ]) + .output() + .unwrap(); + save_stdout(output, &dir, "nextpnr").unwrap(); + + let output = Command::new("ecppack") + .current_dir(dir.clone()) + .args([ + "--compress", + "--freq", + "38.8", + "--input", + "top.asc", + "--bit", + "top.bin", + ]) + .output() + .unwrap(); + save_stdout(output, &dir, "ecppack").unwrap(); + + let output = Command::new("dfu-suffix") + .current_dir(dir.clone()) + .args(["-v", "1209", "-p", "5af0", "-a", "top.bin"]) + .output() + .unwrap(); + save_stdout(output, &dir, "ecppack").unwrap(); +} diff --git a/rust-hdl-bsp-orange-crab/tests/bsp_orange_crab_pulser.rs b/rust-hdl-bsp-orange-crab/tests/bsp_orange_crab_pulser.rs new file mode 100644 index 00000000..0cedd4b7 --- /dev/null +++ b/rust-hdl-bsp-orange-crab/tests/bsp_orange_crab_pulser.rs @@ -0,0 +1,49 @@ +use rust_hdl::prelude::*; +use rust_hdl_bsp_orange_crab::{pins, synth}; +use std::time::Duration; + +#[derive(LogicBlock)] +pub struct OrangeCrabPulser { + pulser: Pulser, + clock: Signal, + leds: Signal>, +} + +impl Logic for OrangeCrabPulser { + #[hdl_gen] + fn update(&mut self) { + self.pulser.enable.next = true; + clock!(self, clock, pulser); + self.leds.next = 0x00.into(); + if self.pulser.pulse.val() { + self.leds.next = 0xAA.into(); + } + } +} + +impl Default for OrangeCrabPulser { + fn default() -> Self { + let pulser = Pulser::new(pins::CLOCK_SPEED.into(), 1.0, Duration::from_millis(250)); + + Self { + pulser, + clock: pins::Pin::clock(), + leds: pins::Pin::leds(), + } + } +} + +#[test] +fn synthesize_orange_crab_pulser() { + [synth::Density::_25F, synth::Density::_85F] + .into_iter() + .for_each(|density| { + let uut = OrangeCrabPulser::default(); + + synth::generate_bitstream( + uut, + target_path!(format!("orange_crab/pulser_{density}")), + density, + ); + }); +} diff --git a/rust-hdl-bsp-orange-crab/tests/bsp_orange_crab_pwm.rs b/rust-hdl-bsp-orange-crab/tests/bsp_orange_crab_pwm.rs new file mode 100644 index 00000000..71d05ab3 --- /dev/null +++ b/rust-hdl-bsp-orange-crab/tests/bsp_orange_crab_pwm.rs @@ -0,0 +1,65 @@ +use rust_hdl::prelude::*; +use rust_hdl_bsp_orange_crab::{pins, synth}; +use std::collections::BTreeMap; + +#[derive(LogicBlock)] +pub struct OrangeCrabPWM { + pwm: PulseWidthModulator

, + clock: Signal, + strobe: Strobe<32>, + leds: Signal>, + rom: ROM, 8>, + counter: DFF>, +} + +impl Logic for OrangeCrabPWM

{ + #[hdl_gen] + fn update(&mut self) { + clock!(self, clock, pwm, strobe, counter); + self.pwm.enable.next = true; + self.rom.address.next = self.counter.q.val(); + self.pwm.threshold.next = self.rom.data.val(); + self.strobe.enable.next = true; + self.leds.next = 0x00.into(); + if self.pwm.active.val() { + self.leds.next = 0xFF.into(); + } + self.counter.d.next = self.counter.q.val() + self.strobe.strobe.val(); + } +} + +impl OrangeCrabPWM

{ + fn new(clock_freq: u64) -> Self { + let rom = (0..256) + .map(|x| (x.to_bits(), snore(x))) + .collect::>(); + + Self { + pwm: PulseWidthModulator::default(), + clock: pins::Pin::clock(), + strobe: Strobe::new(clock_freq, 60.0), + leds: pins::Pin::leds(), + rom: ROM::new(rom), + counter: Default::default(), + } + } +} + +#[test] +fn test_pwm_synthesizes() { + [synth::Density::_25F, synth::Density::_85F] + .into_iter() + .for_each(|density| { + let mut uut: OrangeCrabPWM<6> = OrangeCrabPWM::new(pins::CLOCK_SPEED); + uut.connect_all(); + + let vlog = generate_verilog(&uut); + yosys_validate("pwm_oc", &vlog).unwrap(); + + synth::generate_bitstream( + uut, + target_path!(format!("orange_crab/pwm_oc_{density}")), + density, + ); + }); +}