diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c21a99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +/Cargo.lock + +examples/*/Cargo.lock +examples/*/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..19ed88b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "hub75-pio" +version = "0.1.0" +edition = "2021" +description = "A HUB75 driver for RP2040. Utilizes PIO and DMA to achieve zero CPU overhead." +license = "MIT" + +[dependencies] +pio-proc = "0.2.1" +pio = "0.2.0" + +rp2040-hal = "0.6.0" +embedded-graphics = "0.7.1" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0a14531 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 Krzysztof Jagiello + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2a2aa6 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# hub75-pio-rs + +An **experimental** HUB75 driver for +[RP2040](https://www.raspberrypi.com/products/rp2040/). Utilizes Programmable +I/O (PIO) units in combination with DMA to achieve high refresh rates, true +color depth and zero CPU overhead without sacrificing quality. + +https://user-images.githubusercontent.com/74944/187094663-2f52e020-ccb2-4103-b69b-af8ee2185dd0.mp4 + +## Features + +- Supports LED matrices up to 64x32 pixels with 1:16 scanline +- High refresh rate (approx. 2100 Hz with 24 bit color depth on a 64x32 + display) +- Does not utilize CPU for clocking out data to the display – all the work is + done by the PIOs and the DMA controller +- Uses binary color modulation +- Double buffered +- Implements [embedded-graphics](https://github.com/embedded-graphics/embedded-graphics) traits + +## Requirements + +The current implementation assumes that the following groups of data outputs +are assigned to consecutive pins on the RP2040: + +- R1, G1, B1, R2, G2, B2 +- ADDRA, ADDRB, ADDRC, ADDRD diff --git a/examples/nyan/.cargo/config.toml b/examples/nyan/.cargo/config.toml new file mode 100644 index 0000000..ca56c01 --- /dev/null +++ b/examples/nyan/.cargo/config.toml @@ -0,0 +1,22 @@ +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +runner = "probe-run --chip RP2040" + +rustflags = [ + "-C", "linker=flip-link", + "-C", "link-arg=--nmagic", + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", + + # Code-size optimizations. + # trap unreachable can save a lot of space, but requires nightly compiler. + # uncomment the next line if you wish to enable it + # "-Z", "trap-unreachable=no", + "-C", "inline-threshold=5", + "-C", "no-vectorize-loops", +] + +[build] +target = "thumbv6m-none-eabi" + +[env] +DEFMT_LOG = "debug" diff --git a/examples/nyan/Cargo.toml b/examples/nyan/Cargo.toml new file mode 100644 index 0000000..a8bb0cb --- /dev/null +++ b/examples/nyan/Cargo.toml @@ -0,0 +1,73 @@ +[package] +edition = "2021" +name = "dashy" +version = "0.1.0" + +[dependencies] +cortex-m = "0.7" +cortex-m-rt = "0.7" +embedded-hal = { version = "0.2.5", features = ["unproven"] } +embedded-time = "0.12" + +defmt = "0.3" +defmt-rtt = "0.3" +panic-probe = { version = "0.3", features = ["print-defmt"] } + +rp-pico = "0.5.0" + +embedded-graphics = "0.7.1" +tinyqoi = "0.1.0" + +hub75-pio = { path = "../../" } + +# cargo build/run +[profile.dev] +codegen-units = 1 +debug = 2 +debug-assertions = true +incremental = false +opt-level = 3 +overflow-checks = true + +# cargo build/run --release +[profile.release] +codegen-units = 1 +debug = 2 +debug-assertions = false +incremental = false +lto = 'fat' +opt-level = 3 +overflow-checks = false + +# do not optimize proc-macro crates = faster builds from scratch +[profile.dev.build-override] +codegen-units = 8 +debug = false +debug-assertions = false +opt-level = 0 +overflow-checks = false + +[profile.release.build-override] +codegen-units = 8 +debug = false +debug-assertions = false +opt-level = 0 +overflow-checks = false + +# cargo test +[profile.test] +codegen-units = 1 +debug = 2 +debug-assertions = true +incremental = false +opt-level = 3 +overflow-checks = true + +# cargo test --release +[profile.bench] +codegen-units = 1 +debug = 2 +debug-assertions = false +incremental = false +lto = 'fat' +opt-level = 3 diff --git a/examples/nyan/Embed.toml b/examples/nyan/Embed.toml new file mode 100644 index 0000000..9c91b13 --- /dev/null +++ b/examples/nyan/Embed.toml @@ -0,0 +1,39 @@ +[default.probe] +protocol = "Swd" +speed = 20000 +# If you only have one probe cargo embed will pick automatically +# Otherwise: add your probe's VID/PID/serial to filter + +## rust-dap +# usb_vid = "6666" +# usb_pid = "4444" +# serial = "test" + + +[default.flashing] +enabled = true + +[default.reset] +enabled = true +halt_afterwards = false + +[default.general] +chip = "RP2040" +log_level = "WARN" +# RP2040 does not support connect_under_reset +connect_under_reset = false + +[default.rtt] +enabled = true +up_mode = "NoBlockSkip" +channels = [ + { up = 0, down = 0, name = "name", up_mode = "NoBlockSkip", format = "Defmt" }, +] +timeout = 3000 +show_timestamps = true +log_enabled = false +log_path = "./logs" + +[default.gdb] +enabled = false +gdb_connection_string = "127.0.0.1:2345" diff --git a/examples/nyan/assets/01.qoi b/examples/nyan/assets/01.qoi new file mode 100644 index 0000000..e769638 Binary files /dev/null and b/examples/nyan/assets/01.qoi differ diff --git a/examples/nyan/assets/02.qoi b/examples/nyan/assets/02.qoi new file mode 100644 index 0000000..b4ee467 Binary files /dev/null and b/examples/nyan/assets/02.qoi differ diff --git a/examples/nyan/assets/03.qoi b/examples/nyan/assets/03.qoi new file mode 100644 index 0000000..c88ecb7 Binary files /dev/null and b/examples/nyan/assets/03.qoi differ diff --git a/examples/nyan/assets/04.qoi b/examples/nyan/assets/04.qoi new file mode 100644 index 0000000..9cef34f Binary files /dev/null and b/examples/nyan/assets/04.qoi differ diff --git a/examples/nyan/assets/05.qoi b/examples/nyan/assets/05.qoi new file mode 100644 index 0000000..41b09e2 Binary files /dev/null and b/examples/nyan/assets/05.qoi differ diff --git a/examples/nyan/assets/06.qoi b/examples/nyan/assets/06.qoi new file mode 100644 index 0000000..94a4f11 Binary files /dev/null and b/examples/nyan/assets/06.qoi differ diff --git a/examples/nyan/assets/07.qoi b/examples/nyan/assets/07.qoi new file mode 100644 index 0000000..80a7f0e Binary files /dev/null and b/examples/nyan/assets/07.qoi differ diff --git a/examples/nyan/assets/08.qoi b/examples/nyan/assets/08.qoi new file mode 100644 index 0000000..ea6086f Binary files /dev/null and b/examples/nyan/assets/08.qoi differ diff --git a/examples/nyan/assets/09.qoi b/examples/nyan/assets/09.qoi new file mode 100644 index 0000000..20acf13 Binary files /dev/null and b/examples/nyan/assets/09.qoi differ diff --git a/examples/nyan/assets/10.qoi b/examples/nyan/assets/10.qoi new file mode 100644 index 0000000..97334de Binary files /dev/null and b/examples/nyan/assets/10.qoi differ diff --git a/examples/nyan/assets/11.qoi b/examples/nyan/assets/11.qoi new file mode 100644 index 0000000..61ec958 Binary files /dev/null and b/examples/nyan/assets/11.qoi differ diff --git a/examples/nyan/assets/12.qoi b/examples/nyan/assets/12.qoi new file mode 100644 index 0000000..59d2cac Binary files /dev/null and b/examples/nyan/assets/12.qoi differ diff --git a/examples/nyan/build.rs b/examples/nyan/build.rs new file mode 100644 index 0000000..d534cc3 --- /dev/null +++ b/examples/nyan/build.rs @@ -0,0 +1,31 @@ +//! This build script copies the `memory.x` file from the crate root into +//! a directory where the linker can always find it at build time. +//! For many projects this is optional, as the linker always searches the +//! project root directory -- wherever `Cargo.toml` is. However, if you +//! are using a workspace or have a more complicated build setup, this +//! build script becomes required. Additionally, by requesting that +//! Cargo re-run the build script whenever `memory.x` is changed, +//! updating `memory.x` ensures a rebuild of the application with the +//! new memory settings. + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `memory.x` + // here, we ensure the build script is only re-run when + // `memory.x` is changed. + println!("cargo:rerun-if-changed=memory.x"); +} diff --git a/examples/nyan/debug_probes.md b/examples/nyan/debug_probes.md new file mode 100644 index 0000000..04092ef --- /dev/null +++ b/examples/nyan/debug_probes.md @@ -0,0 +1,36 @@ +# Compatible CMSIS-DAP debug probes + +## Raspberry Pi Pico + + You can use a second Pico as your debugger. + + - Download this file: https://github.com/majbthrd/DapperMime/releases/download/20210225/raspberry_pi_pico-DapperMime.uf2 + - Boot the Pico in bootloader mode by holding the bootset button while plugging it in + - Open the drive RPI-RP2 when prompted + - Copy raspberry_pi_pico-DapperMime.uf2 from Downloads into RPI-RP2 + - Connect the debug pins of your CMSIS-DAP Pico to the target one + - Connect GP2 on the Probe to SWCLK on the Target + - Connect GP3 on the Probe to SWDIO on the Target + - Connect a ground line from the CMSIS-DAP Probe to the Target too + +## WeAct MiniF4 +https://therealprof.github.io/blog/usb-c-pill-part1/ + +## HS-Probe +https://github.com/probe-rs/hs-probe + +## ST-LINK v2 clone +It's getting harder to source these with stm32f103's as time goes on, so you might be better off choosing a stm32f103 dev board + +Firmware: https://github.com/devanlai/dap42 + +## LPC-Link2 +https://www.nxp.com/design/microcontrollers-developer-resources/lpc-link2:OM13054 + +## MCU-Link +https://www.nxp.com/part/MCU-LINK#/ + +## DAPLink +You can use DAPLink firmware with any of it's supported chips (LPC4322, LPC11U35, K20, K22, KL26). You'll need to use the 'develop' branch to use GCC to build it. You'll need to find a chip with the correct + +Firmware source: https://github.com/ARMmbed/DAPLink/tree/develop diff --git a/examples/nyan/memory.x b/examples/nyan/memory.x new file mode 100644 index 0000000..070eac7 --- /dev/null +++ b/examples/nyan/memory.x @@ -0,0 +1,15 @@ +MEMORY { + BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 + FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 + RAM : ORIGIN = 0x20000000, LENGTH = 256K +} + +EXTERN(BOOT2_FIRMWARE) + +SECTIONS { + /* ### Boot loader */ + .boot2 ORIGIN(BOOT2) : + { + KEEP(*(.boot2)); + } > BOOT2 +} INSERT BEFORE .text; \ No newline at end of file diff --git a/examples/nyan/src/main.rs b/examples/nyan/src/main.rs new file mode 100644 index 0000000..231008f --- /dev/null +++ b/examples/nyan/src/main.rs @@ -0,0 +1,123 @@ +//! Displays an animated Nyan cat +#![no_std] +#![no_main] +#![feature(generic_const_exprs)] + +use bsp::entry; +use defmt::*; +use defmt_rtt as _; +use embedded_graphics::geometry::Point; +use embedded_graphics::image::Image; +use embedded_graphics::prelude::*; +use panic_probe as _; +use tinyqoi::Qoi; + +use bsp::hal::pio::PIOExt; +use bsp::hal::{ + clocks::{init_clocks_and_plls, Clock}, + pac, + sio::Sio, + watchdog::Watchdog, +}; +use hub75_pio; +use hub75_pio::dma::DMAExt; + +use rp_pico as bsp; + +static mut DISPLAY_BUFFER: hub75_pio::DisplayMemory<64, 32, 8> = hub75_pio::DisplayMemory::new(); + +const FRAMES: [&[u8]; 12] = [ + include_bytes!("../assets/01.qoi"), + include_bytes!("../assets/02.qoi"), + include_bytes!("../assets/03.qoi"), + include_bytes!("../assets/04.qoi"), + include_bytes!("../assets/05.qoi"), + include_bytes!("../assets/06.qoi"), + include_bytes!("../assets/07.qoi"), + include_bytes!("../assets/08.qoi"), + include_bytes!("../assets/09.qoi"), + include_bytes!("../assets/10.qoi"), + include_bytes!("../assets/11.qoi"), + include_bytes!("../assets/12.qoi"), +]; + +#[entry] +fn main() -> ! { + info!("Program start"); + + let mut pac = pac::Peripherals::take().unwrap(); + let core = pac::CorePeripherals::take().unwrap(); + let mut watchdog = Watchdog::new(pac.WATCHDOG); + let sio = Sio::new(pac.SIO); + + // External high-speed crystal on the pico board is 12Mhz + let external_xtal_freq_hz = 12_000_000u32; + let clocks = init_clocks_and_plls( + external_xtal_freq_hz, + pac.XOSC, + pac.CLOCKS, + pac.PLL_SYS, + pac.PLL_USB, + &mut pac.RESETS, + &mut watchdog, + ) + .ok() + .unwrap(); + + let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); + + let pins = bsp::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + + // Split PIO0 SM + let (mut pio, sm0, sm1, sm2, _) = pac.PIO0.split(&mut pac.RESETS); + + // Reset DMA + let resets = pac.RESETS; + resets.reset.modify(|_, w| w.dma().set_bit()); + resets.reset.modify(|_, w| w.dma().clear_bit()); + while resets.reset_done.read().dma().bit_is_clear() {} + + // Split DMA + let dma = pac.DMA.split(); + + let mut display = unsafe { + hub75_pio::Display::new( + &mut DISPLAY_BUFFER, + hub75_pio::DisplayPins { + r1: pins.gpio0.into(), + g1: pins.gpio1.into(), + b1: pins.gpio2.into(), + r2: pins.gpio3.into(), + g2: pins.gpio4.into(), + b2: pins.gpio5.into(), + addra: pins.gpio6.into(), + addrb: pins.gpio7.into(), + addrc: pins.gpio8.into(), + addrd: pins.gpio9.into(), + clk: pins.gpio10.into(), + lat: pins.gpio11.into(), + oe: pins.gpio12.into(), + }, + &mut pio, + (sm0, sm1, sm2), + (dma.ch0, dma.ch1, dma.ch2, dma.ch3), + ) + }; + + // Display Nyancat + loop { + for raw_frame in FRAMES { + let frame = Qoi::new(raw_frame).unwrap(); + Image::new(&frame, Point::zero()) + .draw(&mut display) + .unwrap(); + display.commit(); + delay.delay_ms(100); + } + } +} diff --git a/src/dma.rs b/src/dma.rs new file mode 100644 index 0000000..583f50e --- /dev/null +++ b/src/dma.rs @@ -0,0 +1,89 @@ +//! Exposes a way of splitting the DMA block into separate channels. +//! +//! Shamelessly stolen from +//! [https://github.com/rp-rs/rp-hal/pull/209](https://github.com/rp-rs/rp-hal/pull/209) +use core::marker::PhantomData; +use rp2040_hal::pac as rp2040_pac; +use rp2040_hal::pac::DMA; + +/// DMA unit. +pub trait DMAExt { + /// Splits the DMA unit into its individual channels. + fn split(self) -> Channels; +} + +/// DMA channel. +pub struct Channel { + _phantom: PhantomData, +} + +/// DMA channel identifier. +pub trait ChannelIndex { + /// Numerical index of the DMA channel (0..11). + fn id() -> u8; +} + +macro_rules! channels { + ( + $($CHX:ident: ($chX:ident, $x:expr),)+ + ) => { + impl DMAExt for DMA { + fn split(self) -> Channels { + Channels { + $( + $chX: Channel { + _phantom: PhantomData, + }, + )+ + } + } + } + + /// Set of DMA channels. + pub struct Channels { + $( + /// DMA channel. + pub $chX: Channel<$CHX>, + )+ + } + $( + /// DMA channel identifier. + pub struct $CHX; + impl ChannelIndex for $CHX { + fn id() -> u8 { + $x + } + } + )+ + } +} + +channels! { + CH0: (ch0, 0), + CH1: (ch1, 1), + CH2: (ch2, 2), + CH3: (ch3, 3), + CH4: (ch4, 4), + CH5: (ch5, 5), + CH6: (ch6, 6), + CH7: (ch7, 7), + CH8: (ch8, 8), + CH9: (ch9, 9), + CH10:(ch10, 10), + CH11:(ch11, 11), +} + +pub trait ChannelRegs { + unsafe fn ptr() -> *const rp2040_pac::dma::CH; + fn regs(&self) -> &rp2040_pac::dma::CH; +} + +impl ChannelRegs for Channel { + unsafe fn ptr() -> *const rp2040_pac::dma::CH { + &(*rp2040_pac::DMA::ptr()).ch[CH::id() as usize] as *const _ + } + + fn regs(&self) -> &rp2040_pac::dma::CH { + unsafe { &*Self::ptr() } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9b8665e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,598 @@ +//! # hub75-pio +//! +//! An **experimental** HUB75 driver for [RP2040](https://www.raspberrypi.com/products/rp2040/). +//! Utilizes Programmable I/O (PIO) units in combination with DMA to achieve high refresh rates, +//! true color depth and zero CPU overhead without sacrificing quality. +//! +//! ## Features +//! +//! - Supports LED matrices up to 64x32 pixels with 1:16 scanline +//! - High refresh rate (approx. 2100 Hz with 24 bit color depth on a 64x32 +//! display) +//! - Does not utilize CPU for clocking out data to the display – all the work is +//! done by the PIOs and the DMA controller +//! - Uses binary color modulation +//! - Double buffered +//! - Implements [embedded-graphics](https://github.com/embedded-graphics/embedded-graphics) traits +//! +//! ## Requirements +//! +//! The current implementation assumes that the following groups of data outputs +//! are assigned to consecutive pins on the RP2040: +//! +//! - R1, G1, B1, R2, G2, B2 +//! - ADDRA, ADDRB, ADDRC, ADDRD + +#![no_std] +#![feature(generic_const_exprs)] +#![feature(const_for)] + +use crate::dma::{Channel, ChannelIndex, ChannelRegs}; +use core::convert::TryInto; +use embedded_graphics::{pixelcolor::Rgb888, prelude::*}; +use rp2040_hal::gpio::dynpin::{DynPin, DynPinMode}; +use rp2040_hal::gpio::FunctionConfig; +use rp2040_hal::pio::{ + PIOBuilder, PIOExt, PinDir, ShiftDirection, StateMachineIndex, UninitStateMachine, PIO, +}; + +pub mod dma; + +/// Clock divider for the PIO SM. Set to 1.0 as we want to go full speed. +const PIO_CLK_DIV: f32 = 1.0; + +/// Framebuffer size in bytes +#[doc(hidden)] +pub const fn fb_bytes(w: usize, h: usize, b: usize) -> usize { + w * h / 2 * b +} + +/// Computes an array with number of clock ticks to wait for every n-th color bit +const fn delays() -> [u32; B] { + let mut arr = [0; B]; + let mut i = 0; + while i < arr.len() { + arr[i] = (1 << i) - 1; + i += 1; + } + arr +} + +/// Backing storage for the framebuffers +/// +/// ## Memory layout +/// +/// The pixel buffer is organized in a way way so that transmiting the content of it requires no +/// manipulation on the PIO. +/// +/// PIO reads the pixel buffer one byte at a time from low to high, then shifts out the pixels from +/// left to right. +/// +/// ### Pixel tuple +/// +/// At the lowest level, every byte in the buffer contains a so called pixel tuple. Within a tuple +/// you will find the nth bit of the R/G/B-componts of two colors. The reason for packing two +/// colors together is the scan rate of the display. The display this library is targetting is has +/// a 1:16 scan rate, which means that when you are addressing a pixel at row 0, you are at the +/// same time addressing a pixel in the same column at row 16. +/// +/// The tuple has the following structure: +/// +/// ``` +/// XXBGRBGR +/// --222111 +/// ``` +/// +/// Currently we are wasting two bits per tuple (byte) – marked as X above. +/// +/// ### Buffer structure +/// +/// The diagram below attempts to visualize the structure of the buffer for a 64x32 display +/// configured for 24 bit color depth. X, Y are the pixel coordinates on the screen and N stands +/// for the big-endian position of the bit in the color to be displayed. +/// +/// ``` +/// N 8 1 0 +/// X | 63| | 3 2| 1| 0| 63| | 0| +/// Y 0 |XXBGRBGR|...|XXBGRBGR|XXBGRBGR|XXBGRBGR|XXBGRBGR|XXBGRBGR|...|XXBGRBGR| +/// |XX222111|...|XX222111|XX222111|XX222111|XX222111|XX222111|...|XX222111| +/// |XX000000|...|XX000000|XX000000|XX000000|XX000000|XX000000|...|XX000000| +/// |--------|...|--------|--------|--------|--------|--------|...|--------| +/// Y 1 |XXBGRBGR|...|XXBGRBGR|XXBGRBGR|XXBGRBGR|XXBGRBGR|XXBGRBGR|...|XXBGRBGR| +/// |XX222111|...|XX222111|XX222111|XX222111|XX222111|XX222111|...|XX222111| +/// |XX000000|...|XX000000|XX000000|XX000000|XX000000|XX000000|...|XX000000| +/// |--------|...|--------|--------|--------|--------|--------|...|--------| +/// .................................................|.....................| +/// .................................................|.....................| +/// .................................................|.....................| +/// Y 15 |XXBGRBGR|...|XXBGRBGR|XXBGRBGR|XXBGRBGR|XXBGRBGR|XXBGRBGR|...|XXBGRBGR| +/// |XX222111|...|XX222111|XX222111|XX222111|XX222111|XX222111|...|XX222111| +/// |XX000000|...|XX000000|XX000000|XX000000|XX000000|XX000000|...|XX000000| +/// |--------|...|--------|--------|--------|--------|--------|...|--------| +/// ``` +pub struct DisplayMemory +where + [(); fb_bytes(W, H, B)]: Sized, +{ + fbptr: [u32; 1], + fb0: [u8; fb_bytes(W, H, B)], + fb1: [u8; fb_bytes(W, H, B)], + delays: [u32; B], + delaysptr: [u32; 1], +} + +impl DisplayMemory +where + [(); fb_bytes(W, H, B)]: Sized, +{ + pub const fn new() -> Self { + let fb0 = [0; fb_bytes(W, H, B)]; + let fb1 = [0; fb_bytes(W, H, B)]; + let fbptr: [u32; 1] = [0]; + let delays = delays(); + let delaysptr: [u32; 1] = [0]; + DisplayMemory { + fbptr, + fb0, + fb1, + delays, + delaysptr, + } + } +} + +/// Mapping between GPIO pins and HUB75 pins +pub struct DisplayPins { + pub r1: DynPin, + pub g1: DynPin, + pub b1: DynPin, + pub r2: DynPin, + pub g2: DynPin, + pub b2: DynPin, + pub clk: DynPin, + pub addra: DynPin, + pub addrb: DynPin, + pub addrc: DynPin, + pub addrd: DynPin, + pub lat: DynPin, + pub oe: DynPin, +} + +/// The HUB75 display driver +pub struct Display +where + [(); fb_bytes(W, H, B)]: Sized, +{ + mem: &'static mut DisplayMemory, +} + +impl Display +where + [(); fb_bytes(W, H, B)]: Sized, +{ + /// Creates a new display + /// + /// Algo: + /// 1. Enable output + /// 2. Clock out the row: + /// 1. Set rgb pins + /// 2. Cycle the clock once + /// 3. Go back to 2 and repeat 63 more times + /// 4. Disable output + /// 5. Cycle the latch + /// 6. Select the row + /// + /// # Arguments + /// + /// * `pins`: Pins to use for the communication with the display + /// * `pio_sms`: PIO state machines to be used to drive the display + /// * `dma_chs`: DMA channels to be used to drive the PIO state machines + pub fn new( + buffer: &'static mut DisplayMemory, + mut pins: DisplayPins, + pio_block: &mut PIO, + pio_sms: ( + UninitStateMachine<(PE, SM0)>, + UninitStateMachine<(PE, SM1)>, + UninitStateMachine<(PE, SM2)>, + ), + dma_chs: (Channel, Channel, Channel, Channel), + ) -> Self + where + PE: PIOExt + FunctionConfig, + SM0: StateMachineIndex, + SM1: StateMachineIndex, + SM2: StateMachineIndex, + CH0: ChannelIndex, + CH1: ChannelIndex, + CH2: ChannelIndex, + CH3: ChannelIndex, + { + // Use correct PIO here + pins.r1 + .try_into_mode(DynPinMode::Function(PE::DYN)) + .unwrap(); + pins.g1 + .try_into_mode(DynPinMode::Function(PE::DYN)) + .unwrap(); + pins.b1 + .try_into_mode(DynPinMode::Function(PE::DYN)) + .unwrap(); + pins.r2 + .try_into_mode(DynPinMode::Function(PE::DYN)) + .unwrap(); + pins.g2 + .try_into_mode(DynPinMode::Function(PE::DYN)) + .unwrap(); + pins.b2 + .try_into_mode(DynPinMode::Function(PE::DYN)) + .unwrap(); + pins.clk + .try_into_mode(DynPinMode::Function(PE::DYN)) + .unwrap(); + pins.addra + .try_into_mode(DynPinMode::Function(PE::DYN)) + .unwrap(); + pins.addrb + .try_into_mode(DynPinMode::Function(PE::DYN)) + .unwrap(); + pins.addrc + .try_into_mode(DynPinMode::Function(PE::DYN)) + .unwrap(); + pins.addrd + .try_into_mode(DynPinMode::Function(PE::DYN)) + .unwrap(); + pins.lat + .try_into_mode(DynPinMode::Function(PE::DYN)) + .unwrap(); + pins.oe + .try_into_mode(DynPinMode::Function(PE::DYN)) + .unwrap(); + + // Setup PIO SMs + let (data_sm, row_sm, oe_sm) = pio_sms; + + // Data SM + let (data_sm, data_sm_tx) = { + let program_data = pio_proc::pio_asm!( + ".side_set 1", + "out isr, 32 side 0b0", + ".wrap_target", + // Wait for the row program to set the ADDR pins + "wait 1 irq 5 side 0b0", + "mov x isr side 0b0", + "pixel:", + "out pins, 8 side 0b0 [2]", + "jmp x-- pixel side 0b1 [1]", // clock out the pixel + "irq 4 side 0b0", // tell the row program to set the next row + ".wrap", + ); + let installed = pio_block.install(&program_data.program).unwrap(); + let (mut sm, _, mut tx) = PIOBuilder::from_program(installed) + .out_pins(pins.r1.id().num, 6) + .side_set_pin_base(pins.clk.id().num) + .out_sticky(false) + .clock_divisor(PIO_CLK_DIV) + .out_shift_direction(ShiftDirection::Right) + .in_shift_direction(ShiftDirection::Right) + .autopull(true) + .build(data_sm); + sm.set_pindirs([ + (pins.r1.id().num, PinDir::Output), + (pins.g1.id().num, PinDir::Output), + (pins.b1.id().num, PinDir::Output), + (pins.r2.id().num, PinDir::Output), + (pins.g2.id().num, PinDir::Output), + (pins.b2.id().num, PinDir::Output), + (pins.clk.id().num, PinDir::Output), + ]); + // Configure the width of the screen + tx.write((W - 1).try_into().unwrap()); + (sm, tx) + }; + + let row_sm = { + // Row program + let program_data = pio_proc::pio_asm!( + ".side_set 1", + "pull side 0b0", // Pull the height / 2 into OSR + "out isr, 32 side 0b0", // Store height / 2 in ISR + "pull side 0b0", // Pull the color depth - 1 into OSR + ".wrap_target", + "mov x, isr side 0b0", + "addr:", + "mov pins, ~x side 0b0", // Set the row address + "mov y, osr side 0b0", + "row:", + "irq 5 side 0b0", // Signal to the data SM that row is set + "wait 1 irq 4 side 0b0", // Wait until the data SM asks for next row + "nop side 0b1", // Latch the data + "nop side 0b1", // Latch the data (one more pulse required at high speed) + "irq 6 side 0b0", // Run a step in the delay program + "wait 1 irq 7 side 0b0", // Wait for the OE delay to complete + "jmp y-- row side 0b0", + "jmp x-- addr side 0b0", + ".wrap", + ); + let installed = pio_block.install(&program_data.program).unwrap(); + let (mut sm, _, mut tx) = PIOBuilder::from_program(installed) + .out_pins(pins.addra.id().num, 4) + .side_set_pin_base(pins.lat.id().num) + .clock_divisor(PIO_CLK_DIV) + .out_sticky(true) + .build(row_sm); + sm.set_pindirs([ + (pins.addra.id().num, PinDir::Output), + (pins.addrb.id().num, PinDir::Output), + (pins.addrc.id().num, PinDir::Output), + (pins.addrd.id().num, PinDir::Output), + (pins.lat.id().num, PinDir::Output), + ]); + // Configure the height of the screen + tx.write((H / 2 - 1).try_into().unwrap()); + // Configure the color depth + tx.write((B - 1).try_into().unwrap()); + sm + }; + + let (oe_sm, oe_sm_tx) = { + // Control the delay using DMA - buffer with 8 bytes specifying the length of the delays + // Delay program (controls OE) + let program_data = pio_proc::pio_asm!( + ".wrap_target", + "out x, 32", + "wait 1 irq 6", + "delay:", + "set pins, 0b0 [1]", + "jmp x-- delay", + "set pins, 0b1", + "irq 7 ", + ".wrap", + ); + let installed = pio_block.install(&program_data.program).unwrap(); + let (mut sm, _, tx) = PIOBuilder::from_program(installed) + .set_pins(pins.oe.id().num, 1) + .clock_divisor(PIO_CLK_DIV) + .autopull(true) + .out_sticky(true) + .build(oe_sm); + sm.set_pindirs([(pins.oe.id().num, PinDir::Output)]); + (sm, tx) + }; + + // Setup DMA + let (fb_ch, fb_loop_ch, oe_ch, oe_loop_ch) = dma_chs; + + // TODO: move this to a better place + buffer.fbptr[0] = buffer.fb0.as_ptr() as u32; + buffer.delaysptr[0] = buffer.delays.as_ptr() as u32; + + // Framebuffer channel + fb_ch.regs().ch_al1_ctrl.write(|w| unsafe { + w + // Increase the read addr as we progress through the buffer + .incr_read() + .bit(true) + // Do not increase the write addr because we always want to write to PIO FIFO + .incr_write() + .bit(false) + // Read 32 bits at a time + .data_size() + .size_word() + // Setup PIO FIFO as data request trigger + .treq_sel() + .bits(data_sm_tx.dreq_value()) + // Turn off interrupts + .irq_quiet() + .bit(true) + // Chain to the channel selecting the framebuffers + .chain_to() + .bits(CH1::id()) + // Enable the channel + .en() + .bit(true) + }); + fb_ch + .regs() + .ch_read_addr + .write(|w| unsafe { w.bits(buffer.fbptr[0]) }); + fb_ch + .regs() + .ch_trans_count + .write(|w| unsafe { w.bits((fb_bytes(W, H, B) / 4) as u32) }); + fb_ch + .regs() + .ch_write_addr + .write(|w| unsafe { w.bits(data_sm_tx.fifo_address() as u32) }); + + // Framebuffer loop channel + fb_loop_ch.regs().ch_al1_ctrl.write(|w| unsafe { + w + // Do not increase the read addr. We always want to read a single value + .incr_read() + .bit(false) + // Do not increase the write addr because we always want to write to PIO FIFO + .incr_write() + .bit(false) + // Read 32 bits at a time + .data_size() + .size_word() + // No pacing + .treq_sel() + .permanent() + // Turn off interrupts + .irq_quiet() + .bit(true) + // Chain it back to the channel sending framebuffer data + .chain_to() + .bits(CH0::id()) + // Start up the DMA channel + .en() + .bit(true) + }); + fb_loop_ch + .regs() + .ch_read_addr + .write(|w| unsafe { w.bits(buffer.fbptr.as_ptr() as u32) }); + fb_loop_ch + .regs() + .ch_trans_count + .write(|w| unsafe { w.bits(1) }); + fb_loop_ch + .regs() + .ch_al2_write_addr_trig + .write(|w| unsafe { w.bits(fb_ch.regs().ch_read_addr.as_ptr() as u32) }); + + // Output enable channel + oe_ch.regs().ch_al1_ctrl.write(|w| unsafe { + w + // Increase the read addr as we progress through the buffer + .incr_read() + .bit(true) + // Do not increase the write addr because we always want to write to PIO FIFO + .incr_write() + .bit(false) + // Read 32 bits at a time + .data_size() + .size_word() + // Setup PIO FIFO as data request trigger + .treq_sel() + .bits(oe_sm_tx.dreq_value()) + // Turn off interrupts + .irq_quiet() + .bit(true) + // Chain to the channel selecting the framebuffers + .chain_to() + .bits(CH3::id()) + // Enable the channel + .en() + .bit(true) + }); + oe_ch + .regs() + .ch_read_addr + .write(|w| unsafe { w.bits(buffer.delays.as_ptr() as u32) }); + oe_ch + .regs() + .ch_trans_count + .write(|w| unsafe { w.bits(buffer.delays.len().try_into().unwrap()) }); + oe_ch + .regs() + .ch_write_addr + .write(|w| unsafe { w.bits(oe_sm_tx.fifo_address() as u32) }); + + // Output enable loop channel + oe_loop_ch.regs().ch_al1_ctrl.write(|w| unsafe { + w + // Do not increase the read addr. We always want to read a single value + .incr_read() + .bit(false) + // Do not increase the write addr because we always want to write to PIO FIFO + .incr_write() + .bit(false) + // Read 32 bits at a time + .data_size() + .size_word() + // No pacing + .treq_sel() + .permanent() + // Turn off interrupts + .irq_quiet() + .bit(true) + // Chain it back to the channel sending framebuffer data + .chain_to() + .bits(CH2::id()) + // Start up the DMA channel + .en() + .bit(true) + }); + oe_loop_ch + .regs() + .ch_read_addr + .write(|w| unsafe { w.bits(buffer.delaysptr.as_ptr() as u32) }); + oe_loop_ch + .regs() + .ch_trans_count + .write(|w| unsafe { w.bits(buffer.delaysptr.len().try_into().unwrap()) }); + oe_loop_ch + .regs() + .ch_al2_write_addr_trig + .write(|w| unsafe { w.bits(oe_ch.regs().ch_read_addr.as_ptr() as u32) }); + + data_sm.start(); + row_sm.start(); + oe_sm.start(); + + Display { mem: buffer } + } + + /// Flips the display buffers + /// + /// Has to be called once you have drawn something onto the currently inactive buffer. + pub fn commit(&mut self) { + if self.mem.fbptr[0] == (self.mem.fb0.as_ptr() as u32) { + self.mem.fbptr[0] = self.mem.fb1.as_ptr() as u32; + self.mem.fb0[0..].fill(0); + } else { + self.mem.fbptr[0] = self.mem.fb0.as_ptr() as u32; + self.mem.fb1[0..].fill(0); + } + } + + /// Paints the given pixel coordinates with the given color + /// + /// Note that the coordinates are 0-indexed. + pub fn set_pixel(&mut self, x: usize, y: usize, color: C) { + // invert the screen + let x = W - 1 - x; + let y = H - 1 - y; + // Half of the screen + let h = y > (H / 2) - 1; + let shift = if h { 3 } else { 0 }; + for b in 0..B { + // Extract the n-th bit of each component of the color and pack it together + let cr = color.r() >> b & 0b1; + let cg = color.g() >> b & 0b1; + let cb = color.b() >> b & 0b1; + let c = cb << 2 | cg << 1 | cr; + let idx = (b) * W + x + ((y % (H / 2)) * W * B); + if self.mem.fbptr[0] == (self.mem.fb0.as_ptr() as u32) { + self.mem.fb1[idx] &= !(0b111 << shift); + self.mem.fb1[idx] |= (c << shift) as u8; + } else { + self.mem.fb0[idx] &= !(0b111 << shift); + self.mem.fb0[idx] |= (c << shift) as u8; + } + } + } +} + +impl OriginDimensions for Display +where + [(); fb_bytes(W, H, B)]: Sized, +{ + fn size(&self) -> Size { + Size::new(W.try_into().unwrap(), H.try_into().unwrap()) + } +} + +impl DrawTarget for Display +where + [(); fb_bytes(W, H, B)]: Sized, +{ + type Color = Rgb888; + type Error = core::convert::Infallible; + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + for Pixel(coord, color) in pixels.into_iter() { + if coord.x < W.try_into().unwrap() && coord.y < H.try_into().unwrap() { + self.set_pixel(coord.x as usize, coord.y as usize, color); + } + } + + Ok(()) + } +}