From 26840daf13ff6fec72e612778a2a3c4bbf4237a3 Mon Sep 17 00:00:00 2001 From: Jacob Lifshay Date: Wed, 22 Oct 2025 20:09:41 -0700 Subject: [PATCH 1/2] arty_a7: add divided clocks as available input peripherals so you're not stuck with 100MHz --- crates/fayalite/src/vendor/xilinx/arty_a7.rs | 107 ++++++++++++++---- .../fayalite/src/vendor/xilinx/primitives.rs | 2 +- 2 files changed, 86 insertions(+), 23 deletions(-) diff --git a/crates/fayalite/src/vendor/xilinx/arty_a7.rs b/crates/fayalite/src/vendor/xilinx/arty_a7.rs index beeee0a..549e631 100644 --- a/crates/fayalite/src/vendor/xilinx/arty_a7.rs +++ b/crates/fayalite/src/vendor/xilinx/arty_a7.rs @@ -3,7 +3,7 @@ use crate::{ intern::{Intern, Interned}, - module::{instance_with_loc, wire_with_loc}, + module::{instance_with_loc, reg_builder_with_loc, wire_with_loc}, platform::{ DynPlatform, Peripheral, PeripheralRef, Peripherals, PeripheralsBuilderFactory, PeripheralsBuilderFinished, Platform, PlatformAspectSet, @@ -12,7 +12,7 @@ use crate::{ prelude::*, vendor::xilinx::{ Device, XdcCreateClockAnnotation, XdcIOStandardAnnotation, XdcLocationAnnotation, - primitives::{self, BUFGCE, STARTUPE2_default_inputs}, + primitives, }, }; use ordered_float::NotNan; @@ -66,7 +66,7 @@ arty_a7_platform! { #[derive(Debug)] pub struct ArtyA7Peripherals { - clk100: Peripheral, + clk100_div_pow2: [Peripheral; 4], rst: Peripheral, rst_sync: Peripheral, ld0: Peripheral, @@ -83,7 +83,7 @@ pub struct ArtyA7Peripherals { impl Peripherals for ArtyA7Peripherals { fn append_peripherals<'a>(&'a self, peripherals: &mut Vec>) { let Self { - clk100, + clk100_div_pow2, rst, rst_sync, ld0, @@ -95,7 +95,7 @@ impl Peripherals for ArtyA7Peripherals { ld6, ld7, } = self; - clk100.append_peripherals(peripherals); + clk100_div_pow2.append_peripherals(peripherals); rst.append_peripherals(peripherals); rst_sync.append_peripherals(peripherals); ld0.append_peripherals(peripherals); @@ -168,9 +168,20 @@ impl Platform for ArtyA7Platform { 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: builder.input_peripheral("clk100", ClockInput::new(100e6)), + clk100_div_pow2, rst: builder.input_peripheral("rst", Reset), rst_sync: builder.input_peripheral("rst_sync", SyncReset), ld0: builder.output_peripheral("ld0", RgbLed), @@ -192,7 +203,7 @@ impl Platform for ArtyA7Platform { fn add_peripherals_in_wrapper_module(&self, m: &ModuleBuilder, peripherals: Self::Peripherals) { let ArtyA7Peripherals { - clk100, + clk100_div_pow2, rst, rst_sync, ld0, @@ -249,30 +260,82 @@ impl Platform for ArtyA7Platform { connect(buf.T, false); buf.I }; - let clock_annotation = XdcCreateClockAnnotation { - period: NotNan::new(1e9 / clk100.ty().frequency()).expect("known to be valid"), - }; + 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", - STARTUPE2_default_inputs(), + primitives::STARTUPE2_default_inputs(), SourceLocation::builtin(), ); - let clk100_sync = instance_with_loc("clk100_sync", BUFGCE(), SourceLocation::builtin()); - connect(clk100_sync.CE, startup.EOS); - connect(clk100_sync.I, clk100_buf); - let clk100_out = wire_with_loc("clk100_out", SourceLocation::builtin(), Clock); - connect(clk100_out, clk100_sync.O); - annotate(clk100_out, clock_annotation); - annotate(clk100_out, DontTouchAnnotation); - if let Some(clk100) = clk100.into_used() { - connect(clk100.instance_io_field().clk, clk100_out); + 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); + 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); + 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); + 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); } 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, clk100_sync.O); - connect(rst_sync.inp, rst_buf); + connect(rst_sync.clk, clk_out); + connect(rst_sync.inp, rst_buf | !startup.EOS); rst_sync.out }; if let Some(rst) = rst.into_used() { diff --git a/crates/fayalite/src/vendor/xilinx/primitives.rs b/crates/fayalite/src/vendor/xilinx/primitives.rs index 5dc2567..9e22d26 100644 --- a/crates/fayalite/src/vendor/xilinx/primitives.rs +++ b/crates/fayalite/src/vendor/xilinx/primitives.rs @@ -33,7 +33,7 @@ pub fn BUFGCE() { #[hdl] let CE: Bool = m.input(); #[hdl] - let I: Bool = m.input(); + let I: Clock = m.input(); } #[hdl_module(extern)] From b3cc28e2b60cd04821599932c02cb4e14028b1bc Mon Sep 17 00:00:00 2001 From: Jacob Lifshay Date: Wed, 22 Oct 2025 05:06:57 -0700 Subject: [PATCH 2/2] add transmit-only UART example --- crates/fayalite/examples/tx_only_uart.rs | 188 +++++++++++++++++++ crates/fayalite/src/platform/peripherals.rs | 10 + crates/fayalite/src/vendor/xilinx/arty_a7.rs | 15 +- 3 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 crates/fayalite/examples/tx_only_uart.rs diff --git a/crates/fayalite/examples/tx_only_uart.rs b/crates/fayalite/examples/tx_only_uart.rs new file mode 100644 index 0000000..5c20b39 --- /dev/null +++ b/crates/fayalite/examples/tx_only_uart.rs @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// See Notices.txt for copyright information +use clap::builder::TypedValueParser; +use fayalite::{ + build::{ToArgs, WriteArgs}, + platform::PeripheralRef, + prelude::*, +}; +use ordered_float::NotNan; + +fn pick_clock<'a>( + platform_io_builder: &PlatformIOBuilder<'a>, +) -> PeripheralRef<'a, peripherals::ClockInput> { + let mut clks = platform_io_builder.peripherals_with_type::(); + clks.sort_by_key(|clk| { + // sort clocks by preference, smaller return values means higher preference + let mut frequency = clk.ty().frequency(); + let priority; + if frequency < 10e6 { + frequency = -frequency; // prefer bigger frequencies + priority = 1; + } else if frequency > 50e6 { + // prefer smaller frequencies + priority = 2; // least preferred + } else { + priority = 0; // most preferred + frequency = (frequency - 25e6).abs(); // prefer closer to 25MHz + } + (priority, NotNan::new(frequency).expect("should be valid")) + }); + clks[0] +} + +#[hdl_module] +fn tx_only_uart( + platform_io_builder: PlatformIOBuilder<'_>, + divisor: f64, + message: impl AsRef<[u8]>, +) { + let message = message.as_ref(); + let clk_input = pick_clock(&platform_io_builder).use_peripheral(); + let rst = platform_io_builder.peripherals_with_type::()[0].use_peripheral(); + let cd = #[hdl] + ClockDomain { + clk: clk_input.clk, + rst, + }; + let numerator = 1u128 << 16; + let denominator = (divisor * numerator as f64).round() as u128; + + #[hdl] + let remainder_reg: UInt<128> = reg_builder().clock_domain(cd).reset(0u128); + + #[hdl] + let sum: UInt<128> = wire(); + connect_any(sum, remainder_reg + numerator); + + #[hdl] + let tick_reg = reg_builder().clock_domain(cd).reset(false); + connect(tick_reg, false); + + #[hdl] + let next_remainder: UInt<128> = wire(); + connect(remainder_reg, next_remainder); + + #[hdl] + if sum.cmp_ge(denominator) { + connect_any(next_remainder, sum - denominator); + connect(tick_reg, true); + } else { + connect(next_remainder, sum); + } + + #[hdl] + let uart_state_reg = reg_builder().clock_domain(cd).reset(0_hdl_u4); + #[hdl] + let next_uart_state: UInt<4> = wire(); + + connect_any(next_uart_state, uart_state_reg + 1u8); + + #[hdl] + let message_mem: Array> = wire(Array[UInt::new_static()][message.len()]); + for (message, message_mem) in message.iter().zip(message_mem) { + connect(message_mem, *message); + } + #[hdl] + let addr_reg: UInt<32> = reg_builder().clock_domain(cd).reset(0u32); + #[hdl] + let next_addr: UInt<32> = wire(); + connect(next_addr, addr_reg); + + #[hdl] + let tx = reg_builder().clock_domain(cd).reset(true); + + #[hdl] + let tx_bits: Array = wire(); + + connect(tx_bits[0], false); // start bit + connect(tx_bits[9], true); // stop bit + + for i in 0..8 { + connect(tx_bits[i + 1], message_mem[addr_reg][i]); // data bits + } + + connect(tx, tx_bits[uart_state_reg]); + + #[hdl] + if uart_state_reg.cmp_eq(Expr::ty(tx_bits).len() - 1) { + connect(next_uart_state, 0_hdl_u4); + let next_addr_val = addr_reg + 1u8; + #[hdl] + if next_addr_val.cmp_lt(message.len()) { + connect_any(next_addr, next_addr_val); + } else { + connect(next_addr, 0u32); + } + } + + #[hdl] + if tick_reg { + connect(uart_state_reg, next_uart_state); + connect(addr_reg, next_addr); + } + + for uart in platform_io_builder.peripherals_with_type::() { + connect(uart.use_peripheral().tx, tx); + } + + #[hdl] + let io = m.add_platform_io(platform_io_builder); +} + +fn parse_baud_rate( + v: impl AsRef, +) -> Result, Box> { + let retval: NotNan = v + .as_ref() + .parse() + .map_err(|_| "invalid baud rate, must be a finite positive floating-point value")?; + if *retval > 0.0 && retval.is_finite() { + Ok(retval) + } else { + Err("baud rate must be finite and positive".into()) + } +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug, clap::Args)] +pub struct ExtraArgs { + #[arg(long, value_parser = clap::builder::StringValueParser::new().try_map(parse_baud_rate), default_value = "115200")] + pub baud_rate: NotNan, + #[arg(long, default_value = "Hello World from Fayalite!!!\r\n", value_parser = clap::builder::NonEmptyStringValueParser::new())] + pub message: String, +} + +impl ToArgs for ExtraArgs { + fn to_args(&self, args: &mut (impl WriteArgs + ?Sized)) { + let Self { baud_rate, message } = self; + args.write_display_arg(format_args!("--baud-rate={baud_rate}")); + args.write_long_option_eq("message", message); + } +} + +fn main() { + type Cli = BuildCli; + Cli::main( + "tx_only_uart", + |_, platform, ExtraArgs { baud_rate, message }| { + Ok(JobParams::new(platform.try_wrap_main_module(|io| { + let clk = pick_clock(&io).ty(); + let divisor = clk.frequency() / *baud_rate; + let baud_rate_error = |msg| { + ::command() + .error(clap::error::ErrorKind::ValueValidation, msg) + }; + const HUGE_DIVISOR: f64 = u64::MAX as f64; + match divisor { + divisor if !divisor.is_finite() => { + return Err(baud_rate_error("bad baud rate")); + } + HUGE_DIVISOR.. => return Err(baud_rate_error("baud rate is too small")), + 4.0.. => {} + _ => return Err(baud_rate_error("baud rate is too large")), + } + Ok(tx_only_uart(io, divisor, message)) + })?)) + }, + ); +} diff --git a/crates/fayalite/src/platform/peripherals.rs b/crates/fayalite/src/platform/peripherals.rs index 3ff4d6c..90c6640 100644 --- a/crates/fayalite/src/platform/peripherals.rs +++ b/crates/fayalite/src/platform/peripherals.rs @@ -50,3 +50,13 @@ pub struct RgbLed { pub g: Bool, pub b: Bool, } + +#[hdl] +/// UART, used as an output from the FPGA +pub struct Uart { + /// transmit from the FPGA's perspective + pub tx: Bool, + /// receive from the FPGA's perspective + #[hdl(flip)] + pub rx: Bool, +} diff --git a/crates/fayalite/src/vendor/xilinx/arty_a7.rs b/crates/fayalite/src/vendor/xilinx/arty_a7.rs index 549e631..552eb4a 100644 --- a/crates/fayalite/src/vendor/xilinx/arty_a7.rs +++ b/crates/fayalite/src/vendor/xilinx/arty_a7.rs @@ -7,7 +7,7 @@ use crate::{ platform::{ DynPlatform, Peripheral, PeripheralRef, Peripherals, PeripheralsBuilderFactory, PeripheralsBuilderFinished, Platform, PlatformAspectSet, - peripherals::{ClockInput, Led, RgbLed}, + peripherals::{ClockInput, Led, RgbLed, Uart}, }, prelude::*, vendor::xilinx::{ @@ -77,6 +77,7 @@ pub struct ArtyA7Peripherals { ld5: Peripheral, ld6: Peripheral, ld7: Peripheral, + uart: Peripheral, // TODO: add rest of peripherals when we need them } @@ -94,6 +95,7 @@ impl Peripherals for ArtyA7Peripherals { ld5, ld6, ld7, + uart, } = self; clk100_div_pow2.append_peripherals(peripherals); rst.append_peripherals(peripherals); @@ -106,6 +108,7 @@ impl Peripherals for ArtyA7Peripherals { ld5.append_peripherals(peripherals); ld6.append_peripherals(peripherals); ld7.append_peripherals(peripherals); + uart.append_peripherals(peripherals); } } @@ -192,6 +195,7 @@ impl Platform for ArtyA7Platform { 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(), ) @@ -214,6 +218,7 @@ impl Platform for ArtyA7Platform { 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); @@ -373,6 +378,14 @@ impl Platform for ArtyA7Platform { 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 {