added asynch SPI implementation #22

Merged
muellerr merged 1 commits from asynch-spi into main 2026-04-24 11:30:52 +02:00
5 changed files with 625 additions and 17 deletions
+2 -1
View File
@@ -39,5 +39,6 @@ pub fn disable_ram_scrubbing() {
}
pub use vorago_shared_hal::sysconfig::{
assert_peripheral_reset, disable_peripheral_clock, enable_peripheral_clock,
assert_peripheral_reset, deassert_peripheral_reset, disable_peripheral_clock,
enable_peripheral_clock, reset_peripheral_for_cycles,
};
+7
View File
@@ -225,4 +225,11 @@ pub(crate) mod shared {
#[bit(0, w)]
rx_fifo: bool,
}
impl FifoClear {
pub const ALL: Self = Self::builder()
.with_tx_fifo(true)
.with_rx_fifo(true)
.build();
}
}
+584
View File
@@ -0,0 +1,584 @@
use core::{cell::RefCell, convert::Infallible};
use arbitrary_int::u5;
use critical_section::Mutex;
use embassy_sync::waitqueue::AtomicWaker;
use portable_atomic::AtomicBool;
use raw_slice::{RawBufSlice, RawBufSliceMut};
use crate::{
shared::{FifoClear, TriggerLevel},
spi::regs::{Data, InterruptClear, InterruptControl, InterruptStatus},
};
#[cfg(feature = "vor1x")]
pub const NUM_SPIS: usize = 3;
#[cfg(feature = "vor4x")]
pub const NUM_SPIS: usize = 4;
static WAKERS: [AtomicWaker; NUM_SPIS] = [const { AtomicWaker::new() }; NUM_SPIS];
static TRANSFER_CONTEXTS: [Mutex<RefCell<TransferContext>>; NUM_SPIS] =
[const { Mutex::new(RefCell::new(TransferContext::new())) }; NUM_SPIS];
// Completion flag. Kept outside of the context structure as an atomic to avoid
// critical section.
static DONE: [AtomicBool; NUM_SPIS] = [const { AtomicBool::new(false) }; NUM_SPIS];
/// This is a generic interrupt handler to handle asynchronous SPI operations for a given
/// SPI peripheral.
///
/// The user has to call this once in the interrupt handler responsible for the SPI interrupts on
/// the given SPI bank.
pub fn on_interrupt(peripheral: super::Bank) {
let mut spi = unsafe { peripheral.steal_regs() };
let idx = peripheral as usize;
let interrupt_enabled = spi.read_interrupt_control();
let isr = spi.read_interrupt_status();
// IRQ is not related.
if interrupt_enabled.raw_value() == 0 {
return;
}
// Prevent spurious interrupts from messing with out logic here.
spi.write_interrupt_control(InterruptControl::DISABLE_ALL);
spi.write_interrupt_clear(InterruptClear::ALL);
let mut context = critical_section::with(|cs| {
let context_ref = TRANSFER_CONTEXTS[idx].borrow(cs);
*context_ref.borrow()
});
// No transfer active.
if context.transfer_type.is_none() {
return;
}
let transfer_type = context.transfer_type.unwrap();
match transfer_type {
TransferType::Read => on_interrupt_read(idx, &mut context, &mut spi, isr),
TransferType::Write => on_interrupt_write(idx, &mut context, &mut spi, isr),
TransferType::Transfer => on_interrupt_transfer(idx, &mut context, &mut spi, isr),
TransferType::TransferInPlace => {
on_interrupt_transfer_in_place(idx, &mut context, &mut spi, isr)
}
};
}
fn on_interrupt_read(
idx: usize,
context: &mut TransferContext,
spi: &mut super::regs::MmioSpi<'static>,
isr: InterruptStatus,
) {
let read_slice = unsafe { context.rx_slice.get_mut().unwrap() };
let transfer_len = read_slice.len();
// Read data from RX FIFO first.
let read_len = calculate_read_len(spi, isr, transfer_len, context.rx_progress);
(0..read_len).for_each(|_| {
read_slice[context.rx_progress] = (spi.read_data().data() & 0xFF) as u8;
context.rx_progress += 1;
});
// The FIFO still needs to be pumped.
while context.tx_progress < read_slice.len() && spi.read_status().tx_not_full() {
spi.write_data(Data::new_with_raw_value(0));
context.tx_progress += 1;
}
isr_finish_handler(idx, spi, context, transfer_len)
}
fn on_interrupt_write(
idx: usize,
context: &mut TransferContext,
spi: &mut super::regs::MmioSpi<'static>,
isr: InterruptStatus,
) {
let write_slice = unsafe { context.tx_slice.get().unwrap() };
let transfer_len = write_slice.len();
// Read data from RX FIFO first.
let read_len = calculate_read_len(spi, isr, transfer_len, context.rx_progress);
(0..read_len).for_each(|_| {
spi.read_data();
context.rx_progress += 1;
});
// Data still needs to be sent
while context.tx_progress < transfer_len && spi.read_status().tx_not_full() {
spi.write_data(Data::new_with_raw_value(
write_slice[context.tx_progress] as u32,
));
context.tx_progress += 1;
}
isr_finish_handler(idx, spi, context, transfer_len)
}
fn on_interrupt_transfer(
idx: usize,
context: &mut TransferContext,
spi: &mut super::regs::MmioSpi<'static>,
isr: InterruptStatus,
) {
let read_slice = unsafe { context.rx_slice.get_mut().unwrap() };
let read_len = read_slice.len();
let write_slice = unsafe { context.tx_slice.get().unwrap() };
let write_len = write_slice.len();
let transfer_len = core::cmp::max(read_len, write_len);
// Send data first to avoid overwriting data that still needs to be sent.
while context.tx_progress < transfer_len && spi.read_status().tx_not_full() {
if context.tx_progress < write_len {
spi.write_data(Data::new_with_raw_value(
write_slice[context.tx_progress] as u32,
));
} else {
// Dummy write.
spi.write_data(Data::new_with_raw_value(0));
}
context.tx_progress += 1;
}
// Read data from RX FIFO.
let read_len = calculate_read_len(spi, isr, transfer_len, context.rx_progress);
(0..read_len).for_each(|_| {
if context.rx_progress < read_len {
read_slice[context.rx_progress] = (spi.read_data().data() & 0xFF) as u8;
} else {
spi.read_data();
}
context.rx_progress += 1;
});
isr_finish_handler(idx, spi, context, transfer_len)
}
fn on_interrupt_transfer_in_place(
idx: usize,
context: &mut TransferContext,
spi: &mut super::regs::MmioSpi<'static>,
isr: InterruptStatus,
) {
let transfer_slice = unsafe { context.rx_slice.get_mut().unwrap() };
let transfer_len = transfer_slice.len();
// Send data first to avoid overwriting data that still needs to be sent.
while context.tx_progress < transfer_len && spi.read_status().tx_not_full() {
spi.write_data(Data::new_with_raw_value(
transfer_slice[context.tx_progress] as u32,
));
context.tx_progress += 1;
}
// Read data from RX FIFO.
let read_len = calculate_read_len(spi, isr, transfer_len, context.rx_progress);
(0..read_len).for_each(|_| {
transfer_slice[context.rx_progress] = (spi.read_data().data() & 0xFF) as u8;
context.rx_progress += 1;
});
isr_finish_handler(idx, spi, context, transfer_len)
}
fn calculate_read_len(
spi: &mut super::regs::MmioSpi<'static>,
isr: InterruptStatus,
total_read_len: usize,
rx_progress: usize,
) -> usize {
if isr.rx() {
core::cmp::min(super::FIFO_DEPTH, total_read_len - rx_progress)
} else if spi.read_status().rx_not_empty() {
let fifo_level = spi.read_state().rx_fifo();
core::cmp::min(total_read_len - rx_progress, fifo_level as usize)
} else {
0
}
}
/// Generic handler after RX FIFO and TX FIFO were handled. Checks and handles finished
/// and unfinished conditions.
fn isr_finish_handler(
idx: usize,
spi: &mut super::regs::MmioSpi<'static>,
context: &mut TransferContext,
transfer_len: usize,
) {
// Transfer finish condition.
if context.rx_progress == context.tx_progress && context.rx_progress == transfer_len {
finish_transfer(idx, context, spi);
return;
}
unfinished_transfer(spi, transfer_len, context.rx_progress);
// If the transfer is done, the context structure was already written back.
// Write back updated context structure.
critical_section::with(|cs| {
let context_ref = TRANSFER_CONTEXTS[idx].borrow(cs);
*context_ref.borrow_mut() = *context;
});
}
fn finish_transfer(
idx: usize,
context: &mut TransferContext,
spi: &mut super::regs::MmioSpi<'static>,
) {
// Write back updated context structure.
critical_section::with(|cs| {
let context_ref = TRANSFER_CONTEXTS[idx].borrow(cs);
*context_ref.borrow_mut() = *context;
});
spi.write_rx_fifo_trigger(TriggerLevel::new(u5::new(0x08)));
spi.write_tx_fifo_trigger(TriggerLevel::new(u5::new(0x00)));
// Interrupts were already disabled and cleared.
DONE[idx].store(true, core::sync::atomic::Ordering::Relaxed);
WAKERS[idx].wake();
}
fn unfinished_transfer(
spi: &mut super::regs::MmioSpi<'static>,
transfer_len: usize,
rx_progress: usize,
) {
let new_trig_level = core::cmp::min(super::FIFO_DEPTH, transfer_len - rx_progress);
spi.write_rx_fifo_trigger(TriggerLevel::new(u5::new(new_trig_level as u8)));
// Re-enable interrupts with the new RX FIFO trigger level.
spi.write_interrupt_control(InterruptControl::ENABLE_ALL);
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum TransferType {
Read,
Write,
Transfer,
TransferInPlace,
}
#[derive(Default, Debug, Copy, Clone)]
pub struct TransferContext {
transfer_type: Option<TransferType>,
tx_progress: usize,
rx_progress: usize,
tx_slice: RawBufSlice,
rx_slice: RawBufSliceMut,
}
#[allow(clippy::new_without_default)]
impl TransferContext {
pub const fn new() -> Self {
Self {
transfer_type: None,
tx_progress: 0,
rx_progress: 0,
tx_slice: RawBufSlice::new_nulled(),
rx_slice: RawBufSliceMut::new_nulled(),
}
}
}
pub struct SpiFuture<'spi> {
bank: super::Bank,
spi: &'spi mut super::Spi<u8>,
finished_regularly: core::cell::Cell<bool>,
}
impl<'spi> SpiFuture<'spi> {
fn new_for_read(spi: &'spi mut super::Spi<u8>, bank: super::Bank, words: &mut [u8]) -> Self {
if words.is_empty() {
panic!("words length unexpectedly 0");
}
let idx = bank as usize;
DONE[idx].store(false, core::sync::atomic::Ordering::Relaxed);
spi.regs
.write_interrupt_control(InterruptControl::DISABLE_ALL);
spi.regs.write_fifo_clear(FifoClear::ALL);
spi.regs.modify_ctrl1(|v| v.with_mtxpause(true));
let write_idx = core::cmp::min(super::FIFO_DEPTH, words.len());
// Send dummy bytes.
(0..write_idx).for_each(|_| {
spi.regs.write_data(Data::new_with_raw_value(0));
});
Self::set_triggers(spi, write_idx, words.len());
critical_section::with(|cs| {
let context_ref = TRANSFER_CONTEXTS[idx].borrow(cs);
let mut context = context_ref.borrow_mut();
context.transfer_type = Some(TransferType::Read);
unsafe {
context.rx_slice.set(words);
}
context.tx_slice.set_null();
context.tx_progress = write_idx;
context.rx_progress = 0;
spi.regs.write_interrupt_clear(InterruptClear::ALL);
spi.regs
.write_interrupt_control(InterruptControl::ENABLE_ALL);
spi.regs.modify_ctrl1(|v| v.with_mtxpause(false));
});
Self {
bank,
spi,
finished_regularly: core::cell::Cell::new(false),
}
}
fn new_for_write(spi: &'spi mut super::Spi<u8>, bank: super::Bank, words: &[u8]) -> Self {
if words.is_empty() {
panic!("words length unexpectedly 0");
}
let (idx, write_idx) = Self::generic_init_transfer(spi, bank, words);
critical_section::with(|cs| {
let context_ref = TRANSFER_CONTEXTS[idx].borrow(cs);
let mut context = context_ref.borrow_mut();
context.transfer_type = Some(TransferType::Write);
unsafe {
context.tx_slice.set(words);
}
context.rx_slice.set_null();
context.tx_progress = write_idx;
context.rx_progress = 0;
spi.regs.write_interrupt_clear(InterruptClear::ALL);
spi.regs
.write_interrupt_control(InterruptControl::ENABLE_ALL);
spi.regs.modify_ctrl1(|v| v.with_mtxpause(false));
});
Self {
bank,
spi,
finished_regularly: core::cell::Cell::new(false),
}
}
fn new_for_transfer(
spi: &'spi mut super::Spi<u8>,
spi_id: super::Bank,
read: &mut [u8],
write: &[u8],
) -> Self {
if read.is_empty() || write.is_empty() {
panic!("read or write buffer unexpectedly empty");
}
let (idx, write_idx) = Self::generic_init_transfer(spi, spi_id, write);
critical_section::with(|cs| {
let context_ref = TRANSFER_CONTEXTS[idx].borrow(cs);
let mut context = context_ref.borrow_mut();
context.transfer_type = Some(TransferType::Transfer);
unsafe {
context.tx_slice.set(write);
context.rx_slice.set(read);
}
context.tx_progress = write_idx;
context.rx_progress = 0;
spi.regs.write_interrupt_clear(InterruptClear::ALL);
spi.regs
.write_interrupt_control(InterruptControl::ENABLE_ALL);
spi.regs.modify_ctrl1(|v| v.with_mtxpause(false));
});
Self {
bank: spi_id,
spi,
finished_regularly: core::cell::Cell::new(false),
}
}
fn new_for_transfer_in_place(
spi: &'spi mut super::Spi<u8>,
spi_id: super::Bank,
words: &mut [u8],
) -> Self {
if words.is_empty() {
panic!("read and write buffer unexpectedly empty");
}
let (idx, write_idx) = Self::generic_init_transfer(spi, spi_id, words);
critical_section::with(|cs| {
let context_ref = TRANSFER_CONTEXTS[idx].borrow(cs);
let mut context = context_ref.borrow_mut();
context.transfer_type = Some(TransferType::TransferInPlace);
unsafe {
context.rx_slice.set(words);
}
context.tx_slice.set_null();
context.tx_progress = write_idx;
context.rx_progress = 0;
spi.regs.write_interrupt_clear(InterruptClear::ALL);
spi.regs
.write_interrupt_control(InterruptControl::ENABLE_ALL);
spi.regs.modify_ctrl1(|v| v.with_mtxpause(false));
});
Self {
bank: spi_id,
spi,
finished_regularly: core::cell::Cell::new(false),
}
}
fn generic_init_transfer(
spi: &mut super::Spi<u8>,
bank: super::Bank,
write: &[u8],
) -> (usize, usize) {
let idx = bank as usize;
DONE[idx].store(false, core::sync::atomic::Ordering::Relaxed);
spi.regs
.write_interrupt_control(InterruptControl::DISABLE_ALL);
spi.regs.write_fifo_clear(FifoClear::ALL);
spi.regs.modify_ctrl1(|v| v.with_mtxpause(true));
let write_idx = core::cmp::min(super::FIFO_DEPTH, write.len());
(0..write_idx).for_each(|idx| {
spi.regs
.write_data(Data::new_with_raw_value(write[idx] as u32));
});
Self::set_triggers(spi, write_idx, write.len());
(idx, write_idx)
}
fn set_triggers(spi: &mut super::Spi<u8>, write_idx: usize, write_len: usize) {
// This should never fail because it is never larger than the FIFO depth.
spi.regs
.write_rx_fifo_trigger(TriggerLevel::new(u5::new(write_idx as u8)));
// We want to re-fill the TX FIFO before it is completely empty if the full transfer size
// is larger than the FIFO depth. I am not sure whether the default value of 1 ensures
// this because the PG says that this interrupt is triggered when the FIFO has less than
// threshold entries.
if write_len > super::FIFO_DEPTH {
spi.regs
.write_tx_fifo_trigger(TriggerLevel::new(u5::new(2)));
} else {
spi.regs
.write_tx_fifo_trigger(TriggerLevel::new(u5::new(0)));
}
}
}
impl<'spi> Future for SpiFuture<'spi> {
type Output = ();
fn poll(
self: core::pin::Pin<&mut Self>,
cx: &mut core::task::Context<'_>,
) -> core::task::Poll<Self::Output> {
WAKERS[self.bank as usize].register(cx.waker());
if DONE[self.bank as usize].swap(false, core::sync::atomic::Ordering::Relaxed) {
critical_section::with(|cs| {
let mut ctx = TRANSFER_CONTEXTS[self.bank as usize]
.borrow(cs)
.borrow_mut();
*ctx = TransferContext::default();
});
self.finished_regularly.set(true);
return core::task::Poll::Ready(());
}
core::task::Poll::Pending
}
}
impl<'spi> Drop for SpiFuture<'spi> {
fn drop(&mut self) {
if !self.finished_regularly.get() {
// It might be sufficient to disable and enable the SPI.. But this definitely
// ensures the SPI is fully reset.
self.spi.regs.write_interrupt_clear(InterruptClear::ALL);
self.spi
.regs
.write_interrupt_control(InterruptControl::DISABLE_ALL);
self.spi.regs.write_fifo_clear(FifoClear::ALL);
}
}
}
/// Asynchronous SPI driver.
///
/// This is the primary data structure used to perform non-blocking SPI operations.
/// It implements the [embedded_hal_async::spi::SpiBus] as well.
pub struct SpiAsync(pub super::Spi<u8>);
impl SpiAsync {
pub fn new(
mut spi: super::Spi<u8>,
#[cfg(feature = "vor1x")] opt_irq_cfg: Option<crate::InterruptConfig>,
) -> Self {
#[cfg(feature = "vor1x")]
if let Some(irq_cfg) = opt_irq_cfg {
spi.regs
.write_interrupt_control(InterruptControl::DISABLE_ALL);
spi.regs.write_interrupt_clear(InterruptClear::ALL);
if irq_cfg.route {
crate::enable_peripheral_clock(crate::PeripheralSelect::Irqsel);
unsafe { va108xx::Irqsel::steal() }
.spi(spi.id as usize)
.write(|w| unsafe { w.bits(irq_cfg.id as u32) });
}
if irq_cfg.enable_in_nvic {
// Safety: User has specifically configured this.
unsafe { crate::enable_nvic_interrupt(irq_cfg.id) };
}
}
// Disable blockmode for asynchronous mode.
spi.regs
.modify_ctrl1(|v| v.with_bm_stall(false).with_blockmode(false));
Self(spi)
}
fn read(&mut self, words: &mut [u8]) -> Option<SpiFuture<'_>> {
if words.is_empty() {
return None;
}
let id = self.0.id;
Some(SpiFuture::new_for_read(&mut self.0, id, words))
}
fn write(&mut self, words: &[u8]) -> Option<SpiFuture<'_>> {
if words.is_empty() {
return None;
}
let id = self.0.id;
Some(SpiFuture::new_for_write(&mut self.0, id, words))
}
fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Option<SpiFuture<'_>> {
if read.is_empty() || write.is_empty() {
return None;
}
let id = self.0.id;
Some(SpiFuture::new_for_transfer(&mut self.0, id, read, write))
}
fn transfer_in_place(&mut self, words: &mut [u8]) -> Option<SpiFuture<'_>> {
if words.is_empty() {
return None;
}
let id = self.0.id;
Some(SpiFuture::new_for_transfer_in_place(&mut self.0, id, words))
}
}
impl embedded_hal_async::spi::ErrorType for SpiAsync {
type Error = Infallible;
}
impl embedded_hal_async::spi::SpiBus for SpiAsync {
async fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> {
self.read(words).unwrap().await;
Ok(())
}
async fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> {
self.write(words).unwrap().await;
Ok(())
}
async fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Self::Error> {
self.transfer(read, write).unwrap().await;
Ok(())
}
async fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Self::Error> {
self.transfer_in_place(words).unwrap().await;
Ok(())
}
async fn flush(&mut self) -> Result<(), Self::Error> {
Ok(())
}
}
+3 -4
View File
@@ -12,8 +12,11 @@ use va416xx as pac;
pub use regs::{Bank, HwChipSelectId};
pub mod asynch;
pub mod regs;
pub const FIFO_DEPTH: usize = 16;
pub fn configure_pin_as_hw_cs_pin<P: AnyPin + HwCsProvider>(_pin: P) -> HwChipSelectId {
IoPeriphPin::new(P::ID, P::FUN_SEL, None);
P::CS_ID
@@ -520,7 +523,6 @@ pub fn clk_div_for_target_clock(sys_clk: Hertz, spi_clk: Hertz) -> Option<u16> {
pub struct Spi<Word = u8> {
id: Bank,
regs: regs::MmioSpi<'static>,
cfg: SpiConfig,
/// Fill word for read-only SPI transactions.
fill_word: Word,
blockmode: bool,
@@ -653,7 +655,6 @@ where
Spi {
id: spi_sel,
regs: regs::Spi::new_mmio(spi_sel),
cfg: spi_cfg,
fill_word: Default::default(),
bmstall: spi_cfg.bmstall,
blockmode: spi_cfg.blockmode,
@@ -1031,7 +1032,6 @@ impl From<Spi<u8>> for Spi<u16> {
Spi {
id: old_spi.id,
regs: old_spi.regs,
cfg: old_spi.cfg,
blockmode: old_spi.blockmode,
fill_word: Default::default(),
bmstall: old_spi.bmstall,
@@ -1049,7 +1049,6 @@ impl From<Spi<u16>> for Spi<u8> {
Spi {
id: old_spi.id,
regs: old_spi.regs,
cfg: old_spi.cfg,
blockmode: old_spi.blockmode,
fill_word: Default::default(),
bmstall: old_spi.bmstall,
+29 -12
View File
@@ -25,11 +25,11 @@ cfg_if::cfg_if! {
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Bank {
Spi0,
Spi1,
Spi2,
Spi0 = 0,
Spi1 = 1,
Spi2 = 2,
#[cfg(feature = "vor4x")]
Spi3,
Spi3 = 3,
}
impl Bank {
@@ -157,7 +157,7 @@ impl ClockPrescaler {
}
}
#[bitbybit::bitfield(u32, debug, defmt_bitfields(feature = "defmt"))]
#[bitbybit::bitfield(u32, debug, default = 0x0, defmt_bitfields(feature = "defmt"))]
pub struct InterruptControl {
/// TX FIFO count <= TX FIFO trigger level.
#[bit(3, rw)]
@@ -174,9 +174,19 @@ pub struct InterruptControl {
rx_overrun: bool,
}
impl InterruptControl {
pub const DISABLE_ALL: Self = Self::ZERO;
pub const ENABLE_ALL: Self = Self::builder()
.with_tx(true)
.with_rx(true)
.with_rx_timeout(true)
.with_rx_overrun(true)
.build();
}
#[bitbybit::bitfield(u32, debug, defmt_bitfields(feature = "defmt"))]
pub struct InterruptStatus {
/// TX FIFO count <= TX FIFO trigger level.
/// TX FIFO count < TX FIFO trigger level.
#[bit(3, r)]
tx: bool,
/// RX FIFO count >= RX FIFO trigger level.
@@ -191,7 +201,7 @@ pub struct InterruptStatus {
rx_overrun: bool,
}
#[bitbybit::bitfield(u32)]
#[bitbybit::bitfield(u32, default = 0x0)]
#[derive(Debug)]
pub struct InterruptClear {
/// Clearing the RX interrupt or reading data from the FIFO resets the timeout counter.
@@ -201,6 +211,13 @@ pub struct InterruptClear {
rx_overrun: bool,
}
impl InterruptClear {
pub const ALL: Self = Self::builder()
.with_rx_timeout(true)
.with_rx_overrun(true)
.build();
}
#[bitbybit::bitfield(u32, debug, defmt_bitfields(feature = "defmt"))]
pub struct State {
#[bits(0..=7, r)]
@@ -221,21 +238,21 @@ pub struct Spi {
#[mmio(PureRead)]
status: Status,
clkprescale: ClockPrescaler,
irq_enb: InterruptControl,
interrupt_control: InterruptControl,
/// Raw interrupt status.
#[mmio(PureRead)]
irq_raw: InterruptStatus,
interrupt_status_raw: InterruptStatus,
/// Enabled interrupt status.
#[mmio(PureRead)]
irq_status: InterruptStatus,
interrupt_status: InterruptStatus,
#[mmio(Write)]
irq_clear: InterruptClear,
interrupt_clear: InterruptClear,
rx_fifo_trigger: TriggerLevel,
tx_fifo_trigger: TriggerLevel,
#[mmio(Write)]
fifo_clear: FifoClear,
#[mmio(PureRead)]
state: u32,
state: State,
#[cfg(feature = "vor1x")]
_reserved: [u32; 0x3F2],
#[cfg(feature = "vor4x")]