Files
zynq7000-rs/examples/zedboard/src/bin/uart-non-blocking.rs
Robin Mueller f61e5e282f
Some checks failed
ci / Check build (push) Has been cancelled
ci / Check formatting (push) Has been cancelled
ci / Check Documentation Build (push) Has been cancelled
ci / Clippy (push) Has been cancelled
ci / Check build (pull_request) Has been cancelled
ci / Check formatting (pull_request) Has been cancelled
ci / Check Documentation Build (pull_request) Has been cancelled
ci / Clippy (pull_request) Has been cancelled
Introduce Rust FSBL
2025-08-29 12:55:34 +02:00

575 lines
20 KiB
Rust

//! This example application shows usage of the non-blocking APIs offered for
//! various UART peripherals which are part of the PS or the sample bitstream.
//!
//! This example ONLY works with the provided `zedboard-fpga-design`.
//! The example FPGA design contains the following components relevant for this design
//!
//! - An AXI UARTLite peripheral, with the interrupt signal connected to the PS
//! - An AXI UART16550 peripheral, with the interrupt signal connected to the PS
//! - A custom UART multiplexer component. This multiplexer has various configuration modes
//! configurable via 3 EMIO pins:
//! 1. Route UART0 RX to pin JA1 and TX to JA2.
//! 2. Route AXI UARTLite RX to pin JA1 and TX to JA2.
//! 3. Route AXI UART16550 RX to pin JA1 and TX to JA2.
//! 4. Route UART0 to AXI UARTLite.
//! 5. Route UART0 to AXI 16550.
//! 6. Route AXI UARTLite to AXI 16550.
//! - Options 4, 5 and 6 allow to test and show the non-blocking API without the need for external
//! hardware.
//!
//! This example runs one embassy task for each UART, which periodically sends variable sized
//! string content out via its TX port. Each UART peripheral also permanently listens to all
//! incoming RX data via interrupts. Received data will be sent from the interrupt handler
//! to the main thread and will be printed to the main console on UART 1.
#![no_std]
#![no_main]
extern crate alloc;
use alloc::format;
use axi_uart16550::AxiUart16550;
use axi_uartlite::AxiUartlite;
use core::{cell::RefCell, panic::PanicInfo};
use cortex_ar::asm::nop;
use critical_section::Mutex;
use embassy_executor::Spawner;
use embassy_time::{Duration, Ticker};
use embedded_alloc::LlffHeap as Heap;
use embedded_hal::digital::StatefulOutputPin;
use embedded_io::Write as _;
use heapless::spsc::Queue;
use log::{error, info, warn};
use zynq7000_hal::{
BootMode,
clocks::Clocks,
configure_level_shifter,
gic::{GicConfigurator, GicInterruptHelper, Interrupt},
gpio::{GpioPins, Output, PinState},
gtc::GlobalTimerCounter,
l2_cache,
time::Hertz,
uart::{ClockConfigRaw, Uart, UartConfig},
};
pub enum UartMode {
Uart0ToUartlite,
Uart0ToUart16550,
UartliteToUart16550,
}
const UART_MODE: UartMode = UartMode::Uart0ToUart16550;
const INIT_STRING: &str = "-- Zynq 7000 Zedboard non-blocking UART example --\n\r";
#[global_allocator]
static HEAP: Heap = Heap::empty();
use zynq7000::{PsPeripherals, slcr::LevelShifterConfig};
use zynq7000_rt as _;
// Define the clock frequency as a constant
const PS_CLOCK_FREQUENCY: Hertz = Hertz::from_raw(33_333_300);
const AXI_UARTLITE_BASE_ADDR: u32 = 0x42C0_0000;
const AXI_UAR16550_BASE_ADDR: u32 = 0x43C0_0000;
pub const UARTLITE_PL_INT_ID: usize = 0;
pub const UART16550_PL_INT_ID: usize = 1;
const RB_SIZE: usize = 512;
// These queues are used to send all data received in the UART interrupt handlers to the main
// processing thread.
static QUEUE_UART_0: static_cell::ConstStaticCell<heapless::spsc::Queue<u8, RB_SIZE>> =
static_cell::ConstStaticCell::new(Queue::new());
static QUEUE_UARTLITE: static_cell::ConstStaticCell<heapless::spsc::Queue<u8, RB_SIZE>> =
static_cell::ConstStaticCell::new(Queue::new());
static QUEUE_UART16550: static_cell::ConstStaticCell<heapless::spsc::Queue<u8, RB_SIZE>> =
static_cell::ConstStaticCell::new(Queue::new());
// Those are all used by the interrupt handler, so we have to do the Mutex dance.
static RX_UART_0: Mutex<RefCell<Option<zynq7000_hal::uart::Rx>>> = Mutex::new(RefCell::new(None));
static UART_0_PROD: Mutex<RefCell<Option<heapless::spsc::Producer<'static, u8, RB_SIZE>>>> =
Mutex::new(RefCell::new(None));
static UARTLITE_PROD: Mutex<RefCell<Option<heapless::spsc::Producer<'static, u8, RB_SIZE>>>> =
Mutex::new(RefCell::new(None));
static UART16550_PROD: Mutex<RefCell<Option<heapless::spsc::Producer<'static, u8, RB_SIZE>>>> =
Mutex::new(RefCell::new(None));
/// Entry point (not called like a normal main function)
#[unsafe(no_mangle)]
pub extern "C" fn boot_core(cpu_id: u32) -> ! {
if cpu_id != 0 {
panic!("unexpected CPU ID {}", cpu_id);
}
main();
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum UartSel {
Uart0 = 0b000,
Uartlite = 0b001,
Uart16550 = 0b010,
Uart0ToUartlite = 0b011,
Uart0ToUart16550 = 0b100,
UartliteToUart16550 = 0b101,
}
pub struct UartMultiplexer {
sel_pins: [Output; 3],
}
impl UartMultiplexer {
pub fn new(mut sel_pins: [Output; 3]) -> Self {
for pin in sel_pins.iter_mut() {
pin.set_low();
}
Self { sel_pins }
}
pub fn select(&mut self, sel: UartSel) {
// TODO: A pin group switcher would be nice to do this in one go.
match sel {
UartSel::Uart0 => {
self.sel_pins[2].set_low();
self.sel_pins[1].set_low();
self.sel_pins[0].set_low();
}
UartSel::Uartlite => {
self.sel_pins[2].set_low();
self.sel_pins[1].set_low();
self.sel_pins[0].set_high();
}
UartSel::Uart16550 => {
self.sel_pins[2].set_low();
self.sel_pins[1].set_high();
self.sel_pins[0].set_low();
}
UartSel::Uart0ToUartlite => {
self.sel_pins[2].set_low();
self.sel_pins[1].set_high();
self.sel_pins[0].set_high();
}
UartSel::Uart0ToUart16550 => {
self.sel_pins[2].set_high();
self.sel_pins[1].set_low();
self.sel_pins[0].set_low();
}
UartSel::UartliteToUart16550 => {
self.sel_pins[2].set_high();
self.sel_pins[1].set_low();
self.sel_pins[0].set_high();
}
}
}
}
#[embassy_executor::main]
#[unsafe(export_name = "main")]
async fn main(spawner: Spawner) -> ! {
let mut dp = PsPeripherals::take().unwrap();
l2_cache::init_with_defaults(&mut dp.l2c);
// Enable PS-PL level shifters.
configure_level_shifter(LevelShifterConfig::EnableAll);
// Clock was already initialized by PS7 Init TCL script or FSBL, we just read it.
let clocks = Clocks::new_from_regs(PS_CLOCK_FREQUENCY).unwrap();
// Set up the global interrupt controller.
let mut gic = GicConfigurator::new_with_init(dp.gicc, dp.gicd);
gic.enable_all_interrupts();
gic.set_all_spi_interrupt_targets_cpu0();
// AXI UARTLite documentation mentions that a rising-edge sensitive interrupt is generated,
// but the GIC configures high-level sensitivity for the PL interrupts by default. We
// need to change it to edge sensitivity.
gic.set_pl_interrupt_sensitivity(UARTLITE_PL_INT_ID, zynq7000_hal::gic::SpiSensitivity::Edge)
.unwrap();
gic.enable();
unsafe {
gic.enable_interrupts();
}
let mut gpio_pins = GpioPins::new(dp.gpio);
// Set up global timer counter and embassy time driver.
let gtc = GlobalTimerCounter::new(dp.gtc, clocks.arm_clocks());
zynq7000_embassy::init(clocks.arm_clocks(), gtc);
// Set up the UART, we are logging with it.
let uart_clk_config = ClockConfigRaw::new_autocalc_with_error(clocks.io_clocks(), 115200)
.unwrap()
.0;
let mut log_uart = Uart::new_with_mio(
dp.uart_1,
UartConfig::new_with_clk_config(uart_clk_config),
(gpio_pins.mio.mio48, gpio_pins.mio.mio49),
)
.unwrap();
log_uart.write_all(INIT_STRING.as_bytes()).unwrap();
// Initialize the allocator BEFORE you use it
{
use core::mem::MaybeUninit;
// 10 kB heap.
const HEAP_SIZE: usize = 1024 * 10;
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
}
// Safety: We are not multi-threaded yet.
unsafe {
zynq7000_hal::log::uart_blocking::init_unsafe_single_core(
log_uart,
log::LevelFilter::Trace,
false,
)
};
// Set up UART multiplexing before creating and configuring the UARTs.
let mut uart_mux = UartMultiplexer::new([
Output::new_for_emio(gpio_pins.emio.take(8).unwrap(), PinState::Low),
Output::new_for_emio(gpio_pins.emio.take(9).unwrap(), PinState::Low),
Output::new_for_emio(gpio_pins.emio.take(10).unwrap(), PinState::Low),
]);
match UART_MODE {
UartMode::Uart0ToUartlite => uart_mux.select(UartSel::Uart0ToUartlite),
UartMode::Uart0ToUart16550 => uart_mux.select(UartSel::Uart0ToUart16550),
UartMode::UartliteToUart16550 => uart_mux.select(UartSel::UartliteToUart16550),
}
// UART0 routed through EMIO to PL pins.
let uart_0 =
Uart::new_with_emio(dp.uart_0, UartConfig::new_with_clk_config(uart_clk_config)).unwrap();
// Safety: Valid address of AXI UARTLITE.
let mut uartlite = unsafe { AxiUartlite::new(AXI_UARTLITE_BASE_ADDR) };
// We need to call this before splitting the structure, because the interrupt signal is
// used for both TX and RX, so the API is only exposed for this structure.
uartlite.enable_interrupt();
let (clk_config, error) =
axi_uart16550::ClkConfig::new_autocalc_with_error(clocks.pl_clocks()[0], 115200).unwrap();
assert!(error < 0.02);
let _uart_16550 = unsafe {
AxiUart16550::new(
AXI_UAR16550_BASE_ADDR,
axi_uart16550::UartConfig::new_with_clk_config(clk_config),
)
};
let boot_mode = BootMode::new_from_regs();
info!("Boot mode: {:?}", boot_mode);
let mio_led = Output::new_for_mio(gpio_pins.mio.mio7, PinState::Low);
let emio_leds: [Output; 8] = [
Output::new_for_emio(gpio_pins.emio.take(0).unwrap(), PinState::Low),
Output::new_for_emio(gpio_pins.emio.take(1).unwrap(), PinState::Low),
Output::new_for_emio(gpio_pins.emio.take(2).unwrap(), PinState::Low),
Output::new_for_emio(gpio_pins.emio.take(3).unwrap(), PinState::Low),
Output::new_for_emio(gpio_pins.emio.take(4).unwrap(), PinState::Low),
Output::new_for_emio(gpio_pins.emio.take(5).unwrap(), PinState::Low),
Output::new_for_emio(gpio_pins.emio.take(6).unwrap(), PinState::Low),
Output::new_for_emio(gpio_pins.emio.take(7).unwrap(), PinState::Low),
];
let (uart_0_tx, mut uart_0_rx) = uart_0.split();
let (uartlite_tx, _uartlite_rx) = uartlite.split();
let (uart_16550_tx, mut uart_16550_rx) = _uart_16550.split();
let (uart_0_prod, mut uart_0_cons) = QUEUE_UART_0.take().split();
let (uartlite_prod, mut uartlite_cons) = QUEUE_UARTLITE.take().split();
let (uart16550_prod, mut uart16550_cons) = QUEUE_UART16550.take().split();
// Use our helper function to start RX handling.
uart_0_rx.start_interrupt_driven_reception();
// Use our helper function to start RX handling.
uart_16550_rx.start_interrupt_driven_reception();
critical_section::with(|cs| {
UART_0_PROD.borrow(cs).borrow_mut().replace(uart_0_prod);
UARTLITE_PROD.borrow(cs).borrow_mut().replace(uartlite_prod);
UART16550_PROD
.borrow(cs)
.borrow_mut()
.replace(uart16550_prod);
RX_UART_0.borrow(cs).borrow_mut().replace(uart_0_rx);
});
spawner.spawn(led_task(mio_led, emio_leds)).unwrap();
match UART_MODE {
UartMode::Uart0ToUartlite => {
spawner.spawn(uartlite_task(uartlite_tx)).unwrap();
spawner.spawn(uart_0_task(uart_0_tx)).unwrap();
}
UartMode::Uart0ToUart16550 => {
spawner.spawn(uart_0_task(uart_0_tx)).unwrap();
spawner.spawn(uart_16550_task(uart_16550_tx)).unwrap();
}
UartMode::UartliteToUart16550 => {
spawner.spawn(uartlite_task(uartlite_tx)).unwrap();
spawner.spawn(uart_16550_task(uart_16550_tx)).unwrap();
}
}
let mut read_buf: [u8; RB_SIZE] = [0; RB_SIZE];
let mut ticker = Ticker::every(Duration::from_millis(200));
loop {
let read_bytes = uart_0_cons.len();
(0..read_bytes).for_each(|i| {
read_buf[i] = unsafe { uart_0_cons.dequeue_unchecked() };
});
if read_bytes > 0 {
info!("Received {read_bytes} bytes on UART0");
info!(
"Data: {:?}",
core::str::from_utf8(&read_buf[0..read_bytes]).unwrap()
);
}
let read_bytes = uartlite_cons.len();
(0..read_bytes).for_each(|i| {
read_buf[i] = unsafe { uartlite_cons.dequeue_unchecked() };
});
if read_bytes > 0 {
info!("Received {read_bytes} bytes on UARTLITE");
info!(
"Data: {:?}",
core::str::from_utf8(&read_buf[0..read_bytes]).unwrap()
);
}
let read_bytes = uart16550_cons.len();
(0..read_bytes).for_each(|i| {
read_buf[i] = unsafe { uart16550_cons.dequeue_unchecked() };
});
if read_bytes > 0 {
info!("Received {read_bytes} bytes on UART16550");
info!(
"Data: {:?}",
core::str::from_utf8(&read_buf[0..read_bytes]).unwrap()
);
}
ticker.next().await; // Wait for the next cycle of the ticker
}
}
fn build_print_string(prefix: &str, base_str: &str) -> alloc::string::String {
format!("{prefix} {base_str}\n\r")
}
#[embassy_executor::task]
async fn led_task(mut mio_led: Output, mut emio_leds: [Output; 8]) {
let mut ticker = Ticker::every(Duration::from_millis(1000));
let mut led_idx = 0;
loop {
mio_led.toggle().unwrap();
emio_leds[led_idx].toggle().unwrap();
led_idx += 1;
if led_idx >= emio_leds.len() {
led_idx = 0;
}
ticker.next().await;
}
}
#[embassy_executor::task]
async fn uartlite_task(uartlite: axi_uartlite::Tx) {
let mut ticker = Ticker::every(Duration::from_millis(1000));
let str0 = build_print_string("UARTLITE:", "Hello World");
let str1 = build_print_string(
"UARTLITE:",
"Long Hello which should completely fill the FIFO",
);
let mut idx = 0;
let print_strs = [str0.as_bytes(), str1.as_bytes()];
let mut tx_async = axi_uartlite::tx_async::TxAsync::new(uartlite, 0).unwrap();
loop {
tx_async.write(print_strs[idx]).await;
idx += 1;
if idx == 2 {
idx = 0;
}
ticker.next().await;
}
}
#[embassy_executor::task]
async fn uart_0_task(uart_tx: zynq7000_hal::uart::Tx) {
let mut ticker = Ticker::every(Duration::from_millis(1000));
let mut tx_async = zynq7000_hal::uart::TxAsync::new(uart_tx);
let str0 = build_print_string("UART0:", "Hello World");
let str1 = build_print_string(
"UART0:",
"Long Hello which should completely fill the 64 byte FIFO of the UART0 peripheral",
);
let mut idx = 0;
let print_strs = [str0.as_bytes(), str1.as_bytes()];
loop {
tx_async.write(print_strs[idx]).await;
idx += 1;
if idx == 2 {
idx = 0;
}
ticker.next().await;
}
}
#[embassy_executor::task]
async fn uart_16550_task(uart_tx: axi_uart16550::Tx) {
let mut ticker = Ticker::every(Duration::from_millis(1000));
let mut tx_async = axi_uart16550::TxAsync::new(uart_tx, 0, embassy_time::Delay).unwrap();
let str0 = build_print_string("UART16550:", "Hello World");
let str1 = build_print_string(
"UART16550:",
"Long Hello which should completely fill the FIFO",
);
let mut idx = 0;
let print_strs = [str0.as_bytes(), str1.as_bytes()];
loop {
tx_async.write(print_strs[idx]).await;
idx += 1;
if idx == 2 {
idx = 0;
}
ticker.next().await;
}
}
#[unsafe(no_mangle)]
pub extern "C" fn _irq_handler() {
let mut gic_helper = GicInterruptHelper::new();
let irq_info = gic_helper.acknowledge_interrupt();
match irq_info.interrupt() {
Interrupt::Sgi(_) => (),
Interrupt::Ppi(ppi_interrupt) => {
if ppi_interrupt == zynq7000_hal::gic::PpiInterrupt::GlobalTimer {
unsafe {
zynq7000_embassy::on_interrupt();
}
}
}
Interrupt::Spi(spi_interrupt) => match spi_interrupt {
zynq7000_hal::gic::SpiInterrupt::Pl0 => {
on_interrupt_axi_uartlite();
}
zynq7000_hal::gic::SpiInterrupt::Pl1 => {
on_interrupt_axi_16550();
}
zynq7000_hal::gic::SpiInterrupt::Uart0 => {
on_interrupt_uart_0();
}
_ => (),
},
Interrupt::Invalid(_) => (),
Interrupt::Spurious => (),
}
gic_helper.end_of_interrupt(irq_info);
}
pub fn on_interrupt_axi_uartlite() {
let mut buf = [0; axi_uartlite::FIFO_DEPTH];
let mut rx = unsafe { axi_uartlite::Rx::steal(AXI_UARTLITE_BASE_ADDR as usize) };
// Handle RX first: Empty the FIFO content into local buffer.
let read_bytes = rx.on_interrupt_rx(&mut buf);
// Handle TX next: Handle pending asynchronous TX operations.
let mut tx = unsafe { axi_uartlite::Tx::steal(AXI_UARTLITE_BASE_ADDR as usize) };
axi_uartlite::on_interrupt_tx(&mut tx, 0);
// Send received RX data to main task.
if read_bytes > 0 {
critical_section::with(|cs| {
let mut prod_ref_mut = UARTLITE_PROD.borrow(cs).borrow_mut();
let prod = prod_ref_mut.as_mut().unwrap();
(0..read_bytes).for_each(|i| {
prod.enqueue(buf[i]).expect("failed to enqueue byte");
});
});
}
}
pub fn on_interrupt_axi_16550() {
let mut buf = [0; axi_uart16550::FIFO_DEPTH];
let mut read_bytes = 0;
let mut rx = unsafe { axi_uart16550::Rx::steal(AXI_UAR16550_BASE_ADDR as usize) };
let iir = rx.read_iir();
if let Ok(int_id) = iir.int_id() {
match int_id {
axi_uart16550::registers::IntId2::ReceiverLineStatus => {
let errors = rx.on_interrupt_receiver_line_status(iir);
warn!("Receiver line status error: {errors:?}");
}
axi_uart16550::registers::IntId2::RxDataAvailable
| axi_uart16550::registers::IntId2::CharTimeout => {
read_bytes = rx.on_interrupt_data_available_or_char_timeout(int_id, &mut buf);
}
axi_uart16550::registers::IntId2::ThrEmpty => {
let mut tx = unsafe { axi_uart16550::Tx::steal(AXI_UAR16550_BASE_ADDR as usize) };
axi_uart16550::tx_async::on_interrupt_tx(&mut tx, 0);
}
axi_uart16550::registers::IntId2::ModemStatus => (),
}
}
// Send received RX data to main task.
if read_bytes > 0 {
critical_section::with(|cs| {
let mut prod_ref_mut = UART16550_PROD.borrow(cs).borrow_mut();
let prod = prod_ref_mut.as_mut().unwrap();
(0..read_bytes).for_each(|i| {
prod.enqueue(buf[i]).expect("failed to enqueue byte");
});
});
}
}
fn on_interrupt_uart_0() {
let mut buf = [0; zynq7000_hal::uart::FIFO_DEPTH];
let mut read_bytes = 0;
// Handle RX first: Empty the FIFO content into local buffer.
critical_section::with(|cs| {
let mut uart0 = RX_UART_0.borrow(cs).borrow_mut();
read_bytes = uart0
.as_mut()
.unwrap()
.on_interrupt(&mut buf, true)
.read_bytes();
});
// Handle TX next: Handle pending asynchronous TX operations.
zynq7000_hal::uart::on_interrupt_tx(zynq7000_hal::uart::UartId::Uart0);
// Send received RX data to main task.
if read_bytes > 0 {
critical_section::with(|cs| {
let mut prod_ref_mut = UART_0_PROD.borrow(cs).borrow_mut();
let prod = prod_ref_mut.as_mut().unwrap();
(0..read_bytes).for_each(|i| {
prod.enqueue(buf[i]).expect("failed to enqueue byte");
});
});
}
}
#[unsafe(no_mangle)]
pub extern "C" fn _abort_handler() {
loop {
nop();
}
}
#[unsafe(no_mangle)]
pub extern "C" fn _undefined_handler() {
loop {
nop();
}
}
#[unsafe(no_mangle)]
pub extern "C" fn _prefetch_handler() {
loop {
nop();
}
}
/// Panic handler
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
error!("Panic: {info:?}");
loop {}
}