Async OLED example #77

Merged
muellerr merged 1 commits from oled-asynch into main 2026-05-08 14:43:50 +02:00
6 changed files with 326 additions and 15 deletions
+2 -2
View File
@@ -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 }
@@ -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 {}
}
+6 -6
View File
@@ -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;
}
+8 -2
View File
@@ -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
+4 -4
View File
@@ -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]
+1 -1
View File
@@ -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.