From 9ed6ac32ce59665b64cdd2fb2732912ebcfc46a5 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 8 May 2026 14:42:50 +0200 Subject: [PATCH] Async OLED example --- firmware/examples/zedboard/Cargo.toml | 4 +- .../examples/zedboard/src/bin/oled-async.rs | 305 ++++++++++++++++++ firmware/examples/zedboard/src/bin/oled.rs | 12 +- firmware/zynq7000-hal/src/spi/asynch.rs | 10 +- firmware/zynq7000-hal/src/spi/mod.rs | 8 +- firmware/zynq7000/src/spi.rs | 2 +- 6 files changed, 326 insertions(+), 15 deletions(-) create mode 100644 firmware/examples/zedboard/src/bin/oled-async.rs diff --git a/firmware/examples/zedboard/Cargo.toml b/firmware/examples/zedboard/Cargo.toml index 25ae486..8a8cd08 100644 --- a/firmware/examples/zedboard/Cargo.toml +++ b/firmware/examples/zedboard/Cargo.toml @@ -27,14 +27,14 @@ critical-section = "1" static_cell = "2" embedded-alloc = "0.7" embedded-hal = "1" -embedded-hal-bus = "0.3" +embedded-hal-bus = { version = "0.3", features = ["async"] } embedded-hal-async = "1" dummy-pin = "1" fugit = "0.4" fugit-03 = { version = "0.3", package = "fugit" } embedded-graphics = "0.8" log = "0.4" -ssd1306 = { version = "0.10" } +ssd1306 = { version = "0.10", features = ["async"] } tinybmp = "0.7" rand = { version = "0.10", default-features = false } diff --git a/firmware/examples/zedboard/src/bin/oled-async.rs b/firmware/examples/zedboard/src/bin/oled-async.rs new file mode 100644 index 0000000..7b5751e --- /dev/null +++ b/firmware/examples/zedboard/src/bin/oled-async.rs @@ -0,0 +1,305 @@ +#![no_std] +#![no_main] + +use aarch32_cpu::asm::nop; +use core::panic::PanicInfo; +use dummy_pin::DummyPin; +use embassy_executor::Spawner; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_time::{Delay, Duration, Ticker}; +use embedded_graphics::{Drawable as _, geometry::Point}; +use embedded_hal::digital::StatefulOutputPin; +use embedded_hal_async::delay::DelayNs as _; +use embedded_hal_bus::spi::{ExclusiveDevice, NoDelay}; +use embedded_io::Write; +use log::{error, info}; +use ssd1306::{Ssd1306Async, prelude::*}; +use zedboard::PS_CLOCK_FREQUENCY; +use zynq7000_hal::{ + BootMode, clocks, gic, gpio, gtc, + spi::{self, SpiAsync}, + time::Hertz, + uart::{self, TxAsync}, +}; + +use embedded_graphics::image::Image; +use tinybmp::Bmp; + +use zynq7000_rt as _; + +const INIT_STRING: &str = "-- Zynq 7000 Zedboard OLED example --\n\r"; + +/// Entry point which calls the embassy main method. +#[zynq7000_rt::entry] +fn entry_point() -> ! { + main(); +} + +#[embassy_executor::main] +async fn main(spawner: Spawner) -> ! { + let periphs = zynq7000_hal::init(zynq7000_hal::Config { + init_l2_cache: true, + level_shifter_config: Some(zynq7000_hal::LevelShifterConfig::EnableAll), + interrupt_config: Some(zynq7000_hal::InteruptConfig::AllInterruptsToCpu0), + }) + .unwrap(); + // Clock was already initialized by PS7 Init TCL script or FSBL, we just read it. + let mut clocks = clocks::Clocks::new_from_regs(PS_CLOCK_FREQUENCY).unwrap(); + + let target_spi_ref_clock = clocks.arm_clocks().cpu_1x_clk() * 2; + // SPI reference clock must be larger than the CPU 1x clock. Also, taking the largest value + // actually seems to be problematic. We take 200 MHz here, which is significantly larger than + // the CPU 1x clock which is around 110 MHz. + spi::configure_spi_ref_clock(&mut clocks, target_spi_ref_clock); + + assert!( + clocks.io_clocks().spi_clk().to_raw() + > (clocks.arm_clocks().cpu_1x_clk().to_raw() as f32 * 1.2) as u32, + "SPI reference clock must be larger than CPU 1x clock" + ); + + let mut gpio_pins = gpio::GpioPins::new(periphs.gpio); + + // Set up global timer counter and embassy time driver. + let gtc = gtc::GlobalTimerCounter::new(periphs.gtc, clocks.arm_clocks()); + zynq7000_embassy::init(clocks.arm_clocks(), gtc); + + // Set up the UART, we are logging with it. + let uart_clk_config = uart::ClockConfig::new_autocalc_with_error(clocks.io_clocks(), 115200) + .unwrap() + .0; + let mut uart = uart::Uart::new_with_mio_for_uart_1( + periphs.uart_1, + uart::Config::new_with_clk_config(uart_clk_config), + (gpio_pins.mio.mio48, gpio_pins.mio.mio49), + ) + .unwrap(); + uart.write_all(INIT_STRING.as_bytes()).unwrap(); + uart.flush().unwrap(); + + // Safety: We are not multi-threaded yet. + let log_reader = zynq7000_hal::log::asynch::init(log::LevelFilter::Trace) + .expect("Failed to initialize async logger"); + + let boot_mode = BootMode::new_from_regs(); + info!("Boot mode: {:?}", boot_mode); + info!( + "SPI reference clock speed: {:?}", + clocks.io_clocks().spi_clk() + ); + + spawner.spawn(logger_task(uart, log_reader).unwrap()); + + let mio_led = gpio::Output::new_for_mio(gpio_pins.mio.mio7, gpio::PinState::Low); + let emio_leds: [gpio::Output; 8] = [ + gpio::Output::new_for_emio(gpio_pins.emio.take(0).unwrap(), gpio::PinState::Low), + gpio::Output::new_for_emio(gpio_pins.emio.take(1).unwrap(), gpio::PinState::Low), + gpio::Output::new_for_emio(gpio_pins.emio.take(2).unwrap(), gpio::PinState::Low), + gpio::Output::new_for_emio(gpio_pins.emio.take(3).unwrap(), gpio::PinState::Low), + gpio::Output::new_for_emio(gpio_pins.emio.take(4).unwrap(), gpio::PinState::Low), + gpio::Output::new_for_emio(gpio_pins.emio.take(5).unwrap(), gpio::PinState::Low), + gpio::Output::new_for_emio(gpio_pins.emio.take(6).unwrap(), gpio::PinState::Low), + gpio::Output::new_for_emio(gpio_pins.emio.take(7).unwrap(), gpio::PinState::Low), + ]; + spawner.spawn(blinky_task(mio_led, emio_leds).unwrap()); + + let dc_pin = gpio::Output::new_for_emio(gpio_pins.emio.take(11).unwrap(), gpio::PinState::High); + + let mut reset_pin = + gpio::Output::new_for_emio(gpio_pins.emio.take(12).unwrap(), gpio::PinState::High); + let mut oled_vdd_switch = + gpio::Output::new_for_emio(gpio_pins.emio.take(13).unwrap(), gpio::PinState::High); + let mut oled_vbat_switch = + gpio::Output::new_for_emio(gpio_pins.emio.take(14).unwrap(), gpio::PinState::High); + Delay.delay_ms(100).await; + + let spi = spi::Spi::new_for_emio( + periphs.spi_0, + spi::Config::calculate_for_io_clock( + Hertz::MHz(8), + clocks.io_clocks(), + embedded_hal::spi::MODE_0, + spi::SlaveSelectConfig::AutoCsManualStart, + ), + ) + .expect("Failed to initialize SPI"); + let spi_asynch = SpiAsync::new(spi); + let exclusive_device = ExclusiveDevice::new(spi_asynch, DummyPin::new_high(), NoDelay) + .expect("Failed to create exclusive SPI device"); + let spi_if = SPIInterface::new(exclusive_device, dc_pin); + let mut ssd1306 = Ssd1306Async::new(spi_if, DisplaySize128x32, DisplayRotation::Rotate180); + + oled_vdd_switch.set_low(); + oled_vbat_switch.set_low(); + Delay.delay_ms(100).await; + + ssd1306 + .reset(&mut reset_pin, &mut embassy_time::Delay {}) + .await + .expect("display reset error"); + let mut display = ssd1306.into_buffered_graphics_mode(); + display.init().await.expect("display init error"); + + // Include the BMP file data. + let ferris_data = include_bytes!("../../assets/ferris-flat-happy-small.bmp"); + let rust_logo_data = include_bytes!("../../assets/rust-logo-single-path.bmp"); + // Parse the BMP file. + let bmp_rust = Bmp::from_slice(rust_logo_data).expect("BMP loading error"); + let bmp_ferris = Bmp::from_slice(ferris_data).expect("BMP loading error"); + // Draw the image with the top left corner at (10, 20) by wrapping it in + // an embedded-graphics `Image`. + Image::new(&bmp_rust, Point::new(0, 0)) + .draw(&mut display) + .expect("image drawing error"); + Image::new(&bmp_ferris, Point::new(32, 0)) + .draw(&mut display) + .expect("image drawing error"); + display.flush().await.unwrap(); + let mut ticker = Ticker::every(Duration::from_millis(50)); + let mut ferris = FerrisMovement::new(32, Direction::Right, 32, 76); + + loop { + Image::new(&bmp_ferris, Point::new(ferris.pos as i32, 0)) + .draw(&mut display) + .expect("image drawing error"); + display.flush().await.expect("flush error"); + ferris.step(); + ticker.next().await; + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + Right, + Left, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FerrisMovement { + pub pos: u16, + pub dir: Direction, + pub left_threshold: u16, + pub right_threshold: u16, +} + +impl FerrisMovement { + /// Creates a new movement controller. + /// Assumes: left_threshold <= right_threshold and pos is within that range. + pub fn new(pos: u16, dir: Direction, left_threshold: u16, right_threshold: u16) -> Self { + Self { + pos, + dir, + left_threshold, + right_threshold, + } + } + + /// Move one tick and "bounce" between thresholds. + pub fn step(&mut self) { + match self.dir { + Direction::Right => { + if self.pos >= self.right_threshold { + self.dir = Direction::Left; // flip at right boundary + } else { + self.pos += 1; + } + } + Direction::Left => { + if self.pos <= self.left_threshold { + self.dir = Direction::Right; // flip at left boundary + } else { + self.pos -= 1; + } + } + } + } +} + +#[embassy_executor::task] +pub async fn logger_task( + uart: uart::Uart, + reader: embassy_sync::pipe::Reader<'static, CriticalSectionRawMutex, 4096>, +) -> ! { + let (tx, _) = uart.split(); + let mut tx_async = TxAsync::new(tx); + let mut log_buf: [u8; 2048] = [0; 2048]; + loop { + let read_bytes = reader.read(&mut log_buf).await; + if read_bytes > 0 { + tx_async.write(&log_buf[0..read_bytes]).unwrap().await; + } + } +} + +#[embassy_executor::task] +pub async fn blinky_task(mut mio_led: gpio::Output, mut emio_leds: [gpio::Output; 8]) { + let mut ticker = Ticker::every(Duration::from_millis(200)); + loop { + mio_led.toggle().unwrap(); + + // Create a wave pattern for emio_leds + for led in emio_leds.iter_mut() { + led.toggle().unwrap(); + ticker.next().await; // Wait for the next ticker for each toggle + } + ticker.next().await; + } +} + +#[zynq7000_rt::irq] +fn irq_handler() { + let mut gic_helper = gic::GicInterruptHelper::new(); + let irq_info = gic_helper.acknowledge_interrupt(); + match irq_info.interrupt() { + gic::Interrupt::Sgi(_) => (), + gic::Interrupt::Ppi(ppi_interrupt) => { + if ppi_interrupt == gic::PpiInterrupt::GlobalTimer { + unsafe { + zynq7000_embassy::on_interrupt(); + } + } + } + gic::Interrupt::Spi(spi_interrupt) => match spi_interrupt { + gic::SpiInterrupt::Uart1 => { + zynq7000_hal::uart::tx_async::on_interrupt_tx(uart::UartId::Uart1); + } + gic::SpiInterrupt::Spi0 => { + zynq7000_hal::spi::asynch::on_interrupt(spi::SpiId::Spi0); + } + _ => { + log::warn!("Unhandled SPI interrupt: {:?}", spi_interrupt); + } + }, + gic::Interrupt::Invalid(_) => (), + gic::Interrupt::Spurious => (), + } + gic_helper.end_of_interrupt(irq_info); +} + +#[zynq7000_rt::exception(DataAbort)] +fn data_abort_handler(_faulting_addr: usize) -> ! { + loop { + nop(); + } +} + +#[zynq7000_rt::exception(Undefined)] +fn undefined_handler(_faulting_addr: usize) -> ! { + loop { + nop(); + } +} + +#[zynq7000_rt::exception(PrefetchAbort)] +fn prefetch_handler(_faulting_addr: usize) -> ! { + loop { + nop(); + } +} + +/// Panic handler +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + error!("Panic: {info:?}"); + loop {} +} diff --git a/firmware/examples/zedboard/src/bin/oled.rs b/firmware/examples/zedboard/src/bin/oled.rs index 8109087..e9d30c3 100644 --- a/firmware/examples/zedboard/src/bin/oled.rs +++ b/firmware/examples/zedboard/src/bin/oled.rs @@ -133,7 +133,7 @@ async fn main(spawner: Spawner) -> ! { ssd1306.reset(&mut reset_pin, &mut embassy_time::Delay {}); let mut display = ssd1306.into_buffered_graphics_mode(); - display.init().unwrap(); + display.init().expect("display init error"); // Include the BMP file data. let ferris_data = include_bytes!("../../assets/ferris-flat-happy-small.bmp"); @@ -145,19 +145,19 @@ async fn main(spawner: Spawner) -> ! { // an embedded-graphics `Image`. Image::new(&bmp_rust, Point::new(0, 0)) .draw(&mut display) - .unwrap(); + .expect("image draw error"); Image::new(&bmp_ferris, Point::new(32, 0)) .draw(&mut display) - .unwrap(); - display.flush().unwrap(); + .expect("image draw error"); + display.flush().expect("display flush error"); let mut ticker = Ticker::every(Duration::from_millis(50)); let mut ferris = FerrisMovement::new(32, Direction::Right, 32, 76); loop { Image::new(&bmp_ferris, Point::new(ferris.pos as i32, 0)) .draw(&mut display) - .unwrap(); - display.flush().unwrap(); + .expect("image draw error"); + display.flush().expect("display flush error"); ferris.step(); ticker.next().await; } diff --git a/firmware/zynq7000-hal/src/spi/asynch.rs b/firmware/zynq7000-hal/src/spi/asynch.rs index 01b5ac4..c1dea3f 100644 --- a/firmware/zynq7000-hal/src/spi/asynch.rs +++ b/firmware/zynq7000-hal/src/spi/asynch.rs @@ -60,6 +60,12 @@ pub fn on_interrupt(peripheral: SpiId) { if context.transfer_type.is_none() { return; } + + // Write the trigger to one, we want to empty the whole FIFO in the transfer handlers. + // The trigger might have been set to a higher value, and the NOT FULL status bit will be + // cleared when the FIFO falls below the threshold. + spi.write_rx_trig(0x1); + let transfer_type = context.transfer_type.unwrap(); match transfer_type { TransferType::Read => on_interrupt_read(index, &mut context, &mut spi), @@ -433,11 +439,11 @@ impl<'spi> SpiFuture<'spi> { Self::generic_init_transfer(spi, id); let write_idx = core::cmp::min(super::FIFO_DEPTH, write.len()); + Self::set_triggers(spi, write_idx, write.len()); (0..write_idx).for_each(|idx| { spi.inner.write_fifo_unchecked(write[idx]); }); - Self::set_triggers(spi, write_idx, write.len()); // We assume that the slave select configuration was already performed, but we take // care of issuing a start if necessary. spi.issue_manual_start_for_manual_cfg(); @@ -452,7 +458,7 @@ impl<'spi> SpiFuture<'spi> { )) .unwrap(); // We want to re-fill the TX FIFO before it is completely empty if the full transfer size - // is larger than the FIFO depth. Otherwise, set it to 0. Not exactly sure what that does, + // is larger than the FIFO depth. Otherwise, set it to 1. Not exactly sure what that does, // but we do not enable interrupts anyway. if write_len > super::FIFO_DEPTH { spi.inner diff --git a/firmware/zynq7000-hal/src/spi/mod.rs b/firmware/zynq7000-hal/src/spi/mod.rs index 658af2c..89e01b4 100644 --- a/firmware/zynq7000-hal/src/spi/mod.rs +++ b/firmware/zynq7000-hal/src/spi/mod.rs @@ -476,7 +476,7 @@ impl SpiLowLevel { /// the external decoding was enabled via the [Config::enable_external_decoding] option. #[inline] pub fn select_hw_cs(&mut self, chip_select: ChipSelect) { - self.regs.modify_cr(|mut val| { + self.regs.modify_config(|mut val| { val.set_cs_raw(chip_select.raw_reg()); val }); @@ -486,7 +486,7 @@ impl SpiLowLevel { #[inline] pub fn configure_mode(&mut self, mode: Mode) { let (cpol, cpha) = spi_mode_const_to_cpol_cpha(mode); - self.regs.modify_cr(|mut val| { + self.regs.modify_config(|mut val| { val.set_cpha(cpha); val.set_cpol(cpol); val @@ -510,7 +510,7 @@ impl SpiLowLevel { }; let (cpol, cpha) = spi_mode_const_to_cpol_cpha(config.init_mode); - self.regs.write_cr( + self.regs.write_config( zynq7000::spi::Config::builder() .with_modefail_gen_en(false) .with_manual_start(false) @@ -559,7 +559,7 @@ impl SpiLowLevel { #[inline] pub fn issue_manual_start(&mut self) { - self.regs.modify_cr(|val| val.with_manual_start(true)); + self.regs.modify_config(|val| val.with_manual_start(true)); } #[inline] diff --git a/firmware/zynq7000/src/spi.rs b/firmware/zynq7000/src/spi.rs index 5c9e410..0fd8400 100644 --- a/firmware/zynq7000/src/spi.rs +++ b/firmware/zynq7000/src/spi.rs @@ -213,7 +213,7 @@ pub struct DelayControl { #[derive(derive_mmio::Mmio)] #[repr(C)] pub struct Registers { - cr: Config, + config: Config, #[mmio(PureRead, Write)] interrupt_status: InterruptStatus, /// Interrupt Enable Register.