diff --git a/Cargo.toml b/Cargo.toml index 210db7d..0f1849c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "display-driver", "buses/spi", + "buses/qspi", "mipidcs", "panels/co5300", diff --git a/README.md b/README.md index 9cfe9ec..a0f8baf 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,9 @@ display.write_frame(fb).await.unwrap(); ## Display Bus Implementations -- [spi](./buses/spi): SPI bus implementation. +- [SPI](./buses/spi): SPI bus implementation. + +- [QSPI](./buses/qspi): QSPI bus implementation. - [SF32 LCDC](https://github.com/OpenSiFli/sifli-rs/tree/main/sifli-hal): Bus Implementation for SF32LB52x LCDC Hardware. @@ -78,7 +80,7 @@ check [Examples](./examples) for more. - embedded-graphics - `display-driver` is built around async operation and efficient batched transfers, so it does not implement `embedded-graphics`'s `DrawTarget` directly. Use a framebuffer or a tiled buffer when integrating with `embedded-graphics`; see the [Examples](./examples) for practical patterns. + `display-driver` is built around async operation and efficient batched transfers, so it does not implement `embedded-graphics`'s `DrawTarget` directly. Use a framebuffer or a tiled buffer when integrating with `embedded-graphics`; see the [`FrameBufferedDisplayDriver`](display-driver/src/eg/framebuffered.rs) or [Examples](./examples) for practical patterns. - Slint diff --git a/buses/qspi/Cargo.toml b/buses/qspi/Cargo.toml new file mode 100644 index 0000000..23ab45f --- /dev/null +++ b/buses/qspi/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "display-driver-qspi" +version = "0.1.0" +edition = "2021" +authors = ["Decaday "] +categories = ["embedded", "hardware-support", "no-std"] +keywords = ["qspi", "display"] +repository = "https://github.com/decaday/display-driver" +description = "QSPI bus implementation for display-driver" +license = "Apache-2.0" + +[dependencies] +display-driver = { version = "0.1.0", path = "../../display-driver" } diff --git a/buses/qspi/src/hal.rs b/buses/qspi/src/hal.rs new file mode 100644 index 0000000..f16f2d5 --- /dev/null +++ b/buses/qspi/src/hal.rs @@ -0,0 +1,68 @@ + + +/// Line mode for a specific phase of a QSPI transaction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LineMode { + /// Phase is not present + None, + /// Single-line (1-bit) mode + Single, + /// Dual-line (2-bit) mode + Dual, + /// Quad-line (4-bit) mode + Quad, +} + +/// Configuration for a specific phase (Instruction or Address) in a QSPI transaction. +#[derive(Debug, Clone, Copy)] +pub struct PhaseConfig { + /// The value to transmit in this phase + pub value: u32, + /// The number of bytes to transmit (e.g., 1 for an 8-bit instruction, 3 for a 24-bit address) + pub bytes_len: u8, + /// The line mode used for this phase + pub mode: LineMode, +} + +/// A QSPI transaction descriptor. +#[derive(Debug, Clone, Copy)] +pub struct QspiTransaction { + /// The instruction phase configuration, if any + pub instruction: Option, + /// The address phase configuration, if any + pub address: Option, + /// The number of dummy cycles to wait before the data phase + pub dummy_cycles: u8, + /// The line mode used for the data phase + pub data_mode: LineMode, +} + +/// Hardware abstraction trait for QSPI devices. +/// +/// MCU-specific HAL wrappers should implement this trait so that the `QspiDisplayBus` +/// can interact with the hardware over QSPI. +#[allow(async_fn_in_trait)] +pub trait QspiDevice { + /// Error type for the QSPI operations. + type Error: core::fmt::Debug; + + /// Perform a QSPI write operation. + /// + /// The transaction specifies the instruction, address, dummy cycles, and the data phase mode. + /// The `data` slice contains the payload to write during the data phase. + async fn write( + &mut self, + transaction: &QspiTransaction, + data: &[u8], + ) -> Result<(), Self::Error>; + + /// Perform a QSPI read operation. + /// + /// The transaction specifies the instruction, address, dummy cycles, and the data phase mode. + /// The `buffer` slice will be filled with the data read during the data phase. + async fn read( + &mut self, + transaction: &QspiTransaction, + buffer: &mut [u8], + ) -> Result<(), Self::Error>; +} diff --git a/buses/qspi/src/lib.rs b/buses/qspi/src/lib.rs new file mode 100644 index 0000000..c382101 --- /dev/null +++ b/buses/qspi/src/lib.rs @@ -0,0 +1,299 @@ +#![no_std] + +pub mod hal; + +pub use hal::{LineMode, PhaseConfig, QspiDevice, QspiTransaction}; + +use display_driver::{ + bus::{BusHardwareFill, BusRead, DisplayBus, ErrorType, Metadata}, + Area, DisplayError, SolidColor, +}; + +const WRITE_CMD_INST: u8 = 0x02; +const WRITE_RAM_INST: u8 = 0x32; +const READ_INST: u8 = 0x03; + +/// QSPI flash command mapping. +/// It encapsulates the mapping from a 1-byte DCS command to a QSPI flash instruction +/// and address. +#[derive(Debug, Clone, Copy)] +pub struct QspiFlashCommand { + pub inst: u8, + pub cmd: u8, +} + +impl QspiFlashCommand { + /// Create a new QSPI flash command. + pub fn new(inst: u8, cmd: u8) -> Self { + Self { inst, cmd } + } + + /// Convert the command to a 4-byte sequence: [instruction, 0x00, command, 0x00] + pub fn to_bytes(&self) -> [u8; 4] { + [self.inst, 0x00, self.cmd, 0x00] + } + + /// Get the 24-bit address value. + pub fn address_value(&self) -> u32 { + (self.cmd as u32) << 8 + } +} + +/// Configuration for the QSPI display bus. +/// +/// This specifies the line modes to use for different phases +/// of communication with the display. +#[derive(Debug, Clone, Copy)] +pub struct QspiConfig { + /// The line mode used for transmitting parameter data during command read/write operations. + /// + /// Selecting `LineMode::Quad` represents the 1-1-4 mode (1-bit instruction, 1-bit address, 4-bit data). + pub cmd_data_mode: LineMode, + /// The line mode used for transmitting pixel data. + /// + /// Selecting `LineMode::Quad` represents the 1-1-4 mode (1-bit instruction, 1-bit address, 4-bit data). + pub ram_data_mode: LineMode, + /// Number of dummy cycles required before reading or writing data (if any). + pub dummy_cycles: u8, +} + +impl Default for QspiConfig { + fn default() -> Self { + Self { + cmd_data_mode: LineMode::Single, + ram_data_mode: LineMode::Quad, + dummy_cycles: 0, + } + } +} + +/// A QSPI bus implementation for display drivers. +/// +/// **TODO: Protocol Strategy Abstraction** +/// Currently, this bus implementation uses a fixed, hardcoded QSPI protocol sequence. +/// It hardcodes both the QSPI instructions (e.g., `0x02` for command write, `0x32` for RAM write) +/// and the address-mapping strategy (packing a 1-byte MIPI DCS command into the middle byte of +/// a 24-bit address phase, such as `0x00_CMD_00`). +/// While this specific protocol works out-of-the-box for many display drivers like CO5300 and ST77916, +/// it is incompatible with others like ST77903. +/// As we integrate and test more diverse QSPI protocols, we should introduce a dedicated +/// Trait to support customizable and pluggable command/address generation strategies. +pub struct QspiDisplayBus { + qspi: QSPI, + config: QspiConfig, +} + +impl QspiDisplayBus { + /// Creates a new QspiDisplayBus with the given hardware device and configuration. + pub fn new(qspi: QSPI, config: QspiConfig) -> Self { + Self { qspi, config } + } + + /// Helper to generate a 24-bit address using the shared QspiFlashCommand. + #[inline(always)] + fn make_address(cmd: &QspiFlashCommand) -> PhaseConfig { + PhaseConfig { + value: cmd.address_value(), + bytes_len: 3, + mode: LineMode::Single, + } + } + + /// Verifies that the command slice contains exactly one byte. + #[inline(always)] + fn assert_cmd_len(cmd: &[u8]) { + assert_eq!( + cmd.len(), + 1, + "QspiDisplayBus currently only supports single byte DCS commands" + ); + } +} + +impl ErrorType for QspiDisplayBus +where + QSPI: QspiDevice, +{ + type Error = QSPI::Error; +} + +impl DisplayBus for QspiDisplayBus +where + QSPI: QspiDevice, +{ + async fn write_cmd(&mut self, cmd: &[u8]) -> Result<(), Self::Error> { + Self::assert_cmd_len(cmd); + let flash_cmd = QspiFlashCommand::new(WRITE_CMD_INST, cmd[0]); + + let transaction = QspiTransaction { + instruction: Some(PhaseConfig { + value: flash_cmd.inst as u32, + bytes_len: 1, + mode: self.config.cmd_data_mode, + }), + address: Some(Self::make_address(&flash_cmd)), + dummy_cycles: 0, + data_mode: LineMode::None, + }; + + self.qspi.write(&transaction, &[]).await + } + + async fn write_cmd_with_params( + &mut self, + cmd: &[u8], + params: &[u8], + ) -> Result<(), Self::Error> { + Self::assert_cmd_len(cmd); + let flash_cmd = QspiFlashCommand::new(WRITE_CMD_INST, cmd[0]); + + let transaction = QspiTransaction { + instruction: Some(PhaseConfig { + value: flash_cmd.inst as u32, + bytes_len: 1, + mode: self.config.cmd_data_mode, + }), + address: Some(Self::make_address(&flash_cmd)), + dummy_cycles: 0, + data_mode: self.config.cmd_data_mode, + }; + + self.qspi.write(&transaction, params).await + } + + async fn write_pixels( + &mut self, + cmd: &[u8], + data: &[u8], + _metadata: Metadata, + ) -> Result<(), DisplayError> { + Self::assert_cmd_len(cmd); + let flash_cmd = QspiFlashCommand::new(WRITE_RAM_INST, cmd[0]); + + let transaction = QspiTransaction { + instruction: Some(PhaseConfig { + value: flash_cmd.inst as u32, + bytes_len: 1, + mode: self.config.cmd_data_mode, + }), + address: Some(Self::make_address(&flash_cmd)), + dummy_cycles: self.config.dummy_cycles, + data_mode: self.config.ram_data_mode, + }; + + self.qspi.write(&transaction, data).await.map_err(DisplayError::BusError) + } +} + +impl BusRead for QspiDisplayBus +where + QSPI: QspiDevice, +{ + async fn read_data( + &mut self, + cmd: &[u8], + _params: &[u8], + buffer: &mut [u8], + ) -> Result<(), DisplayError> { + Self::assert_cmd_len(cmd); + let flash_cmd = QspiFlashCommand::new(READ_INST, cmd[0]); + + let transaction = QspiTransaction { + instruction: Some(PhaseConfig { + value: flash_cmd.inst as u32, + bytes_len: 1, + mode: self.config.cmd_data_mode, + }), + address: Some(Self::make_address(&flash_cmd)), + dummy_cycles: self.config.dummy_cycles, + data_mode: self.config.cmd_data_mode, + }; + + self.qspi.read(&transaction, buffer).await.map_err(DisplayError::BusError) + } +} + +/// A Decorator that wraps an inner `DisplayBus` and translates 1-byte DCS commands +/// into 4-byte QSPI flash commands (instruction + 3-byte address). +/// +/// This provides the same mapping effect as `QspiDisplayBus` but runs on top of +/// any generic `DisplayBus` implementation, useful for hardware that does not +/// implement the `QspiDevice` Trait. +pub struct QspiFlashBus { + inner: B, +} + +impl QspiFlashBus { + /// Creates a new `QspiFlashBus` wrapping the given inner bus. + pub fn new(inner: B) -> Self { + Self { inner } + } + + #[inline(always)] + fn assert_cmd_len(cmd: &[u8]) { + assert_eq!( + cmd.len(), + 1, + "QspiFlashBus currently only supports single byte DCS commands" + ); + } +} + +impl ErrorType for QspiFlashBus { + type Error = B::Error; +} + +impl DisplayBus for QspiFlashBus { + async fn write_cmd(&mut self, cmd: &[u8]) -> Result<(), Self::Error> { + Self::assert_cmd_len(cmd); + let flash_cmd = QspiFlashCommand::new(WRITE_CMD_INST, cmd[0]); + self.inner.write_cmd(&flash_cmd.to_bytes()).await + } + + async fn write_cmd_with_params( + &mut self, + cmd: &[u8], + params: &[u8], + ) -> Result<(), Self::Error> { + Self::assert_cmd_len(cmd); + let flash_cmd = QspiFlashCommand::new(WRITE_CMD_INST, cmd[0]); + self.inner.write_cmd_with_params(&flash_cmd.to_bytes(), params).await + } + + async fn write_pixels( + &mut self, + cmd: &[u8], + data: &[u8], + metadata: Metadata, + ) -> Result<(), DisplayError> { + Self::assert_cmd_len(cmd); + let flash_cmd = QspiFlashCommand::new(WRITE_RAM_INST, cmd[0]); + self.inner.write_pixels(&flash_cmd.to_bytes(), data, metadata).await + } +} + +impl BusRead for QspiFlashBus { + async fn read_data( + &mut self, + cmd: &[u8], + params: &[u8], + buffer: &mut [u8], + ) -> Result<(), DisplayError> { + Self::assert_cmd_len(cmd); + let flash_cmd = QspiFlashCommand::new(READ_INST, cmd[0]); + self.inner.read_data(&flash_cmd.to_bytes(), params, buffer).await + } +} + +impl BusHardwareFill for QspiFlashBus { + async fn fill_solid( + &mut self, + cmd: &[u8], + color: SolidColor, + area: Area, + ) -> Result<(), DisplayError> { + Self::assert_cmd_len(cmd); + let flash_cmd = QspiFlashCommand::new(WRITE_RAM_INST, cmd[0]); + self.inner.fill_solid(&flash_cmd.to_bytes(), color, area).await + } +} diff --git a/display-driver/src/bus/mod.rs b/display-driver/src/bus/mod.rs index d506ad9..e8a3f63 100644 --- a/display-driver/src/bus/mod.rs +++ b/display-driver/src/bus/mod.rs @@ -1,8 +1,6 @@ #[cfg(feature = "display-interface")] mod display_interface_impl; -pub mod qspi_flash; -pub use qspi_flash::QspiFlashBus; pub mod simple; pub use simple::SimpleDisplayBus; diff --git a/display-driver/src/bus/qspi_flash.rs b/display-driver/src/bus/qspi_flash.rs deleted file mode 100644 index 9a2fe45..0000000 --- a/display-driver/src/bus/qspi_flash.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::{Area, SolidColor}; - -use super::{BusHardwareFill, BusRead, DisplayBus, DisplayError, ErrorType, Metadata}; - -/// An adapter that bridges a standard [`DisplayBus`] to a QSPI-connected display. -/// -/// Some displays attached via QSPI don't use standard display commands directly but instead emulate -/// a SPI Flash memory interface. This adapter wraps an inner bus and automatically transforms -/// standard display commands into the appropriate QSPI Flash opcodes. -pub struct QspiFlashBus { - inner: B, -} - -impl QspiFlashBus { - /// Creates a new QspiFlashBus wrapper. - pub fn new(inner: B) -> Self { - Self { inner } - } - - #[inline] - fn assert_cmd_len(&self, cmd: &[u8]) { - assert_eq!( - cmd.len(), - 1, - "QspiFlashBus only supports single byte commands" - ); - } - - /// Formats the command and address for QSPI transfer. - /// This is used for common write commands except WRITE_RAM. - #[inline] - pub fn to_cmd_and_addr_command(&self, cmd: u8) -> [u8; 4] { - [0x02, 0x00, cmd, 0x00] - } - - /// Formats the command and address for QSPI transfer. - /// This is used for WRITE_RAM command. - #[inline] - pub fn to_cmd_and_addr_write_ram(&self, cmd: u8) -> [u8; 4] { - [0x32, 0x00, cmd, 0x00] - } - - /// Formats the command and address for QSPI transfer. - /// This is used for read commands. - #[inline] - pub fn to_cmd_and_addr_read(&self, cmd: u8) -> [u8; 4] { - [0x03, 0x00, cmd, 0x00] - } -} - -impl ErrorType for QspiFlashBus { - type Error = B::Error; -} - -impl DisplayBus for QspiFlashBus { - async fn write_cmd(&mut self, cmd: &[u8]) -> Result<(), Self::Error> { - self.assert_cmd_len(cmd); - let cmd = self.to_cmd_and_addr_command(cmd[0]); - self.inner.write_cmd(&cmd).await - } - - async fn write_cmd_with_params( - &mut self, - cmd: &[u8], - params: &[u8], - ) -> Result<(), Self::Error> { - self.assert_cmd_len(cmd); - let cmd = self.to_cmd_and_addr_command(cmd[0]); - self.inner.write_cmd_with_params(&cmd, params).await - } - - async fn write_pixels( - &mut self, - cmd: &[u8], - data: &[u8], - metadata: Metadata, - ) -> Result<(), DisplayError> { - self.assert_cmd_len(cmd); - let cmd = self.to_cmd_and_addr_write_ram(cmd[0]); - self.inner.write_pixels(&cmd, data, metadata).await - } - - fn set_reset(&mut self, reset: bool) -> Result<(), DisplayError> { - self.inner.set_reset(reset) - } -} - -impl BusHardwareFill for QspiFlashBus { - async fn fill_solid( - &mut self, - cmd: &[u8], - color: SolidColor, - area: Area, - ) -> Result<(), DisplayError> { - self.assert_cmd_len(cmd); - let cmd = self.to_cmd_and_addr_write_ram(cmd[0]); - self.inner.fill_solid(&cmd, color, area).await - } -} - -impl BusRead for QspiFlashBus { - async fn read_data( - &mut self, - cmd: &[u8], - params: &[u8], - buffer: &mut [u8], - ) -> Result<(), DisplayError> { - self.assert_cmd_len(cmd); - let cmd = self.to_cmd_and_addr_read(cmd[0]); - self.inner.read_data(&cmd, params, buffer).await - } -}