implement simple_uart::simple_uart

This commit is contained in:
Jacob Lifshay 2026-02-26 18:48:43 -08:00
parent 61e8ed96b1
commit 7f7e316b7b
Signed by: programmerjake
SSH key fingerprint: SHA256:HnFTLGpSm4Q4Fj502oCFisjZSoakwEuTsJJMSke63RQ
3 changed files with 185443 additions and 6 deletions

View file

@ -3,7 +3,14 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use crate::config::CpuConfig; use crate::{
config::{CpuConfig, PhantomConstCpuConfig},
main_memory_and_io::{
MemoryInterface, MemoryOperationErrorKind, MemoryOperationFinish,
MemoryOperationFinishKind, MemoryOperationKind, MemoryOperationStart,
},
util::array_vec::ArrayVec,
};
use fayalite::{ use fayalite::{
int::UIntInRange, int::UIntInRange,
prelude::*, prelude::*,
@ -258,6 +265,37 @@ pub enum ReceiverQueueStatus {
QueueOverflowed, QueueOverflowed,
} }
impl ReceiverQueueStatus {
pub fn sim_as_u8(this: impl ToSimValue<Type = Self>) -> u8 {
SimValue::bits(&this.into_sim_value())
.cast_to_static::<UInt<8>>()
.as_int()
}
#[hdl]
pub fn for_queue_len_sim(current_count: usize, queue_size: NonZeroUsize) -> SimValue<Self> {
if current_count == 0 {
#[hdl(sim)]
ReceiverQueueStatus.QueueEmpty()
} else if current_count == queue_size.get() {
#[hdl(sim)]
ReceiverQueueStatus.QueueFull()
} else if current_count == queue_size.get() - 1 {
#[hdl(sim)]
ReceiverQueueStatus.QueueAlmostFull()
} else if current_count > queue_size.get() {
#[hdl(sim)]
ReceiverQueueStatus.QueueOverflowed()
} else {
#[hdl(sim)]
ReceiverQueueStatus.QueueNotEmpty()
}
}
#[hdl]
pub fn for_queue_len_as_u8(current_count: usize, queue_size: NonZeroUsize) -> u8 {
Self::sim_as_u8(Self::for_queue_len_sim(current_count, queue_size))
}
}
/// `rx_synchronized` is [`peripherals::Uart::rx`], but after being synchronized to `cd.clk`. /// `rx_synchronized` is [`peripherals::Uart::rx`], but after being synchronized to `cd.clk`.
#[hdl_module] #[hdl_module]
pub fn receiver(queue_capacity: NonZeroUsize) { pub fn receiver(queue_capacity: NonZeroUsize) {
@ -317,16 +355,32 @@ pub fn receiver(queue_capacity: NonZeroUsize) {
connect(queue.inp.data, inner.output_byte); connect(queue.inp.data, inner.output_byte);
} }
pub const SIMPLE_UART_RECEIVE_OFFSET: u64 = 0;
pub const SIMPLE_UART_TRANSMIT_OFFSET: u64 = 0;
pub const SIMPLE_UART_STATUS_OFFSET: u64 = 1;
pub const SIMPLE_UART_SIZE: u64 = 2;
#[hdl(no_static)]
struct Operation<C: PhantomConstGet<CpuConfig> + PhantomConstCpuConfig> {
start: MemoryOperationStart<C>,
finish: HdlOption<MemoryOperationFinish<C>>,
}
#[hdl_module] #[hdl_module]
pub fn simple_uart( pub fn simple_uart(
config: PhantomConst<CpuConfig>, config: PhantomConst<CpuConfig>,
clock_input_properties: PhantomConst<peripherals::ClockInputProperties>, clock_input_properties: PhantomConst<peripherals::ClockInputProperties>,
baud_rate: f64, baud_rate: f64,
receiver_queue_size: NonZeroUsize,
) { ) {
#[hdl] #[hdl]
let cd: ClockDomain = m.input(); let cd: ClockDomain = m.input();
#[hdl] #[hdl]
let memory_interface: MemoryInterface<PhantomConst<CpuConfig>> =
m.input(MemoryInterface[config]);
#[hdl]
let uart: peripherals::Uart = m.output(); let uart: peripherals::Uart = m.output();
#[hdl] #[hdl]
let rx_sync_intermediate_reg: Bool = reg_builder().clock_domain(cd).reset(true); let rx_sync_intermediate_reg: Bool = reg_builder().clock_domain(cd).reset(true);
annotate(rx_sync_intermediate_reg, DontTouchAnnotation); annotate(rx_sync_intermediate_reg, DontTouchAnnotation);
@ -340,5 +394,194 @@ pub fn simple_uart(
annotate(rx_synchronized, DontTouchAnnotation); annotate(rx_synchronized, DontTouchAnnotation);
connect(rx_synchronized, rx_sync_final_reg); connect(rx_synchronized, rx_sync_final_reg);
todo!("add transmitter and receiver"); #[hdl]
let clk_gen = instance(uart_clock_gen(clock_input_properties, baud_rate));
connect(clk_gen.cd, cd);
#[hdl]
let transmitter = instance(transmitter());
connect(transmitter.cd, cd);
connect(transmitter.tick, clk_gen.tick);
connect(uart.tx, transmitter.tx);
#[hdl]
let receiver = instance(receiver(receiver_queue_size));
connect(receiver.cd, cd);
connect(receiver.tick, clk_gen.tick);
connect(receiver.rx_synchronized, rx_synchronized);
#[hdl]
let operation_reg = reg_builder()
.clock_domain(cd)
.reset(HdlOption[Operation[config]].HdlNone());
let next_fetch_block_ids_ty = memory_interface.ty().next_fetch_block_ids.HdlSome;
#[hdl]
if let HdlSome(operation) = operation_reg {
#[hdl]
let MemoryInterface::<_> {
start,
finish,
next_fetch_block_ids,
config: _,
} = memory_interface;
connect(start.ready, false);
connect(finish.data, operation.finish);
connect(
next_fetch_block_ids,
HdlSome(ArrayVec::from_parts_unchecked(
repeat(
operation.start.fetch_block_id,
next_fetch_block_ids_ty.capacity(),
),
1u8.cast_to(next_fetch_block_ids_ty.len_ty()),
)),
);
connect(transmitter.input_byte.data, HdlNone());
connect(receiver.output_byte.ready, false);
#[hdl]
if let HdlSome(_) = operation.finish {
#[hdl]
if finish.ready {
connect(operation_reg, operation_reg.ty().HdlNone());
}
} else {
#[hdl]
let MemoryOperationStart::<_> {
kind,
addr,
write_data,
rw_mask,
fetch_block_id: _,
config: _,
} = operation.start;
#[hdl]
let valid_addr = wire();
connect(valid_addr, true);
#[hdl]
let all_ready = wire();
connect(all_ready, true);
#[hdl]
let read_data = wire(write_data.ty());
for (byte_index, ((rw_mask, write_data), read_data)) in rw_mask
.into_iter()
.zip(write_data)
.zip(read_data)
.enumerate()
{
let byte_addr = (addr | byte_index).cast_to_static::<UInt<64>>();
connect(read_data, 0u8);
#[hdl]
if rw_mask {
#[hdl]
match kind {
MemoryOperationKind::Read => {
#[hdl]
if byte_addr.cmp_eq(SIMPLE_UART_RECEIVE_OFFSET) {
connect(receiver.output_byte.ready, valid_addr);
#[hdl]
if let HdlSome(byte) = receiver.output_byte.data {
connect(read_data, byte);
}
// if there is no byte ready yet, we read a zero to avoid blocking the CPU on external inputs.
} else if byte_addr.cmp_eq(SIMPLE_UART_STATUS_OFFSET) {
connect(
read_data,
receiver
.queue_status
.cast_to_bits()
.cast_to_static::<UInt<8>>(),
);
} else {
connect(valid_addr, false);
}
}
MemoryOperationKind::Write => {
#[hdl]
if byte_addr.cmp_eq(SIMPLE_UART_TRANSMIT_OFFSET) {
#[hdl]
if !transmitter.input_byte.ready {
connect(all_ready, false);
}
#[hdl]
if valid_addr {
connect(transmitter.input_byte.data, HdlSome(write_data));
}
} else {
connect(valid_addr, false);
}
}
}
}
}
#[hdl]
if !valid_addr {
connect(
operation_reg,
HdlSome(
#[hdl]
Operation::<_> {
start: operation.start,
finish: HdlSome(
#[hdl]
MemoryOperationFinish::<_> {
kind: MemoryOperationFinishKind
.Error(MemoryOperationErrorKind.Generic()),
read_data,
config,
},
),
},
),
);
} else if all_ready {
connect(
operation_reg,
HdlSome(
#[hdl]
Operation::<_> {
start: operation.start,
finish: HdlSome(
#[hdl]
MemoryOperationFinish::<_> {
kind: MemoryOperationFinishKind.Success(kind),
read_data,
config,
},
),
},
),
);
}
}
} else {
#[hdl]
let MemoryInterface::<_> {
start,
finish,
next_fetch_block_ids,
config: _,
} = memory_interface;
connect(start.ready, true);
connect(finish.data, finish.ty().data.HdlNone());
connect(
next_fetch_block_ids,
HdlSome(next_fetch_block_ids_ty.new_sim(next_fetch_block_ids_ty.element().zero())),
);
#[hdl]
if let HdlSome(start) = start.data {
connect(
operation_reg,
HdlSome(
#[hdl]
Operation::<_> {
start,
finish: operation_reg.ty().HdlSome.finish.HdlNone(),
},
),
);
}
connect(transmitter.input_byte.data, HdlNone());
connect(receiver.output_byte.ready, false);
}
} }

184803
crates/cpu/tests/expected/simple_uart.vcd generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,29 @@
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
// See Notices.txt for copyright information // See Notices.txt for copyright information
use cpu::main_memory_and_io::simple_uart::{ use cpu::{
ReceiverQueueStatus, receiver, receiver_no_queue, transmitter, uart_clock_gen, config::{CpuConfig, UnitConfig},
main_memory_and_io::{
MemoryInterface, MemoryOperationErrorKind, MemoryOperationFinish,
MemoryOperationFinishKind, MemoryOperationKind, MemoryOperationStart,
simple_uart::{
ReceiverQueueStatus, SIMPLE_UART_RECEIVE_OFFSET, SIMPLE_UART_SIZE,
SIMPLE_UART_STATUS_OFFSET, SIMPLE_UART_TRANSMIT_OFFSET, receiver, receiver_no_queue,
simple_uart, transmitter, uart_clock_gen,
},
},
next_pc::FETCH_BLOCK_ID_WIDTH,
unit::UnitKind,
util::array_vec::ArrayVec,
}; };
use fayalite::{ use fayalite::{
bundle::BundleType,
intern::{Intern, Interned}, intern::{Intern, Interned},
prelude::*, prelude::*,
sim::vcd::VcdWriterDecls, sim::vcd::VcdWriterDecls,
util::{RcWriter, ready_valid::ReadyValid}, util::{RcWriter, ready_valid::ReadyValid},
}; };
use std::num::NonZeroUsize; use std::{borrow::BorrowMut, marker::PhantomData, num::NonZeroUsize};
const fn half_period(frequency: f64) -> SimDuration { const fn half_period(frequency: f64) -> SimDuration {
SimDuration::from_attos((SimDuration::from_secs(1).as_attos() as f64 / frequency / 2.0) as u128) SimDuration::from_attos((SimDuration::from_secs(1).as_attos() as f64 / frequency / 2.0) as u128)
@ -564,4 +577,382 @@ fn test_receiver_and_uart_clock_gen() {
} }
} }
// TODO: test simple_uart() #[hdl_module]
fn simple_uart_loopback(
config: PhantomConst<CpuConfig>,
clock_input_properties: PhantomConst<peripherals::ClockInputProperties>,
baud_rate: f64,
receiver_queue_size: NonZeroUsize,
) {
#[hdl]
let cd: ClockDomain = m.input();
#[hdl]
let memory_interface: MemoryInterface<PhantomConst<CpuConfig>> =
m.input(MemoryInterface[config]);
#[hdl]
let tx: Bool = m.output();
#[hdl]
let inner = instance(simple_uart(
config,
clock_input_properties,
baud_rate,
receiver_queue_size,
));
connect(inner.cd, cd);
connect(inner.memory_interface, memory_interface);
connect(tx, inner.uart.tx);
connect(inner.uart.rx, inner.uart.tx);
}
struct MemoryOperationRunner<Sim: BorrowMut<Simulation<T>>, T: BundleType> {
sim: Sim,
clk: Expr<Clock>,
memory_interface: Expr<MemoryInterface<PhantomConst<CpuConfig>>>,
next_fetch_block_id: u64,
_phantom: PhantomData<T>,
}
const FETCH_WIDTH_IN_BYTES: usize = 1 << CpuConfig::DEFAULT_LOG2_FETCH_WIDTH_IN_BYTES;
impl<Sim: BorrowMut<Simulation<T>>, T: BundleType> MemoryOperationRunner<Sim, T> {
#[hdl]
fn run_memory_operation(
&mut self,
kind: impl ToSimValue<Type = MemoryOperationKind>,
addr: u64,
write_data: [u8; FETCH_WIDTH_IN_BYTES],
rw_mask: [bool; FETCH_WIDTH_IN_BYTES],
timeout_cycles: usize,
) -> Result<[u8; FETCH_WIDTH_IN_BYTES], SimValue<MemoryOperationErrorKind>> {
let kind = kind.into_sim_value();
assert_eq!(addr % FETCH_WIDTH_IN_BYTES as u64, 0);
let config = self.memory_interface.ty().config;
let fetch_block_id = self
.next_fetch_block_id
.cast_to_static::<UInt<{ FETCH_BLOCK_ID_WIDTH }>>();
self.next_fetch_block_id += 1;
println!(
"started: MemoryOperationStart {{\n \
kind: {kind:?},\n \
addr: {addr:#x},\n \
write_data: {write_data:?},\n \
rw_mask: {rw_mask:?},\n \
fetch_block_id: {fetch_block_id},\n\
}}"
);
let start_value = #[hdl(sim)]
MemoryOperationStart::<_> {
kind,
addr,
write_data: &write_data[..],
rw_mask: &rw_mask[..],
fetch_block_id,
config,
};
#[hdl]
let MemoryInterface::<_> {
start,
finish,
next_fetch_block_ids,
config: _,
} = self.memory_interface;
let sim = self.sim.borrow_mut();
let check_io = |sim: &mut Simulation<T>,
expected_start_ready: bool,
expected_finish_data_is_some: bool,
expected_next_fetch_block_ids: &[_]| {
assert_eq!(sim.read_bool(start.ready), expected_start_ready);
let finish_data = sim.read(finish.data);
let finish_data_is_some = #[hdl(sim)]
if let HdlSome(_) = &finish_data {
true
} else {
false
};
assert_eq!(
finish_data_is_some, expected_finish_data_is_some,
"finish.data: {finish_data:?}"
);
#[hdl(sim)]
if let HdlSome(next_fetch_block_ids) = sim.read(next_fetch_block_ids) {
let next_fetch_block_ids: Vec<_> =
ArrayVec::elements_sim_ref(&next_fetch_block_ids)
.iter()
.map(|v| v.as_int())
.collect();
assert_eq!(next_fetch_block_ids, expected_next_fetch_block_ids);
} else {
panic!("next_fetch_block_ids should be HdlSome");
}
};
check_io(sim, true, false, &[]);
sim.write(
start.data,
#[hdl(sim)]
(start.ty().data).HdlSome(&start_value),
);
sim.write(finish.ready, true);
sim.advance_time(CLOCK_HALF_PERIOD);
sim.write_clock(self.clk, true);
sim.advance_time(CLOCK_HALF_PERIOD);
sim.write_clock(self.clk, false);
sim.write(
start.data,
#[hdl(sim)]
(start.ty().data).HdlNone(),
);
check_io(sim, false, false, &[start_value.fetch_block_id.as_int()]);
for _ in 0..timeout_cycles {
sim.advance_time(CLOCK_HALF_PERIOD);
sim.write_clock(self.clk, true);
sim.advance_time(CLOCK_HALF_PERIOD);
sim.write_clock(self.clk, false);
#[hdl(sim)]
if let HdlSome(finish_data) = sim.read(finish.data) {
#[hdl(sim)]
let MemoryOperationFinish::<_> {
kind,
read_data,
config: _,
} = finish_data;
assert_eq!(read_data.len(), FETCH_WIDTH_IN_BYTES);
let read_data = std::array::from_fn(|i| read_data[i].as_int());
println!(
"finished: MemoryOperationFinish {{\n \
kind: {kind:?},\n \
read_data: {read_data:?},\n\
}}"
);
check_io(sim, false, true, &[start_value.fetch_block_id.as_int()]);
sim.advance_time(CLOCK_HALF_PERIOD);
sim.write_clock(self.clk, true);
sim.advance_time(CLOCK_HALF_PERIOD);
sim.write_clock(self.clk, false);
sim.write(finish.ready, false);
check_io(sim, true, false, &[]);
return #[hdl(sim)]
match kind {
MemoryOperationFinishKind::Success(kind) => {
assert_eq!(
SimValue::bits(&kind),
SimValue::bits(&start_value.kind),
"finish_data.kind={kind:?}",
);
Ok(read_data)
}
MemoryOperationFinishKind::Error(error) => Err(error),
};
}
check_io(sim, false, false, &[start_value.fetch_block_id.as_int()]);
}
panic!("memory operation took too long");
}
#[hdl]
fn rw_bytes(
&mut self,
start_addr: u64,
is_read: bool,
bytes: &mut [u8],
timeout_cycles: usize,
) -> Result<(), SimValue<MemoryOperationErrorKind>> {
let offset_in_chunk = start_addr as usize % FETCH_WIDTH_IN_BYTES;
let masked_addr = start_addr - offset_in_chunk as u64;
assert!(
offset_in_chunk
.checked_add(bytes.len())
.is_some_and(|v| v <= FETCH_WIDTH_IN_BYTES)
);
let bytes_range = offset_in_chunk..(offset_in_chunk + bytes.len());
let mut write_data = [0u8; FETCH_WIDTH_IN_BYTES];
let mut rw_mask = [false; FETCH_WIDTH_IN_BYTES];
rw_mask[bytes_range.clone()].fill(true);
if !is_read {
write_data[bytes_range.clone()].copy_from_slice(bytes);
}
let read_bytes = self.run_memory_operation(
if is_read {
#[hdl(sim)]
MemoryOperationKind.Read()
} else {
#[hdl(sim)]
MemoryOperationKind.Write()
},
masked_addr,
write_data,
rw_mask,
timeout_cycles,
)?;
if is_read {
bytes.copy_from_slice(&read_bytes[bytes_range]);
}
Ok(())
}
#[hdl]
fn read_bytes<const N: usize>(
&mut self,
start_addr: u64,
timeout_cycles: usize,
) -> Result<[u8; N], SimValue<MemoryOperationErrorKind>> {
let mut retval = [0; _];
self.rw_bytes(start_addr, true, &mut retval, timeout_cycles)?;
Ok(retval)
}
#[hdl]
fn write_bytes<const N: usize>(
&mut self,
start_addr: u64,
mut bytes: [u8; N],
timeout_cycles: usize,
) -> Result<(), SimValue<MemoryOperationErrorKind>> {
self.rw_bytes(start_addr, false, &mut bytes, timeout_cycles)
}
}
#[test]
#[hdl]
fn test_simple_uart() {
let _n = SourceLocation::normalize_files_for_tests();
let config = CpuConfig::new(
vec![
UnitConfig::new(UnitKind::AluBranch),
UnitConfig::new(UnitKind::AluBranch),
],
NonZeroUsize::new(20).unwrap(),
);
const RECEIVER_QUEUE_SIZE: NonZeroUsize = NonZeroUsize::new(16).expect("not zero");
let m = simple_uart_loopback(
PhantomConst::new_sized(config),
clock_input_properties(),
BAUD_RATE,
RECEIVER_QUEUE_SIZE,
);
let mut sim = Simulation::new(m);
let writer = RcWriter::default();
sim.add_trace_writer(VcdWriterDecls::new(writer.clone()));
struct DumpVcdOnDrop {
writer: Option<RcWriter>,
}
impl Drop for DumpVcdOnDrop {
fn drop(&mut self) {
if let Some(mut writer) = self.writer.take() {
let vcd = String::from_utf8(writer.take()).unwrap();
println!("####### VCD:\n{vcd}\n#######");
}
}
}
let mut writer = DumpVcdOnDrop {
writer: Some(writer),
};
sim.write(
sim.io().memory_interface.start.data,
sim.io().ty().memory_interface.start.data.HdlNone(),
);
sim.write(sim.io().memory_interface.finish.ready, false);
sim.write_clock(sim.io().cd.clk, false);
sim.write_reset(sim.io().cd.rst, true);
sim.advance_time(CLOCK_HALF_PERIOD);
sim.write_clock(sim.io().cd.clk, true);
sim.advance_time(CLOCK_HALF_PERIOD);
sim.write_clock(sim.io().cd.clk, false);
sim.write_reset(sim.io().cd.rst, false);
let mut mem_op_runner = MemoryOperationRunner {
clk: sim.io().cd.clk,
memory_interface: sim.io().memory_interface,
next_fetch_block_id: 0,
_phantom: PhantomData,
sim,
};
let empty_status = ReceiverQueueStatus::sim_as_u8(
#[hdl(sim)]
ReceiverQueueStatus.QueueEmpty(),
);
assert_eq!(
mem_op_runner
.read_bytes(SIMPLE_UART_STATUS_OFFSET, 1)
.unwrap(),
[empty_status],
);
mem_op_runner
.write_bytes(SIMPLE_UART_STATUS_OFFSET, [0], 1)
.unwrap_err();
assert_eq!(
mem_op_runner
.read_bytes(SIMPLE_UART_RECEIVE_OFFSET, 1)
.unwrap(),
[0],
);
const { assert!(SIMPLE_UART_RECEIVE_OFFSET + 1 == SIMPLE_UART_STATUS_OFFSET) };
assert_eq!(
mem_op_runner
.read_bytes(SIMPLE_UART_RECEIVE_OFFSET, 1)
.unwrap(),
[0, empty_status],
);
for i in 0..2 * FETCH_WIDTH_IN_BYTES as u64 {
mem_op_runner
.read_bytes::<1>(SIMPLE_UART_SIZE + i, 1)
.unwrap_err();
mem_op_runner
.write_bytes(SIMPLE_UART_SIZE + i, [0], 1)
.unwrap_err();
}
const TEST_TEXT: &str = "Test test test test\n";
const DELAY: usize = 2;
const {
assert!(
TEST_TEXT.len() > RECEIVER_QUEUE_SIZE.get() + DELAY,
"TEST_TEXT must be long enough to make the queue overflow when accounting for DELAY"
)
};
for (i, b) in TEST_TEXT.bytes().enumerate() {
let current_count = i + 1;
mem_op_runner
.write_bytes(SIMPLE_UART_TRANSMIT_OFFSET, [b], 300)
.unwrap();
let expected_status = ReceiverQueueStatus::for_queue_len_as_u8(
current_count.saturating_sub(DELAY),
RECEIVER_QUEUE_SIZE,
);
assert_eq!(
mem_op_runner
.read_bytes(SIMPLE_UART_STATUS_OFFSET, 1)
.unwrap(),
[expected_status],
);
}
for (i, mut expected_byte) in TEST_TEXT.bytes().enumerate() {
let read = mem_op_runner
.read_bytes(SIMPLE_UART_RECEIVE_OFFSET, 1)
.unwrap();
let current_count = if i == 0 {
RECEIVER_QUEUE_SIZE.get() + 1 // initial read starts with overflow
} else {
RECEIVER_QUEUE_SIZE.get().saturating_sub(i)
};
if current_count == 0 {
expected_byte = 0;
}
let expected_status =
ReceiverQueueStatus::for_queue_len_as_u8(current_count, RECEIVER_QUEUE_SIZE);
assert_eq!(read, [expected_byte, expected_status]);
}
let vcd = String::from_utf8(writer.writer.take().unwrap().take()).unwrap();
println!("####### VCD:\n{vcd}\n#######");
if vcd != include_str!("expected/simple_uart.vcd") {
panic!();
}
}