├── .github ├── CODEOWNERS ├── workflows │ ├── rustfmt.yaml │ └── ci.yaml └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── mipidsi-async ├── src │ └── lib.rs ├── README.md └── Cargo.toml ├── examples ├── spi-ili9486-esp32-c3 │ ├── .gitignore │ ├── rust-toolchain.toml │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── spi-st7789-rpi-zero-w │ ├── .gitignore │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs └── parallel_ili9341_rp_pico │ ├── .gitignore │ ├── memory.x │ ├── Cargo.toml │ ├── .cargo │ └── config.toml │ ├── build.rs │ └── src │ └── main.rs ├── src ├── dcs │ ├── macros.rs │ ├── set_scroll_start.rs │ ├── set_invert_mode.rs │ ├── set_page_address.rs │ ├── set_column_address.rs │ ├── set_scroll_area.rs │ ├── set_tearing_effect.rs │ ├── set_pixel_format.rs │ └── set_address_mode.rs ├── models │ ├── st7796.rs │ ├── ili934x.rs │ ├── ili948x.rs │ ├── st7789.rs │ ├── ili9341.rs │ ├── ili9342c.rs │ ├── ili9486.rs │ ├── ili9488.rs │ ├── rm67162.rs │ ├── gc9107.rs │ ├── st7735s.rs │ ├── gc9a01.rs │ └── ili9225.rs ├── interface │ ├── spi.rs │ └── parallel.rs ├── dcs.rs ├── _troubleshooting.rs ├── interface.rs ├── options.rs ├── test_image.rs ├── graphics.rs ├── models.rs ├── builder.rs ├── batch.rs └── options │ └── orientation.rs ├── Cargo.toml ├── LICENSE ├── docs ├── colors_both_wrong.svg ├── colors_wrong_subpixel_order.svg ├── colors_wrong_color_inversion.svg ├── colors_correct.svg └── MIGRATION.md ├── tests └── external.rs ├── assets └── test_image.svg ├── README.md └── CHANGELOG.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @almindor -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /mipidsi-async/src/lib.rs: -------------------------------------------------------------------------------- 1 | // TODO 2 | -------------------------------------------------------------------------------- /examples/spi-ili9486-esp32-c3/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /examples/spi-st7789-rpi-zero-w/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /examples/parallel_ili9341_rp_pico/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /mipidsi-async/README.md: -------------------------------------------------------------------------------- 1 | # mipidsi-async 2 | 3 | Placeholder for the future async version of [mipidsi](../mipidsi/README.md). 4 | 5 | -------------------------------------------------------------------------------- /examples/spi-ili9486-esp32-c3/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | components = ["rust-src"] 4 | targets = ["riscv32imc-unknown-none-elf"] 5 | -------------------------------------------------------------------------------- /examples/spi-st7789-rpi-zero-w/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.arm-unknown-linux-musleabihf] 2 | linker = "arm-linux-musleabihf-gcc" 3 | 4 | [target.arm-unknown-linux-gnueabihf] 5 | linker = "arm-linux-gnueabihf-gcc" 6 | -------------------------------------------------------------------------------- /examples/spi-st7789-rpi-zero-w/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spi-st7789-rpi-zero-w" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | mipidsi = { path = "../../" } 8 | embedded-graphics = "0.8.0" 9 | rppal = { version = "0.17.1", features = ["hal"] } 10 | embedded-hal-bus = "0.2.0" 11 | embedded-hal = "1.0.0" 12 | 13 | [workspace] 14 | -------------------------------------------------------------------------------- /examples/parallel_ili9341_rp_pico/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 3 | FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 4 | RAM : ORIGIN = 0x20000000, LENGTH = 256K 5 | } 6 | 7 | EXTERN(BOOT2_FIRMWARE) 8 | 9 | SECTIONS { 10 | /* ### Boot loader */ 11 | .boot2 ORIGIN(BOOT2) : 12 | { 13 | KEEP(*(.boot2)); 14 | } > BOOT2 15 | } INSERT BEFORE .text; -------------------------------------------------------------------------------- /examples/spi-ili9486-esp32-c3/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.riscv32imc-unknown-none-elf] 2 | runner = "espflash flash --monitor" 3 | 4 | [build] 5 | rustflags = [ 6 | "-C", "link-arg=-Tlinkall.x", 7 | # Required to obtain backtraces (e.g. when using the "esp-backtrace" crate.) 8 | # NOTE: May negatively impact performance of produced code 9 | "-C", "force-frame-pointers", 10 | ] 11 | 12 | target = "riscv32imc-unknown-none-elf" 13 | 14 | [unstable] 15 | build-std = ["core"] 16 | -------------------------------------------------------------------------------- /mipidsi-async/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mipidsi-async" 3 | description = "MIPI Display Command Set compatible generic driver async version" 4 | version = "0.1.0" 5 | authors = ["Ales Katona "] 6 | edition = "2018" 7 | license = "MIT" 8 | repository = "https://github.com/almindor/mipidsi" 9 | keywords = ["embedded-hal-driver", "mipi", "dcs", "display"] 10 | readme = "README.md" 11 | documentation = "https://docs.rs/mipidsi" 12 | rust-version = "1.75" 13 | 14 | -------------------------------------------------------------------------------- /examples/parallel_ili9341_rp_pico/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "parallel_ili9341_rp_pico" 4 | version = "0.1.0" 5 | 6 | [dependencies] 7 | cortex-m = "0.7.7" 8 | cortex-m-rt = "0.7.3" 9 | embedded-hal = "1.0.0" 10 | 11 | defmt = "0.3.6" 12 | defmt-rtt = "0.4" 13 | panic-probe = { version = "0.3", features = ["print-defmt"] } 14 | 15 | rp-pico = "0.9.0" 16 | 17 | embedded-graphics = "0.8.0" 18 | embedded-graphics-core = "0.4.0" 19 | mipidsi = { path = "../../" } 20 | 21 | [workspace] 22 | -------------------------------------------------------------------------------- /src/dcs/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! dcs_basic_command { 2 | ( 3 | #[doc = $tt:tt] 4 | $instr_name:ident, 5 | $instr:expr 6 | ) => { 7 | #[doc = $tt] 8 | pub struct $instr_name; 9 | 10 | impl DcsCommand for $instr_name { 11 | fn instruction(&self) -> u8 { 12 | $instr 13 | } 14 | 15 | fn fill_params_buf(&self, _buffer: &mut [u8]) -> usize { 16 | 0 17 | } 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/rustfmt.yaml: -------------------------------------------------------------------------------- 1 | 2 | on: 3 | push: 4 | branches: [ staging, trying, master ] 5 | pull_request: 6 | 7 | name: Code formatting check 8 | 9 | jobs: 10 | fmt: 11 | name: Rustfmt 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | profile: minimal 18 | toolchain: stable 19 | override: true 20 | components: rustfmt 21 | - uses: actions-rs/cargo@v1 22 | with: 23 | command: fmt 24 | args: --all -- --check -------------------------------------------------------------------------------- /examples/spi-ili9486-esp32-c3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spi_ili9486_esp32c3" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | mipidsi = { path = "../../" } 8 | hal = { package = "esp-hal", version = "0.21", features = ["esp32c3"] } 9 | esp-backtrace = { version = "0.14", features = [ 10 | "esp32c3", 11 | "panic-handler", 12 | "exception-handler", 13 | "println", 14 | ] } 15 | esp-println = { version = "0.12", features = ["esp32c3"] } 16 | embedded-graphics = "0.8.0" 17 | fugit = "0.3.7" 18 | embedded-hal-bus = "0.2.0" 19 | 20 | [profile.dev.package.esp-hal] 21 | debug = false 22 | 23 | [workspace] 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mipidsi" 3 | description = "MIPI Display Command Set compatible generic driver" 4 | version = "0.9.0" 5 | authors = ["Ales Katona "] 6 | edition = "2021" 7 | license = "MIT" 8 | repository = "https://github.com/almindor/mipidsi" 9 | keywords = ["embedded-hal-driver", "mipi", "dcs", "display"] 10 | readme = "README.md" 11 | documentation = "https://docs.rs/mipidsi" 12 | rust-version = "1.75" 13 | 14 | [dependencies] 15 | defmt = { version = "1.0.1", optional = true } 16 | embedded-graphics-core = "0.4.0" 17 | embedded-hal = "1.0.0" 18 | 19 | [dependencies.heapless] 20 | optional = true 21 | version = "0.8.0" 22 | 23 | [dev-dependencies] 24 | embedded-graphics = "0.8.1" 25 | 26 | [features] 27 | default = ["batch"] 28 | batch = ["heapless"] 29 | defmt = ["dep:defmt", "embedded-hal/defmt-03"] 30 | 31 | [workspace] 32 | members = ["mipidsi-async"] 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Aleš Katona. 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Check troubleshooting** 11 | Before submitting a new issue, please check the [troubleshooting guide](https://github.com/almindor/mipidsi/blob/master/docs/TROUBLESHOOTING.md) to make sure it hasn't already been addressed there. 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the bug 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Used hardware and software** 26 | Please provide details about the hardware and software you are using: 27 | - `mipidsi` version: e.g. latest `master` or `0.9.1` 28 | - MCU: e.g. ESP32S3 29 | - HAL: e.g. esp_hal 0.23.1 30 | - Display Controller IC: e.g. ILI9341 31 | - Type of connection: e.g. SPI 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /src/dcs/set_scroll_start.rs: -------------------------------------------------------------------------------- 1 | //! Module for the VSCAD visual scroll offset instruction constructors 2 | 3 | use super::DcsCommand; 4 | 5 | /// Set Scroll Start 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 8 | pub struct SetScrollStart(u16); 9 | 10 | impl SetScrollStart { 11 | /// Creates a new Set Scroll Start command. 12 | pub const fn new(offset: u16) -> Self { 13 | Self(offset) 14 | } 15 | } 16 | 17 | impl DcsCommand for SetScrollStart { 18 | fn instruction(&self) -> u8 { 19 | 0x37 20 | } 21 | 22 | fn fill_params_buf(&self, buffer: &mut [u8]) -> usize { 23 | let bytes = self.0.to_be_bytes(); 24 | buffer[0] = bytes[0]; 25 | buffer[1] = bytes[1]; 26 | 27 | 2 28 | } 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use super::*; 34 | 35 | #[test] 36 | fn vscad_fills_offset_properly() { 37 | let vscad = SetScrollStart::new(320); 38 | 39 | let mut buffer = [0u8; 2]; 40 | assert_eq!(vscad.fill_params_buf(&mut buffer), 2); 41 | assert_eq!(buffer, [0x1, 0x40]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/parallel_ili9341_rp_pico/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 2 | # Choose a default "cargo run" tool: 3 | # - probe-run provides flashing and defmt via a hardware debugger 4 | # - cargo embed offers flashing, rtt, defmt and a gdb server via a hardware debugger 5 | # it is configured via the Embed.toml in the root of this project 6 | # - elf2uf2-rs loads firmware over USB when the rp2040 is in boot mode 7 | # runner = "probe-run --chip RP2040" 8 | # runner = "cargo embed" 9 | runner = "elf2uf2-rs -d" 10 | 11 | rustflags = [ 12 | "-C", "linker=flip-link", 13 | "-C", "link-arg=--nmagic", 14 | "-C", "link-arg=-Tlink.x", 15 | "-C", "link-arg=-Tdefmt.x", 16 | 17 | # Code-size optimizations. 18 | # trap unreachable can save a lot of space, but requires nightly compiler. 19 | # uncomment the next line if you wish to enable it 20 | # "-Z", "trap-unreachable=no", 21 | "-C", "inline-threshold=5", 22 | "-C", "no-vectorize-loops", 23 | ] 24 | 25 | [build] 26 | target = "thumbv6m-none-eabi" 27 | 28 | [env] 29 | DEFMT_LOG = "debug" 30 | 31 | [target.thumbv6m-none-eabi] 32 | runner = "elf2uf2-rs -d" 33 | -------------------------------------------------------------------------------- /src/dcs/set_invert_mode.rs: -------------------------------------------------------------------------------- 1 | use crate::options::ColorInversion; 2 | 3 | use super::DcsCommand; 4 | 5 | /// Set Invert Mode 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 7 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 8 | pub struct SetInvertMode(ColorInversion); 9 | 10 | impl SetInvertMode { 11 | /// Construct a new SetInvertMode DCS with the given value 12 | pub fn new(color_inversion: ColorInversion) -> Self { 13 | SetInvertMode(color_inversion) 14 | } 15 | } 16 | 17 | impl DcsCommand for SetInvertMode { 18 | fn instruction(&self) -> u8 { 19 | match self.0 { 20 | ColorInversion::Normal => 0x20, 21 | ColorInversion::Inverted => 0x21, 22 | } 23 | } 24 | 25 | fn fill_params_buf(&self, _buffer: &mut [u8]) -> usize { 26 | 0 27 | } 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | 34 | #[test] 35 | fn set_invert_mode_chooses_correct_instruction() { 36 | let ste = SetInvertMode(ColorInversion::Inverted); 37 | 38 | let mut buffer = [0u8; 0]; 39 | assert_eq!(ste.instruction(), 0x21); 40 | assert_eq!(ste.fill_params_buf(&mut buffer), 0); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/models/st7796.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics_core::pixelcolor::Rgb565; 2 | use embedded_hal::delay::DelayNs; 3 | 4 | use crate::{ 5 | dcs::SetAddressMode, 6 | interface::{Interface, InterfaceKind}, 7 | models::{Model, ModelInitError}, 8 | options::ModelOptions, 9 | ConfigurationError, 10 | }; 11 | 12 | /// ST7796 display in Rgb565 color mode. 13 | pub struct ST7796; 14 | 15 | impl Model for ST7796 { 16 | type ColorFormat = Rgb565; 17 | const FRAMEBUFFER_SIZE: (u16, u16) = (320, 480); 18 | 19 | fn init( 20 | &mut self, 21 | di: &mut DI, 22 | delay: &mut DELAY, 23 | options: &ModelOptions, 24 | ) -> Result> 25 | where 26 | DELAY: DelayNs, 27 | DI: Interface, 28 | { 29 | if !matches!( 30 | DI::KIND, 31 | InterfaceKind::Serial4Line | InterfaceKind::Parallel8Bit | InterfaceKind::Parallel16Bit 32 | ) { 33 | return Err(ModelInitError::InvalidConfiguration( 34 | ConfigurationError::UnsupportedInterface, 35 | )); 36 | } 37 | 38 | super::ST7789.init(di, delay, options) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/dcs/set_page_address.rs: -------------------------------------------------------------------------------- 1 | //! Module for the RASET address window instruction constructors 2 | 3 | use super::DcsCommand; 4 | 5 | /// Set Page Address 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 8 | pub struct SetPageAddress { 9 | start_row: u16, 10 | end_row: u16, 11 | } 12 | 13 | impl SetPageAddress { 14 | /// Creates a new Set Page Address command. 15 | pub const fn new(start_row: u16, end_row: u16) -> Self { 16 | Self { start_row, end_row } 17 | } 18 | } 19 | 20 | impl DcsCommand for SetPageAddress { 21 | fn instruction(&self) -> u8 { 22 | 0x2B 23 | } 24 | 25 | fn fill_params_buf(&self, buffer: &mut [u8]) -> usize { 26 | buffer[0..2].copy_from_slice(&self.start_row.to_be_bytes()); 27 | buffer[2..4].copy_from_slice(&self.end_row.to_be_bytes()); 28 | 29 | 4 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::*; 36 | 37 | #[test] 38 | fn raset_fills_data_properly() { 39 | let raset = SetPageAddress::new(0, 320); 40 | 41 | let mut buffer = [0u8; 4]; 42 | assert_eq!(raset.fill_params_buf(&mut buffer), 4); 43 | assert_eq!(buffer, [0, 0, 0x1, 0x40]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/colors_both_wrong.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/colors_wrong_subpixel_order.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/colors_wrong_color_inversion.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/dcs/set_column_address.rs: -------------------------------------------------------------------------------- 1 | //! Module for the CASET address window instruction constructors 2 | 3 | use super::DcsCommand; 4 | 5 | /// Set Column Address 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 8 | pub struct SetColumnAddress { 9 | start_column: u16, 10 | end_column: u16, 11 | } 12 | 13 | impl SetColumnAddress { 14 | /// Creates a new Set Column Address command. 15 | pub const fn new(start_column: u16, end_column: u16) -> Self { 16 | Self { 17 | start_column, 18 | end_column, 19 | } 20 | } 21 | } 22 | 23 | impl DcsCommand for SetColumnAddress { 24 | fn instruction(&self) -> u8 { 25 | 0x2A 26 | } 27 | 28 | fn fill_params_buf(&self, buffer: &mut [u8]) -> usize { 29 | buffer[0..2].copy_from_slice(&self.start_column.to_be_bytes()); 30 | buffer[2..4].copy_from_slice(&self.end_column.to_be_bytes()); 31 | 32 | 4 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use super::*; 39 | 40 | #[test] 41 | fn caset_fills_data_properly() { 42 | let caset = SetColumnAddress::new(0, 320); 43 | 44 | let mut buffer = [0u8; 4]; 45 | assert_eq!(caset.fill_params_buf(&mut buffer), 4); 46 | assert_eq!(buffer, [0, 0, 0x1, 0x40]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/parallel_ili9341_rp_pico/build.rs: -------------------------------------------------------------------------------- 1 | //! This build script copies the `memory.x` file from the crate root into 2 | //! a directory where the linker can always find it at build time. 3 | //! For many projects this is optional, as the linker always searches the 4 | //! project root directory -- wherever `Cargo.toml` is. However, if you 5 | //! are using a workspace or have a more complicated build setup, this 6 | //! build script becomes required. Additionally, by requesting that 7 | //! Cargo re-run the build script whenever `memory.x` is changed, 8 | //! updating `memory.x` ensures a rebuild of the application with the 9 | //! new memory settings. 10 | 11 | use std::env; 12 | use std::fs::File; 13 | use std::io::Write; 14 | use std::path::PathBuf; 15 | 16 | fn main() { 17 | // Put `memory.x` in our output directory and ensure it's 18 | // on the linker search path. 19 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 20 | File::create(out.join("memory.x")) 21 | .unwrap() 22 | .write_all(include_bytes!("memory.x")) 23 | .unwrap(); 24 | println!("cargo:rustc-link-search={}", out.display()); 25 | 26 | // By default, Cargo will re-run a build script whenever 27 | // any file in the project changes. By specifying `memory.x` 28 | // here, we ensure the build script is only re-run when 29 | // `memory.x` is changed. 30 | println!("cargo:rerun-if-changed=memory.x"); 31 | } 32 | -------------------------------------------------------------------------------- /docs/colors_correct.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/dcs/set_scroll_area.rs: -------------------------------------------------------------------------------- 1 | //! Module for the VSCRDEF visual scroll definition instruction constructors 2 | 3 | use super::DcsCommand; 4 | 5 | /// Set Scroll Area 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 8 | pub struct SetScrollArea { 9 | tfa: u16, 10 | vsa: u16, 11 | bfa: u16, 12 | } 13 | 14 | impl SetScrollArea { 15 | /// Creates a new Set Scroll Area command. 16 | /// 17 | /// VSA should default to the display's height (or width) framebuffer size. 18 | pub const fn new(tfa: u16, vsa: u16, bfa: u16) -> Self { 19 | Self { tfa, vsa, bfa } 20 | } 21 | } 22 | 23 | impl DcsCommand for SetScrollArea { 24 | fn instruction(&self) -> u8 { 25 | 0x33 26 | } 27 | 28 | fn fill_params_buf(&self, buffer: &mut [u8]) -> usize { 29 | let tfa_bytes = self.tfa.to_be_bytes(); 30 | let vsa_bytes = self.vsa.to_be_bytes(); 31 | let bfa_bytes = self.bfa.to_be_bytes(); 32 | 33 | buffer[0] = tfa_bytes[0]; 34 | buffer[1] = tfa_bytes[1]; 35 | buffer[2] = vsa_bytes[0]; 36 | buffer[3] = vsa_bytes[1]; 37 | buffer[4] = bfa_bytes[0]; 38 | buffer[5] = bfa_bytes[1]; 39 | 40 | 6 41 | } 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use super::*; 47 | 48 | #[test] 49 | fn vscrdef_fills_buffer_properly() { 50 | let vscrdef = SetScrollArea::new(0, 320, 0); 51 | 52 | let mut buffer = [0u8; 6]; 53 | assert_eq!(vscrdef.fill_params_buf(&mut buffer), 6); 54 | assert_eq!(buffer, [0, 0, 0x1, 0x40, 0, 0]); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/models/ili934x.rs: -------------------------------------------------------------------------------- 1 | use embedded_hal::delay::DelayNs; 2 | 3 | use crate::{ 4 | dcs::{ 5 | EnterNormalMode, ExitSleepMode, InterfaceExt, PixelFormat, SetAddressMode, SetDisplayOn, 6 | SetInvertMode, SetPixelFormat, 7 | }, 8 | interface::Interface, 9 | options::ModelOptions, 10 | }; 11 | 12 | /// Common init for all ILI934x controllers and color formats. 13 | pub fn init_common( 14 | di: &mut DI, 15 | delay: &mut DELAY, 16 | options: &ModelOptions, 17 | pixel_format: PixelFormat, 18 | ) -> Result 19 | where 20 | DELAY: DelayNs, 21 | DI: Interface, 22 | { 23 | let madctl = SetAddressMode::from(options); 24 | 25 | // 15.4: It is necessary to wait 5msec after releasing RESX before sending commands. 26 | // 8.2.2: It will be necessary to wait 5msec before sending new command following software reset. 27 | delay.delay_us(5_000); 28 | 29 | di.write_command(madctl)?; 30 | di.write_raw(0xB4, &[0x0])?; 31 | di.write_command(SetInvertMode::new(options.invert_colors))?; 32 | di.write_command(SetPixelFormat::new(pixel_format))?; 33 | 34 | di.write_command(EnterNormalMode)?; 35 | 36 | // 8.2.12: It will be necessary to wait 120msec after sending Sleep In command (when in Sleep Out mode) 37 | // before Sleep Out command can be sent. 38 | // The reset might have implicitly called the Sleep In command if the controller is reinitialized. 39 | delay.delay_us(120_000); 40 | 41 | di.write_command(ExitSleepMode)?; 42 | 43 | // 8.2.12: It takes 120msec to become Sleep Out mode after SLPOUT command issued. 44 | // 13.2 Power ON Sequence: Delay should be 60ms + 80ms 45 | delay.delay_us(140_000); 46 | 47 | di.write_command(SetDisplayOn)?; 48 | 49 | Ok(madctl) 50 | } 51 | -------------------------------------------------------------------------------- /examples/spi-st7789-rpi-zero-w/README.md: -------------------------------------------------------------------------------- 1 | # SPI ST7789 on a Raspberry Pi Zero W Example 2 | 3 | This example demonstrates how to use the [Display HAT Mini by Pomoroni](https://shop.pimoroni.com/products/display-hat-mini?variant=39496084717651) on a Raspberry Pi Zero W. 4 | 5 | The example shows a scrolling text and a pulsing RGB LED. 6 | 7 | Buttons: 8 | 9 | - A: change LED color 10 | - B: exit 11 | - X: move text up 12 | - Y: move text down 13 | 14 | ## Pre-requisite 15 | 16 | **Enable SPI** by any of this options: 17 | 18 | - `sudo raspi-config` 19 | - `sudo raspi-config nonint do_spi 0` 20 | - manually adding `dtparam=spi=on` to /boot/config.txt 21 | - the graphical Raspberry Pi Configuration UI 22 | 23 | [More info about SPI](https://docs.golemparts.com/rppal/0.14.1/rppal/spi/index.html#spi0) 24 | 25 | ## Build, strip, copy, and run 26 | 27 | ### Mac OS 28 | 29 | Pre-requisite: musl-cross (Homebrew users: `brew install FiloSottile/musl-cross/musl-cross --without-x86_64 --with-arm-hf`) 30 | 31 | ```bash 32 | # build for rpi zero w 33 | cargo build --release --target=arm-unknown-linux-musleabihf 34 | # look at the size of the bin file 35 | ls -lh target/arm-unknown-linux-musleabihf/release/spi-st7789-rpi-zero-w 36 | # strip it 37 | arm-linux-musleabihf-strip target/arm-unknown-linux-musleabihf/release/spi-st7789-rpi-zero-w 38 | # look at it now ;) 39 | ls -lh target/arm-unknown-linux-musleabihf/release/spi-st7789-rpi-zero-w 40 | # copy over ssh 41 | scp target/arm-unknown-linux-musleabihf/release/spi-st7789-rpi-zero-w pi@raspberrypi.local:~/ 42 | # ssh into the rpi to run it 43 | ssh pi@raspberrypi.local 44 | # run it 45 | ./spi-st7789-rpi-zero-w 46 | ``` 47 | 48 | ### Linux 49 | 50 | Not tested. Follow this article [Raspberry Pi Zero Raspbian/Rust Primer](https://dev.to/jeikabu/raspberry-pi-zero-raspbian-rust-primer-3aj6). 51 | -------------------------------------------------------------------------------- /tests/external.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics_core::pixelcolor::Rgb565; 2 | use embedded_hal::delay::DelayNs; 3 | use mipidsi::{ 4 | dcs::{ 5 | BitsPerPixel, EnterNormalMode, ExitSleepMode, InterfaceExt, PixelFormat, SetAddressMode, 6 | SetDisplayOn, SetInvertMode, SetPixelFormat, 7 | }, 8 | interface::Interface, 9 | models::{Model, ModelInitError}, 10 | options::ModelOptions, 11 | }; 12 | 13 | /// Copy of the ST7789 driver to check if it can also be implemented in another 14 | /// crate. 15 | pub struct ExternalST7789; 16 | 17 | impl Model for ExternalST7789 { 18 | type ColorFormat = Rgb565; 19 | const FRAMEBUFFER_SIZE: (u16, u16) = (240, 320); 20 | 21 | fn init( 22 | &mut self, 23 | di: &mut DI, 24 | delay: &mut DELAY, 25 | options: &ModelOptions, 26 | ) -> Result> 27 | where 28 | DELAY: DelayNs, 29 | DI: Interface, 30 | { 31 | let madctl = SetAddressMode::from(options); 32 | 33 | delay.delay_us(150_000); 34 | 35 | di.write_command(ExitSleepMode)?; 36 | delay.delay_us(10_000); 37 | 38 | // set hw scroll area based on framebuffer size 39 | di.write_command(madctl)?; 40 | 41 | di.write_command(SetInvertMode::new(options.invert_colors))?; 42 | 43 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 44 | di.write_command(SetPixelFormat::new(pf))?; 45 | delay.delay_us(10_000); 46 | di.write_command(EnterNormalMode)?; 47 | delay.delay_us(10_000); 48 | di.write_command(SetDisplayOn)?; 49 | 50 | // DISPON requires some time otherwise we risk SPI data issues 51 | delay.delay_us(120_000); 52 | 53 | Ok(madctl) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/models/ili948x.rs: -------------------------------------------------------------------------------- 1 | use embedded_hal::delay::DelayNs; 2 | 3 | use crate::{ 4 | dcs::{ 5 | EnterNormalMode, ExitSleepMode, InterfaceExt, PixelFormat, SetAddressMode, SetDisplayOn, 6 | SetInvertMode, SetPixelFormat, 7 | }, 8 | interface::Interface, 9 | models::ModelInitError, 10 | options::ModelOptions, 11 | }; 12 | 13 | /// Common init for all ILI948x models and color formats. 14 | pub fn init_common( 15 | di: &mut DI, 16 | delay: &mut DELAY, 17 | options: &ModelOptions, 18 | pixel_format: PixelFormat, 19 | ) -> Result> 20 | where 21 | DELAY: DelayNs, 22 | DI: Interface, 23 | { 24 | let madctl = SetAddressMode::from(options); 25 | di.write_command(ExitSleepMode)?; // turn off sleep 26 | di.write_command(SetPixelFormat::new(pixel_format))?; // pixel format 27 | di.write_command(madctl)?; // left -> right, bottom -> top RGB 28 | // dcs.write_command(Instruction::VCMOFSET, &[0x00, 0x48, 0x00, 0x48])?; //VCOM Control 1 [00 40 00 40] 29 | // dcs.write_command(Instruction::INVCO, &[0x0])?; //Inversion Control [00] 30 | di.write_command(SetInvertMode::new(options.invert_colors))?; 31 | 32 | // optional gamma setup 33 | // dcs.write_raw(Instruction::PGC, &[0x00, 0x2C, 0x2C, 0x0B, 0x0C, 0x04, 0x4C, 0x64, 0x36, 0x03, 0x0E, 0x01, 0x10, 0x01, 0x00])?; // Positive Gamma Control 34 | // dcs.write_raw(Instruction::NGC, &[0x0F, 0x37, 0x37, 0x0C, 0x0F, 0x05, 0x50, 0x32, 0x36, 0x04, 0x0B, 0x00, 0x19, 0x14, 0x0F])?; // Negative Gamma Control 35 | 36 | di.write_raw(0xB6, &[0b0000_0010, 0x02, 0x3B])?; // DFC 37 | di.write_command(EnterNormalMode)?; // turn to normal mode 38 | di.write_command(SetDisplayOn)?; // turn on display 39 | 40 | // DISPON requires some time otherwise we risk SPI data issues 41 | delay.delay_us(120_000); 42 | 43 | Ok(madctl) 44 | } 45 | -------------------------------------------------------------------------------- /src/dcs/set_tearing_effect.rs: -------------------------------------------------------------------------------- 1 | use crate::options::TearingEffect; 2 | 3 | use super::DcsCommand; 4 | 5 | /// Set Tearing Effect 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 7 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 8 | pub struct SetTearingEffect(TearingEffect); 9 | 10 | impl SetTearingEffect { 11 | /// Construct a new SetTearingEffect DCS with the given value 12 | pub fn new(tearing_effect: TearingEffect) -> Self { 13 | SetTearingEffect(tearing_effect) 14 | } 15 | } 16 | 17 | impl DcsCommand for SetTearingEffect { 18 | fn instruction(&self) -> u8 { 19 | match self.0 { 20 | TearingEffect::Off => 0x34, 21 | TearingEffect::Vertical => 0x35, 22 | TearingEffect::HorizontalAndVertical => 0x35, 23 | } 24 | } 25 | 26 | fn fill_params_buf(&self, buffer: &mut [u8]) -> usize { 27 | match self.0 { 28 | TearingEffect::Off => 0, 29 | TearingEffect::Vertical => { 30 | buffer[0] = 0x0; 31 | 1 32 | } 33 | TearingEffect::HorizontalAndVertical => { 34 | buffer[0] = 0x1; 35 | 1 36 | } 37 | } 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn set_tearing_effect_both_fills_param_properly() { 47 | let ste = SetTearingEffect(TearingEffect::HorizontalAndVertical); 48 | 49 | let mut buffer = [0u8; 1]; 50 | assert_eq!(ste.instruction(), 0x35); 51 | assert_eq!(ste.fill_params_buf(&mut buffer), 1); 52 | assert_eq!(buffer, [0x1]); 53 | } 54 | 55 | #[test] 56 | fn set_tearing_effect_off_fills_param_properly() { 57 | let ste = SetTearingEffect(TearingEffect::Off); 58 | 59 | let mut buffer = [0u8; 0]; 60 | assert_eq!(ste.instruction(), 0x34); 61 | assert_eq!(ste.fill_params_buf(&mut buffer), 0); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/models/st7789.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics_core::pixelcolor::Rgb565; 2 | use embedded_hal::delay::DelayNs; 3 | 4 | use crate::{ 5 | dcs::{ 6 | BitsPerPixel, EnterNormalMode, ExitSleepMode, InterfaceExt, PixelFormat, SetAddressMode, 7 | SetDisplayOn, SetInvertMode, SetPixelFormat, 8 | }, 9 | interface::{Interface, InterfaceKind}, 10 | models::{Model, ModelInitError}, 11 | options::ModelOptions, 12 | ConfigurationError, 13 | }; 14 | 15 | /// ST7789 display in Rgb565 color mode. 16 | pub struct ST7789; 17 | 18 | impl Model for ST7789 { 19 | type ColorFormat = Rgb565; 20 | const FRAMEBUFFER_SIZE: (u16, u16) = (240, 320); 21 | 22 | fn init( 23 | &mut self, 24 | di: &mut DI, 25 | delay: &mut DELAY, 26 | options: &ModelOptions, 27 | ) -> Result> 28 | where 29 | DELAY: DelayNs, 30 | DI: Interface, 31 | { 32 | if !matches!( 33 | DI::KIND, 34 | InterfaceKind::Serial4Line | InterfaceKind::Parallel8Bit | InterfaceKind::Parallel16Bit 35 | ) { 36 | return Err(ModelInitError::InvalidConfiguration( 37 | ConfigurationError::UnsupportedInterface, 38 | )); 39 | } 40 | 41 | let madctl = SetAddressMode::from(options); 42 | 43 | delay.delay_us(150_000); 44 | 45 | di.write_command(ExitSleepMode)?; 46 | delay.delay_us(10_000); 47 | 48 | // set hw scroll area based on framebuffer size 49 | di.write_command(madctl)?; 50 | 51 | di.write_command(SetInvertMode::new(options.invert_colors))?; 52 | 53 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 54 | di.write_command(SetPixelFormat::new(pf))?; 55 | delay.delay_us(10_000); 56 | di.write_command(EnterNormalMode)?; 57 | delay.delay_us(10_000); 58 | di.write_command(SetDisplayOn)?; 59 | 60 | // DISPON requires some time otherwise we risk SPI data issues 61 | delay.delay_us(120_000); 62 | 63 | Ok(madctl) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/models/ili9341.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics_core::pixelcolor::{Rgb565, Rgb666}; 2 | use embedded_hal::delay::DelayNs; 3 | 4 | use crate::{ 5 | dcs::{BitsPerPixel, PixelFormat, SetAddressMode}, 6 | interface::{Interface, InterfaceKind}, 7 | models::{ili934x, Model, ModelInitError}, 8 | options::ModelOptions, 9 | ConfigurationError, 10 | }; 11 | 12 | /// ILI9341 display in Rgb565 color mode. 13 | pub struct ILI9341Rgb565; 14 | 15 | /// ILI9341 display in Rgb666 color mode. 16 | pub struct ILI9341Rgb666; 17 | 18 | impl Model for ILI9341Rgb565 { 19 | type ColorFormat = Rgb565; 20 | const FRAMEBUFFER_SIZE: (u16, u16) = (240, 320); 21 | 22 | fn init( 23 | &mut self, 24 | di: &mut DI, 25 | delay: &mut DELAY, 26 | options: &ModelOptions, 27 | ) -> Result> 28 | where 29 | DELAY: DelayNs, 30 | DI: Interface, 31 | { 32 | if !matches!( 33 | DI::KIND, 34 | InterfaceKind::Serial4Line | InterfaceKind::Parallel8Bit | InterfaceKind::Parallel16Bit 35 | ) { 36 | return Err(ModelInitError::InvalidConfiguration( 37 | ConfigurationError::UnsupportedInterface, 38 | )); 39 | } 40 | 41 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 42 | ili934x::init_common(di, delay, options, pf).map_err(Into::into) 43 | } 44 | } 45 | 46 | impl Model for ILI9341Rgb666 { 47 | type ColorFormat = Rgb666; 48 | const FRAMEBUFFER_SIZE: (u16, u16) = (240, 320); 49 | 50 | fn init( 51 | &mut self, 52 | di: &mut DI, 53 | delay: &mut DELAY, 54 | options: &ModelOptions, 55 | ) -> Result> 56 | where 57 | DELAY: DelayNs, 58 | DI: Interface, 59 | { 60 | if !matches!( 61 | DI::KIND, 62 | InterfaceKind::Serial4Line | InterfaceKind::Parallel8Bit | InterfaceKind::Parallel16Bit 63 | ) { 64 | return Err(ModelInitError::InvalidConfiguration( 65 | ConfigurationError::UnsupportedInterface, 66 | )); 67 | } 68 | 69 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 70 | ili934x::init_common(di, delay, options, pf).map_err(Into::into) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/models/ili9342c.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics_core::pixelcolor::{Rgb565, Rgb666}; 2 | use embedded_hal::delay::DelayNs; 3 | 4 | use crate::{ 5 | dcs::{BitsPerPixel, PixelFormat, SetAddressMode}, 6 | interface::{Interface, InterfaceKind}, 7 | models::{ili934x, Model, ModelInitError}, 8 | options::ModelOptions, 9 | ConfigurationError, 10 | }; 11 | 12 | /// ILI9342C display in Rgb565 color mode. 13 | pub struct ILI9342CRgb565; 14 | 15 | /// ILI9342C display in Rgb666 color mode. 16 | pub struct ILI9342CRgb666; 17 | 18 | impl Model for ILI9342CRgb565 { 19 | type ColorFormat = Rgb565; 20 | const FRAMEBUFFER_SIZE: (u16, u16) = (320, 240); 21 | 22 | fn init( 23 | &mut self, 24 | di: &mut DI, 25 | delay: &mut DELAY, 26 | options: &ModelOptions, 27 | ) -> Result> 28 | where 29 | DELAY: DelayNs, 30 | DI: Interface, 31 | { 32 | if !matches!( 33 | DI::KIND, 34 | InterfaceKind::Serial4Line | InterfaceKind::Parallel8Bit | InterfaceKind::Parallel16Bit 35 | ) { 36 | return Err(ModelInitError::InvalidConfiguration( 37 | ConfigurationError::UnsupportedInterface, 38 | )); 39 | } 40 | 41 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 42 | ili934x::init_common(di, delay, options, pf).map_err(Into::into) 43 | } 44 | } 45 | 46 | impl Model for ILI9342CRgb666 { 47 | type ColorFormat = Rgb666; 48 | const FRAMEBUFFER_SIZE: (u16, u16) = (320, 240); 49 | 50 | fn init( 51 | &mut self, 52 | di: &mut DI, 53 | delay: &mut DELAY, 54 | options: &ModelOptions, 55 | ) -> Result> 56 | where 57 | DELAY: DelayNs, 58 | DI: Interface, 59 | { 60 | if !matches!( 61 | DI::KIND, 62 | InterfaceKind::Serial4Line | InterfaceKind::Parallel8Bit | InterfaceKind::Parallel16Bit 63 | ) { 64 | return Err(ModelInitError::InvalidConfiguration( 65 | ConfigurationError::UnsupportedInterface, 66 | )); 67 | } 68 | 69 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 70 | ili934x::init_common(di, delay, options, pf).map_err(Into::into) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/models/ili9486.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics_core::pixelcolor::{Rgb565, Rgb666}; 2 | use embedded_hal::delay::DelayNs; 3 | 4 | use crate::{ 5 | dcs::{BitsPerPixel, PixelFormat, SetAddressMode}, 6 | interface::{Interface, InterfaceKind}, 7 | models::{ili948x, Model, ModelInitError}, 8 | options::ModelOptions, 9 | ConfigurationError, 10 | }; 11 | 12 | /// ILI9486 display in Rgb565 color mode. 13 | pub struct ILI9486Rgb565; 14 | 15 | /// ILI9486 display in Rgb666 color mode. 16 | pub struct ILI9486Rgb666; 17 | 18 | impl Model for ILI9486Rgb565 { 19 | type ColorFormat = Rgb565; 20 | const FRAMEBUFFER_SIZE: (u16, u16) = (320, 480); 21 | 22 | fn init( 23 | &mut self, 24 | di: &mut DI, 25 | delay: &mut DELAY, 26 | options: &ModelOptions, 27 | ) -> Result> 28 | where 29 | DELAY: DelayNs, 30 | DI: Interface, 31 | { 32 | if !matches!( 33 | DI::KIND, 34 | InterfaceKind::Parallel8Bit | InterfaceKind::Parallel16Bit 35 | ) { 36 | return Err(ModelInitError::InvalidConfiguration( 37 | ConfigurationError::UnsupportedInterface, 38 | )); 39 | } 40 | 41 | delay.delay_us(120_000); 42 | 43 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 44 | ili948x::init_common(di, delay, options, pf) 45 | } 46 | } 47 | 48 | impl Model for ILI9486Rgb666 { 49 | type ColorFormat = Rgb666; 50 | const FRAMEBUFFER_SIZE: (u16, u16) = (320, 480); 51 | 52 | fn init( 53 | &mut self, 54 | di: &mut DI, 55 | delay: &mut DELAY, 56 | options: &ModelOptions, 57 | ) -> Result> 58 | where 59 | DELAY: DelayNs, 60 | DI: Interface, 61 | { 62 | if !matches!( 63 | DI::KIND, 64 | InterfaceKind::Serial4Line | InterfaceKind::Parallel8Bit | InterfaceKind::Parallel16Bit 65 | ) { 66 | return Err(ModelInitError::InvalidConfiguration( 67 | ConfigurationError::UnsupportedInterface, 68 | )); 69 | } 70 | 71 | delay.delay_us(120_000); 72 | 73 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 74 | ili948x::init_common(di, delay, options, pf) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/models/ili9488.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics_core::pixelcolor::{Rgb565, Rgb666}; 2 | use embedded_hal::delay::DelayNs; 3 | 4 | use crate::{ 5 | dcs::{BitsPerPixel, PixelFormat, SetAddressMode}, 6 | interface::{Interface, InterfaceKind}, 7 | models::{ili948x, Model, ModelInitError}, 8 | options::ModelOptions, 9 | ConfigurationError, 10 | }; 11 | 12 | /// ILI9488 display in Rgb565 color mode. 13 | pub struct ILI9488Rgb565; 14 | 15 | /// ILI9488 display in Rgb666 color mode. 16 | pub struct ILI9488Rgb666; 17 | 18 | impl Model for ILI9488Rgb565 { 19 | type ColorFormat = Rgb565; 20 | const FRAMEBUFFER_SIZE: (u16, u16) = (320, 480); 21 | 22 | fn init( 23 | &mut self, 24 | di: &mut DI, 25 | delay: &mut DELAY, 26 | options: &ModelOptions, 27 | ) -> Result> 28 | where 29 | DELAY: DelayNs, 30 | DI: Interface, 31 | { 32 | if !matches!( 33 | DI::KIND, 34 | InterfaceKind::Serial4Line | InterfaceKind::Parallel8Bit | InterfaceKind::Parallel16Bit 35 | ) { 36 | return Err(ModelInitError::InvalidConfiguration( 37 | ConfigurationError::UnsupportedInterface, 38 | )); 39 | } 40 | 41 | delay.delay_us(120_000); 42 | 43 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 44 | ili948x::init_common(di, delay, options, pf) 45 | } 46 | } 47 | 48 | impl Model for ILI9488Rgb666 { 49 | type ColorFormat = Rgb666; 50 | const FRAMEBUFFER_SIZE: (u16, u16) = (320, 480); 51 | 52 | fn init( 53 | &mut self, 54 | di: &mut DI, 55 | delay: &mut DELAY, 56 | options: &ModelOptions, 57 | ) -> Result> 58 | where 59 | DELAY: DelayNs, 60 | DI: Interface, 61 | { 62 | if !matches!( 63 | DI::KIND, 64 | InterfaceKind::Serial4Line | InterfaceKind::Parallel8Bit | InterfaceKind::Parallel16Bit 65 | ) { 66 | return Err(ModelInitError::InvalidConfiguration( 67 | ConfigurationError::UnsupportedInterface, 68 | )); 69 | } 70 | 71 | delay.delay_us(120_000); 72 | 73 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 74 | ili948x::init_common(di, delay, options, pf) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /assets/test_image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/rm67162.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics_core::pixelcolor::Rgb565; 2 | use embedded_hal::delay::DelayNs; 3 | 4 | use crate::{ 5 | dcs::{ 6 | BitsPerPixel, ExitSleepMode, InterfaceExt, PixelFormat, SetAddressMode, SetDisplayOn, 7 | SetInvertMode, SetPixelFormat, 8 | }, 9 | interface::{Interface, InterfaceKind}, 10 | models::{Model, ModelInitError}, 11 | options::ModelOptions, 12 | ConfigurationError, 13 | }; 14 | 15 | /// RM67162 AMOLED display driver implementation 16 | /// 17 | /// Supports: 18 | /// - 16-bit RGB565 color 19 | /// - 240x536 resolution 20 | /// 21 | /// This driver was developed for the Lilygo T-Display-S3 AMOLED display (v2). 22 | /// The initialization sequence is based on Lilygo's Arduino example code. 23 | /// 24 | /// Currently only tested with 240x536 resolution displays. 25 | /// While it may work with other display sizes, this is untested and could lead to unexpected behavior. 26 | /// If you encounter issues with different display sizes, please report them. 27 | /// 28 | pub struct RM67162; 29 | 30 | impl Model for RM67162 { 31 | type ColorFormat = Rgb565; 32 | const FRAMEBUFFER_SIZE: (u16, u16) = (240, 536); 33 | 34 | fn init( 35 | &mut self, 36 | di: &mut DI, 37 | delay: &mut DELAY, 38 | options: &ModelOptions, 39 | ) -> Result> 40 | where 41 | DELAY: DelayNs, 42 | DI: Interface, 43 | { 44 | if !matches!( 45 | DI::KIND, 46 | InterfaceKind::Serial4Line | InterfaceKind::Parallel8Bit 47 | ) { 48 | return Err(ModelInitError::InvalidConfiguration( 49 | ConfigurationError::UnsupportedInterface, 50 | )); 51 | } 52 | 53 | let madctl = SetAddressMode::from(options); 54 | 55 | di.write_raw(0xFE, &[0x04])?; 56 | di.write_raw(0x6A, &[0x00])?; 57 | di.write_raw(0xFE, &[0x05])?; 58 | di.write_raw(0xFE, &[0x07])?; 59 | di.write_raw(0x07, &[0x4F])?; 60 | di.write_raw(0xFE, &[0x01])?; 61 | di.write_raw(0x2A, &[0x02])?; 62 | di.write_raw(0x2B, &[0x73])?; 63 | di.write_raw(0xFE, &[0x0A])?; 64 | di.write_raw(0x29, &[0x10])?; 65 | di.write_raw(0xFE, &[0x00])?; 66 | di.write_raw(0x51, &[0xaf])?; // Set brightness 67 | di.write_raw(0x53, &[0x20])?; 68 | di.write_raw(0x35, &[0x00])?; 69 | 70 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 71 | di.write_command(SetPixelFormat::new(pf))?; 72 | 73 | di.write_raw(0xC4, &[0x80])?; // enable SRAM access via SPI 74 | 75 | di.write_command(madctl)?; 76 | 77 | di.write_command(SetInvertMode::new(options.invert_colors))?; 78 | 79 | di.write_command(ExitSleepMode)?; 80 | delay.delay_us(120_000); 81 | 82 | di.write_command(SetDisplayOn)?; 83 | 84 | Ok(madctl) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ staging, trying, master ] 4 | pull_request: 5 | 6 | name: Continuous integration 7 | 8 | jobs: 9 | ci-linux: 10 | runs-on: ubuntu-22.04 11 | continue-on-error: ${{ matrix.experimental || false }} 12 | strategy: 13 | matrix: 14 | # All generated code should be running on stable now, MSRV is 1.75.0 15 | rust: [nightly, stable, 1.75.0] 16 | buildflags: [--no-default-features, ""] 17 | 18 | include: 19 | # Nightly is only for reference and allowed to fail 20 | - rust: nightly 21 | experimental: true 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: dtolnay/rust-toolchain@master 26 | with: 27 | toolchain: ${{ matrix.rust }} 28 | - name: Run CI script for x86_64-unknown-linux-gnu 29 | run: | 30 | cargo check ${{ matrix.buildflags }} 31 | - name: Run tests 32 | run: | 33 | cargo test 34 | 35 | # On macOS and Windows, we at least make sure that the crate builds and links. 36 | build-other: 37 | strategy: 38 | matrix: 39 | os: 40 | - macOS-latest 41 | - windows-latest 42 | buildflags: [--no-default-features, ""] 43 | runs-on: ${{ matrix.os }} 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: dtolnay/rust-toolchain@stable 48 | - name: Build crate for host OS 49 | run: | 50 | cargo build ${{ matrix.buildflags }} 51 | - name: Test on host OS 52 | run: | 53 | cargo test 54 | 55 | build-example: 56 | name: Build example 57 | runs-on: ubuntu-22.04 58 | strategy: 59 | fail-fast: false 60 | matrix: 61 | example: 62 | - parallel_ili9341_rp_pico 63 | - spi-ili9486-esp32-c3 64 | - spi-st7789-rpi-zero-w 65 | include: 66 | - target: thumbv6m-none-eabi 67 | example: parallel_ili9341_rp_pico 68 | - target: riscv32imc-unknown-none-elf 69 | example: spi-ili9486-esp32-c3 70 | - target: arm-unknown-linux-gnueabihf 71 | example: spi-st7789-rpi-zero-w 72 | 73 | steps: 74 | - uses: actions/checkout@v4 75 | - uses: dtolnay/rust-toolchain@stable 76 | with: 77 | target: ${{ matrix.target }} 78 | - if: ${{ startsWith(matrix.target, 'arm') }} 79 | run: | 80 | sudo apt-get update 81 | sudo apt-get install gcc-arm-linux-gnueabihf 82 | - run: cargo install flip-link 83 | - name: Build ${{ matrix.example }} example 84 | run: cargo build --target ${{ matrix.target }} 85 | working-directory: ./examples/${{ matrix.example }} 86 | 87 | build-docs: 88 | runs-on: ubuntu-22.04 89 | 90 | steps: 91 | - uses: actions/checkout@v4 92 | - uses: dtolnay/rust-toolchain@stable 93 | - name: Build docs 94 | run: | 95 | RUSTDOCFLAGS=-Dwarnings cargo doc 96 | -------------------------------------------------------------------------------- /src/models/gc9107.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics_core::pixelcolor::Rgb565; 2 | use embedded_hal::delay::DelayNs; 3 | 4 | use crate::{ 5 | dcs::{ 6 | BitsPerPixel, ExitSleepMode, InterfaceExt, PixelFormat, SetAddressMode, SetDisplayOn, 7 | SetInvertMode, SetPixelFormat, 8 | }, 9 | interface::{Interface, InterfaceKind}, 10 | models::{Model, ModelInitError}, 11 | options::ModelOptions, 12 | ConfigurationError, 13 | }; 14 | 15 | /// GC9107 display in Rgb565 color mode. 16 | pub struct GC9107; 17 | 18 | impl Model for GC9107 { 19 | type ColorFormat = Rgb565; 20 | const FRAMEBUFFER_SIZE: (u16, u16) = (128, 160); 21 | 22 | fn init( 23 | &mut self, 24 | di: &mut DI, 25 | delay: &mut DELAY, 26 | options: &ModelOptions, 27 | ) -> Result> 28 | where 29 | DELAY: DelayNs, 30 | DI: Interface, 31 | { 32 | if !matches!( 33 | DI::KIND, 34 | InterfaceKind::Serial4Line | InterfaceKind::Parallel8Bit 35 | ) { 36 | return Err(ModelInitError::InvalidConfiguration( 37 | ConfigurationError::UnsupportedInterface, 38 | )); 39 | } 40 | 41 | delay.delay_ms(200); 42 | 43 | di.write_raw(0xFE, &[])?; 44 | delay.delay_ms(5); 45 | di.write_raw(0xEF, &[])?; 46 | delay.delay_ms(5); 47 | 48 | di.write_raw(0xB0, &[0xC0])?; 49 | di.write_raw(0xB2, &[0x2F])?; 50 | di.write_raw(0xB3, &[0x03])?; 51 | di.write_raw(0xB6, &[0x19])?; 52 | di.write_raw(0xB7, &[0x01])?; 53 | 54 | let madctl = SetAddressMode::from(options); 55 | di.write_command(madctl)?; 56 | 57 | di.write_raw(0xAC, &[0xCB])?; 58 | di.write_raw(0xAB, &[0x0E])?; 59 | 60 | di.write_raw(0xB4, &[0x04])?; 61 | 62 | di.write_raw(0xA8, &[0x19])?; 63 | 64 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 65 | di.write_command(SetPixelFormat::new(pf))?; 66 | 67 | di.write_raw(0xB8, &[0x08])?; 68 | 69 | di.write_raw(0xE8, &[0x24])?; 70 | 71 | di.write_raw(0xE9, &[0x48])?; 72 | 73 | di.write_raw(0xEA, &[0x22])?; 74 | 75 | di.write_raw(0xC6, &[0x30])?; 76 | di.write_raw(0xC7, &[0x18])?; 77 | 78 | di.write_raw( 79 | 0xF0, 80 | &[ 81 | 0x01, 0x2b, 0x23, 0x3c, 0xb7, 0x12, 0x17, 0x60, 0x00, 0x06, 0x0c, 0x17, 0x12, 0x1f, 82 | ], 83 | )?; 84 | 85 | di.write_raw( 86 | 0xF1, 87 | &[ 88 | 0x05, 0x2e, 0x2d, 0x44, 0xd6, 0x15, 0x17, 0xa0, 0x02, 0x0d, 0x0d, 0x1a, 0x18, 0x1f, 89 | ], 90 | )?; 91 | 92 | di.write_command(SetInvertMode::new(options.invert_colors))?; 93 | 94 | di.write_command(ExitSleepMode)?; // turn off sleep 95 | delay.delay_ms(120); 96 | 97 | di.write_command(SetDisplayOn)?; // turn on display 98 | 99 | Ok(madctl) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/models/st7735s.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics_core::pixelcolor::Rgb565; 2 | use embedded_hal::delay::DelayNs; 3 | 4 | use crate::{ 5 | dcs::{ 6 | BitsPerPixel, ExitSleepMode, InterfaceExt, PixelFormat, SetAddressMode, SetDisplayOn, 7 | SetInvertMode, SetPixelFormat, 8 | }, 9 | interface::{Interface, InterfaceKind}, 10 | models::{Model, ModelInitError}, 11 | options::ModelOptions, 12 | ConfigurationError, 13 | }; 14 | 15 | /// ST7735s display in Rgb565 color mode. 16 | pub struct ST7735s; 17 | 18 | impl Model for ST7735s { 19 | type ColorFormat = Rgb565; 20 | const FRAMEBUFFER_SIZE: (u16, u16) = (132, 162); 21 | 22 | fn init( 23 | &mut self, 24 | di: &mut DI, 25 | delay: &mut DELAY, 26 | options: &ModelOptions, 27 | ) -> Result> 28 | where 29 | DELAY: DelayNs, 30 | DI: Interface, 31 | { 32 | if !matches!( 33 | DI::KIND, 34 | InterfaceKind::Serial4Line | InterfaceKind::Parallel8Bit | InterfaceKind::Parallel16Bit 35 | ) { 36 | return Err(ModelInitError::InvalidConfiguration( 37 | ConfigurationError::UnsupportedInterface, 38 | )); 39 | } 40 | 41 | let madctl = SetAddressMode::from(options); 42 | 43 | delay.delay_us(200_000); 44 | 45 | di.write_command(ExitSleepMode)?; // turn off sleep 46 | delay.delay_us(120_000); 47 | 48 | di.write_command(SetInvertMode::new(options.invert_colors))?; // set color inversion 49 | di.write_raw(0xB1, &[0x05, 0x3A, 0x3A])?; // set frame rate 50 | di.write_raw(0xB2, &[0x05, 0x3A, 0x3A])?; // set frame rate 51 | di.write_raw(0xB3, &[0x05, 0x3A, 0x3A, 0x05, 0x3A, 0x3A])?; // set frame rate 52 | di.write_raw(0xB4, &[0b0000_0011])?; // set inversion control 53 | di.write_raw(0xC0, &[0x62, 0x02, 0x04])?; // set power control 1 54 | di.write_raw(0xC1, &[0xC0])?; // set power control 2 55 | di.write_raw(0xC2, &[0x0D, 0x00])?; // set power control 3 56 | di.write_raw(0xC3, &[0x8D, 0x6A])?; // set power control 4 57 | di.write_raw(0xC4, &[0x8D, 0xEE])?; // set power control 5 58 | di.write_raw(0xC5, &[0x0E])?; // set VCOM control 1 59 | di.write_raw( 60 | 0xE0, 61 | &[ 62 | 0x10, 0x0E, 0x02, 0x03, 0x0E, 0x07, 0x02, 0x07, 0x0A, 0x12, 0x27, 0x37, 0x00, 0x0D, 63 | 0x0E, 0x10, 64 | ], 65 | )?; // set GAMMA +Polarity characteristics 66 | di.write_raw( 67 | 0xE1, 68 | &[ 69 | 0x10, 0x0E, 0x03, 0x03, 0x0F, 0x06, 0x02, 0x08, 0x0A, 0x13, 0x26, 0x36, 0x00, 0x0D, 70 | 0x0E, 0x10, 71 | ], 72 | )?; // set GAMMA -Polarity characteristics 73 | 74 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 75 | di.write_command(SetPixelFormat::new(pf))?; // set interface pixel format, 16bit pixel into frame memory 76 | 77 | di.write_command(madctl)?; // set memory data access control, Top -> Bottom, RGB, Left -> Right 78 | di.write_command(SetDisplayOn)?; // turn on display 79 | 80 | Ok(madctl) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mipidsi 2 | 3 |

