From 671d83b186723762bde9f6d8dbb453bf3b3b70b8 Mon Sep 17 00:00:00 2001 From: Jacob Lifshay Date: Wed, 22 Oct 2025 05:06:57 -0700 Subject: [PATCH] WIP add transmit-only UART example --- crates/fayalite/examples/tx_only_uart.rs | 169 +++++++++++++++++++ crates/fayalite/src/platform/peripherals.rs | 10 ++ crates/fayalite/src/vendor/xilinx/arty_a7.rs | 15 +- 3 files changed, 193 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..c50e987 --- /dev/null +++ b/crates/fayalite/examples/tx_only_uart.rs @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// See Notices.txt for copyright information +use clap::builder::TypedValueParser; +use fayalite::{ + build::{ToArgs, WriteArgs}, + prelude::*, +}; +use ordered_float::NotNan; + +#[hdl_module] +fn tx_only_uart( + platform_io_builder: PlatformIOBuilder<'_>, + divisor: f64, + message: impl AsRef<[u8]>, +) { + let message = message.as_ref(); + let clk_input = + platform_io_builder.peripherals_with_type::()[0].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 mut message_mem = memory_with_init(message); + message_mem.read_latency(4); + #[hdl] + let startup_reg = reg_builder().clock_domain(cd).reset(0u128); + connect_any(startup_reg, (startup_reg << 1) | 1u8); + let message_read = message_mem.new_read_port(); + connect(message_read.clk, cd.clk); + connect(message_read.en, true); + #[hdl] + let addr_reg: UInt<32> = reg_builder().clock_domain(cd).reset(0u32); + connect_any(message_read.addr, addr_reg); + #[hdl] + let next_addr: UInt<32> = wire(); + connect(next_addr, addr_reg); + + #[hdl] + let tx = reg_builder().clock_domain(cd).reset(true); + + #[hdl] + if !startup_reg[message_mem.get_read_latency()] { + connect(next_uart_state, 0_hdl_u4); + connect(tx, true); + } else if uart_state_reg.cmp_eq(0_hdl_u4) { + connect(tx, false); // start bit + } else if uart_state_reg.cmp_le(8_hdl_u4) { + connect( + tx, + (message_read.data >> (uart_state_reg - 1_hdl_u4))[0].cast_to_static(), + ); // data bit + } else { + connect(tx, true); // stop bit + 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 = io.peripherals_with_type::()[0].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 beeee0a..0ba214e 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.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); } } @@ -181,6 +184,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(), ) @@ -203,6 +207,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); @@ -310,6 +315,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 {