4 | Crates.io 5 | Docs.rs 6 | Discuss driver on embedded-graphics on Matrix 7 |

8 | 9 | This crate provides a generic display driver to connect to TFT displays 10 | that implement the [MIPI Display Command Set](https://www.mipi.org/specifications/display-command-set). 11 | 12 | Uses `interface::Interface` to talk to the hardware via transports (currently SPI and Parallel GPIO). 13 | 14 | An optional batching of draws is supported via the `batch` feature (default on) 15 | 16 | _NOTES_: 17 | 18 | - The name of this crate is a bit unfortunate as this driver works with displays that use the MIPI Display Command Set but MIPI Display Serial Interface is NOT supported at this time. 19 | 20 | ## License 21 | 22 | Licensed under MIT license ([LICENSE](LICENSE) or http://opensource.org/licenses/MIT). 23 | 24 | ## Architecture 25 | 26 | The `Display` driver itself contains most of the functionality. Each specific display model implements the `Model` trait for every color format it supports. Each model can also have different variants which are handled via the `Builder` struct. 27 | 28 | [embedded-graphics-core](https://crates.io/crates/embedded-graphics-core) is used to provide the drawing API. 29 | 30 | ## Models 31 | 32 | Each supported display model can be used either through the `Builder::with_model` call or through a shortcut function such as `Builder::st7789` if provided. External crates can be used to provide additional models, and can even expand the display constructor pool via trait extension. 33 | 34 | Variants that require different screen sizes and window addressing offsets are now supported via the `Builder` logic as well (see docs). 35 | 36 | ### List of supported models 37 | 38 | - GC9107 39 | - GC9A01 40 | - ILI9341 41 | - ILI9342C 42 | - ILI9486 43 | - ILI9488 44 | - RM67162 45 | - ST7735 46 | - ST7789 47 | - ST7796 48 | 49 | ## Troubleshooting 50 | 51 | Refer to the [troubleshooting guide](https://docs.rs/mipidsi/latest/mipidsi/_troubleshooting/index.html) if you experience problems like a blank screen or incorrect colors. 52 | 53 | ## Example 54 | 55 | ```rust 56 | // create a SpiInterface from SpiDevice, a DC pin and a buffer 57 | let mut buffer = [0u8; 512]; 58 | let di = SpiInterface::new(spi, dc, &mut buffer); 59 | // create the ILI9486 display driver in rgb666 color mode from the display interface and use a HW reset pin during init 60 | let mut display = Builder::new(ILI9486Rgb666, di) 61 | .reset_pin(rst) 62 | .init(&mut delay)?; // delay provider from your MCU 63 | // clear the display to black 64 | display.clear(Rgb666::BLACK)?; 65 | ``` 66 | 67 | Refer to the [examples directory](./examples/) and to the [crate documentation](https://docs.rs/mipidsi) for more complete examples. 68 | 69 | ## Migration 70 | 71 | See [MIGRATION.md](./docs/MIGRATION.md) document. 72 | 73 | ## Minimum Supported Rust Version (MSRV) 74 | 75 | This crate is guaranteed to compile on stable Rust 1.75.0 and up. It _might_ 76 | compile with older versions but that may change in any new patch release. 77 | -------------------------------------------------------------------------------- /src/dcs/set_pixel_format.rs: -------------------------------------------------------------------------------- 1 | //! Module for the COLMOD instruction constructors 2 | 3 | use super::DcsCommand; 4 | 5 | /// Set Pixel Format 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 7 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 8 | pub struct SetPixelFormat(PixelFormat); 9 | 10 | impl SetPixelFormat { 11 | /// Creates a new Set Pixel Format command. 12 | pub const fn new(pixel_format: PixelFormat) -> Self { 13 | Self(pixel_format) 14 | } 15 | } 16 | 17 | impl DcsCommand for SetPixelFormat { 18 | fn instruction(&self) -> u8 { 19 | 0x3A 20 | } 21 | 22 | fn fill_params_buf(&self, buffer: &mut [u8]) -> usize { 23 | buffer[0] = self.0.as_u8(); 24 | 1 25 | } 26 | } 27 | 28 | /// 29 | /// Bits per pixel for DBI and DPI fields of [PixelFormat] 30 | /// 31 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 32 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 33 | #[repr(u8)] 34 | pub enum BitsPerPixel { 35 | /// 3 bits per pixel. 36 | Three = 0b001, 37 | /// 8 bits per pixel. 38 | Eight = 0b010, 39 | /// 12 bits per pixel. 40 | Twelve = 0b011, 41 | /// 16 bits per pixel. 42 | Sixteen = 0b101, 43 | /// 18 bits per pixel. 44 | Eighteen = 0b110, 45 | /// 24 bits per pixel. 46 | TwentyFour = 0b111, 47 | } 48 | 49 | /// 50 | /// Defines pixel format as combination of DPI and DBI 51 | /// 52 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 53 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 54 | pub struct PixelFormat { 55 | dpi: BitsPerPixel, 56 | dbi: BitsPerPixel, 57 | } 58 | 59 | impl PixelFormat { 60 | /// 61 | /// Construct a new [PixelFormat] with given [BitsPerPixel] values 62 | /// for DPI and DBI fields 63 | /// 64 | pub const fn new(dpi: BitsPerPixel, dbi: BitsPerPixel) -> Self { 65 | Self { dpi, dbi } 66 | } 67 | 68 | /// 69 | /// Construct a new [PixelFormat] with same [BitsPerPixel] value 70 | /// for both DPI and DBI fields 71 | /// 72 | pub const fn with_all(bpp: BitsPerPixel) -> Self { 73 | Self { dpi: bpp, dbi: bpp } 74 | } 75 | 76 | /// 77 | /// Returns the corresponding u8 containing both DPI and DBI bits 78 | /// 79 | pub fn as_u8(&self) -> u8 { 80 | (self.dpi as u8) << 4 | (self.dbi as u8) 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use super::*; 87 | 88 | #[test] 89 | fn colmod_rgb565_is_16bit() { 90 | let colmod = SetPixelFormat::new(PixelFormat::new( 91 | BitsPerPixel::Sixteen, 92 | BitsPerPixel::Sixteen, 93 | )); 94 | 95 | let mut bytes = [0u8]; 96 | assert_eq!(colmod.fill_params_buf(&mut bytes), 1); 97 | assert_eq!(bytes, [0b0101_0101u8]); 98 | } 99 | 100 | #[test] 101 | fn colmod_rgb666_is_18bit() { 102 | let colmod = SetPixelFormat::new(PixelFormat::new( 103 | BitsPerPixel::Eighteen, 104 | BitsPerPixel::Eighteen, 105 | )); 106 | 107 | let mut bytes = [0u8]; 108 | assert_eq!(colmod.fill_params_buf(&mut bytes), 1); 109 | assert_eq!(bytes, [0b0110_0110u8]); 110 | } 111 | 112 | #[test] 113 | fn colmod_rgb888_is_24bit() { 114 | let colmod = SetPixelFormat::new(PixelFormat::new( 115 | BitsPerPixel::Eighteen, 116 | BitsPerPixel::TwentyFour, 117 | )); 118 | 119 | let mut bytes = [0u8]; 120 | assert_eq!(colmod.fill_params_buf(&mut bytes), 1); 121 | assert_eq!(bytes, [0b0110_0111u8]); 122 | } 123 | 124 | #[test] 125 | fn test_pixel_format_as_u8() { 126 | let pf = PixelFormat::new(BitsPerPixel::Sixteen, BitsPerPixel::TwentyFour); 127 | assert_eq!(pf.as_u8(), 0b0101_0111); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/interface/spi.rs: -------------------------------------------------------------------------------- 1 | use embedded_hal::{digital::OutputPin, spi::SpiDevice}; 2 | 3 | use super::{Interface, InterfaceKind}; 4 | 5 | /// Spi interface error 6 | #[derive(Clone, Copy, Debug)] 7 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 8 | pub enum SpiError { 9 | /// SPI bus error 10 | Spi(SPI), 11 | /// Data/command pin error 12 | Dc(DC), 13 | } 14 | 15 | /// Spi interface, including a buffer 16 | /// 17 | /// The buffer is used to gather batches of pixel data to be sent over SPI. 18 | /// Larger buffers will genererally be faster (with diminishing returns), at the expense of using more RAM. 19 | /// The buffer should be at least big enough to hold a few pixels of data. 20 | /// 21 | /// You may want to use [static_cell](https://crates.io/crates/static_cell) 22 | /// to obtain a `&'static mut [u8; N]` buffer. 23 | pub struct SpiInterface<'a, SPI, DC> { 24 | spi: SPI, 25 | dc: DC, 26 | buffer: &'a mut [u8], 27 | } 28 | 29 | impl<'a, SPI, DC> SpiInterface<'a, SPI, DC> 30 | where 31 | SPI: SpiDevice, 32 | DC: OutputPin, 33 | { 34 | /// Create new interface 35 | pub fn new(spi: SPI, dc: DC, buffer: &'a mut [u8]) -> Self { 36 | Self { spi, dc, buffer } 37 | } 38 | 39 | /// Release the DC pin and SPI peripheral back, deconstructing the interface 40 | pub fn release(self) -> (SPI, DC) { 41 | (self.spi, self.dc) 42 | } 43 | } 44 | 45 | impl Interface for SpiInterface<'_, SPI, DC> 46 | where 47 | SPI: SpiDevice, 48 | DC: OutputPin, 49 | { 50 | type Word = u8; 51 | type Error = SpiError; 52 | 53 | const KIND: InterfaceKind = InterfaceKind::Serial4Line; 54 | 55 | fn send_command(&mut self, command: u8, args: &[u8]) -> Result<(), Self::Error> { 56 | self.dc.set_low().map_err(SpiError::Dc)?; 57 | self.spi.write(&[command]).map_err(SpiError::Spi)?; 58 | self.dc.set_high().map_err(SpiError::Dc)?; 59 | self.spi.write(args).map_err(SpiError::Spi)?; 60 | Ok(()) 61 | } 62 | 63 | fn send_pixels( 64 | &mut self, 65 | pixels: impl IntoIterator, 66 | ) -> Result<(), Self::Error> { 67 | let mut arrays = pixels.into_iter(); 68 | 69 | assert!(self.buffer.len() >= N); 70 | 71 | let mut done = false; 72 | while !done { 73 | let mut i = 0; 74 | for chunk in self.buffer.chunks_exact_mut(N) { 75 | if let Some(array) = arrays.next() { 76 | let chunk: &mut [u8; N] = chunk.try_into().unwrap(); 77 | *chunk = array; 78 | i += N; 79 | } else { 80 | done = true; 81 | break; 82 | }; 83 | } 84 | self.spi.write(&self.buffer[..i]).map_err(SpiError::Spi)?; 85 | } 86 | Ok(()) 87 | } 88 | 89 | fn send_repeated_pixel( 90 | &mut self, 91 | pixel: [Self::Word; N], 92 | count: u32, 93 | ) -> Result<(), Self::Error> { 94 | let fill_count = core::cmp::min(count, (self.buffer.len() / N) as u32); 95 | let filled_len = fill_count as usize * N; 96 | for chunk in self.buffer[..(filled_len)].chunks_exact_mut(N) { 97 | let chunk: &mut [u8; N] = chunk.try_into().unwrap(); 98 | *chunk = pixel; 99 | } 100 | 101 | let mut count = count; 102 | while count >= fill_count { 103 | self.spi 104 | .write(&self.buffer[..filled_len]) 105 | .map_err(SpiError::Spi)?; 106 | count -= fill_count; 107 | } 108 | if count != 0 { 109 | self.spi 110 | .write(&self.buffer[..(count as usize * pixel.len())]) 111 | .map_err(SpiError::Spi)?; 112 | } 113 | Ok(()) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/dcs.rs: -------------------------------------------------------------------------------- 1 | //! MIPI DCS commands. 2 | 3 | use crate::interface::Interface; 4 | 5 | #[macro_use] 6 | pub(crate) mod macros; 7 | 8 | mod set_address_mode; 9 | pub use set_address_mode::*; 10 | mod set_pixel_format; 11 | pub use set_pixel_format::*; 12 | mod set_column_address; 13 | pub use set_column_address::*; 14 | mod set_page_address; 15 | pub use set_page_address::*; 16 | mod set_scroll_area; 17 | pub use set_scroll_area::*; 18 | mod set_scroll_start; 19 | pub use set_scroll_start::*; 20 | mod set_tearing_effect; 21 | pub use set_tearing_effect::*; 22 | mod set_invert_mode; 23 | pub use set_invert_mode::*; 24 | 25 | /// Common trait for DCS commands. 26 | /// 27 | /// The methods in this traits are used to convert a DCS command into bytes. 28 | pub trait DcsCommand { 29 | /// Returns the instruction code. 30 | fn instruction(&self) -> u8; 31 | 32 | /// Fills the given buffer with the command parameters. 33 | fn fill_params_buf(&self, buffer: &mut [u8]) -> usize; 34 | } 35 | 36 | /// An extension trait for [`Interface`] with support for writing DCS commands. 37 | /// 38 | /// Commands which are part of the manufacturer independent user command set can be sent to the 39 | /// display by using the [`write_command`](Self::write_command) method with one of the command types 40 | /// in this module. 41 | /// 42 | /// All other commands, which do not have an associated type in this module, can be sent using 43 | /// the [`write_raw`](Self::write_raw) method. 44 | pub trait InterfaceExt: Interface { 45 | /// Sends a DCS command to the display interface. 46 | fn write_command(&mut self, command: impl DcsCommand) -> Result<(), Self::Error> { 47 | let mut param_bytes: [u8; 16] = [0; 16]; 48 | let n = command.fill_params_buf(&mut param_bytes); 49 | self.write_raw(command.instruction(), ¶m_bytes[..n]) 50 | } 51 | 52 | /// Sends a raw command with the given `instruction` to the display interface. 53 | /// 54 | /// The `param_bytes` slice can contain the instruction parameters, which are sent as data after 55 | /// the instruction code was sent. If no parameters are required an empty slice can be passed to 56 | /// this method. 57 | /// 58 | /// This method is intended to be used for sending commands which are not part of the MIPI DCS 59 | /// user command set. Use [`write_command`](Self::write_command) for commands in the user 60 | /// command set. 61 | fn write_raw(&mut self, instruction: u8, param_bytes: &[u8]) -> Result<(), Self::Error> { 62 | self.send_command(instruction, param_bytes) 63 | } 64 | } 65 | 66 | impl InterfaceExt for T {} 67 | 68 | // DCS commands that don't use any parameters 69 | 70 | dcs_basic_command!( 71 | /// Software Reset 72 | SoftReset, 73 | 0x01 74 | ); 75 | dcs_basic_command!( 76 | /// Enter Sleep Mode 77 | EnterSleepMode, 78 | 0x10 79 | ); 80 | dcs_basic_command!( 81 | /// Exit Sleep Mode 82 | ExitSleepMode, 83 | 0x11 84 | ); 85 | dcs_basic_command!( 86 | /// Enter Partial Mode 87 | EnterPartialMode, 88 | 0x12 89 | ); 90 | dcs_basic_command!( 91 | /// Enter Normal Mode 92 | EnterNormalMode, 93 | 0x13 94 | ); 95 | dcs_basic_command!( 96 | /// Turn Display Off 97 | SetDisplayOff, 98 | 0x28 99 | ); 100 | 101 | dcs_basic_command!( 102 | /// Turn Display On 103 | SetDisplayOn, 104 | 0x29 105 | ); 106 | dcs_basic_command!( 107 | /// Exit Idle Mode 108 | ExitIdleMode, 109 | 0x38 110 | ); 111 | dcs_basic_command!( 112 | /// Enter Idle Mode 113 | EnterIdleMode, 114 | 0x39 115 | ); 116 | // dcs_basic_command!( 117 | // /// Turn off Color Invert Mode 118 | // ExitInvertMode, 119 | // 0x21 120 | // ); 121 | // dcs_basic_command!( 122 | // /// Turn on Color Invert Mode 123 | // EnterInvertMode, 124 | // 0x20 125 | // ); 126 | dcs_basic_command!( 127 | /// Initiate Framebuffer Memory Write 128 | WriteMemoryStart, 129 | 0x2C 130 | ); 131 | -------------------------------------------------------------------------------- /examples/spi-ili9486-esp32-c3/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use embedded_hal_bus::spi::ExclusiveDevice; 5 | /* --- Needed by ESP32-c3 --- */ 6 | use esp_backtrace as _; 7 | use hal::{ 8 | delay::Delay, 9 | gpio::{Io, Level, Output}, 10 | prelude::*, 11 | rtc_cntl::Rtc, 12 | spi::{master::Spi, SpiMode}, 13 | timer::timg::TimerGroup, 14 | }; 15 | /* -------------------------- */ 16 | 17 | use embedded_graphics::{ 18 | pixelcolor::Rgb565, 19 | prelude::*, 20 | primitives::{Circle, Primitive, PrimitiveStyle, Triangle}, 21 | }; 22 | 23 | // Provides the parallel port and display interface builders 24 | use mipidsi::interface::SpiInterface; 25 | 26 | // Provides the Display builder 27 | use mipidsi::{models::ILI9486Rgb565, Builder}; 28 | 29 | use fugit::RateExtU32; 30 | 31 | #[entry] 32 | fn main() -> ! { 33 | let peripherals = hal::init(hal::Config::default()); 34 | // let peripherals = Peripherals::take(); 35 | // let system = SystemControl::new(peripherals.SYSTEM); 36 | // let clocks = ClockControl::boot_defaults(system.clock_control).freeze(); 37 | let io = Io::new(peripherals.GPIO, peripherals.IO_MUX); 38 | 39 | // Disable the RTC and TIMG watchdog timers 40 | let mut rtc = Rtc::new(peripherals.LPWR); 41 | let timer_group0 = TimerGroup::new(peripherals.TIMG0); 42 | let mut wdt0 = timer_group0.wdt; 43 | let timer_group1 = TimerGroup::new(peripherals.TIMG1); 44 | let mut wdt1 = timer_group1.wdt; 45 | rtc.swd.disable(); 46 | rtc.rwdt.disable(); 47 | wdt0.disable(); 48 | wdt1.disable(); 49 | 50 | // Define the delay struct, needed for the display driver 51 | let mut delay = Delay::new(); 52 | 53 | // Define the Data/Command select pin as a digital output 54 | let dc = Output::new(io.pins.gpio7, Level::Low); 55 | // Define the reset pin as digital outputs and make it high 56 | let mut rst = Output::new(io.pins.gpio8, Level::Low); 57 | rst.set_high(); 58 | 59 | // Define the SPI pins and create the SPI interface 60 | let sck = io.pins.gpio12; 61 | let miso = io.pins.gpio11; 62 | let mosi = io.pins.gpio13; 63 | let cs = io.pins.gpio10; 64 | let spi = Spi::new(peripherals.SPI2, 100_u32.kHz(), SpiMode::Mode0).with_pins( 65 | sck, 66 | mosi, 67 | miso, 68 | hal::gpio::NoPin, 69 | ); 70 | 71 | let cs_output = Output::new(cs, Level::High); 72 | let spi_device = ExclusiveDevice::new_no_delay(spi, cs_output).unwrap(); 73 | 74 | let mut buffer = [0_u8; 512]; 75 | 76 | // Define the display interface with no chip select 77 | let di = SpiInterface::new(spi_device, dc, &mut buffer); 78 | 79 | // Define the display from the display interface and initialize it 80 | let mut display = Builder::new(ILI9486Rgb565, di) 81 | .reset_pin(rst) 82 | .init(&mut delay) 83 | .unwrap(); 84 | 85 | // Make the display all black 86 | display.clear(Rgb565::BLACK).unwrap(); 87 | 88 | // Draw a smiley face with white eyes and a red mouth 89 | draw_smiley(&mut display).unwrap(); 90 | 91 | loop { 92 | // Do nothing 93 | } 94 | } 95 | 96 | fn draw_smiley>(display: &mut T) -> Result<(), T::Error> { 97 | // Draw the left eye as a circle located at (50, 100), with a diameter of 40, filled with white 98 | Circle::new(Point::new(50, 100), 40) 99 | .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE)) 100 | .draw(display)?; 101 | 102 | // Draw the right eye as a circle located at (50, 200), with a diameter of 40, filled with white 103 | Circle::new(Point::new(50, 200), 40) 104 | .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE)) 105 | .draw(display)?; 106 | 107 | // Draw an upside down red triangle to represent a smiling mouth 108 | Triangle::new( 109 | Point::new(130, 140), 110 | Point::new(130, 200), 111 | Point::new(160, 170), 112 | ) 113 | .into_styled(PrimitiveStyle::with_fill(Rgb565::RED)) 114 | .draw(display)?; 115 | 116 | // Cover the top part of the mouth with a black triangle so it looks closed instead of open 117 | Triangle::new( 118 | Point::new(130, 150), 119 | Point::new(130, 190), 120 | Point::new(150, 170), 121 | ) 122 | .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) 123 | .draw(display)?; 124 | 125 | Ok(()) 126 | } 127 | -------------------------------------------------------------------------------- /src/_troubleshooting.rs: -------------------------------------------------------------------------------- 1 | //! # Troubleshooting guide 2 | //! 3 | //! This guide lists common issues that can cause a blank or corrupted display. 4 | //! 5 | //! ## Display stays black/blank 6 | //! 7 | //! ### Reset pin 8 | //! 9 | //! The reset pin on all supported display controllers is active low, requiring 10 | //! it to be driven **high** in order for the display to operate. It is 11 | //! recommended to connect the reset pin to a GPIO pin and let this crate 12 | //! control the pin by passing it to the builder via the `reset_pin` method. If 13 | //! this isn't possible in the target application the user must make sure that 14 | //! the reset pin on the display controller is kept in the high state before 15 | //! `init` is called. 16 | //! 17 | //! ### Backlight pin 18 | //! 19 | //! This driver does **NOT** handle the backlight pin to keep the code simpler. 20 | //! Users must control the backlight manually. First thing to try is to see if 21 | //! setting the backlight pin to high fixes the issue. 22 | //! 23 | //! ### Transport misconfiguration (e.g. SPI) 24 | //! 25 | //! Make sure that the transport layer is configured correctly. Typical mistakes 26 | //! are the use of wrong SPI MODE or too fast transfer speeds that are not 27 | //! supported by the display 28 | //! 29 | //! ## Incorrect colors 30 | //! 31 | //! The way colors are displayed depend on the subpixel layout and technology 32 | //! (like TN or IPS) used by the LCD panel. These physical parameters aren't 33 | //! known by the display controller and must be manually set by the user as 34 | //! `Builder` settings when the display is initialized. 35 | //! 36 | //! To make it easier to identify the correct settings the `mipidsi` crate 37 | //! provides a [`TestImage`](crate::TestImage), which can be used to verify the 38 | //! color settings and adjust them in case they are incorrect. 39 | //! 40 | //! ``` 41 | //! use embedded_graphics::prelude::*; 42 | //! use mipidsi::{Builder, TestImage, models::ILI9486Rgb666}; 43 | //! 44 | //! # let di = mipidsi::_mock::MockDisplayInterface; 45 | //! # let rst = mipidsi::_mock::MockOutputPin; 46 | //! # let mut delay = mipidsi::_mock::MockDelay; 47 | //! let mut display = Builder::new(ILI9486Rgb666, di) 48 | //! .reset_pin(rst) 49 | //! .init(&mut delay) 50 | //! .unwrap();; 51 | //! 52 | //! TestImage::new().draw(&mut display)?; 53 | //! # Ok::<(), core::convert::Infallible>(()) 54 | //! ``` 55 | //! 56 | //! The expected output from drawing the test image is: 57 | //! 58 | #![doc = include_str!("../docs/colors_correct.svg")] 59 | //! 60 | //! If the test image isn't displayed as expected use one of the reference image 61 | //! below the determine which settings need to be added to the 62 | //! [`Builder`](crate::Builder). 63 | //! 64 | //! ### Wrong subpixel order 65 | //! 66 | #![doc = include_str!("../docs/colors_wrong_subpixel_order.svg")] 67 | //! 68 | //! ``` 69 | //! # use embedded_graphics::prelude::*; 70 | //! # use mipidsi::{Builder, TestImage, models::ILI9486Rgb666}; 71 | //! # 72 | //! # let di = mipidsi::_mock::MockDisplayInterface; 73 | //! # let mut delay = mipidsi::_mock::MockDelay; 74 | //! # let mut display = Builder::new(ILI9486Rgb666, di) 75 | //! .color_order(mipidsi::options::ColorOrder::Bgr) 76 | //! # .init(&mut delay).unwrap(); 77 | //! ``` 78 | //! 79 | //! ### Wrong color inversion 80 | //! 81 | #![doc = include_str!("../docs/colors_wrong_color_inversion.svg")] 82 | //! 83 | //! ``` 84 | //! # use embedded_graphics::prelude::*; 85 | //! # use mipidsi::{Builder, TestImage, models::ILI9486Rgb666}; 86 | //! # 87 | //! # let di = mipidsi::_mock::MockDisplayInterface; 88 | //! # let mut delay = mipidsi::_mock::MockDelay; 89 | //! # let mut display = Builder::new(ILI9486Rgb666, di) 90 | //! .invert_colors(mipidsi::options::ColorInversion::Inverted) 91 | //! # .init(&mut delay).unwrap(); 92 | //! ``` 93 | //! 94 | //! ### Wrong subpixel order and color inversion 95 | //! 96 | #![doc = include_str!("../docs/colors_both_wrong.svg")] 97 | //! 98 | //! ``` 99 | //! # use embedded_graphics::prelude::*; 100 | //! # use mipidsi::{Builder, TestImage, models::ILI9486Rgb666}; 101 | //! # 102 | //! # let di = mipidsi::_mock::MockDisplayInterface; 103 | //! # let mut delay = mipidsi::_mock::MockDelay; 104 | //! # let mut display = Builder::new(ILI9486Rgb666, di) 105 | //! .color_order(mipidsi::options::ColorOrder::Bgr) 106 | //! .invert_colors(mipidsi::options::ColorInversion::Inverted) 107 | //! # .init(&mut delay).unwrap(); 108 | //! ``` 109 | -------------------------------------------------------------------------------- /src/dcs/set_address_mode.rs: -------------------------------------------------------------------------------- 1 | //! Module for the MADCTL instruction constructors 2 | 3 | use crate::options::{ 4 | ColorOrder, HorizontalRefreshOrder, MemoryMapping, ModelOptions, Orientation, RefreshOrder, 5 | VerticalRefreshOrder, 6 | }; 7 | 8 | use super::DcsCommand; 9 | 10 | /// Set Address Mode 11 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 12 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 13 | pub struct SetAddressMode(u8); 14 | 15 | impl SetAddressMode { 16 | /// Creates a new Set Address Mode command. 17 | pub const fn new( 18 | color_order: ColorOrder, 19 | orientation: Orientation, 20 | refresh_order: RefreshOrder, 21 | ) -> Self { 22 | Self(0) 23 | .with_color_order(color_order) 24 | .with_orientation(orientation) 25 | .with_refresh_order(refresh_order) 26 | } 27 | 28 | /// Returns this Madctl with [ColorOrder] set to new value 29 | #[must_use] 30 | pub const fn with_color_order(self, color_order: ColorOrder) -> Self { 31 | let mut result = self; 32 | match color_order { 33 | ColorOrder::Rgb => result.0 &= 0b1111_0111, 34 | ColorOrder::Bgr => result.0 |= 0b0000_1000, 35 | } 36 | 37 | result 38 | } 39 | 40 | /// Returns this Madctl with [Orientation] set to new value 41 | #[must_use] 42 | pub const fn with_orientation(self, orientation: Orientation) -> Self { 43 | let mut result = self.0; 44 | result &= 0b0001_1111; 45 | 46 | let mapping = MemoryMapping::from_orientation(orientation); 47 | if mapping.reverse_rows { 48 | result |= 1 << 7; 49 | } 50 | if mapping.reverse_columns { 51 | result |= 1 << 6; 52 | } 53 | if mapping.swap_rows_and_columns { 54 | result |= 1 << 5; 55 | } 56 | 57 | Self(result) 58 | } 59 | 60 | /// Returns this Madctl with [RefreshOrder] set to new value 61 | #[must_use] 62 | pub const fn with_refresh_order(self, refresh_order: RefreshOrder) -> Self { 63 | let mut result = self; 64 | let value = match (refresh_order.vertical, refresh_order.horizontal) { 65 | (VerticalRefreshOrder::TopToBottom, HorizontalRefreshOrder::LeftToRight) => 0b0000_0000, 66 | (VerticalRefreshOrder::TopToBottom, HorizontalRefreshOrder::RightToLeft) => 0b0000_0100, 67 | (VerticalRefreshOrder::BottomToTop, HorizontalRefreshOrder::LeftToRight) => 0b0001_0000, 68 | (VerticalRefreshOrder::BottomToTop, HorizontalRefreshOrder::RightToLeft) => 0b0001_0100, 69 | }; 70 | 71 | result.0 = (result.0 & 0b1110_1011) | value; 72 | 73 | result 74 | } 75 | } 76 | 77 | impl DcsCommand for SetAddressMode { 78 | fn instruction(&self) -> u8 { 79 | 0x36 80 | } 81 | 82 | fn fill_params_buf(&self, buffer: &mut [u8]) -> usize { 83 | buffer[0] = self.0; 84 | 1 85 | } 86 | } 87 | 88 | impl From<&ModelOptions> for SetAddressMode { 89 | fn from(options: &ModelOptions) -> Self { 90 | Self::default() 91 | .with_color_order(options.color_order) 92 | .with_orientation(options.orientation) 93 | .with_refresh_order(options.refresh_order) 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use crate::options::Rotation; 100 | 101 | use super::*; 102 | 103 | #[test] 104 | fn madctl_bit_operations() { 105 | let madctl = SetAddressMode::default() 106 | .with_color_order(ColorOrder::Bgr) 107 | .with_refresh_order(RefreshOrder::new( 108 | VerticalRefreshOrder::BottomToTop, 109 | HorizontalRefreshOrder::RightToLeft, 110 | )) 111 | .with_orientation(Orientation::default().rotate(Rotation::Deg270)); 112 | 113 | let mut bytes = [0u8]; 114 | assert_eq!(madctl.fill_params_buf(&mut bytes), 1); 115 | assert_eq!(bytes, [0b1011_1100u8]); 116 | 117 | let madctl = madctl.with_orientation(Orientation::default()); 118 | assert_eq!(madctl.fill_params_buf(&mut bytes), 1); 119 | assert_eq!(bytes, [0b0001_1100u8]); 120 | 121 | let madctl = madctl.with_color_order(ColorOrder::Rgb); 122 | assert_eq!(madctl.fill_params_buf(&mut bytes), 1); 123 | assert_eq!(bytes, [0b0001_0100u8]); 124 | 125 | let madctl = madctl.with_refresh_order(RefreshOrder::default()); 126 | assert_eq!(madctl.fill_params_buf(&mut bytes), 1); 127 | assert_eq!(bytes, [0b0000_0000u8]); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /examples/parallel_ili9341_rp_pico/src/main.rs: -------------------------------------------------------------------------------- 1 | // This example is made for the Raspberry Pi Pico, using the `rp-hal` 2 | // It demonstrates how to set up an ili9341 display with the Rgb666 color space 3 | // using a parallel port 4 | 5 | /* --- Needed by RPI Pico --- */ 6 | #![no_std] 7 | #![no_main] 8 | use bsp::entry; 9 | use bsp::hal::{ 10 | clocks::{init_clocks_and_plls, Clock}, 11 | gpio, pac, 12 | sio::Sio, 13 | watchdog::Watchdog, 14 | }; 15 | use defmt_rtt as _; 16 | use mipidsi::models::ILI9341Rgb666; 17 | use mipidsi::options::ColorOrder; 18 | use panic_probe as _; 19 | use rp_pico as bsp; 20 | /* -------------------------- */ 21 | 22 | use embedded_graphics::{ 23 | // Provides the necessary functions to draw on the display 24 | draw_target::DrawTarget, 25 | // Provides colors from the Rgb666 color space 26 | pixelcolor::Rgb666, 27 | prelude::RgbColor, 28 | }; 29 | 30 | // Provides the parallel port and display interface builders 31 | use mipidsi::interface::{Generic8BitBus, ParallelInterface}; 32 | 33 | // Provides the Display builder 34 | use mipidsi::Builder; 35 | 36 | #[entry] 37 | fn main() -> ! { 38 | // Define the pico's singleton objects 39 | let mut pac = pac::Peripherals::take().unwrap(); 40 | let core = pac::CorePeripherals::take().unwrap(); 41 | let mut watchdog = Watchdog::new(pac.WATCHDOG); 42 | let sio = Sio::new(pac.SIO); 43 | 44 | // Define the pico's clocks, needed for the delay 45 | let external_xtal_freq_hz = 12_000_000u32; 46 | let clocks = init_clocks_and_plls( 47 | external_xtal_freq_hz, 48 | pac.XOSC, 49 | pac.CLOCKS, 50 | pac.PLL_SYS, 51 | pac.PLL_USB, 52 | &mut pac.RESETS, 53 | &mut watchdog, 54 | ) 55 | .ok() 56 | .unwrap(); 57 | 58 | // Define the delay struct, needed for the display driver 59 | let mut delay = DelayCompat(cortex_m::delay::Delay::new( 60 | core.SYST, 61 | clocks.system_clock.freq().to_Hz(), 62 | )); 63 | 64 | // Define the pins, needed to define the display interface 65 | let pins = bsp::Pins::new( 66 | pac.IO_BANK0, 67 | pac.PADS_BANK0, 68 | sio.gpio_bank0, 69 | &mut pac.RESETS, 70 | ); 71 | 72 | // Define the reset and write enable pins as digital outputs and make them high 73 | let rst = pins 74 | .gpio7 75 | .into_push_pull_output_in_state(gpio::PinState::High); 76 | let wr = pins 77 | .gpio5 78 | .into_push_pull_output_in_state(gpio::PinState::High); 79 | 80 | // Define the Data/Command select pin as a digital output 81 | let dc = pins.gpio6.into_push_pull_output(); 82 | 83 | // Define the pins used for the parallel interface as digital outputs 84 | let lcd_d0 = pins.gpio15.into_push_pull_output(); 85 | let lcd_d1 = pins.gpio14.into_push_pull_output(); 86 | let lcd_d2 = pins.gpio13.into_push_pull_output(); 87 | let lcd_d3 = pins.gpio12.into_push_pull_output(); 88 | let lcd_d4 = pins.gpio11.into_push_pull_output(); 89 | let lcd_d5 = pins.gpio10.into_push_pull_output(); 90 | let lcd_d6 = pins.gpio9.into_push_pull_output(); 91 | let lcd_d7 = pins.gpio8.into_push_pull_output(); 92 | 93 | // Define the parallel bus with the previously defined parallel port pins 94 | let bus = Generic8BitBus::new(( 95 | lcd_d0, lcd_d1, lcd_d2, lcd_d3, lcd_d4, lcd_d5, lcd_d6, lcd_d7, 96 | )); 97 | 98 | // Define the display interface from a generic 8 bit bus, a Data/Command select pin and a write enable pin 99 | let di = ParallelInterface::new(bus, dc, wr); 100 | 101 | // Define the display from the display bus, set the color order as Bgr and initialize it with 102 | // the delay struct and the reset pin 103 | let mut display = Builder::new(ILI9341Rgb666, di) 104 | .reset_pin(rst) 105 | .color_order(ColorOrder::Bgr) 106 | .init(&mut delay) 107 | .unwrap(); 108 | 109 | // Set the display all red 110 | display.clear(Rgb666::RED).unwrap(); 111 | 112 | loop { 113 | // Do nothing 114 | } 115 | } 116 | 117 | /// Wrapper around `Delay` to implement the embedded-hal 1.0 delay. 118 | /// 119 | /// This can be removed when a new version of the `cortex_m` crate is released. 120 | struct DelayCompat(cortex_m::delay::Delay); 121 | 122 | impl embedded_hal::delay::DelayNs for DelayCompat { 123 | fn delay_ns(&mut self, mut ns: u32) { 124 | while ns > 1000 { 125 | self.0.delay_us(1); 126 | ns = ns.saturating_sub(1000); 127 | } 128 | } 129 | 130 | fn delay_us(&mut self, us: u32) { 131 | self.0.delay_us(us); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/models/gc9a01.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics_core::pixelcolor::Rgb565; 2 | use embedded_hal::delay::DelayNs; 3 | 4 | use crate::{ 5 | dcs::{ 6 | BitsPerPixel, ExitSleepMode, InterfaceExt, PixelFormat, SetAddressMode, SetDisplayOn, 7 | SetInvertMode, SetPixelFormat, 8 | }, 9 | interface::{Interface, InterfaceKind}, 10 | models::{Model, ModelInitError}, 11 | options::ModelOptions, 12 | ConfigurationError, 13 | }; 14 | 15 | /// GC9A01 display in Rgb565 color mode. 16 | pub struct GC9A01; 17 | 18 | impl Model for GC9A01 { 19 | type ColorFormat = Rgb565; 20 | const FRAMEBUFFER_SIZE: (u16, u16) = (240, 240); 21 | 22 | fn init( 23 | &mut self, 24 | di: &mut DI, 25 | delay: &mut DELAY, 26 | options: &ModelOptions, 27 | ) -> Result> 28 | where 29 | DELAY: DelayNs, 30 | DI: Interface, 31 | { 32 | if !matches!( 33 | DI::KIND, 34 | InterfaceKind::Serial4Line | InterfaceKind::Parallel8Bit | InterfaceKind::Parallel16Bit 35 | ) { 36 | return Err(ModelInitError::InvalidConfiguration( 37 | ConfigurationError::UnsupportedInterface, 38 | )); 39 | } 40 | 41 | let madctl = SetAddressMode::from(options); 42 | 43 | delay.delay_us(200_000); 44 | 45 | di.write_raw(0xEF, &[])?; // inter register enable 2 46 | di.write_raw(0xEB, &[0x14])?; 47 | di.write_raw(0xFE, &[])?; // inter register enable 1 48 | di.write_raw(0xEF, &[])?; // inter register enable 2 49 | di.write_raw(0xEB, &[0x14])?; 50 | 51 | di.write_raw(0x84, &[0x40])?; 52 | di.write_raw(0x85, &[0xFF])?; 53 | di.write_raw(0x86, &[0xFF])?; 54 | di.write_raw(0x87, &[0xFF])?; 55 | di.write_raw(0x88, &[0x0A])?; 56 | di.write_raw(0x89, &[0x21])?; 57 | di.write_raw(0x8A, &[0x00])?; 58 | di.write_raw(0x8B, &[0x80])?; 59 | di.write_raw(0x8C, &[0x01])?; 60 | di.write_raw(0x8D, &[0x01])?; 61 | di.write_raw(0x8E, &[0xFF])?; 62 | di.write_raw(0x8F, &[0xFF])?; 63 | 64 | di.write_raw(0xB6, &[0x00, 0x20])?; // display function control 65 | 66 | di.write_command(madctl)?; // set memory data access control, Top -> Bottom, RGB, Left -> Right 67 | 68 | let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::()); 69 | di.write_command(SetPixelFormat::new(pf))?; // set interface pixel format, 16bit pixel into frame memory 70 | 71 | di.write_raw(0x90, &[0x08, 0x08, 0x08, 0x08])?; 72 | di.write_raw(0xBD, &[0x06])?; 73 | di.write_raw(0xBC, &[0x00])?; 74 | di.write_raw(0xFF, &[0x60, 0x01, 0x04])?; 75 | 76 | di.write_raw(0xC3, &[0x13])?; // power control 2 77 | di.write_raw(0xC4, &[0x13])?; // power control 3 78 | di.write_raw(0xC9, &[0x22])?; // power control 4 79 | 80 | di.write_raw(0xBE, &[0x11])?; 81 | di.write_raw(0xE1, &[0x10, 0x0E])?; 82 | di.write_raw(0xDF, &[0x20, 0x0c, 0x02])?; 83 | 84 | di.write_raw(0xF0, &[0x45, 0x09, 0x08, 0x08, 0x26, 0x2A])?; // gamma 1 85 | di.write_raw(0xF1, &[0x43, 0x70, 0x72, 0x36, 0x37, 0x6f])?; // gamma 2 86 | di.write_raw(0xF2, &[0x45, 0x09, 0x08, 0x08, 0x26, 0x2A])?; // gamma 3 87 | di.write_raw(0xF3, &[0x43, 0x70, 0x72, 0x36, 0x37, 0x6f])?; // gamma 4 88 | 89 | di.write_raw(0xED, &[0x18, 0x0B])?; 90 | di.write_raw(0xAE, &[0x77])?; 91 | di.write_raw(0xCD, &[0x63])?; 92 | 93 | di.write_raw( 94 | 0x70, 95 | &[0x07, 0x07, 0x04, 0x0E, 0x0F, 0x09, 0x07, 0x08, 0x03], 96 | )?; 97 | 98 | di.write_raw(0xE8, &[0x34])?; // framerate 99 | 100 | di.write_raw( 101 | 0x62, 102 | &[ 103 | 0x18, 0x0D, 0x71, 0xED, 0x70, 0x70, 0x18, 0x0F, 0x71, 0xEF, 0x70, 0x70, 104 | ], 105 | )?; 106 | di.write_raw( 107 | 0x63, 108 | &[ 109 | 0x18, 0x11, 0x71, 0xF1, 0x70, 0x70, 0x18, 0x13, 0x71, 0xF3, 0x70, 0x70, 110 | ], 111 | )?; 112 | di.write_raw(0x64, &[0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07])?; 113 | di.write_raw( 114 | 0x66, 115 | &[0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00], 116 | )?; 117 | di.write_raw( 118 | 0x67, 119 | &[0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98], 120 | )?; 121 | 122 | di.write_raw(0x74, &[0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00])?; 123 | di.write_raw(0x98, &[0x3e, 0x07])?; 124 | 125 | di.write_command(SetInvertMode::new(options.invert_colors))?; // set color inversion 126 | 127 | di.write_command(ExitSleepMode)?; // turn off sleep 128 | delay.delay_us(120_000); 129 | 130 | di.write_command(SetDisplayOn)?; // turn on display 131 | 132 | Ok(madctl) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /examples/spi-st7789-rpi-zero-w/src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | # SPI ST7789 on a Raspberry Pi Zero W Example 3 | 4 | This example demonstrates how to use the [Display HAT Mini by Pomoroni](https://shop.pimoroni.com/products/display-hat-mini?variant=39496084717651) 5 | on a Raspberry Pi Zero W. 6 | 7 | The example shows a scrolling text and a pulsing RGB LED. 8 | 9 | Buttons: 10 | 11 | - A: change LED color 12 | - B: exit 13 | - X: move text up 14 | - Y: move text down 15 | 16 | Read the README.md for more information. 17 | */ 18 | 19 | use embedded_graphics::{ 20 | mono_font::{ascii::FONT_10X20, MonoTextStyle}, 21 | pixelcolor::Rgb565, 22 | prelude::*, 23 | text::Text, 24 | }; 25 | use embedded_hal_bus::spi::ExclusiveDevice; 26 | use mipidsi::interface::SpiInterface; 27 | use mipidsi::{models::ST7789, options::ColorInversion, Builder}; 28 | use rppal::gpio::Gpio; 29 | use rppal::hal::Delay; 30 | use rppal::spi::{Bus, Mode, SlaveSelect, Spi}; 31 | use std::process::ExitCode; 32 | 33 | // Pins 34 | 35 | const SPI_DC: u8 = 9; 36 | const BACKLIGHT: u8 = 13; 37 | 38 | const BUTTON_A: u8 = 5; 39 | const BUTTON_B: u8 = 6; 40 | const BUTTON_X: u8 = 16; 41 | const BUTTON_Y: u8 = 24; 42 | 43 | const LED_R: u8 = 17; 44 | const LED_G: u8 = 27; 45 | const LED_B: u8 = 22; 46 | 47 | // Display 48 | const W: i32 = 320; 49 | const H: i32 = 240; 50 | 51 | fn main() -> ExitCode { 52 | // GPIO 53 | let gpio = Gpio::new().unwrap(); 54 | let dc = gpio.get(SPI_DC).unwrap().into_output(); 55 | let mut backlight = gpio.get(BACKLIGHT).unwrap().into_output(); 56 | 57 | // LEDs 58 | let mut led_r = gpio.get(LED_R).unwrap().into_output(); 59 | let mut led_g = gpio.get(LED_G).unwrap().into_output(); 60 | let mut led_b = gpio.get(LED_B).unwrap().into_output(); 61 | 62 | // Buttons 63 | let button_a = gpio.get(BUTTON_A).unwrap().into_input_pullup(); 64 | let button_b = gpio.get(BUTTON_B).unwrap().into_input_pullup(); 65 | let button_x = gpio.get(BUTTON_X).unwrap().into_input_pullup(); 66 | let button_y = gpio.get(BUTTON_Y).unwrap().into_input_pullup(); 67 | 68 | // SPI Display 69 | let spi = Spi::new(Bus::Spi0, SlaveSelect::Ss1, 60_000_000_u32, Mode::Mode0).unwrap(); 70 | let spi_device = ExclusiveDevice::new_no_delay(spi, NoCs).unwrap(); 71 | let mut buffer = [0_u8; 512]; 72 | let di = SpiInterface::new(spi_device, dc, &mut buffer); 73 | let mut delay = Delay::new(); 74 | let mut display = Builder::new(ST7789, di) 75 | .display_size(W as u16, H as u16) 76 | .invert_colors(ColorInversion::Inverted) 77 | .init(&mut delay) 78 | .unwrap(); 79 | 80 | // Text 81 | let char_w = 10; 82 | let char_h = 20; 83 | let text_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE); 84 | let text = "Hello World ^_^;"; 85 | let mut text_x = W; 86 | let mut text_y = H / 2; 87 | 88 | // Alternating color 89 | let colors = [Rgb565::RED, Rgb565::GREEN, Rgb565::BLUE]; 90 | 91 | // Clear the display initially 92 | display.clear(colors[0]).unwrap(); 93 | 94 | // Turn on backlight 95 | backlight.set_high(); 96 | 97 | // Set LEDs to PWM mode 98 | led_r.set_pwm_frequency(50., 1.).unwrap(); 99 | led_g.set_pwm_frequency(50., 1.).unwrap(); 100 | led_b.set_pwm_frequency(50., 1.).unwrap(); 101 | 102 | let start = std::time::Instant::now(); 103 | let mut last = std::time::Instant::now(); 104 | let mut led_flags = 0b000; 105 | let mut counter = 0; 106 | loop { 107 | let elapsed = last.elapsed().as_secs_f64(); 108 | if elapsed < 0.125 { 109 | continue; 110 | } 111 | last = std::time::Instant::now(); 112 | counter += 1; 113 | 114 | // X: move text up 115 | if button_x.is_low() { 116 | text_y -= char_h; 117 | } 118 | // Y: move text down 119 | if button_y.is_low() { 120 | text_y += char_h; 121 | } 122 | // A: change led color 123 | if button_a.is_low() { 124 | led_flags = (led_flags + 1) % 8; 125 | } 126 | // B: exit 127 | if button_b.is_low() { 128 | break; 129 | } 130 | 131 | // Fill the display with alternating colors every 8 frames 132 | display.clear(colors[(counter / 8) % colors.len()]).unwrap(); 133 | 134 | // Draw text 135 | let right = Text::new(text, Point::new(text_x, text_y), text_style) 136 | .draw(&mut display) 137 | .unwrap(); 138 | text_x = if right.x <= 0 { W } else { text_x - char_w }; 139 | 140 | // Led 141 | let y = ((start.elapsed().as_secs_f64().sin() + 1.) * 50.).round() / 100.; 142 | led_r 143 | .set_pwm_frequency(50., if led_flags & 0b100 != 0 { y } else { 1. }) 144 | .unwrap(); 145 | led_g 146 | .set_pwm_frequency(50., if led_flags & 0b010 != 0 { y } else { 1. }) 147 | .unwrap(); 148 | led_b 149 | .set_pwm_frequency(50., if led_flags & 0b001 != 0 { y } else { 1. }) 150 | .unwrap(); 151 | } 152 | 153 | // Turn off backlight and clear the display 154 | backlight.set_low(); 155 | display.clear(Rgb565::BLACK).unwrap(); 156 | 157 | ExitCode::SUCCESS 158 | } 159 | 160 | /// Noop `OutputPin` implementation. 161 | /// 162 | /// This is passed to `ExclusiveDevice`, because the CS pin is handle in 163 | /// hardware. 164 | struct NoCs; 165 | 166 | impl embedded_hal::digital::OutputPin for NoCs { 167 | fn set_low(&mut self) -> Result<(), Self::Error> { 168 | Ok(()) 169 | } 170 | 171 | fn set_high(&mut self) -> Result<(), Self::Error> { 172 | Ok(()) 173 | } 174 | } 175 | 176 | impl embedded_hal::digital::ErrorType for NoCs { 177 | type Error = core::convert::Infallible; 178 | } 179 | -------------------------------------------------------------------------------- /src/interface.rs: -------------------------------------------------------------------------------- 1 | //! Interface traits and implementations 2 | 3 | mod spi; 4 | use embedded_graphics_core::pixelcolor::{Rgb565, Rgb666, RgbColor}; 5 | pub use spi::*; 6 | 7 | mod parallel; 8 | pub use parallel::*; 9 | 10 | /// Command and pixel interface 11 | pub trait Interface { 12 | /// The native width of the interface 13 | /// 14 | /// In most cases this will be u8, except for larger parallel interfaces such as 15 | /// 16 bit (currently supported) 16 | /// or 9 or 18 bit (currently unsupported) 17 | type Word: Copy; 18 | 19 | /// Error type 20 | type Error: core::fmt::Debug; 21 | 22 | /// Kind 23 | const KIND: InterfaceKind; 24 | 25 | /// Send a command with optional parameters 26 | fn send_command(&mut self, command: u8, args: &[u8]) -> Result<(), Self::Error>; 27 | 28 | /// Send a sequence of pixels 29 | /// 30 | /// `WriteMemoryStart` must be sent before calling this function 31 | fn send_pixels( 32 | &mut self, 33 | pixels: impl IntoIterator, 34 | ) -> Result<(), Self::Error>; 35 | 36 | /// Send the same pixel value multiple times 37 | /// 38 | /// `WriteMemoryStart` must be sent before calling this function 39 | fn send_repeated_pixel( 40 | &mut self, 41 | pixel: [Self::Word; N], 42 | count: u32, 43 | ) -> Result<(), Self::Error>; 44 | } 45 | 46 | impl Interface for &mut T { 47 | type Word = T::Word; 48 | type Error = T::Error; 49 | 50 | const KIND: InterfaceKind = T::KIND; 51 | 52 | fn send_command(&mut self, command: u8, args: &[u8]) -> Result<(), Self::Error> { 53 | T::send_command(self, command, args) 54 | } 55 | 56 | fn send_pixels( 57 | &mut self, 58 | pixels: impl IntoIterator, 59 | ) -> Result<(), Self::Error> { 60 | T::send_pixels(self, pixels) 61 | } 62 | 63 | fn send_repeated_pixel( 64 | &mut self, 65 | pixel: [Self::Word; N], 66 | count: u32, 67 | ) -> Result<(), Self::Error> { 68 | T::send_repeated_pixel(self, pixel, count) 69 | } 70 | } 71 | 72 | fn rgb565_to_bytes(pixel: Rgb565) -> [u8; 2] { 73 | embedded_graphics_core::pixelcolor::raw::ToBytes::to_be_bytes(pixel) 74 | } 75 | fn rgb565_to_u16(pixel: Rgb565) -> [u16; 1] { 76 | [u16::from_ne_bytes( 77 | embedded_graphics_core::pixelcolor::raw::ToBytes::to_ne_bytes(pixel), 78 | )] 79 | } 80 | fn rgb666_to_bytes(pixel: Rgb666) -> [u8; 3] { 81 | [pixel.r(), pixel.g(), pixel.b()].map(|x| x << 2) 82 | } 83 | 84 | /// This is an implementation detail, it should not be implemented or used outside this crate 85 | pub trait InterfacePixelFormat { 86 | // this should just be 87 | // const N: usize; 88 | // fn convert(self) -> [Word; Self::N]; 89 | // but that doesn't work yet 90 | 91 | #[doc(hidden)] 92 | fn send_pixels>( 93 | di: &mut DI, 94 | pixels: impl IntoIterator, 95 | ) -> Result<(), DI::Error>; 96 | 97 | #[doc(hidden)] 98 | fn send_repeated_pixel>( 99 | di: &mut DI, 100 | pixel: Self, 101 | count: u32, 102 | ) -> Result<(), DI::Error>; 103 | } 104 | 105 | impl InterfacePixelFormat for Rgb565 { 106 | fn send_pixels>( 107 | di: &mut DI, 108 | pixels: impl IntoIterator, 109 | ) -> Result<(), DI::Error> { 110 | di.send_pixels(pixels.into_iter().map(rgb565_to_bytes)) 111 | } 112 | 113 | fn send_repeated_pixel>( 114 | di: &mut DI, 115 | pixel: Self, 116 | count: u32, 117 | ) -> Result<(), DI::Error> { 118 | di.send_repeated_pixel(rgb565_to_bytes(pixel), count) 119 | } 120 | } 121 | 122 | impl InterfacePixelFormat for Rgb666 { 123 | fn send_pixels>( 124 | di: &mut DI, 125 | pixels: impl IntoIterator, 126 | ) -> Result<(), DI::Error> { 127 | di.send_pixels(pixels.into_iter().map(rgb666_to_bytes)) 128 | } 129 | 130 | fn send_repeated_pixel>( 131 | di: &mut DI, 132 | pixel: Self, 133 | count: u32, 134 | ) -> Result<(), DI::Error> { 135 | di.send_repeated_pixel(rgb666_to_bytes(pixel), count) 136 | } 137 | } 138 | 139 | impl InterfacePixelFormat for Rgb565 { 140 | fn send_pixels>( 141 | di: &mut DI, 142 | pixels: impl IntoIterator, 143 | ) -> Result<(), DI::Error> { 144 | di.send_pixels(pixels.into_iter().map(rgb565_to_u16)) 145 | } 146 | 147 | fn send_repeated_pixel>( 148 | di: &mut DI, 149 | pixel: Self, 150 | count: u32, 151 | ) -> Result<(), DI::Error> { 152 | di.send_repeated_pixel(rgb565_to_u16(pixel), count) 153 | } 154 | } 155 | 156 | /// Interface kind. 157 | /// 158 | /// Specifies the kind of physical connection to the display controller that is 159 | /// supported by this interface. 160 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 161 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 162 | #[non_exhaustive] 163 | pub enum InterfaceKind { 164 | /// Serial interface with data/command pin. 165 | /// 166 | /// SPI style interface with 8 bits per word and an additional pin to 167 | /// distinguish between data and command words. 168 | Serial4Line, 169 | 170 | /// 8 bit parallel interface. 171 | /// 172 | /// 8080 style parallel interface with 8 data pins and chip select, write enable, 173 | /// and command/data signals. 174 | Parallel8Bit, 175 | 176 | /// 16 bit parallel interface. 177 | /// 178 | /// 8080 style parallel interface with 16 data pins and chip select, write enable, 179 | /// and command/data signals. 180 | Parallel16Bit, 181 | } 182 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | //! [ModelOptions] and other helper types. 2 | 3 | use crate::models::Model; 4 | 5 | mod orientation; 6 | pub(crate) use orientation::MemoryMapping; 7 | pub use orientation::{InvalidAngleError, Orientation, Rotation}; 8 | 9 | /// [ModelOptions] are passed to the [`init`](Model::init) method of [Model] 10 | /// implementations. 11 | #[derive(Clone)] 12 | #[non_exhaustive] 13 | pub struct ModelOptions { 14 | /// Subpixel order. 15 | pub color_order: ColorOrder, 16 | /// Initial display orientation. 17 | pub orientation: Orientation, 18 | /// Whether to invert colors for this display/model (INVON) 19 | pub invert_colors: ColorInversion, 20 | /// Display refresh order. 21 | pub refresh_order: RefreshOrder, 22 | /// Display size (w, h) for given display. 23 | pub display_size: (u16, u16), 24 | /// Display offset (x, y) for given display. 25 | pub display_offset: (u16, u16), 26 | } 27 | 28 | impl ModelOptions { 29 | /// Creates model options for the entire framebuffer. 30 | pub fn full_size() -> Self { 31 | Self { 32 | color_order: ColorOrder::default(), 33 | orientation: Orientation::default(), 34 | invert_colors: ColorInversion::default(), 35 | refresh_order: RefreshOrder::default(), 36 | display_size: M::FRAMEBUFFER_SIZE, 37 | display_offset: (0, 0), 38 | } 39 | } 40 | 41 | /// Creates model options for the given size and offset. 42 | pub fn with_all(display_size: (u16, u16), display_offset: (u16, u16)) -> Self { 43 | Self { 44 | color_order: ColorOrder::default(), 45 | orientation: Orientation::default(), 46 | invert_colors: ColorInversion::default(), 47 | refresh_order: RefreshOrder::default(), 48 | display_size, 49 | display_offset, 50 | } 51 | } 52 | 53 | /// Returns the display size based on current orientation and display options. 54 | /// 55 | /// Used by models. 56 | pub(crate) fn display_size(&self) -> (u16, u16) { 57 | if self.orientation.rotation.is_horizontal() { 58 | self.display_size 59 | } else { 60 | (self.display_size.1, self.display_size.0) 61 | } 62 | } 63 | } 64 | 65 | /// Color inversion. 66 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 67 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 68 | pub enum ColorInversion { 69 | /// Normal colors. 70 | Normal, 71 | /// Inverted colors. 72 | Inverted, 73 | } 74 | 75 | impl Default for ColorInversion { 76 | fn default() -> Self { 77 | Self::Normal 78 | } 79 | } 80 | 81 | /// Vertical refresh order. 82 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 83 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 84 | pub enum VerticalRefreshOrder { 85 | /// Refresh from top to bottom. 86 | TopToBottom, 87 | /// Refresh from bottom to top. 88 | BottomToTop, 89 | } 90 | 91 | impl Default for VerticalRefreshOrder { 92 | fn default() -> Self { 93 | Self::TopToBottom 94 | } 95 | } 96 | 97 | impl VerticalRefreshOrder { 98 | /// Returns the opposite refresh order. 99 | #[must_use] 100 | pub const fn flip(self) -> Self { 101 | match self { 102 | Self::TopToBottom => Self::BottomToTop, 103 | Self::BottomToTop => Self::TopToBottom, 104 | } 105 | } 106 | } 107 | 108 | /// Horizontal refresh order. 109 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 110 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 111 | pub enum HorizontalRefreshOrder { 112 | /// Refresh from left to right. 113 | LeftToRight, 114 | /// Refresh from right to left. 115 | RightToLeft, 116 | } 117 | 118 | impl Default for HorizontalRefreshOrder { 119 | fn default() -> Self { 120 | Self::LeftToRight 121 | } 122 | } 123 | 124 | impl HorizontalRefreshOrder { 125 | /// Returns the opposite refresh order. 126 | #[must_use] 127 | pub const fn flip(self) -> Self { 128 | match self { 129 | Self::LeftToRight => Self::RightToLeft, 130 | Self::RightToLeft => Self::LeftToRight, 131 | } 132 | } 133 | } 134 | 135 | /// Display refresh order. 136 | /// 137 | /// Defaults to left to right, top to bottom. 138 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] 139 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 140 | pub struct RefreshOrder { 141 | /// Vertical refresh order. 142 | pub vertical: VerticalRefreshOrder, 143 | /// Horizontal refresh order. 144 | pub horizontal: HorizontalRefreshOrder, 145 | } 146 | 147 | impl RefreshOrder { 148 | /// Creates a new refresh order. 149 | pub const fn new(vertical: VerticalRefreshOrder, horizontal: HorizontalRefreshOrder) -> Self { 150 | Self { 151 | vertical, 152 | horizontal, 153 | } 154 | } 155 | 156 | /// Returns a refresh order with flipped vertical refresh order. 157 | #[must_use] 158 | pub const fn flip_vertical(self) -> Self { 159 | Self { 160 | vertical: self.vertical.flip(), 161 | ..self 162 | } 163 | } 164 | 165 | /// Returns a refresh order with flipped horizontal refresh order. 166 | #[must_use] 167 | pub const fn flip_horizontal(self) -> Self { 168 | Self { 169 | horizontal: self.horizontal.flip(), 170 | ..self 171 | } 172 | } 173 | } 174 | 175 | /// Tearing effect output setting. 176 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 177 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 178 | pub enum TearingEffect { 179 | /// Disable output. 180 | Off, 181 | /// Output vertical blanking information. 182 | Vertical, 183 | /// Output horizontal and vertical blanking information. 184 | HorizontalAndVertical, 185 | } 186 | 187 | /// Subpixel order. 188 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 189 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 190 | pub enum ColorOrder { 191 | /// RGB subpixel order. 192 | Rgb, 193 | /// BGR subpixel order. 194 | Bgr, 195 | } 196 | 197 | impl Default for ColorOrder { 198 | fn default() -> Self { 199 | Self::Rgb 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/test_image.rs: -------------------------------------------------------------------------------- 1 | use core::marker::PhantomData; 2 | 3 | use embedded_graphics_core::{ 4 | geometry::{AnchorPoint, AnchorX}, 5 | prelude::*, 6 | primitives::Rectangle, 7 | }; 8 | 9 | /// Test image. 10 | /// 11 | /// The test image can be used to check if the display is working and to 12 | /// identify the correct orientation and color settings. 13 | /// 14 | /// # Expected output 15 | /// 16 | #[doc = include_str!("../assets/test_image.svg")] 17 | /// 18 | /// Note that the gray border around the image above is only added to make the 19 | /// white border visible on the light rustdoc theme and will not be visible when 20 | /// the test image is drawn. 21 | /// 22 | /// - There should be a one pixel white border around the display. 23 | /// Modify the [display size](crate::Builder::display_size) and [display 24 | /// offset](crate::Builder::display_offset) settings, if at least one 25 | /// edge of the white border isn't drawn or if there is a gap between the 26 | /// white border and the edge of the display. 27 | /// - A white triangle should be drawn in the top left corner and the RGB label text should not be mirrored. 28 | /// Modify the [orientation](crate::Builder::orientation) setting to 29 | /// rotate and mirror the display until the test image is displayed correctly. 30 | /// Note that the white triangle might not be visible on displays with rounded 31 | /// corners. 32 | /// - The colored bars should match the labels. 33 | /// Use the [color inversion](crate::Builder::invert_colors) and [color 34 | /// order](crate::Builder::color_order) settings until the colored bars 35 | /// and labels match. 36 | #[derive(Default)] 37 | pub struct TestImage { 38 | color_type: PhantomData, 39 | } 40 | 41 | impl TestImage { 42 | /// Creates a new test image 43 | pub const fn new() -> Self { 44 | Self { 45 | color_type: PhantomData, 46 | } 47 | } 48 | } 49 | 50 | const BORDER_WIDTH: u32 = 1; 51 | const BORDER_PADDING: u32 = 4; 52 | const TOP_LEFT_MARKER_SIZE: u32 = 20; 53 | 54 | impl Drawable for TestImage { 55 | type Color = C; 56 | type Output = (); 57 | 58 | fn draw(&self, target: &mut D) -> Result 59 | where 60 | D: DrawTarget, 61 | { 62 | draw_border(target, BORDER_WIDTH)?; 63 | 64 | let color_bar_area = target 65 | .bounding_box() 66 | .offset(-i32::try_from(BORDER_WIDTH + BORDER_PADDING).unwrap()); 67 | draw_color_bars(target, &color_bar_area)?; 68 | 69 | draw_top_left_marker(target, &color_bar_area, TOP_LEFT_MARKER_SIZE)?; 70 | 71 | Ok(()) 72 | } 73 | } 74 | 75 | /// Draws a white border around the draw target. 76 | fn draw_border(target: &mut D, width: u32) -> Result<(), D::Error> 77 | where 78 | D: DrawTarget, 79 | D::Color: RgbColor, 80 | { 81 | let bounding_box = target.bounding_box(); 82 | let inner_box = bounding_box.offset(-i32::try_from(width).unwrap()); 83 | 84 | target.fill_contiguous( 85 | &bounding_box, 86 | bounding_box.points().map(|p| { 87 | if inner_box.contains(p) { 88 | D::Color::BLACK 89 | } else { 90 | D::Color::WHITE 91 | } 92 | }), 93 | ) 94 | } 95 | 96 | /// Draws RGB color bars and labels. 97 | fn draw_color_bars(target: &mut D, area: &Rectangle) -> Result<(), D::Error> 98 | where 99 | D: DrawTarget, 100 | D::Color: RgbColor, 101 | { 102 | target.fill_solid(area, RgbColor::GREEN)?; 103 | Character::new(G, area.center()).draw(target)?; 104 | 105 | let rect = area.resized_width(area.size.width / 3, AnchorX::Left); 106 | target.fill_solid(&rect, RgbColor::RED)?; 107 | Character::new(R, rect.center()).draw(target)?; 108 | 109 | let rect = area.resized_width(area.size.width / 3, AnchorX::Right); 110 | target.fill_solid(&rect, RgbColor::BLUE)?; 111 | Character::new(B, rect.center()).draw(target)?; 112 | 113 | Ok(()) 114 | } 115 | 116 | // Draws a triangular marker in the top left corner. 117 | fn draw_top_left_marker(target: &mut D, area: &Rectangle, size: u32) -> Result<(), D::Error> 118 | where 119 | D: DrawTarget, 120 | D::Color: RgbColor, 121 | { 122 | let mut rect = area.resized(Size::new(size, 1), AnchorPoint::TopLeft); 123 | 124 | while rect.size.width > 0 { 125 | target.fill_solid(&rect, D::Color::WHITE)?; 126 | 127 | rect.top_left.y += 1; 128 | rect.size.width -= 1; 129 | } 130 | 131 | Ok(()) 132 | } 133 | 134 | const R: &[u8] = &[ 135 | 0, 0, 0, 0, 0, 0, 0, 0, 0, // 136 | 0, 0, 0, 0, 0, 0, 0, 0, 0, // 137 | 0, 0, 1, 1, 1, 1, 0, 0, 0, // 138 | 0, 0, 1, 0, 0, 0, 1, 0, 0, // 139 | 0, 0, 1, 0, 0, 0, 1, 0, 0, // 140 | 0, 0, 1, 1, 1, 1, 0, 0, 0, // 141 | 0, 0, 1, 0, 1, 0, 0, 0, 0, // 142 | 0, 0, 1, 0, 0, 1, 0, 0, 0, // 143 | 0, 0, 1, 0, 0, 0, 1, 0, 0, // 144 | 0, 0, 0, 0, 0, 0, 0, 0, 0, // 145 | 0, 0, 0, 0, 0, 0, 0, 0, 0, // 146 | ]; 147 | 148 | const G: &[u8] = &[ 149 | 0, 0, 0, 0, 0, 0, 0, 0, 0, // 150 | 0, 0, 0, 0, 0, 0, 0, 0, 0, // 151 | 0, 0, 0, 1, 1, 1, 1, 0, 0, // 152 | 0, 0, 1, 0, 0, 0, 0, 0, 0, // 153 | 0, 0, 1, 0, 0, 0, 0, 0, 0, // 154 | 0, 0, 1, 0, 1, 1, 1, 0, 0, // 155 | 0, 0, 1, 0, 0, 0, 1, 0, 0, // 156 | 0, 0, 1, 0, 0, 0, 1, 0, 0, // 157 | 0, 0, 0, 1, 1, 1, 1, 0, 0, // 158 | 0, 0, 0, 0, 0, 0, 0, 0, 0, // 159 | 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160 | ]; 161 | 162 | const B: &[u8] = &[ 163 | 0, 0, 0, 0, 0, 0, 0, 0, 0, // 164 | 0, 0, 0, 0, 0, 0, 0, 0, 0, // 165 | 0, 0, 1, 1, 1, 1, 0, 0, 0, // 166 | 0, 0, 1, 0, 0, 0, 1, 0, 0, // 167 | 0, 0, 1, 0, 0, 0, 1, 0, 0, // 168 | 0, 0, 1, 1, 1, 1, 0, 0, 0, // 169 | 0, 0, 1, 0, 0, 0, 1, 0, 0, // 170 | 0, 0, 1, 0, 0, 0, 1, 0, 0, // 171 | 0, 0, 1, 1, 1, 1, 0, 0, 0, // 172 | 0, 0, 0, 0, 0, 0, 0, 0, 0, // 173 | 0, 0, 0, 0, 0, 0, 0, 0, 0, // 174 | ]; 175 | 176 | struct Character { 177 | data: &'static [u8], 178 | center: Point, 179 | color_type: PhantomData, 180 | } 181 | 182 | impl Character { 183 | fn new(data: &'static [u8], center: Point) -> Self { 184 | Self { 185 | data, 186 | center, 187 | color_type: PhantomData, 188 | } 189 | } 190 | } 191 | 192 | impl Drawable for Character { 193 | type Color = C; 194 | type Output = (); 195 | 196 | fn draw(&self, target: &mut D) -> Result<(), D::Error> 197 | where 198 | D: DrawTarget, 199 | { 200 | let rect = Rectangle::with_center(self.center, Size::new(9, 11)); 201 | 202 | target.fill_contiguous( 203 | &rect, 204 | self.data.iter().map(|d| { 205 | if *d == 0 { 206 | RgbColor::BLACK 207 | } else { 208 | RgbColor::WHITE 209 | } 210 | }), 211 | ) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | - added `SpiInterface::release` 13 | - added `RM67162` model support 14 | - made `InitError` visible 15 | - added `ILI9488` model support 16 | - added `ILI9228` model support 17 | - added `KIND` constant to `Interface` trait to detect invalid model, color format, and interface combinations 18 | - added `InvalidConfiguration` variant to `InitError` enum 19 | - added `update_address_window` in Model trait. 20 | - added feature-gated defmt support for Interface errors. 21 | 22 | ### Changed 23 | 24 | - changed the returned error type of `Model::init` to a new `ModelInitError` type to allow implementations to report configuration errors 25 | - added new errors returned from `Builder::init` in case of invalid `display_size` or `display_offset` parameters 26 | - Move functions `set_vertical_scroll_offset`, `set_vertical_scroll_region`, `set_tearing_effect`, `update_options`, `software_reset`, `write_memory_start`, `wake` and `sleep` 's dcs command part into Model trait from Display trait. 27 | 28 | ## Removed 29 | 30 | - remove unused `nb` dependency 31 | - remove panic from `Builder::display_size` if invalid size is given 32 | 33 | ## [0.9.0] 34 | 35 | ### Added 36 | 37 | - added `GC9107` model support 38 | 39 | ### Changed 40 | 41 | - replaced `display_interface` with our own trait and implementations for significantly better performance, see the [migration guide](https://github.com/almindor/mipidsi/blob/master/docs/MIGRATION.md#v08---09) for details 42 | 43 | ## [v0.8.0] - 2024-05-24 44 | 45 | ### Added 46 | 47 | - added `GC9A01` model support 48 | - added `Display::wake` method 49 | - added `Display::sleep` method 50 | - added `Display::is_sleeping` method 51 | - added `Display::dcs` method to allow sending custom DCS commands to the device 52 | - added `TestImage::default` 53 | - added `ST7796` model support (#125) 54 | 55 | ### Changed 56 | 57 | - rename all `Builder::with_*` methods to versions without the `with_` prefix to conform to the Rust API guidelines. See [this issue](https://github.com/almindor/mipidsi/issues/113) for more info 58 | - `options` and `error` submodule types are no longer re-exported from the main library 59 | - DCS commands param fields are now all consistently private with added constructors for all such commands 60 | - DCS command constructors (such as `SetAddressMode::new`) are now marked as `const`, so DCS commands can be constructed in 61 | [const contexts](https://doc.rust-lang.org/reference/const_eval.html#const-context) 62 | - replaced `window_offset_handler` function pointer with `offset` field 63 | - default to disabled color inversion for all generic models 64 | - renamed `Display::set_scroll_region` and `Display::set_scroll_offset` into `set_vertical_scroll_region` and `set_vertical_scroll_offset` 65 | - updated to `embedded-hal v1.0.0` 66 | - updated to `display-interface v0.5.0` 67 | - removed `Model::default_options` 68 | - bumped MSRV to `v1.75` 69 | - fixed `DrawTarget::fill_contiguous` for images that overlap the edge of the framebuffer 70 | - replaced model specific `Builder` constructors (like `Builder::gc9a01`) with one generic `Builder::new` constructor 71 | - replaced rest pin parameter in `Builder::init` by `Builder::with_reset_pin` setter 72 | - removed setters and getters from `ModelOptions` and instead made the fields public 73 | - added `non_exhaustive` attribute to `ModelOptions` 74 | - added checks to `Builder::init` to ensure that the display size and display offset are valid 75 | 76 | ### Removed 77 | 78 | - removed `Builder::with_framebuffer_size` 79 | 80 | ## [v0.7.1] - 2023-05-24 81 | 82 | ### Changed 83 | 84 | - fixed MSRV in `Cargo.toml` to match the rest at `v1.61` 85 | 86 | ## [v0.7.0] - 2023-05-24 87 | 88 | ### Changed 89 | 90 | - switched `embedded-graphics-core v0.4.0` 91 | - updated initialization delays `ILI934x` model 92 | 93 | ## [v0.6.0] - 2023-01-12 94 | 95 | ### Added 96 | 97 | - added `Builder::with_window_offset_handler` method 98 | - added `ModelOptions::invert_colors` flag 99 | - added `Builder::with_invert_colors(bool)` method 100 | - added `ILI9341` model support 101 | 102 | ### Changed 103 | 104 | - `Model::init` changed to expect `options: &ModelOptions` 105 | - reworked how `DCS` instructions are handled using the new `dcs` module and `DcsCommand` trait and implementations 106 | - reworked model init functions to use new `dcs` module 107 | 108 | ### Removed 109 | 110 | - removed duplicated `INVON` call in `ST7735s` model init 111 | 112 | ## [v0.5.0] - 2022-10-19 113 | 114 | ### Added 115 | 116 | - added the `Builder` as construction method for displays to simplify configuration 117 | and protect against use-before-init bugs 118 | - added `Model::default_options()` so that each model can provide a sane default regardless of helper constructors 119 | 120 | ### Changed 121 | 122 | - `Model` no longer has to own `ModelOptions` 123 | - `Model::new` was removed 124 | - the optional `RST` reset hw pin is now only used during the `Builder::init` call 125 | 126 | ### Removed 127 | 128 | - removed direct `Display` constructors. Use `Builder` instead (see migration guide) 129 | - removed `DisplayOptions` in favour of `Builder` settings 130 | 131 | ## [v0.4.0] - 2022-09-30 132 | 133 | ### Added 134 | 135 | - support for model variants via `DisplayOptions` 136 | - support for `raspberry pico1` variant of the `ST7789` display 137 | - support for the `waveshare` variants of the `ST7789` display 138 | 139 | ### Changed 140 | 141 | - split [DisplayOptions] into [DisplayOptions] and [ModelOptions] with sizing initialization safety constructors 142 | - refactored `Display::init` and constructors to match new variant code 143 | - fixed off by one error in fill operations 144 | 145 | ### Removed 146 | 147 | - removed "no reset pin" constructor helpers (uses `Option` now) 148 | 149 | ## [v0.3.0] - 2022-08-30 150 | 151 | ### Added 152 | 153 | - added `ILI9342C` model support thanks to [Jesse Braham's](https://github.com/jessebraham) [PR](https://github.com/almindor/mipidsi/pull/25) 154 | 155 | ## [v0.2.2] - 2022-08-26 156 | 157 | ### Changed 158 | 159 | - fix `Display::clear` out of bounds pixels 160 | - remove `ST7789` model `Bgr` bit override 161 | 162 | ## [v0.2.1] - 2022-08-03 163 | 164 | ### Added 165 | 166 | - clarified display model constructor usage in `README` 167 | 168 | ### Changed 169 | 170 | - fix `i32` -> `u16` conversion overflow bug in `batch` module in case of negative coordinates 171 | 172 | ## [v0.2.0] - 2021-04-12 173 | 174 | ### Changed 175 | 176 | - fix RGB/BGR color issue on some models 177 | - expand `Orientation` to use mirror image settings properly 178 | - change `Display::init` to include `DisplayOptions` and allow setting all `MADCTL` values on init, including `Orientation` 179 | - fix issues [#6](https://github.com/almindor/mipidsi/issues/6), [#8](https://github.com/almindor/mipidsi/issues/8) and [#10](https://github.com/almindor/mipidsi/issues/10) 180 | - big thanks to [@brianmay](https://github.com/brianmay) and [@KerryRJ](https://github.com/KerryRJ) 181 | 182 | ## [v0.1.0] - 2021-09-09 183 | 184 | ### Added 185 | 186 | - Initial release 187 | -------------------------------------------------------------------------------- /src/interface/parallel.rs: -------------------------------------------------------------------------------- 1 | use embedded_hal::digital::OutputPin; 2 | 3 | use super::{Interface, InterfaceKind}; 4 | 5 | /// This trait represents the data pins of a parallel bus. 6 | /// 7 | /// See [Generic8BitBus] and [Generic16BitBus] for generic implementations. 8 | pub trait OutputBus { 9 | /// [u8] for 8-bit buses, [u16] for 16-bit buses, etc. 10 | type Word: Copy; 11 | 12 | /// Interface kind. 13 | const KIND: InterfaceKind; 14 | 15 | /// Error type 16 | type Error: core::fmt::Debug; 17 | 18 | /// Set the output bus to a specific value 19 | fn set_value(&mut self, value: Self::Word) -> Result<(), Self::Error>; 20 | } 21 | 22 | macro_rules! generic_bus { 23 | ($GenericxBitBus:ident { type Word = $Word:ident; const KIND: InterfaceKind = $InterfaceKind:expr; Pins {$($PX:ident => $x:tt,)*}}) => { 24 | /// A generic implementation of [OutputBus] using [OutputPin]s 25 | pub struct $GenericxBitBus<$($PX, )*> { 26 | pins: ($($PX, )*), 27 | last: Option<$Word>, 28 | } 29 | 30 | impl<$($PX, )*> $GenericxBitBus<$($PX, )*> 31 | where 32 | $($PX: OutputPin, )* 33 | { 34 | /// Creates a new bus. This does not change the state of the pins. 35 | /// 36 | /// The first pin in the tuple is the least significant bit. 37 | pub fn new(pins: ($($PX, )*)) -> Self { 38 | Self { pins, last: None } 39 | } 40 | 41 | /// Consumes the bus and returns the pins. This does not change the state of the pins. 42 | pub fn release(self) -> ($($PX, )*) { 43 | self.pins 44 | } 45 | } 46 | 47 | impl<$($PX, )* E> OutputBus 48 | for $GenericxBitBus<$($PX, )*> 49 | where 50 | $($PX: OutputPin, )* 51 | E: core::fmt::Debug, 52 | { 53 | type Word = $Word; 54 | type Error = E; 55 | 56 | const KIND: InterfaceKind = $InterfaceKind; 57 | 58 | fn set_value(&mut self, value: Self::Word) -> Result<(), Self::Error> { 59 | if self.last == Some(value) { 60 | // It's quite common for multiple consecutive values to be identical, e.g. when filling or 61 | // clearing the screen, so let's optimize for that case 62 | return Ok(()) 63 | } 64 | 65 | // Sets self.last to None. 66 | // We will update it to Some(value) *after* all the pins are succesfully set. 67 | let last = self.last.take(); 68 | 69 | let changed = match last { 70 | Some(old_value) => value ^ old_value, 71 | None => !0, // all ones, this ensures that we will update all the pins 72 | }; 73 | 74 | $( 75 | let mask = 1 << $x; 76 | if changed & mask != 0 { 77 | if value & mask != 0 { 78 | self.pins.$x.set_high() 79 | } else { 80 | self.pins.$x.set_low() 81 | } 82 | ?; 83 | } 84 | )* 85 | 86 | self.last = Some(value); 87 | Ok(()) 88 | } 89 | } 90 | 91 | impl<$($PX, )*> From<($($PX, )*)> 92 | for $GenericxBitBus<$($PX, )*> 93 | where 94 | $($PX: OutputPin, )* 95 | { 96 | fn from(pins: ($($PX, )*)) -> Self { 97 | Self::new(pins) 98 | } 99 | } 100 | }; 101 | } 102 | 103 | generic_bus! { 104 | Generic8BitBus { 105 | type Word = u8; 106 | const KIND: InterfaceKind = InterfaceKind::Parallel8Bit; 107 | Pins { 108 | P0 => 0, 109 | P1 => 1, 110 | P2 => 2, 111 | P3 => 3, 112 | P4 => 4, 113 | P5 => 5, 114 | P6 => 6, 115 | P7 => 7, 116 | } 117 | } 118 | } 119 | 120 | generic_bus! { 121 | Generic16BitBus { 122 | type Word = u16; 123 | const KIND: InterfaceKind = InterfaceKind::Parallel16Bit; 124 | Pins { 125 | P0 => 0, 126 | P1 => 1, 127 | P2 => 2, 128 | P3 => 3, 129 | P4 => 4, 130 | P5 => 5, 131 | P6 => 6, 132 | P7 => 7, 133 | P8 => 8, 134 | P9 => 9, 135 | P10 => 10, 136 | P11 => 11, 137 | P12 => 12, 138 | P13 => 13, 139 | P14 => 14, 140 | P15 => 15, 141 | } 142 | } 143 | } 144 | 145 | /// Parallel interface error 146 | #[derive(Clone, Copy, Debug)] 147 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 148 | pub enum ParallelError { 149 | /// Bus error 150 | Bus(BUS), 151 | /// Data/command pin error 152 | Dc(DC), 153 | /// Write pin error 154 | Wr(WR), 155 | } 156 | 157 | /// Parallel communication interface 158 | /// 159 | /// This interface implements a "8080" style write-only display interface using any 160 | /// [`OutputBus`] implementation as well as one 161 | /// [`OutputPin`] for the data/command selection and one [`OutputPin`] for the write-enable flag. 162 | /// 163 | /// All pins in the data bus are supposed to be high-active. High for the D/C pin meaning "data" and the 164 | /// write-enable being pulled low before the setting of the bits and supposed to be sampled at a 165 | /// low to high edge. 166 | pub struct ParallelInterface { 167 | bus: BUS, 168 | dc: DC, 169 | wr: WR, 170 | } 171 | 172 | impl ParallelInterface 173 | where 174 | BUS: OutputBus, 175 | BUS::Word: From + Eq, 176 | DC: OutputPin, 177 | WR: OutputPin, 178 | { 179 | /// Create new parallel GPIO interface for communication with a display driver 180 | pub fn new(bus: BUS, dc: DC, wr: WR) -> Self { 181 | Self { bus, dc, wr } 182 | } 183 | 184 | /// Consume the display interface and return 185 | /// the bus and GPIO pins used by it 186 | pub fn release(self) -> (BUS, DC, WR) { 187 | (self.bus, self.dc, self.wr) 188 | } 189 | 190 | fn send_word( 191 | &mut self, 192 | word: BUS::Word, 193 | ) -> Result<(), ParallelError> { 194 | self.wr.set_low().map_err(ParallelError::Wr)?; 195 | self.bus.set_value(word).map_err(ParallelError::Bus)?; 196 | self.wr.set_high().map_err(ParallelError::Wr) 197 | } 198 | } 199 | 200 | impl Interface for ParallelInterface 201 | where 202 | BUS: OutputBus, 203 | BUS::Word: From + Eq, 204 | DC: OutputPin, 205 | WR: OutputPin, 206 | { 207 | type Word = BUS::Word; 208 | type Error = ParallelError; 209 | 210 | const KIND: InterfaceKind = BUS::KIND; 211 | 212 | fn send_command(&mut self, command: u8, args: &[u8]) -> Result<(), Self::Error> { 213 | self.dc.set_low().map_err(ParallelError::Dc)?; 214 | self.send_word(BUS::Word::from(command))?; 215 | self.dc.set_high().map_err(ParallelError::Dc)?; 216 | 217 | for arg in args { 218 | self.send_word(BUS::Word::from(*arg))?; 219 | } 220 | 221 | Ok(()) 222 | } 223 | 224 | fn send_pixels( 225 | &mut self, 226 | pixels: impl IntoIterator, 227 | ) -> Result<(), Self::Error> { 228 | for pixel in pixels { 229 | for word in pixel { 230 | self.send_word(word)?; 231 | } 232 | } 233 | Ok(()) 234 | } 235 | 236 | fn send_repeated_pixel( 237 | &mut self, 238 | pixel: [Self::Word; N], 239 | count: u32, 240 | ) -> Result<(), Self::Error> { 241 | if count == 0 || N == 0 { 242 | return Ok(()); 243 | } 244 | 245 | if let Some(word) = is_same(pixel) { 246 | self.send_word(word)?; 247 | for _ in 1..(count * N as u32) { 248 | self.wr.set_low().map_err(ParallelError::Wr)?; 249 | self.wr.set_high().map_err(ParallelError::Wr)?; 250 | } 251 | Ok(()) 252 | } else { 253 | self.send_pixels((0..count).map(|_| pixel)) 254 | } 255 | } 256 | } 257 | 258 | fn is_same(array: [T; N]) -> Option { 259 | let (&first, rest) = array.split_first()?; 260 | for &x in rest { 261 | if x != first { 262 | return None; 263 | } 264 | } 265 | Some(first) 266 | } 267 | -------------------------------------------------------------------------------- /docs/MIGRATION.md: -------------------------------------------------------------------------------- 1 | # Migration guide for `mipidsi` crate 2 | 3 | ## v0.8 -> 0.9 4 | 5 | ### Users 6 | 7 | * The `display_interface` dependency has been removed in favor of our own trait and implementations. This gives significantly better performance (#149) 8 | 9 | * Replace `display_interface_spi::SPIInterface` with `mipidsi::interface::SpiInterface`. Note that it requires a small/medium sized `&mut [u8]` buffer. 512 bytes works well, your milage may vary. 10 | ```rust 11 | // before 12 | use display_interface_spi::SPIInterface; 13 | 14 | let di = SPIInterface::new(spi_device, dc); 15 | 16 | // after 17 | use mipidsi::interface::SpiInterface; 18 | 19 | let mut buffer = [0_u8; 512]; 20 | let di = SpiInterface::new(spi_device, dc, &mut buffer); 21 | ``` 22 | 23 | * Replace `display_interface_parallel_gpio::PGPIO{8,16}BitInterface` with `mipidsi::interface::ParallelInterface` and use `Generic{8,16}BitBus` from `mipidsi::interface` 24 | ```rust 25 | // before 26 | use display_interface_parallel_gpio::Generic8BitBus; 27 | use display_interface_parallel_gpio::PGPIO8BitInterface; 28 | 29 | let bus = Generic8BitBus::new(pins); 30 | let di = PGPIO8BitInterface::new(bus, dc, wr); 31 | 32 | // after 33 | use mipidsi::interface::Generic8BitBus; 34 | use mipidsi::interface::ParallelInterface; 35 | 36 | let bus = Generic8BitBus::new(pins); 37 | let di = ParallelInterface::new(bus, dc, wr); 38 | ``` 39 | 40 | * If you have a custom impl of the `display_interface_parallel_gpio::OutputBus` trait, replace it with `mipidsi::interface::OutputBus`. This trait is identical except that it uses an associated error type instead of being hardcoded to `display_interface::DisplayError` 41 | 42 | * If you are using `stm32f4xx-hal`'s fsmc, you can copy and paste this adapter: 43 | ```rust 44 | pub struct FsmcAdapter(pub Lcd); 45 | 46 | impl Interface for FsmcAdapter 47 | where 48 | S: SubBank, 49 | WORD: Word + Copy + From, 50 | { 51 | type Word = WORD; 52 | 53 | type Error = Infallible; 54 | 55 | fn send_command(&mut self, command: u8, args: &[u8]) -> Result<(), Self::Error> { 56 | self.0.write_command(command.into()); 57 | for &arg in args { 58 | self.0.write_data(arg.into()); 59 | } 60 | 61 | Ok(()) 62 | } 63 | 64 | fn send_pixels( 65 | &mut self, 66 | pixels: impl IntoIterator, 67 | ) -> Result<(), Self::Error> { 68 | for pixel in pixels { 69 | for word in pixel { 70 | self.0.write_data(word); 71 | } 72 | } 73 | 74 | Ok(()) 75 | } 76 | 77 | fn send_repeated_pixel( 78 | &mut self, 79 | pixel: [Self::Word; N], 80 | count: u32, 81 | ) -> Result<(), Self::Error> { 82 | self.send_pixels((0..count).map(|_| pixel)) 83 | } 84 | } 85 | ``` 86 | 87 | 88 | ## v0.7 -> 0.8 89 | 90 | ### Users 91 | 92 | * The dependencies for the `embedded-hal` and `display-interface` crates have been updated to make `mipidsi` compatible with the 1.0 release of `embedded-hal`. 93 | 94 | * The model specific constructors (like `Builder::ili9341_rgb565`) have been removed. Use the generic `Builder::new` constructor instead: 95 | ```rust 96 | // 0.7 97 | use mipidsi::Builder; 98 | let display = Builder::ili9341_rgb565(di) 99 | .init(&mut delay, None)?; 100 | 101 | // 0.8 102 | use mipidsi::{Builder, models::ILI9341Rgb565}; 103 | let display = Builder::new(ILI9341Rgb565, di) 104 | .init(&mut delay)?; 105 | ``` 106 | * The reset pin parameter from `Builder::init` has been removed. Use the `Builder::reset_pin` setter instead: 107 | ```rust 108 | // 0.7 109 | use mipidsi::Builder; 110 | let display = Builder::new(ili9341_rgb565, di) 111 | .init(&mut delay, Some(rst))?; 112 | 113 | // 0.8 114 | use mipidsi::{Builder, models::ILI9341Rgb565}; 115 | let display = Builder::new(ILI9341Rgb565, di) 116 | .reset_pin(rst) 117 | .init(&mut delay)?; 118 | ``` 119 | 120 | * The `Builder::with_*` methods were renamed to versions without the `with_` prefix to bring the library in compliance with Rust API guidelines (see [this issue](https://github.com/almindor/mipidsi/issues/113) for more info): 121 | ```rust 122 | // 0.7 123 | use mipidsi::Builder; 124 | let display = Builder::new(ili9341_rgb565, di) 125 | .with_invert_colors(ColorInversion::Normal) 126 | .with_orientation(Orienation::default()) 127 | .init(&mut delay, Some(rst))?; 128 | 129 | // 0.8 130 | use mipidsi::{Builder, models::ILI9341Rgb565}; 131 | let display = Builder::new(ILI9341Rgb565, di) 132 | .invert_colors(ColorInversion::Normal) 133 | .orientation(Orienation::default()) 134 | .reset_pin(rst) 135 | .init(&mut delay)?; 136 | ``` 137 | 138 | * The default values for the `invert_colors` and `color_order` settings were inconsistent between different models in `mipidsi` 0.7. In 0.8 all models use the same default values with no color inversion and RGB subpixel order. Take a look at the [troubleshooting guide](https://github.com/almindor/mipidsi/blob/master/docs/TROUBLESHOOTING.md#incorrect-colors) if your display shows incorrect colors after the update. 139 | 140 | ### Model writers 141 | 142 | * The `default_options` function has been removed from the `Model` trait to make the default options consistent between the models. The maximum framebuffer size is now defined by the added `FRAMEBUFFER_SIZE` constant. 143 | 144 | * The type of the `DELAY` parameter in the `init` method has been changed to the `embedded-hal` 1.0 type `DelayNs`. 145 | 146 | ## v0.6 -> 0.7 147 | 148 | No breaking changes. 149 | 150 | ## v0.5 -> 0.6 151 | 152 | ### Users 153 | 154 | * no change, addition of `Builder::with_invert_colors(bool)` allows more combinations of display variants. 155 | 156 | ### Model writers and specific variants 157 | 158 | `Model::init` now expects the `options: &ModelOptions` instead of just `madcl: u8` argument. This allows the use of the `invert_colors` field during init. 159 | 160 | ## v0.4 -> 0.5 161 | 162 | ### Users 163 | 164 | * use `Builder` to construct the `Display` and set any options directly on construction 165 | 166 | #### v0.4 167 | 168 | ```rust 169 | let display = Display::st7789(di, rst, DisplayOptions::default()); 170 | display.init(&mut delay); 171 | ``` 172 | 173 | #### v0.5 174 | 175 | ```rust 176 | let display = Builder::st7789(di) // known model or with_model(model) 177 | .with_display_size(240, 240) // set any options on the builder before init 178 | .init(&mut delay, Some(rst)); // optional reset pin 179 | ``` 180 | 181 | ### Model writers and specific variants 182 | 183 | `Model::new` was reverted and is no longer necessary. Models now don't own the `ModelOptions` which has been moved off to the `Display` directly. `Model::init` has changed to include `madctl` parameter which is now provided by the `Display` and should be used as-is unless overrides are required. 184 | `Model::default_options` was added to facilitate "generic variant" construction. 185 | 186 | Helper constructors have been moved from `Display` to `Builder` with similar implementations as before. 187 | `DisplayOptions` and `ModelOptions` values are now a function of the `Builder` and do not necessitate a constructor helper anymore. e.g. `Display::st7789_240x240(...)` becomes `Builder::st7789(...).with_display_size(240, 240)` controlled by the user. 188 | Any variants can still set all of the options for a variant via a builder shortcut, such as `Builder::st7789_pico1`. 189 | 190 | ## v0.3 -> v0.4 191 | 192 | ### Users 193 | 194 | * `Display` helper constructors now take `DisplayOptions` argument instead of `init` 195 | 196 | #### v0.3 197 | 198 | ```rust 199 | let display = Display::st7789(di, rst); 200 | display.init(&mut delay, DisplayOptions::default()); 201 | ``` 202 | 203 | #### v0.4 204 | 205 | ```rust 206 | let display = Display::st7789(di, rst, DisplayOptions::default()); 207 | display.init(&mut delay); 208 | ``` 209 | 210 | ### Model writers and specific variants 211 | 212 | `Model` now requires that the `Model::new` constructor takes `ModelOptions` which is an artefact of `DisplayOptions` in combination with display sizing and windowing settings. This is used by the "helper constructors" to provide pre-made `Model` variants to users. 213 | 214 | Most display `Model`s require just one constructor but some like the `ST7789` have a lot of variants that have different display, frameber sizes or even windowing clipping offsets. These can now be provided easily by creating a helper constructor that provides the dimensions information to create `ModelOptions`. 215 | 216 | For users that need to use a variant of a `Model` that does not yet have a constructor helper, this can be done manually provided you know what your display dimensions and offsets are. -------------------------------------------------------------------------------- /src/graphics.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics_core::{ 2 | draw_target::DrawTarget, 3 | geometry::{Dimensions, OriginDimensions, Size}, 4 | pixelcolor::RgbColor, 5 | primitives::Rectangle, 6 | Pixel, 7 | }; 8 | use embedded_hal::digital::OutputPin; 9 | 10 | use crate::dcs::InterfaceExt; 11 | use crate::{dcs::BitsPerPixel, interface::Interface}; 12 | use crate::{dcs::WriteMemoryStart, models::Model}; 13 | use crate::{interface::InterfacePixelFormat, Display}; 14 | 15 | impl DrawTarget for Display 16 | where 17 | DI: Interface, 18 | M: Model, 19 | M::ColorFormat: InterfacePixelFormat, 20 | RST: OutputPin, 21 | { 22 | type Error = DI::Error; 23 | type Color = M::ColorFormat; 24 | 25 | #[cfg(not(feature = "batch"))] 26 | fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> 27 | where 28 | I: IntoIterator>, 29 | { 30 | for pixel in pixels { 31 | let x = pixel.0.x as u16; 32 | let y = pixel.0.y as u16; 33 | 34 | self.set_pixel(x, y, pixel.1)?; 35 | } 36 | 37 | Ok(()) 38 | } 39 | 40 | #[cfg(feature = "batch")] 41 | fn draw_iter(&mut self, item: T) -> Result<(), Self::Error> 42 | where 43 | T: IntoIterator>, 44 | { 45 | use crate::batch::DrawBatch; 46 | 47 | self.draw_batch(item) 48 | } 49 | 50 | fn fill_contiguous(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error> 51 | where 52 | I: IntoIterator, 53 | { 54 | let intersection = area.intersection(&self.bounding_box()); 55 | let Some(bottom_right) = intersection.bottom_right() else { 56 | // No intersection -> nothing to draw 57 | return Ok(()); 58 | }; 59 | 60 | // Unchecked casting to u16 cannot fail here because the values are 61 | // clamped to the display size which always fits in an u16. 62 | let sx = intersection.top_left.x as u16; 63 | let sy = intersection.top_left.y as u16; 64 | let ex = bottom_right.x as u16; 65 | let ey = bottom_right.y as u16; 66 | 67 | let count = intersection.size.width * intersection.size.height; 68 | 69 | let mut colors = colors.into_iter(); 70 | 71 | if &intersection == area { 72 | // Draw the original iterator if no edge overlaps the framebuffer 73 | self.set_pixels(sx, sy, ex, ey, take_u32(colors, count)) 74 | } else { 75 | // Skip pixels above and to the left of the intersection 76 | let mut initial_skip = 0; 77 | if intersection.top_left.y > area.top_left.y { 78 | initial_skip += intersection.top_left.y.abs_diff(area.top_left.y) * area.size.width; 79 | } 80 | if intersection.top_left.x > area.top_left.x { 81 | initial_skip += intersection.top_left.x.abs_diff(area.top_left.x); 82 | } 83 | if initial_skip > 0 { 84 | nth_u32(&mut colors, initial_skip - 1); 85 | } 86 | 87 | // Draw only the pixels which don't overlap the edges of the framebuffer 88 | let take_per_row = intersection.size.width; 89 | let skip_per_row = area.size.width - intersection.size.width; 90 | self.set_pixels( 91 | sx, 92 | sy, 93 | ex, 94 | ey, 95 | take_u32(TakeSkip::new(colors, take_per_row, skip_per_row), count), 96 | ) 97 | } 98 | } 99 | 100 | fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> { 101 | let area = area.intersection(&self.bounding_box()); 102 | let Some(bottom_right) = area.bottom_right() else { 103 | // No intersection -> nothing to draw 104 | return Ok(()); 105 | }; 106 | 107 | let count = area.size.width * area.size.height; 108 | 109 | let sx = area.top_left.x as u16; 110 | let sy = area.top_left.y as u16; 111 | let ex = bottom_right.x as u16; 112 | let ey = bottom_right.y as u16; 113 | 114 | self.set_address_window(sx, sy, ex, ey)?; 115 | self.di.write_command(WriteMemoryStart)?; 116 | M::ColorFormat::send_repeated_pixel(&mut self.di, color, count) 117 | } 118 | } 119 | 120 | impl OriginDimensions for Display 121 | where 122 | DI: Interface, 123 | MODEL: Model, 124 | MODEL::ColorFormat: InterfacePixelFormat, 125 | RST: OutputPin, 126 | { 127 | fn size(&self) -> Size { 128 | let ds = self.options.display_size(); 129 | let (width, height) = (u32::from(ds.0), u32::from(ds.1)); 130 | Size::new(width, height) 131 | } 132 | } 133 | 134 | impl BitsPerPixel { 135 | /// Returns the bits per pixel for a embedded-graphics [`RgbColor`]. 136 | pub const fn from_rgb_color() -> Self { 137 | let bpp = C::MAX_R.trailing_ones() + C::MAX_G.trailing_ones() + C::MAX_B.trailing_ones(); 138 | 139 | match bpp { 140 | 3 => Self::Three, 141 | 8 => Self::Eight, 142 | 12 => Self::Twelve, 143 | 16 => Self::Sixteen, 144 | 18 => Self::Eighteen, 145 | 24 => Self::TwentyFour, 146 | _ => panic!("invalid RgbColor bits per pixel"), 147 | } 148 | } 149 | } 150 | 151 | /// An iterator that alternately takes and skips elements of another iterator. 152 | struct TakeSkip { 153 | iter: I, 154 | take: u32, 155 | take_remaining: u32, 156 | skip: u32, 157 | } 158 | 159 | impl TakeSkip { 160 | pub fn new(iter: I, take: u32, skip: u32) -> Self { 161 | Self { 162 | iter, 163 | take, 164 | take_remaining: take, 165 | skip, 166 | } 167 | } 168 | } 169 | 170 | impl Iterator for TakeSkip { 171 | type Item = I::Item; 172 | 173 | fn next(&mut self) -> Option { 174 | if self.take_remaining > 0 { 175 | self.take_remaining -= 1; 176 | self.iter.next() 177 | } else if self.take > 0 { 178 | self.take_remaining = self.take - 1; 179 | nth_u32(&mut self.iter, self.skip) 180 | } else { 181 | None 182 | } 183 | } 184 | } 185 | 186 | #[cfg(not(target_pointer_width = "16"))] 187 | fn take_u32(iter: I, max_count: u32) -> impl Iterator { 188 | iter.take(max_count.try_into().unwrap()) 189 | } 190 | 191 | #[cfg(target_pointer_width = "16")] 192 | fn take_u32(iter: I, max_count: u32) -> impl Iterator { 193 | let mut count = 0; 194 | iter.take_while(move |_| { 195 | count += 1; 196 | count <= max_count 197 | }) 198 | } 199 | 200 | #[cfg(not(target_pointer_width = "16"))] 201 | fn nth_u32(mut iter: I, n: u32) -> Option { 202 | iter.nth(n.try_into().unwrap()) 203 | } 204 | 205 | #[cfg(target_pointer_width = "16")] 206 | fn nth_u32(mut iter: I, n: u32) -> Option { 207 | for _ in 0..n { 208 | iter.next(); 209 | } 210 | iter.next() 211 | } 212 | 213 | #[cfg(test)] 214 | mod test { 215 | use crate::dcs::BitsPerPixel; 216 | use embedded_graphics_core::pixelcolor::*; 217 | 218 | use super::TakeSkip; 219 | 220 | #[test] 221 | fn bpp_from_rgb_color_works() { 222 | assert_eq!( 223 | BitsPerPixel::from_rgb_color::(), 224 | BitsPerPixel::Sixteen 225 | ); 226 | assert_eq!( 227 | BitsPerPixel::from_rgb_color::(), 228 | BitsPerPixel::Eighteen 229 | ); 230 | assert_eq!( 231 | BitsPerPixel::from_rgb_color::(), 232 | BitsPerPixel::TwentyFour 233 | ); 234 | } 235 | 236 | #[test] 237 | #[should_panic] 238 | fn bpp_from_rgb_color_invalid_panics() { 239 | BitsPerPixel::from_rgb_color::(); 240 | } 241 | 242 | #[test] 243 | fn take_skip_iter() { 244 | let mut iter = TakeSkip::new(0..11, 3, 2); 245 | assert_eq!(iter.next(), Some(0)); 246 | assert_eq!(iter.next(), Some(1)); 247 | assert_eq!(iter.next(), Some(2)); 248 | // Skip 3 and 4 249 | assert_eq!(iter.next(), Some(5)); 250 | assert_eq!(iter.next(), Some(6)); 251 | assert_eq!(iter.next(), Some(7)); 252 | // Skip 8 and 9 253 | assert_eq!(iter.next(), Some(10)); 254 | assert_eq!(iter.next(), None); 255 | } 256 | 257 | #[test] 258 | fn take_skip_with_take_equals_zero() { 259 | // take == 0 should not cause an integer overflow or infinite loop and 260 | // just return None 261 | let mut iter = TakeSkip::new(0..11, 0, 2); 262 | assert_eq!(iter.next(), None); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/models.rs: -------------------------------------------------------------------------------- 1 | //! Display models. 2 | 3 | use crate::{ 4 | dcs::{self, InterfaceExt, SetAddressMode}, 5 | interface::Interface, 6 | options::{self, ModelOptions, Rotation}, 7 | ConfigurationError, 8 | }; 9 | use embedded_graphics_core::prelude::RgbColor; 10 | use embedded_hal::delay::DelayNs; 11 | 12 | // existing model implementations 13 | mod gc9107; 14 | mod gc9a01; 15 | mod ili9225; 16 | mod ili9341; 17 | mod ili9342c; 18 | mod ili934x; 19 | mod ili9486; 20 | mod ili9488; 21 | mod ili948x; 22 | mod rm67162; 23 | mod st7735s; 24 | mod st7789; 25 | mod st7796; 26 | 27 | pub use gc9107::*; 28 | pub use gc9a01::*; 29 | pub use ili9225::*; 30 | pub use ili9341::*; 31 | pub use ili9342c::*; 32 | pub use ili9486::*; 33 | pub use ili9488::*; 34 | pub use rm67162::*; 35 | pub use st7735s::*; 36 | pub use st7789::*; 37 | pub use st7796::*; 38 | 39 | /// Display model. 40 | pub trait Model { 41 | /// The color format. 42 | type ColorFormat: RgbColor; 43 | 44 | /// The framebuffer size in pixels. 45 | const FRAMEBUFFER_SIZE: (u16, u16); 46 | 47 | /// Duration of the active low reset pulse in µs. 48 | const RESET_DURATION: u32 = 10; 49 | 50 | /// Initializes the display for this model with MADCTL from [crate::Display] 51 | /// and returns the value of MADCTL set by init 52 | fn init( 53 | &mut self, 54 | di: &mut DI, 55 | delay: &mut DELAY, 56 | options: &ModelOptions, 57 | ) -> Result> 58 | where 59 | DELAY: DelayNs, 60 | DI: Interface; 61 | 62 | /// Updates the address window of the display. 63 | fn update_address_window( 64 | di: &mut DI, 65 | _rotation: Rotation, 66 | sx: u16, 67 | sy: u16, 68 | ex: u16, 69 | ey: u16, 70 | ) -> Result<(), DI::Error> 71 | where 72 | DI: Interface, 73 | { 74 | di.write_command(dcs::SetColumnAddress::new(sx, ex))?; 75 | di.write_command(dcs::SetPageAddress::new(sy, ey)) 76 | } 77 | 78 | /// 79 | /// Need to call [Self::wake] before issuing other commands 80 | /// 81 | fn sleep(di: &mut DI, delay: &mut DELAY) -> Result<(), DI::Error> 82 | where 83 | DI: Interface, 84 | DELAY: DelayNs, 85 | { 86 | di.write_command(dcs::EnterSleepMode)?; 87 | // All supported models requires a 120ms delay before issuing other commands 88 | delay.delay_us(120_000); 89 | Ok(()) 90 | } 91 | /// 92 | /// Wakes the display after it's been set to sleep via [Self::sleep] 93 | /// 94 | fn wake(di: &mut DI, delay: &mut DELAY) -> Result<(), DI::Error> 95 | where 96 | DI: Interface, 97 | DELAY: DelayNs, 98 | { 99 | di.write_command(dcs::ExitSleepMode)?; 100 | // ST7789 and st7735s have the highest minimal delay of 120ms 101 | delay.delay_us(120_000); 102 | Ok(()) 103 | } 104 | /// 105 | /// We need WriteMemoryStart befor write pixel 106 | /// 107 | fn write_memory_start(di: &mut DI) -> Result<(), DI::Error> 108 | where 109 | DI: Interface, 110 | { 111 | di.write_command(dcs::WriteMemoryStart) 112 | } 113 | /// 114 | /// SoftReset 115 | /// 116 | fn software_reset(di: &mut DI) -> Result<(), DI::Error> 117 | where 118 | DI: Interface, 119 | { 120 | di.write_command(dcs::SoftReset) 121 | } 122 | /// 123 | /// This function will been called if user update options 124 | /// 125 | fn update_options(&self, di: &mut DI, options: &ModelOptions) -> Result<(), DI::Error> 126 | where 127 | DI: Interface, 128 | { 129 | let madctl = SetAddressMode::from(options); 130 | di.write_command(madctl) 131 | } 132 | 133 | /// 134 | /// Configures the tearing effect output. 135 | /// 136 | fn set_tearing_effect( 137 | di: &mut DI, 138 | tearing_effect: options::TearingEffect, 139 | _options: &ModelOptions, 140 | ) -> Result<(), DI::Error> 141 | where 142 | DI: Interface, 143 | { 144 | di.write_command(dcs::SetTearingEffect::new(tearing_effect)) 145 | } 146 | 147 | /// Sets the vertical scroll region. 148 | /// 149 | /// The `top_fixed_area` and `bottom_fixed_area` arguments can be used to 150 | /// define an area on the top and/or bottom of the display which won't be 151 | /// affected by scrolling. 152 | /// 153 | /// Note that this method is not affected by the current display orientation 154 | /// and will always scroll vertically relative to the default display 155 | /// orientation. 156 | /// 157 | /// The combined height of the fixed area must not larger than the 158 | /// height of the framebuffer height in the default orientation. 159 | /// 160 | /// After the scrolling region is defined the [`set_vertical_scroll_offset`](Self::set_vertical_scroll_offset) can be 161 | /// used to scroll the display. 162 | fn set_vertical_scroll_region( 163 | di: &mut DI, 164 | top_fixed_area: u16, 165 | bottom_fixed_area: u16, 166 | ) -> Result<(), DI::Error> 167 | where 168 | DI: Interface, 169 | { 170 | let rows = Self::FRAMEBUFFER_SIZE.1; 171 | 172 | let vscrdef = if top_fixed_area + bottom_fixed_area > rows { 173 | dcs::SetScrollArea::new(rows, 0, 0) 174 | } else { 175 | dcs::SetScrollArea::new( 176 | top_fixed_area, 177 | rows - top_fixed_area - bottom_fixed_area, 178 | bottom_fixed_area, 179 | ) 180 | }; 181 | 182 | di.write_command(vscrdef) 183 | } 184 | 185 | /// Sets the vertical scroll offset. 186 | /// 187 | /// Setting the vertical scroll offset shifts the vertical scroll region 188 | /// upwards by `offset` pixels. 189 | /// 190 | /// Use [`set_vertical_scroll_region`](Self::set_vertical_scroll_region) to setup the scroll region, before 191 | /// using this method. 192 | fn set_vertical_scroll_offset(di: &mut DI, offset: u16) -> Result<(), DI::Error> 193 | where 194 | DI: Interface, 195 | { 196 | let vscad = dcs::SetScrollStart::new(offset); 197 | di.write_command(vscad) 198 | } 199 | } 200 | 201 | /// Error returned by [`Model::init`]. 202 | /// 203 | /// This error type is used internally by implementations of the [`Model`] 204 | /// trait. 205 | #[derive(Debug)] 206 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 207 | pub enum ModelInitError { 208 | /// Error caused by the display interface. 209 | Interface(DiError), 210 | 211 | /// Invalid configuration error. 212 | /// 213 | /// This error is returned when the configuration passed to the builder is 214 | /// invalid. For example, when the combination of bit depth and interface 215 | /// kind isn't supported by the selected model. 216 | InvalidConfiguration(ConfigurationError), 217 | } 218 | 219 | impl From for ModelInitError { 220 | fn from(value: DiError) -> Self { 221 | Self::Interface(value) 222 | } 223 | } 224 | 225 | #[cfg(test)] 226 | mod tests { 227 | use embedded_graphics::pixelcolor::Rgb565; 228 | 229 | use crate::{ 230 | Builder, 231 | _mock::{MockDelay, MockDisplayInterface}, 232 | dcs::SetAddressMode, 233 | interface::InterfaceKind, 234 | ConfigurationError, InitError, 235 | }; 236 | 237 | use super::*; 238 | 239 | struct OnlyOneKindModel(InterfaceKind); 240 | 241 | impl Model for OnlyOneKindModel { 242 | type ColorFormat = Rgb565; 243 | 244 | const FRAMEBUFFER_SIZE: (u16, u16) = (16, 16); 245 | 246 | fn init( 247 | &mut self, 248 | _di: &mut DI, 249 | _delay: &mut DELAY, 250 | _options: &ModelOptions, 251 | ) -> Result> 252 | where 253 | DELAY: DelayNs, 254 | DI: Interface, 255 | { 256 | if DI::KIND != self.0 { 257 | return Err(ModelInitError::InvalidConfiguration( 258 | ConfigurationError::UnsupportedInterface, 259 | )); 260 | } 261 | 262 | Ok(SetAddressMode::default()) 263 | } 264 | } 265 | 266 | #[test] 267 | fn test_assert_interface_kind_serial() { 268 | Builder::new( 269 | OnlyOneKindModel(InterfaceKind::Serial4Line), 270 | MockDisplayInterface, 271 | ) 272 | .init(&mut MockDelay) 273 | .unwrap(); 274 | } 275 | 276 | #[test] 277 | fn test_assert_interface_kind_parallel() { 278 | assert!(matches!( 279 | Builder::new( 280 | OnlyOneKindModel(InterfaceKind::Parallel8Bit), 281 | MockDisplayInterface, 282 | ) 283 | .init(&mut MockDelay), 284 | Err(InitError::InvalidConfiguration( 285 | ConfigurationError::UnsupportedInterface 286 | )) 287 | )); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/models/ili9225.rs: -------------------------------------------------------------------------------- 1 | use crate::dcs::DcsCommand; 2 | use crate::dcs::InterfaceExt; 3 | use crate::dcs::SetAddressMode; 4 | use crate::options; 5 | use crate::options::{ColorOrder, Rotation}; 6 | use crate::{ 7 | interface::Interface, 8 | models::{Model, ModelInitError}, 9 | options::ModelOptions, 10 | }; 11 | use embedded_graphics_core::pixelcolor::Rgb565; 12 | use embedded_hal::delay::DelayNs; 13 | 14 | /// ILI9225 display in Rgb565 color mode. 15 | pub struct ILI9225Rgb565; 16 | 17 | const ILI9225_POWER_CTRL1: u8 = 0x10; 18 | const ILI9225_POWER_CTRL2: u8 = 0x11; 19 | const ILI9225_POWER_CTRL3: u8 = 0x12; 20 | const ILI9225_POWER_CTRL4: u8 = 0x13; 21 | const ILI9225_POWER_CTRL5: u8 = 0x14; 22 | 23 | const ILI9225_DRIVER_OUTPUT_CTRL: u8 = 0x01; // Driver Output Control 24 | const ILI9225_LCD_AC_DRIVING_CTRL: u8 = 0x02; // LCD AC Driving Control 25 | const ILI9225_ENTRY_MODE: u8 = 0x03; // Entry Mode 26 | const ILI9225_DISP_CTRL1: u8 = 0x07; // Display Control 1 27 | const ILI9225_BLANK_PERIOD_CTRL1: u8 = 0x08; // Blank Period Control 28 | const ILI9225_FRAME_CYCLE_CTRL: u8 = 0x0B; // Frame Cycle Control 29 | const ILI9225_INTERFACE_CTRL: u8 = 0x0C; // Interface Control 30 | const ILI9225_OSC_CTRL: u8 = 0x0F; // Osc Control 31 | const ILI9225_VCI_RECYCLING: u8 = 0x15; // Osc Control 32 | const ILI9225_RAM_ADDR_SET1: u8 = 0x20; // Osc Control 33 | const ILI9225_RAM_ADDR_SET2: u8 = 0x21; // Osc Control 34 | 35 | const ILI9225_GATE_SCAN_CTRL: u8 = 0x30; // Gate Scan Control Register 36 | const ILI9225_VERTICAL_SCROLL_CTRL1: u8 = 0x31; // Vertical Scroll Control 1 Register 37 | const ILI9225_VERTICAL_SCROLL_CTRL2: u8 = 0x32; // Vertical Scroll Control 2 Register 38 | const ILI9225_VERTICAL_SCROLL_CTRL3: u8 = 0x33; // Vertical Scroll Control 3 Register 39 | const ILI9225_PARTIAL_DRIVING_POS1: u8 = 0x34; // Partial Driving Position 1 Register 40 | const ILI9225_PARTIAL_DRIVING_POS2: u8 = 0x35; // Partial Driving Position 2 Register 41 | const ILI9225_HORIZONTAL_WINDOW_ADDR1: u8 = 0x36; // Horizontal Address Start Position 42 | const ILI9225_HORIZONTAL_WINDOW_ADDR2: u8 = 0x37; // Horizontal Address End Position 43 | const ILI9225_VERTICAL_WINDOW_ADDR1: u8 = 0x38; // Vertical Address Start Position 44 | const ILI9225_VERTICAL_WINDOW_ADDR2: u8 = 0x39; // Vertical Address End Position 45 | 46 | const ILI9225_GAMMA_CTRL1: u8 = 0x50; // Gamma Control 1 47 | const ILI9225_GAMMA_CTRL2: u8 = 0x51; // Gamma Control 2 48 | const ILI9225_GAMMA_CTRL3: u8 = 0x52; // Gamma Control 3 49 | const ILI9225_GAMMA_CTRL4: u8 = 0x53; // Gamma Control 4 50 | const ILI9225_GAMMA_CTRL5: u8 = 0x54; // Gamma Control 5 51 | const ILI9225_GAMMA_CTRL6: u8 = 0x55; // Gamma Control 6 52 | const ILI9225_GAMMA_CTRL7: u8 = 0x56; // Gamma Control 7 53 | const ILI9225_GAMMA_CTRL8: u8 = 0x57; // Gamma Control 8 54 | const ILI9225_GAMMA_CTRL9: u8 = 0x58; // Gamma Control 9 55 | const ILI9225_GAMMA_CTRL10: u8 = 0x59; // Gamma Control 10 56 | 57 | fn options_write_cmd(di: &mut DI, options: &ModelOptions) -> Result<(), DI::Error> 58 | where 59 | DI: Interface, 60 | { 61 | // Command 1: DRIVER_OUTPUT_CTRL (0x01) 62 | let driver_high_byte = match options.orientation.rotation { 63 | Rotation::Deg0 => 0x01, 64 | Rotation::Deg90 => 0x00, 65 | Rotation::Deg180 => 0x02, 66 | Rotation::Deg270 => 0x03, 67 | }; 68 | 69 | let driver_params = [driver_high_byte, 0x1C]; 70 | di.write_raw(ILI9225_DRIVER_OUTPUT_CTRL, &driver_params)?; 71 | 72 | // Command 2: ENTRY_MODE (0x03) 73 | let color_order_byte = match options.color_order { 74 | ColorOrder::Rgb => 0x00, 75 | ColorOrder::Bgr => 0x10, 76 | }; 77 | 78 | let entry_low_byte = if options.orientation.rotation.is_vertical() { 79 | 0x38 80 | } else { 81 | 0x30 82 | }; 83 | let entry_params = [color_order_byte, entry_low_byte]; 84 | di.write_raw(ILI9225_ENTRY_MODE, &entry_params)?; 85 | 86 | Ok(()) 87 | } 88 | 89 | fn options2ctrl_low(options: &ModelOptions) -> u8 { 90 | 0b10011 91 | | match options.invert_colors { 92 | options::ColorInversion::Normal => 0, 93 | options::ColorInversion::Inverted => 0b100, 94 | } 95 | } 96 | 97 | impl Model for ILI9225Rgb565 { 98 | type ColorFormat = Rgb565; 99 | const FRAMEBUFFER_SIZE: (u16, u16) = (176, 220); 100 | const RESET_DURATION: u32 = 1000; 101 | 102 | fn init( 103 | &mut self, 104 | di: &mut DI, 105 | delay: &mut DELAY, 106 | options: &ModelOptions, 107 | ) -> Result> 108 | where 109 | DELAY: DelayNs, 110 | DI: Interface, 111 | { 112 | let madctl = SetAddressMode::from(options); 113 | 114 | /* Set SS bit and direction output from S528 to S1 */ 115 | di.write_raw(ILI9225_POWER_CTRL1, &[0x00, 0x00])?; // Set SAP,DSTB,STB 116 | di.write_raw(ILI9225_POWER_CTRL2, &[0x00, 0x00])?; // Set APON,PON,AON,VCI1EN,VC 117 | di.write_raw(ILI9225_POWER_CTRL3, &[0x00, 0x00])?; // Set BT,DC1,DC2,DC3 118 | di.write_raw(ILI9225_POWER_CTRL4, &[0x00, 0x00])?; // Set GVDD 119 | di.write_raw(ILI9225_POWER_CTRL5, &[0x00, 0x00])?; // Set VCOMH/VCOML voltage 120 | 121 | delay.delay_us(40_000); 122 | 123 | di.write_raw(ILI9225_POWER_CTRL1, &[0x00, 0x18])?; // Set APON,PON,AON,VCI1EN,VC 124 | di.write_raw(ILI9225_POWER_CTRL2, &[0x61, 0x21])?; // Set BT,DC1,DC2,DC3 125 | di.write_raw(ILI9225_POWER_CTRL3, &[0x00, 0x6F])?; // Set GVDD /*007F 0088 */ 126 | di.write_raw(ILI9225_POWER_CTRL4, &[0x49, 0x5F])?; // Set VCOMH/VCOML voltage 127 | di.write_raw(ILI9225_POWER_CTRL5, &[0x08, 0x00])?; // Set SAP,DSTB,STB 128 | delay.delay_us(10_000); 129 | di.write_raw(ILI9225_POWER_CTRL2, &[0x10, 0x3B])?; // Set APON,PON,AON,VCI1EN,VC 130 | delay.delay_us(30_000); 131 | 132 | di.write_raw(ILI9225_LCD_AC_DRIVING_CTRL, &[0x01, 0x00])?; // set 1 line inversion 133 | 134 | options_write_cmd(di, options)?; 135 | di.write_raw(ILI9225_DISP_CTRL1, &[0x00, 0x00])?; // Display off 136 | di.write_raw(ILI9225_BLANK_PERIOD_CTRL1, &[0x08, 0x08])?; // set the back porch and front porch 137 | di.write_raw(ILI9225_FRAME_CYCLE_CTRL, &[0x11, 0x00])?; // set the clocks number per line 138 | di.write_raw(ILI9225_INTERFACE_CTRL, &[0x00, 0x00])?; // CPU interface 139 | di.write_raw(ILI9225_OSC_CTRL, &[0x0F, 0x01])?; // Set Osc /*0e01*/ 140 | di.write_raw(ILI9225_VCI_RECYCLING, &[0x00, 0x20])?; // Set VCI recycling 141 | di.write_raw(ILI9225_RAM_ADDR_SET1, &[0x00, 0x00])?; // RAM Address 142 | di.write_raw(ILI9225_RAM_ADDR_SET2, &[0x00, 0x00])?; // RAM Address 143 | 144 | /* Set GRAM area */ 145 | di.write_raw(ILI9225_GATE_SCAN_CTRL, &[0x00, 0x00])?; 146 | di.write_raw(ILI9225_VERTICAL_SCROLL_CTRL1, &[0x00, 0xDB])?; 147 | di.write_raw(ILI9225_VERTICAL_SCROLL_CTRL2, &[0x00, 0x00])?; 148 | di.write_raw(ILI9225_VERTICAL_SCROLL_CTRL3, &[0x00, 0x00])?; 149 | di.write_raw(ILI9225_PARTIAL_DRIVING_POS1, &[0x00, 0xDB])?; 150 | di.write_raw(ILI9225_PARTIAL_DRIVING_POS2, &[0x00, 0x00])?; 151 | di.write_raw(ILI9225_HORIZONTAL_WINDOW_ADDR1, &[0x00, 0xAF])?; 152 | di.write_raw(ILI9225_HORIZONTAL_WINDOW_ADDR2, &[0x00, 0x00])?; 153 | di.write_raw(ILI9225_VERTICAL_WINDOW_ADDR1, &[0x00, 0xDB])?; 154 | di.write_raw(ILI9225_VERTICAL_WINDOW_ADDR2, &[0x00, 0x00])?; 155 | 156 | /* Set GAMMA curve */ 157 | di.write_raw(ILI9225_GAMMA_CTRL1, &[0x00, 0x00])?; 158 | di.write_raw(ILI9225_GAMMA_CTRL2, &[0x08, 0x08])?; 159 | di.write_raw(ILI9225_GAMMA_CTRL3, &[0x08, 0x0A])?; 160 | di.write_raw(ILI9225_GAMMA_CTRL4, &[0x00, 0x0A])?; 161 | di.write_raw(ILI9225_GAMMA_CTRL5, &[0x0A, 0x08])?; 162 | di.write_raw(ILI9225_GAMMA_CTRL6, &[0x08, 0x08])?; 163 | di.write_raw(ILI9225_GAMMA_CTRL7, &[0x00, 0x00])?; 164 | di.write_raw(ILI9225_GAMMA_CTRL8, &[0x0A, 0x00])?; 165 | di.write_raw(ILI9225_GAMMA_CTRL9, &[0x07, 0x10])?; 166 | di.write_raw(ILI9225_GAMMA_CTRL10, &[0x07, 0x10])?; 167 | 168 | di.write_raw(ILI9225_DISP_CTRL1, &[0x00, 0x12])?; 169 | delay.delay_us(50_000); 170 | 171 | let low = options2ctrl_low(options); 172 | 173 | di.write_raw(ILI9225_DISP_CTRL1, &[0x10, low])?; 174 | delay.delay_us(50_000); 175 | 176 | Ok(madctl) 177 | } 178 | 179 | fn update_address_window( 180 | di: &mut DI, 181 | rotation: Rotation, 182 | sx: u16, 183 | sy: u16, 184 | ex: u16, 185 | ey: u16, 186 | ) -> Result<(), DI::Error> 187 | where 188 | DI: Interface, 189 | { 190 | match rotation { 191 | Rotation::Deg0 | Rotation::Deg180 => { 192 | di.write_raw(0x37, &sx.to_be_bytes())?; 193 | di.write_raw(0x36, &ex.to_be_bytes())?; 194 | di.write_raw(0x39, &sy.to_be_bytes())?; 195 | di.write_raw(0x38, &ey.to_be_bytes())?; 196 | di.write_raw(0x20, &sx.to_be_bytes())?; 197 | di.write_raw(0x21, &sy.to_be_bytes()) 198 | } 199 | Rotation::Deg90 | Rotation::Deg270 => { 200 | di.write_raw(0x39, &sx.to_be_bytes())?; 201 | di.write_raw(0x38, &ex.to_be_bytes())?; 202 | di.write_raw(0x37, &sy.to_be_bytes())?; 203 | di.write_raw(0x36, &ey.to_be_bytes())?; 204 | di.write_raw(0x21, &sx.to_be_bytes())?; 205 | di.write_raw(0x20, &sy.to_be_bytes()) 206 | } 207 | } 208 | } 209 | 210 | fn sleep(di: &mut DI, delay: &mut DELAY) -> Result<(), DI::Error> 211 | where 212 | DI: Interface, 213 | DELAY: DelayNs, 214 | { 215 | di.write_raw(ILI9225_DISP_CTRL1, &[0x00, 0x00])?; 216 | delay.delay_us(50_000); 217 | di.write_raw(ILI9225_POWER_CTRL2, &[0x00, 0x07])?; 218 | delay.delay_us(50_000); 219 | di.write_raw(ILI9225_POWER_CTRL1, &[0x0A, 0x01]) 220 | } 221 | 222 | fn wake(di: &mut DI, delay: &mut DELAY) -> Result<(), DI::Error> 223 | where 224 | DI: Interface, 225 | DELAY: DelayNs, 226 | { 227 | di.write_raw(ILI9225_POWER_CTRL1, &[0x0A, 0x00])?; 228 | di.write_raw(ILI9225_POWER_CTRL2, &[0x10, 0x3B])?; 229 | delay.delay_us(50_000); 230 | di.write_raw(ILI9225_DISP_CTRL1, &[0x10, 0x17]) 231 | } 232 | 233 | fn write_memory_start(di: &mut DI) -> Result<(), DI::Error> 234 | where 235 | DI: Interface, 236 | { 237 | di.write_command(WriteMemoryStartILI9225) 238 | } 239 | 240 | fn update_options(&self, di: &mut DI, options: &ModelOptions) -> Result<(), DI::Error> 241 | where 242 | DI: Interface, 243 | { 244 | options_write_cmd(di, options) 245 | } 246 | fn set_tearing_effect( 247 | di: &mut DI, 248 | tearing_effect: options::TearingEffect, 249 | options: &ModelOptions, 250 | ) -> Result<(), DI::Error> 251 | where 252 | DI: Interface, 253 | { 254 | let low = options2ctrl_low(options); 255 | // Acroding the datasheet, TEMON only one bit, 256 | let high = match tearing_effect { 257 | options::TearingEffect::Off => 0, 258 | options::TearingEffect::Vertical => 0x10, 259 | options::TearingEffect::HorizontalAndVertical => 0x10, 260 | }; 261 | 262 | di.write_raw(ILI9225_DISP_CTRL1, &[high, low]) 263 | } 264 | fn set_vertical_scroll_region( 265 | _di: &mut DI, 266 | _top_fixed_area: u16, 267 | _bottom_fixed_area: u16, 268 | ) -> Result<(), DI::Error> 269 | where 270 | DI: Interface, 271 | { 272 | // Not support, ignore it 273 | Ok(()) 274 | } 275 | fn set_vertical_scroll_offset(_di: &mut DI, _offset: u16) -> Result<(), DI::Error> 276 | where 277 | DI: Interface, 278 | { 279 | // Not support, ignore it 280 | Ok(()) 281 | } 282 | } 283 | 284 | #[path = "../dcs/macros.rs"] 285 | #[macro_use] 286 | mod dcs_macros; 287 | 288 | dcs_basic_command!( 289 | /// Initiate Framebuffer Memory Write 290 | WriteMemoryStartILI9225, 291 | 0x22 292 | ); 293 | 294 | dcs_basic_command!( 295 | /// Software Reset 296 | SoftResetILI9225, 297 | 0x28 298 | ); 299 | -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | //! [super::Display] builder module 2 | 3 | use embedded_hal::{ 4 | delay::DelayNs, 5 | digital::{self, OutputPin}, 6 | }; 7 | 8 | use crate::{ 9 | dcs::InterfaceExt, 10 | interface::{Interface, InterfacePixelFormat}, 11 | models::{Model, ModelInitError}, 12 | options::{ColorInversion, ColorOrder, ModelOptions, Orientation, RefreshOrder}, 13 | Display, 14 | }; 15 | 16 | /// Builder for [Display] instances. 17 | /// 18 | /// Exposes all possible display options. 19 | /// 20 | /// # Examples 21 | /// 22 | /// ``` 23 | /// use mipidsi::{Builder, options::ColorOrder, models::ILI9342CRgb565}; 24 | /// 25 | /// # let di = mipidsi::_mock::MockDisplayInterface; 26 | /// # let rst = mipidsi::_mock::MockOutputPin; 27 | /// # let mut delay = mipidsi::_mock::MockDelay; 28 | /// let mut display = Builder::new(ILI9342CRgb565, di) 29 | /// .reset_pin(rst) 30 | /// .color_order(ColorOrder::Bgr) 31 | /// .display_size(320, 240) 32 | /// .init(&mut delay).unwrap(); 33 | /// ``` 34 | pub struct Builder 35 | where 36 | DI: Interface, 37 | MODEL: Model, 38 | MODEL::ColorFormat: InterfacePixelFormat, 39 | { 40 | di: DI, 41 | model: MODEL, 42 | rst: Option, 43 | options: ModelOptions, 44 | } 45 | 46 | impl Builder 47 | where 48 | DI: Interface, 49 | MODEL: Model, 50 | MODEL::ColorFormat: InterfacePixelFormat, 51 | { 52 | /// 53 | /// Constructs a new builder for given [Model]. 54 | /// 55 | #[must_use] 56 | pub fn new(model: MODEL, di: DI) -> Self { 57 | Self { 58 | di, 59 | model, 60 | rst: None, 61 | options: ModelOptions::full_size::(), 62 | } 63 | } 64 | } 65 | 66 | impl Builder 67 | where 68 | DI: Interface, 69 | MODEL: Model, 70 | MODEL::ColorFormat: InterfacePixelFormat, 71 | RST: OutputPin, 72 | { 73 | /// 74 | /// Sets the invert color flag 75 | /// 76 | #[must_use] 77 | pub fn invert_colors(mut self, color_inversion: ColorInversion) -> Self { 78 | self.options.invert_colors = color_inversion; 79 | self 80 | } 81 | 82 | /// 83 | /// Sets the [ColorOrder] 84 | /// 85 | #[must_use] 86 | pub fn color_order(mut self, color_order: ColorOrder) -> Self { 87 | self.options.color_order = color_order; 88 | self 89 | } 90 | 91 | /// 92 | /// Sets the [Orientation] 93 | /// 94 | #[must_use] 95 | pub fn orientation(mut self, orientation: Orientation) -> Self { 96 | self.options.orientation = orientation; 97 | self 98 | } 99 | 100 | /// 101 | /// Sets refresh order 102 | /// 103 | #[must_use] 104 | pub fn refresh_order(mut self, refresh_order: RefreshOrder) -> Self { 105 | self.options.refresh_order = refresh_order; 106 | self 107 | } 108 | 109 | /// Sets the display size. 110 | /// 111 | /// 112 | #[must_use] 113 | pub fn display_size(mut self, width: u16, height: u16) -> Self { 114 | self.options.display_size = (width, height); 115 | self 116 | } 117 | 118 | /// 119 | /// Sets the display offset 120 | /// 121 | #[must_use] 122 | pub fn display_offset(mut self, x: u16, y: u16) -> Self { 123 | self.options.display_offset = (x, y); 124 | self 125 | } 126 | 127 | /// Sets the reset pin. 128 | /// 129 | /// ### WARNING 130 | /// The reset pin needs to be in *high* state in order for the display to operate. 131 | /// If it wasn't provided the user needs to ensure this is the case. 132 | /// 133 | #[must_use] 134 | pub fn reset_pin(self, rst: RST2) -> Builder { 135 | Builder { 136 | di: self.di, 137 | model: self.model, 138 | rst: Some(rst), 139 | options: self.options, 140 | } 141 | } 142 | 143 | /// 144 | /// Consumes the builder to create a new [Display] with an optional reset [OutputPin]. 145 | /// Blocks using the provided [DelayNs] `delay_source` to perform the display initialization. 146 | /// The display will be awake ready to use, no need to call [Display::wake] after init. 147 | /// 148 | /// Returns [InitError] if the area defined by the [`display_size`](Self::display_size) 149 | /// and [`display_offset`](Self::display_offset) settings is (partially) outside the framebuffer. 150 | pub fn init( 151 | mut self, 152 | delay_source: &mut impl DelayNs, 153 | ) -> Result, InitError> { 154 | let to_u32 = |(a, b)| (u32::from(a), u32::from(b)); 155 | let (width, height) = to_u32(self.options.display_size); 156 | let (offset_x, offset_y) = to_u32(self.options.display_offset); 157 | let (max_width, max_height) = to_u32(MODEL::FRAMEBUFFER_SIZE); 158 | 159 | if width == 0 || height == 0 || width > max_width || height > max_height { 160 | return Err(InitError::InvalidConfiguration( 161 | ConfigurationError::InvalidDisplaySize, 162 | )); 163 | } 164 | 165 | if width + offset_x > max_width { 166 | return Err(InitError::InvalidConfiguration( 167 | ConfigurationError::InvalidDisplayOffset, 168 | )); 169 | } 170 | 171 | if height + offset_y > max_height { 172 | return Err(InitError::InvalidConfiguration( 173 | ConfigurationError::InvalidDisplayOffset, 174 | )); 175 | } 176 | 177 | match self.rst { 178 | Some(ref mut rst) => { 179 | rst.set_low().map_err(InitError::ResetPin)?; 180 | delay_source.delay_us(MODEL::RESET_DURATION); 181 | rst.set_high().map_err(InitError::ResetPin)?; 182 | } 183 | None => self 184 | .di 185 | .write_command(crate::dcs::SoftReset) 186 | .map_err(InitError::Interface)?, 187 | } 188 | 189 | let madctl = self.model.init(&mut self.di, delay_source, &self.options)?; 190 | 191 | let display = Display { 192 | di: self.di, 193 | model: self.model, 194 | rst: self.rst, 195 | options: self.options, 196 | madctl, 197 | sleeping: false, // TODO: init should lock state 198 | }; 199 | 200 | Ok(display) 201 | } 202 | } 203 | 204 | /// Error returned by [`Builder::init`]. 205 | #[derive(Debug)] 206 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 207 | pub enum InitError { 208 | /// Error caused by the display interface. 209 | Interface(DI), 210 | 211 | /// Error caused by the reset pin's [`OutputPin`](embedded_hal::digital::OutputPin) implementation. 212 | ResetPin(P), 213 | 214 | /// Invalid configuration error. 215 | /// 216 | /// This error is returned when the configuration passed to the builder is 217 | /// invalid. For example, when the combination of bit depth and interface 218 | /// kind isn't supported by the selected model. 219 | InvalidConfiguration(ConfigurationError), 220 | } 221 | 222 | /// Specifics of [InitError::InvalidConfiguration] if configuration was found invalid 223 | #[non_exhaustive] 224 | #[derive(Debug)] 225 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 226 | pub enum ConfigurationError { 227 | /// Unsupported interface kind. 228 | /// 229 | /// The chosen interface isn't supported by the selected model. Note that 230 | /// some controller models don't support all combinations of physical 231 | /// interface and color formats. To resolve this, try to use another color 232 | /// format if available (e.g. [`ILI9486Rgb666`](crate::models::ILI9486Rgb666) instead of 233 | /// [`ILI9486Rgb565`](crate::models::ILI9486Rgb565) if you use a SPI connection) 234 | UnsupportedInterface, 235 | /// Invalid display size 236 | /// 237 | /// Display dimensions provided in [Builder::display_size] were invalid, e.g. width or height of 0 238 | InvalidDisplaySize, 239 | /// Invalid display offset. 240 | /// 241 | /// The active display area, defined by [`display_size`](Builder::display_size) and 242 | /// [`display_offset`](Builder::display_offset), extends beyond the boundaries of 243 | /// the controller's framebuffer. To resolve this, reduce the offset to a maximum value of 244 | /// [`FRAMEBUFFER_SIZE`](Model::FRAMEBUFFER_SIZE) minus [`display_size`](Builder::display_size). 245 | InvalidDisplayOffset, 246 | } 247 | 248 | impl From> for InitError { 249 | fn from(value: ModelInitError) -> Self { 250 | match value { 251 | ModelInitError::Interface(e) => Self::Interface(e), 252 | ModelInitError::InvalidConfiguration(ce) => Self::InvalidConfiguration(ce), 253 | } 254 | } 255 | } 256 | 257 | /// Marker type for no reset pin. 258 | pub enum NoResetPin {} 259 | 260 | impl digital::OutputPin for NoResetPin { 261 | fn set_low(&mut self) -> Result<(), Self::Error> { 262 | Ok(()) 263 | } 264 | 265 | fn set_high(&mut self) -> Result<(), Self::Error> { 266 | Ok(()) 267 | } 268 | } 269 | 270 | impl digital::ErrorType for NoResetPin { 271 | type Error = core::convert::Infallible; 272 | } 273 | 274 | #[cfg(test)] 275 | mod tests { 276 | use crate::{ 277 | _mock::{MockDelay, MockDisplayInterface, MockOutputPin}, 278 | models::ILI9341Rgb565, 279 | }; 280 | 281 | use super::*; 282 | 283 | #[test] 284 | fn init_without_reset_pin() { 285 | let _: Display<_, _, NoResetPin> = Builder::new(ILI9341Rgb565, MockDisplayInterface) 286 | .init(&mut MockDelay) 287 | .unwrap(); 288 | } 289 | 290 | #[test] 291 | fn init_reset_pin() { 292 | let _: Display<_, _, MockOutputPin> = Builder::new(ILI9341Rgb565, MockDisplayInterface) 293 | .reset_pin(MockOutputPin) 294 | .init(&mut MockDelay) 295 | .unwrap(); 296 | } 297 | 298 | #[test] 299 | fn error_too_wide() { 300 | assert!(matches!( 301 | Builder::new(ILI9341Rgb565, MockDisplayInterface) 302 | .reset_pin(MockOutputPin) 303 | .display_size(241, 320) 304 | .init(&mut MockDelay), 305 | Err(InitError::InvalidConfiguration( 306 | ConfigurationError::InvalidDisplaySize 307 | )) 308 | )) 309 | } 310 | 311 | #[test] 312 | fn error_too_tall() { 313 | assert!(matches!( 314 | Builder::new(ILI9341Rgb565, MockDisplayInterface) 315 | .reset_pin(MockOutputPin) 316 | .display_size(240, 321) 317 | .init(&mut MockDelay), 318 | Err(InitError::InvalidConfiguration( 319 | ConfigurationError::InvalidDisplaySize 320 | )), 321 | )) 322 | } 323 | 324 | #[test] 325 | fn error_offset_invalid_x() { 326 | assert!(matches!( 327 | Builder::new(ILI9341Rgb565, MockDisplayInterface) 328 | .reset_pin(MockOutputPin) 329 | .display_size(240, 320) 330 | .display_offset(1, 0) 331 | .init(&mut MockDelay), 332 | Err(InitError::InvalidConfiguration( 333 | ConfigurationError::InvalidDisplayOffset 334 | )), 335 | )) 336 | } 337 | 338 | #[test] 339 | fn error_offset_invalid_y() { 340 | assert!(matches!( 341 | Builder::new(ILI9341Rgb565, MockDisplayInterface) 342 | .reset_pin(MockOutputPin) 343 | .display_size(240, 310) 344 | .display_offset(0, 11) 345 | .init(&mut MockDelay), 346 | Err(InitError::InvalidConfiguration( 347 | ConfigurationError::InvalidDisplayOffset 348 | )), 349 | )) 350 | } 351 | 352 | #[test] 353 | fn error_zero_size() { 354 | assert!(matches!( 355 | Builder::new(ILI9341Rgb565, MockDisplayInterface) 356 | .reset_pin(MockOutputPin) 357 | .display_size(0, 0) 358 | .init(&mut MockDelay), 359 | Err(InitError::InvalidConfiguration( 360 | ConfigurationError::InvalidDisplaySize 361 | )), 362 | )) 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/batch.rs: -------------------------------------------------------------------------------- 1 | //! Original code from: [this repo](https://github.com/lupyuen/piet-embedded/blob/master/piet-embedded-graphics/src/batch.rs) 2 | //! Batch the pixels to be rendered into Pixel Rows and Pixel Blocks (contiguous Pixel Rows). 3 | //! This enables the pixels to be rendered efficiently as Pixel Blocks, which may be transmitted in a single Non-Blocking SPI request. 4 | use crate::{ 5 | interface::{Interface, InterfacePixelFormat}, 6 | models::Model, 7 | Display, 8 | }; 9 | use embedded_graphics_core::prelude::*; 10 | use embedded_hal::digital::OutputPin; 11 | 12 | pub trait DrawBatch 13 | where 14 | DI: Interface, 15 | M: Model, 16 | M::ColorFormat: InterfacePixelFormat, 17 | I: IntoIterator>, 18 | { 19 | fn draw_batch(&mut self, item_pixels: I) -> Result<(), DI::Error>; 20 | } 21 | 22 | impl DrawBatch for Display 23 | where 24 | DI: Interface, 25 | M: Model, 26 | M::ColorFormat: InterfacePixelFormat, 27 | I: IntoIterator>, 28 | RST: OutputPin, 29 | { 30 | fn draw_batch(&mut self, item_pixels: I) -> Result<(), DI::Error> { 31 | // Get the pixels for the item to be rendered. 32 | let pixels = item_pixels.into_iter(); 33 | // Batch the pixels into Pixel Rows. 34 | let rows = to_rows(pixels); 35 | // Batch the Pixel Rows into Pixel Blocks. 36 | let blocks = to_blocks(rows); 37 | // For each Pixel Block... 38 | for PixelBlock { 39 | x_left, 40 | x_right, 41 | y_top, 42 | y_bottom, 43 | colors, 44 | .. 45 | } in blocks 46 | { 47 | // Render the Pixel Block. 48 | self.set_pixels(x_left, y_top, x_right, y_bottom, colors)?; 49 | 50 | // Dump out the Pixel Blocks for the square in test_display() 51 | /* if x_left >= 60 && x_left <= 150 && x_right >= 60 && x_right <= 150 && y_top >= 60 && y_top <= 150 && y_bottom >= 60 && y_bottom <= 150 { 52 | console::print("pixel block ("); console::printint(x_left as i32); console::print(", "); console::printint(y_top as i32); //// 53 | console::print("), ("); console::printint(x_right as i32); console::print(", "); console::printint(y_bottom as i32); console::print(")\n"); //// 54 | } */ 55 | } 56 | Ok(()) 57 | } 58 | } 59 | 60 | /// Max number of pixels per Pixel Row 61 | const MAX_ROW_SIZE: usize = 50; 62 | /// Max number of pixels per Pixel Block 63 | const MAX_BLOCK_SIZE: usize = 100; 64 | 65 | /// Consecutive color words for a Pixel Row 66 | type RowColors = heapless::Vec; 67 | /// Consecutive color words for a Pixel Block 68 | type BlockColors = heapless::Vec; 69 | 70 | /// Iterator for each Pixel Row in the pixel data. A Pixel Row consists of contiguous pixels on the same row. 71 | #[derive(Debug, Clone)] 72 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 73 | pub struct RowIterator 74 | where 75 | C: PixelColor, 76 | P: Iterator>, 77 | { 78 | /// Pixels to be batched into rows 79 | pixels: P, 80 | /// Start column number 81 | x_left: u16, 82 | /// End column number 83 | x_right: u16, 84 | /// Row number 85 | y: u16, 86 | /// List of pixel colours for the entire row 87 | colors: RowColors, 88 | /// True if this is the first pixel for the row 89 | first_pixel: bool, 90 | } 91 | 92 | /// Iterator for each Pixel Block in the pixel data. A Pixel Block consists of contiguous Pixel Rows with the same start and end column number. 93 | #[derive(Debug, Clone)] 94 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 95 | pub struct BlockIterator 96 | where 97 | C: PixelColor, 98 | R: Iterator>, 99 | { 100 | /// Pixel Rows to be batched into blocks 101 | rows: R, 102 | /// Start column number 103 | x_left: u16, 104 | /// End column number 105 | x_right: u16, 106 | /// Start row number 107 | y_top: u16, 108 | /// End row number 109 | y_bottom: u16, 110 | /// List of pixel colours for the entire block, row by row 111 | colors: BlockColors, 112 | /// True if this is the first row for the block 113 | first_row: bool, 114 | } 115 | 116 | /// A row of contiguous pixels 117 | pub struct PixelRow 118 | where 119 | C: PixelColor, 120 | { 121 | /// Start column number 122 | pub x_left: u16, 123 | /// End column number 124 | pub x_right: u16, 125 | /// Row number 126 | pub y: u16, 127 | /// List of pixel colours for the entire row 128 | pub colors: RowColors, 129 | } 130 | 131 | /// A block of contiguous pixel rows with the same start and end column number 132 | pub struct PixelBlock 133 | where 134 | C: PixelColor, 135 | { 136 | /// Start column number 137 | pub x_left: u16, 138 | /// End column number 139 | pub x_right: u16, 140 | /// Start row number 141 | pub y_top: u16, 142 | /// End row number 143 | pub y_bottom: u16, 144 | /// List of pixel colours for the entire block, row by row 145 | pub colors: BlockColors, 146 | } 147 | 148 | /// Batch the pixels into Pixel Rows, which are contiguous pixels on the same row. 149 | /// P can be any Pixel Iterator (e.g. a rectangle). 150 | fn to_rows(pixels: P) -> RowIterator 151 | where 152 | C: PixelColor, 153 | P: Iterator>, 154 | { 155 | RowIterator:: { 156 | pixels, 157 | x_left: 0, 158 | x_right: 0, 159 | y: 0, 160 | colors: RowColors::new(), 161 | first_pixel: true, 162 | } 163 | } 164 | 165 | /// Batch the Pixel Rows into Pixel Blocks, which are contiguous Pixel Rows with the same start and end column number 166 | /// R can be any Pixel Row Iterator. 167 | fn to_blocks(rows: R) -> BlockIterator 168 | where 169 | C: PixelColor, 170 | R: Iterator>, 171 | { 172 | BlockIterator:: { 173 | rows, 174 | x_left: 0, 175 | x_right: 0, 176 | y_top: 0, 177 | y_bottom: 0, 178 | colors: BlockColors::new(), 179 | first_row: true, 180 | } 181 | } 182 | 183 | /// Implement the Iterator for Pixel Rows. 184 | /// P can be any Pixel Iterator (e.g. a rectangle). 185 | impl Iterator for RowIterator 186 | where 187 | C: PixelColor, 188 | P: Iterator>, 189 | { 190 | /// This Iterator returns Pixel Rows 191 | type Item = PixelRow; 192 | 193 | /// Return the next Pixel Row of contiguous pixels on the same row 194 | fn next(&mut self) -> Option { 195 | // Loop over all pixels until we have composed a Pixel Row, or we have run out of pixels. 196 | loop { 197 | // Get the next pixel. 198 | let next_pixel = self.pixels.next(); 199 | match next_pixel { 200 | None => { 201 | // If no more pixels... 202 | if self.first_pixel { 203 | return None; // No pixels to group 204 | } 205 | // Else return previous pixels as row. 206 | let row = PixelRow { 207 | x_left: self.x_left, 208 | x_right: self.x_right, 209 | y: self.y, 210 | colors: self.colors.clone(), 211 | }; 212 | self.colors.clear(); 213 | self.first_pixel = true; 214 | return Some(row); 215 | } 216 | Some(Pixel(coord, color)) => { 217 | if coord.x < 0 || coord.y < 0 { 218 | continue; 219 | } 220 | // If there is a pixel... 221 | let x = coord.x as u16; 222 | let y = coord.y as u16; 223 | // Save the first pixel as the row start and handle next pixel. 224 | if self.first_pixel { 225 | self.first_pixel = false; 226 | self.x_left = x; 227 | self.x_right = x; 228 | self.y = y; 229 | self.colors.clear(); 230 | if self.colors.push(color).is_err() { 231 | return None; 232 | } 233 | continue; 234 | } 235 | // If this pixel is adjacent to the previous pixel, add to the row. 236 | if x == self.x_right.wrapping_add(1) 237 | && y == self.y 238 | && self.colors.push(color).is_ok() 239 | { 240 | // Don't add pixel if too many pixels in the row. 241 | self.x_right = x; 242 | continue; 243 | } 244 | // Else return previous pixels as row. 245 | let row = PixelRow { 246 | x_left: self.x_left, 247 | x_right: self.x_right, 248 | y: self.y, 249 | colors: self.colors.clone(), 250 | }; 251 | self.x_left = x; 252 | self.x_right = x; 253 | self.y = y; 254 | self.colors.clear(); 255 | if self.colors.push(color).is_err() { 256 | return None; 257 | } 258 | return Some(row); 259 | } 260 | } 261 | } 262 | } 263 | } 264 | 265 | /// Implement the Iterator for Pixel Blocks. 266 | /// R can be any Pixel Row Iterator. 267 | impl Iterator for BlockIterator 268 | where 269 | C: PixelColor, 270 | R: Iterator>, 271 | { 272 | /// This Iterator returns Pixel Blocks 273 | type Item = PixelBlock; 274 | 275 | /// Return the next Pixel Block of contiguous Pixel Rows with the same start and end column number 276 | fn next(&mut self) -> Option { 277 | // Loop over all Pixel Rows until we have composed a Pixel Block, or we have run out of Pixel Rows. 278 | loop { 279 | // Get the next Pixel Row. 280 | let next_row = self.rows.next(); 281 | match next_row { 282 | None => { 283 | // If no more Pixel Rows... 284 | if self.first_row { 285 | return None; // No rows to group 286 | } 287 | // Else return previous rows as block. 288 | let row = PixelBlock { 289 | x_left: self.x_left, 290 | x_right: self.x_right, 291 | y_top: self.y_top, 292 | y_bottom: self.y_bottom, 293 | colors: self.colors.clone(), 294 | }; 295 | self.colors.clear(); 296 | self.first_row = true; 297 | return Some(row); 298 | } 299 | Some(PixelRow { 300 | x_left, 301 | x_right, 302 | y, 303 | colors, 304 | .. 305 | }) => { 306 | // If there is a Pixel Row... 307 | // Save the first row as the block start and handle next block. 308 | if self.first_row { 309 | self.first_row = false; 310 | self.x_left = x_left; 311 | self.x_right = x_right; 312 | self.y_top = y; 313 | self.y_bottom = y; 314 | self.colors.clear(); 315 | self.colors.extend_from_slice(&colors).expect("never"); 316 | continue; 317 | } 318 | // If this row is adjacent to the previous row and same size, add to the block. 319 | if y == self.y_bottom + 1 && x_left == self.x_left && x_right == self.x_right { 320 | // Don't add row if too many pixels in the block. 321 | if self.colors.extend_from_slice(&colors).is_ok() { 322 | self.y_bottom = y; 323 | continue; 324 | } 325 | } 326 | // Else return previous rows as block. 327 | let row = PixelBlock { 328 | x_left: self.x_left, 329 | x_right: self.x_right, 330 | y_top: self.y_top, 331 | y_bottom: self.y_bottom, 332 | colors: self.colors.clone(), 333 | }; 334 | self.x_left = x_left; 335 | self.x_right = x_right; 336 | self.y_top = y; 337 | self.y_bottom = y; 338 | self.colors.clear(); 339 | self.colors.extend_from_slice(&colors).expect("never"); 340 | return Some(row); 341 | } 342 | } 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/options/orientation.rs: -------------------------------------------------------------------------------- 1 | /// Display rotation. 2 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 3 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 4 | pub enum Rotation { 5 | /// No rotation. 6 | Deg0, 7 | /// 90° clockwise rotation. 8 | Deg90, 9 | /// 180° clockwise rotation. 10 | Deg180, 11 | /// 270° clockwise rotation. 12 | Deg270, 13 | } 14 | 15 | impl Rotation { 16 | /// Returns the rotation in degrees. 17 | pub const fn degree(self) -> i32 { 18 | match self { 19 | Self::Deg0 => 0, 20 | Self::Deg90 => 90, 21 | Self::Deg180 => 180, 22 | Self::Deg270 => 270, 23 | } 24 | } 25 | 26 | /// Converts an angle into a rotation. 27 | /// 28 | /// Returns an error if the angle isn't an integer multiple of 90°. 29 | pub const fn try_from_degree(mut angle: i32) -> Result { 30 | if angle < 0 || angle > 270 { 31 | angle = angle.rem_euclid(360) 32 | } 33 | 34 | Ok(match angle { 35 | 0 => Self::Deg0, 36 | 90 => Self::Deg90, 37 | 180 => Self::Deg180, 38 | 270 => Self::Deg270, 39 | _ => return Err(InvalidAngleError), 40 | }) 41 | } 42 | 43 | /// Rotates one rotation by another rotation. 44 | #[must_use] 45 | pub const fn rotate(self, other: Rotation) -> Self { 46 | match Self::try_from_degree(self.degree() + other.degree()) { 47 | Ok(r) => r, 48 | Err(_) => unreachable!(), 49 | } 50 | } 51 | 52 | /// Returns `true` if the rotation is horizontal (0° or 180°). 53 | pub const fn is_horizontal(self) -> bool { 54 | matches!(self, Self::Deg0 | Self::Deg180) 55 | } 56 | 57 | /// Returns `true` if the rotation is vertical (90° or 270°). 58 | pub const fn is_vertical(self) -> bool { 59 | matches!(self, Self::Deg90 | Self::Deg270) 60 | } 61 | } 62 | 63 | /// Invalid angle error. 64 | /// 65 | /// The error type returned by [`Rotation::try_from_degree`]. 66 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 67 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 68 | pub struct InvalidAngleError; 69 | 70 | /// Display orientation. 71 | /// 72 | /// A display orientation describes how the display content is oriented relative 73 | /// to the default orientation of the display. 74 | /// 75 | /// # Examples 76 | /// 77 | /// ``` 78 | /// use mipidsi::options::{Orientation, Rotation}; 79 | /// 80 | /// // Rotate display content by 90 degree clockwise. 81 | /// let rotated = Orientation::new().rotate(Rotation::Deg90); 82 | /// 83 | /// // Flip display content horizontally. 84 | /// let flipped = Orientation::new().flip_horizontal(); 85 | /// ``` 86 | /// 87 | /// Multiple transformations can be combined to build more complex orientations: 88 | /// 89 | /// ``` 90 | /// use mipidsi::options::{Orientation, Rotation}; 91 | /// 92 | /// let orientation = Orientation::new().rotate(Rotation::Deg270).flip_vertical(); 93 | /// 94 | /// // Note that the order of operations is important: 95 | /// assert_ne!(orientation, Orientation::new().flip_vertical().rotate(Rotation::Deg270)); 96 | /// assert_eq!(orientation, Orientation::new().flip_vertical().rotate(Rotation::Deg90)); 97 | /// ``` 98 | /// 99 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 100 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 101 | pub struct Orientation { 102 | /// Rotation. 103 | pub rotation: Rotation, 104 | /// Mirrored. 105 | pub mirrored: bool, 106 | } 107 | 108 | impl Orientation { 109 | /// Creates a default orientation. 110 | pub const fn new() -> Self { 111 | Self { 112 | rotation: Rotation::Deg0, 113 | mirrored: false, 114 | } 115 | } 116 | 117 | /// Rotates the orientation. 118 | #[must_use] 119 | pub const fn rotate(self, rotation: Rotation) -> Self { 120 | Self { 121 | rotation: self.rotation.rotate(rotation), 122 | mirrored: self.mirrored, 123 | } 124 | } 125 | 126 | /// Flips the orientation across the horizontal display axis. 127 | #[must_use] 128 | const fn flip_horizontal_absolute(self) -> Self { 129 | Self { 130 | rotation: self.rotation, 131 | mirrored: !self.mirrored, 132 | } 133 | } 134 | 135 | /// Flips the orientation across the vertical display axis. 136 | #[must_use] 137 | const fn flip_vertical_absolute(self) -> Self { 138 | Self { 139 | rotation: self.rotation.rotate(Rotation::Deg180), 140 | mirrored: !self.mirrored, 141 | } 142 | } 143 | 144 | /// Flips the orientation across the horizontal axis. 145 | #[must_use] 146 | pub const fn flip_horizontal(self) -> Self { 147 | if self.rotation.is_vertical() { 148 | self.flip_vertical_absolute() 149 | } else { 150 | self.flip_horizontal_absolute() 151 | } 152 | } 153 | 154 | /// Flips the orientation across the vertical axis. 155 | #[must_use] 156 | pub const fn flip_vertical(self) -> Self { 157 | if self.rotation.is_vertical() { 158 | self.flip_horizontal_absolute() 159 | } else { 160 | self.flip_vertical_absolute() 161 | } 162 | } 163 | } 164 | 165 | impl Default for Orientation { 166 | fn default() -> Self { 167 | Self::new() 168 | } 169 | } 170 | 171 | /// Memory mapping. 172 | /// 173 | /// A memory mapping describes how a framebuffer is mapped to the physical 174 | /// row and columns of a display. 175 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 176 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 177 | pub struct MemoryMapping { 178 | /// Rows and columns are swapped. 179 | pub swap_rows_and_columns: bool, 180 | 181 | /// Rows are reversed. 182 | pub reverse_rows: bool, 183 | /// Columns are reversed. 184 | pub reverse_columns: bool, 185 | } 186 | 187 | impl MemoryMapping { 188 | /// `const` variant of `From` impl. 189 | pub const fn from_orientation(orientation: Orientation) -> Self { 190 | let (reverse_rows, reverse_columns) = match orientation.rotation { 191 | Rotation::Deg0 => (false, false), 192 | Rotation::Deg90 => (false, true), 193 | Rotation::Deg180 => (true, true), 194 | Rotation::Deg270 => (true, false), 195 | }; 196 | 197 | Self { 198 | reverse_rows, 199 | reverse_columns: reverse_columns ^ orientation.mirrored, 200 | swap_rows_and_columns: orientation.rotation.is_vertical(), 201 | } 202 | } 203 | } 204 | 205 | impl From for MemoryMapping { 206 | fn from(orientation: Orientation) -> Self { 207 | Self::from_orientation(orientation) 208 | } 209 | } 210 | 211 | #[cfg(test)] 212 | mod tests { 213 | use super::*; 214 | 215 | #[test] 216 | fn try_from_degree() { 217 | let mut expected = [ 218 | Rotation::Deg0, 219 | Rotation::Deg90, 220 | Rotation::Deg180, 221 | Rotation::Deg270, 222 | ] 223 | .iter() 224 | .copied() 225 | .cycle(); 226 | 227 | for angle in (-720..=720).step_by(90) { 228 | assert_eq!( 229 | Rotation::try_from_degree(angle).unwrap(), 230 | expected.next().unwrap(), 231 | "{angle}" 232 | ); 233 | } 234 | } 235 | 236 | #[test] 237 | fn try_from_degree_error() { 238 | assert_eq!(Rotation::try_from_degree(1), Err(InvalidAngleError)); 239 | assert_eq!(Rotation::try_from_degree(-1), Err(InvalidAngleError)); 240 | assert_eq!(Rotation::try_from_degree(i32::MIN), Err(InvalidAngleError)); 241 | assert_eq!(Rotation::try_from_degree(i32::MAX), Err(InvalidAngleError)); 242 | } 243 | 244 | /// Abbreviated constructor for orientations. 245 | const fn orientation(rotation: Rotation, mirrored: bool) -> Orientation { 246 | Orientation { rotation, mirrored } 247 | } 248 | 249 | #[test] 250 | fn flip_horizontal() { 251 | use Rotation::*; 252 | 253 | for ((rotation, mirrored), (expected_rotation, expected_mirrored)) in [ 254 | ((Deg0, false), (Deg0, true)), 255 | ((Deg90, false), (Deg270, true)), 256 | ((Deg180, false), (Deg180, true)), 257 | ((Deg270, false), (Deg90, true)), 258 | ((Deg0, true), (Deg0, false)), 259 | ((Deg90, true), (Deg270, false)), 260 | ((Deg180, true), (Deg180, false)), 261 | ((Deg270, true), (Deg90, false)), 262 | ] 263 | .iter() 264 | .copied() 265 | { 266 | assert_eq!( 267 | orientation(rotation, mirrored).flip_horizontal(), 268 | orientation(expected_rotation, expected_mirrored) 269 | ); 270 | } 271 | } 272 | 273 | #[test] 274 | fn flip_vertical() { 275 | use Rotation::*; 276 | 277 | for ((rotation, mirrored), (expected_rotation, expected_mirrored)) in [ 278 | ((Deg0, false), (Deg180, true)), 279 | ((Deg90, false), (Deg90, true)), 280 | ((Deg180, false), (Deg0, true)), 281 | ((Deg270, false), (Deg270, true)), 282 | ((Deg0, true), (Deg180, false)), 283 | ((Deg90, true), (Deg90, false)), 284 | ((Deg180, true), (Deg0, false)), 285 | ((Deg270, true), (Deg270, false)), 286 | ] 287 | .iter() 288 | .copied() 289 | { 290 | assert_eq!( 291 | orientation(rotation, mirrored).flip_vertical(), 292 | orientation(expected_rotation, expected_mirrored) 293 | ); 294 | } 295 | } 296 | 297 | fn draw_memory_mapping(order: MemoryMapping) -> [[u8; 3]; 3] { 298 | let mut buffer = [[0u8; 3]; 3]; 299 | 300 | let (max_x, max_y) = if order.swap_rows_and_columns { 301 | (1, 2) 302 | } else { 303 | (2, 1) 304 | }; 305 | 306 | let mut i = 1..; 307 | for y in 0..2 { 308 | for x in 0..3 { 309 | let (x, y) = if order.swap_rows_and_columns { 310 | (y, x) 311 | } else { 312 | (x, y) 313 | }; 314 | let x = if order.reverse_columns { max_x - x } else { x }; 315 | let y = if order.reverse_rows { max_y - y } else { y }; 316 | 317 | buffer[y as usize][x as usize] = i.next().unwrap(); 318 | } 319 | } 320 | 321 | buffer 322 | } 323 | 324 | #[test] 325 | fn test_draw_memory_mapping() { 326 | assert_eq!( 327 | &draw_memory_mapping(MemoryMapping { 328 | reverse_rows: false, 329 | reverse_columns: false, 330 | swap_rows_and_columns: false, 331 | }), 332 | &[ 333 | [1, 2, 3], // 334 | [4, 5, 6], // 335 | [0, 0, 0], // 336 | ] 337 | ); 338 | 339 | assert_eq!( 340 | &draw_memory_mapping(MemoryMapping { 341 | reverse_rows: true, 342 | reverse_columns: false, 343 | swap_rows_and_columns: false, 344 | }), 345 | &[ 346 | [4, 5, 6], // 347 | [1, 2, 3], // 348 | [0, 0, 0], // 349 | ] 350 | ); 351 | 352 | assert_eq!( 353 | &draw_memory_mapping(MemoryMapping { 354 | reverse_rows: false, 355 | reverse_columns: true, 356 | swap_rows_and_columns: false, 357 | }), 358 | &[ 359 | [3, 2, 1], // 360 | [6, 5, 4], // 361 | [0, 0, 0], // 362 | ] 363 | ); 364 | 365 | assert_eq!( 366 | &draw_memory_mapping(MemoryMapping { 367 | reverse_rows: true, 368 | reverse_columns: true, 369 | swap_rows_and_columns: false, 370 | }), 371 | &[ 372 | [6, 5, 4], // 373 | [3, 2, 1], // 374 | [0, 0, 0], // 375 | ] 376 | ); 377 | 378 | assert_eq!( 379 | &draw_memory_mapping(MemoryMapping { 380 | reverse_rows: false, 381 | reverse_columns: false, 382 | swap_rows_and_columns: true, 383 | }), 384 | &[ 385 | [1, 4, 0], // 386 | [2, 5, 0], // 387 | [3, 6, 0], // 388 | ] 389 | ); 390 | 391 | assert_eq!( 392 | &draw_memory_mapping(MemoryMapping { 393 | reverse_rows: true, 394 | reverse_columns: false, 395 | swap_rows_and_columns: true, 396 | }), 397 | &[ 398 | [3, 6, 0], // 399 | [2, 5, 0], // 400 | [1, 4, 0], // 401 | ] 402 | ); 403 | 404 | assert_eq!( 405 | &draw_memory_mapping(MemoryMapping { 406 | reverse_rows: false, 407 | reverse_columns: true, 408 | swap_rows_and_columns: true, 409 | }), 410 | &[ 411 | [4, 1, 0], // 412 | [5, 2, 0], // 413 | [6, 3, 0], // 414 | ] 415 | ); 416 | 417 | assert_eq!( 418 | &draw_memory_mapping(MemoryMapping { 419 | reverse_rows: true, 420 | reverse_columns: true, 421 | swap_rows_and_columns: true, 422 | }), 423 | &[ 424 | [6, 3, 0], // 425 | [5, 2, 0], // 426 | [4, 1, 0], // 427 | ] 428 | ); 429 | } 430 | 431 | #[test] 432 | fn into_memory_order_not_mirrored() { 433 | assert_eq!( 434 | &draw_memory_mapping(orientation(Rotation::Deg0, false).into()), 435 | &[ 436 | [1, 2, 3], // 437 | [4, 5, 6], // 438 | [0, 0, 0], // 439 | ] 440 | ); 441 | 442 | assert_eq!( 443 | &draw_memory_mapping(orientation(Rotation::Deg90, false).into()), 444 | &[ 445 | [4, 1, 0], // 446 | [5, 2, 0], // 447 | [6, 3, 0], // 448 | ] 449 | ); 450 | 451 | assert_eq!( 452 | &draw_memory_mapping(orientation(Rotation::Deg180, false).into()), 453 | &[ 454 | [6, 5, 4], // 455 | [3, 2, 1], // 456 | [0, 0, 0], // 457 | ] 458 | ); 459 | 460 | assert_eq!( 461 | &draw_memory_mapping(orientation(Rotation::Deg270, false).into()), 462 | &[ 463 | [3, 6, 0], // 464 | [2, 5, 0], // 465 | [1, 4, 0], // 466 | ] 467 | ); 468 | } 469 | 470 | #[test] 471 | fn into_memory_order_mirrored() { 472 | assert_eq!( 473 | &draw_memory_mapping(orientation(Rotation::Deg0, true).into()), 474 | &[ 475 | [3, 2, 1], // 476 | [6, 5, 4], // 477 | [0, 0, 0], // 478 | ] 479 | ); 480 | 481 | assert_eq!( 482 | &draw_memory_mapping(orientation(Rotation::Deg90, true).into()), 483 | &[ 484 | [1, 4, 0], // 485 | [2, 5, 0], // 486 | [3, 6, 0], // 487 | ] 488 | ); 489 | 490 | assert_eq!( 491 | &draw_memory_mapping(orientation(Rotation::Deg180, true).into()), 492 | &[ 493 | [4, 5, 6], // 494 | [1, 2, 3], // 495 | [0, 0, 0], // 496 | ] 497 | ); 498 | 499 | assert_eq!( 500 | &draw_memory_mapping(orientation(Rotation::Deg270, true).into()), 501 | &[ 502 | [6, 3, 0], // 503 | [5, 2, 0], // 504 | [4, 1, 0], // 505 | ] 506 | ); 507 | } 508 | 509 | #[test] 510 | fn equivalent_orientations() { 511 | let o1 = Orientation::new().rotate(Rotation::Deg270).flip_vertical(); 512 | let o2 = Orientation::new().rotate(Rotation::Deg90).flip_horizontal(); 513 | let o3 = Orientation::new() 514 | .flip_horizontal() 515 | .rotate(Rotation::Deg270); 516 | let o4 = Orientation::new().flip_vertical().rotate(Rotation::Deg90); 517 | 518 | assert_eq!(o1, o2); 519 | assert_eq!(o1, o3); 520 | assert_eq!(o1, o4); 521 | } 522 | } 523 | --------------------------------------------------------------------------------