├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── rustfmt.toml └── src ├── displays.rs ├── error.rs ├── framebuffer.rs ├── lib.rs └── pixelcolor.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "memory-lcd-spi" 3 | version = "0.0.8" 4 | edition = "2021" 5 | authors = ["Andelf "] 6 | repository = "https://github.com/andelf/memory-lcd-spi" 7 | documentation = "https://docs.rs/memory-lcd-spi" 8 | homepage = "https://github.com/andelf/memory-lcd-spi" 9 | categories = ["embedded", "hardware-support"] 10 | description = "A driver for Sharp's Memory LCD or JDI's Memory In Pixel display, LPM013M126A, LPM009M360A, LS027B7DH01, etc." 11 | keywords = [ 12 | "memory-lcd", 13 | "embedded-graphics", 14 | "memory-in-pixel", 15 | "sharp", 16 | "jdi", 17 | ] 18 | readme = "README.md" 19 | license = "MIT/Apache-2.0" 20 | 21 | [dependencies] 22 | defmt = { version = "0.3.8", optional = true } 23 | embedded-graphics-core = { version = "0.4.0", features = ["defmt"] } 24 | embedded-hal = { version = "1.0.0" } 25 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Andelf 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # memory-lcd-spi 2 | 3 | `embedded-hal` driver for Sharp's Memory LCD and JDI's Memory In Pixel displays. 4 | 5 | [![crates.io](https://img.shields.io/crates/v/memory-lcd-spi.svg)](https://crates.io/crates/memory-lcd-spi) 6 | [![Docs](https://docs.rs/memory-lcd-spi/badge.svg)](https://docs.rs/memory-lcd-spi) 7 | 8 | ## Features 9 | 10 | - Rotation support 11 | - 8-color mode with `Rgb111` color 12 | - black/white mode for fast update 13 | 14 | ## Tested 15 | 16 | - JDI's LPM013M126A or LPM013M126C, 176x176 1.3inch, RGB111 17 | - JDI's LPM009M360A, 72x144 0.9inch, RGB111 18 | - JDI's LPM027M128C, 400x240 2.7inch, RGB111 19 | - Sharp's LS006B7DH01, 64x64 0.56inch 20 | - Sharp's LS013B7DH03, 128x128 1.28inch 21 | - Sharp's LS027B7DH01, 400x240 2.7inch 22 | - ... 23 | 24 | ## Usage 25 | 26 | ```rust 27 | let mut display: MemoryLCD, _, _> = MemoryLCD::new(spi, cs); 28 | 29 | display.set_rotation(memory_lcd_spi::framebuffer::Rotation::Deg90); 30 | display.clear(BinaryColor::Off); 31 | 32 | // drawing code with embedded-graphics 33 | Line::new( 34 | Point::new(0, 0), 35 | Point::new(20, 20), 36 | ) 37 | .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) 38 | .draw(&mut *display) // Yes, explicit deref is required 39 | .unwrap(); 40 | 41 | display.update(&mut delay); 42 | ``` 43 | 44 | Or `Rgb111` mode: 45 | 46 | ```rust 47 | let mut display: MemoryLCD, _, _> = MemoryLCD::new(spi, cs); 48 | display.clear(Rgb111::BLACK); 49 | ``` 50 | 51 | > **Note** 52 | > 53 | > `DISP` pin is not managed by this driver. You should control it by yourself. 54 | > 55 | > `EXTCOMIN` in is not managed by this driver. Follow the datasheet, use either 60Hz PWM or GND. 56 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | 2 | max_width = 120 3 | format_code_in_doc_comments = true 4 | imports_granularity="Crate" 5 | format_macro_matchers = true 6 | format_macro_bodies = true 7 | -------------------------------------------------------------------------------- /src/displays.rs: -------------------------------------------------------------------------------- 1 | //! Predefined display setting. 2 | 3 | use embedded_graphics_core::pixelcolor::BinaryColor; 4 | 5 | use crate::{ 6 | framebuffer::{Framebuffer4Bit, FramebufferBW, Sharp, JDI}, 7 | pixelcolor::Rgb111, 8 | DisplaySpec, 9 | }; 10 | 11 | /// 1.28 inch Memory LCD, aka. LPM013M126C 12 | pub struct LPM013M126A { 13 | _color: core::marker::PhantomData, 14 | } 15 | 16 | impl DisplaySpec for LPM013M126A { 17 | const WIDTH: u16 = 176; 18 | const HEIGHT: u16 = 176; 19 | 20 | type Framebuffer = Framebuffer4Bit<{ Self::WIDTH }, { Self::HEIGHT }>; 21 | } 22 | 23 | impl DisplaySpec for LPM013M126A { 24 | const WIDTH: u16 = 176; 25 | const HEIGHT: u16 = 176; 26 | 27 | type Framebuffer = FramebufferBW<{ Self::WIDTH }, { Self::HEIGHT }, JDI>; 28 | } 29 | 30 | /// 0.85inch 8-color display 31 | pub struct LPM009M360A { 32 | _color: core::marker::PhantomData, 33 | } 34 | 35 | impl DisplaySpec for LPM009M360A { 36 | const WIDTH: u16 = 72; 37 | const HEIGHT: u16 = 144; 38 | 39 | type Framebuffer = Framebuffer4Bit<{ Self::WIDTH }, { Self::HEIGHT }>; 40 | } 41 | 42 | impl DisplaySpec for LPM009M360A { 43 | const WIDTH: u16 = 72; 44 | const HEIGHT: u16 = 144; 45 | 46 | type Framebuffer = FramebufferBW<{ Self::WIDTH }, { Self::HEIGHT }, JDI>; 47 | } 48 | 49 | /// 0.56inch, 64x64 BW display, 13pin 0.3mm FPC 50 | pub struct LS006B7DH01; 51 | 52 | impl DisplaySpec for LS006B7DH01 { 53 | const WIDTH: u16 = 64; 54 | const HEIGHT: u16 = 64; 55 | 56 | type Framebuffer = FramebufferBW<{ Self::WIDTH }, { Self::HEIGHT }, Sharp>; 57 | } 58 | 59 | /// 1.28inch, 128x128 BW display, 10pin 0.5mm FPC 60 | pub struct LS013B7DH03; 61 | 62 | impl DisplaySpec for LS013B7DH03 { 63 | const WIDTH: u16 = 128; 64 | const HEIGHT: u16 = 128; 65 | 66 | type Framebuffer = FramebufferBW<{ Self::WIDTH }, { Self::HEIGHT }, Sharp>; 67 | } 68 | 69 | /// 2.8inch, 400x240 BW display, 10pin 0.5mm FPC 70 | pub struct LS027B7DH01; 71 | 72 | impl DisplaySpec for LS027B7DH01 { 73 | const WIDTH: u16 = 400; 74 | const HEIGHT: u16 = 240; 75 | 76 | type Framebuffer = FramebufferBW<{ Self::WIDTH }, { Self::HEIGHT }, Sharp>; 77 | } 78 | 79 | /// 2.8inch, 400x240 BW display, 10pin 0.5mm FPC 80 | pub struct LPM027M128C; 81 | 82 | impl DisplaySpec for LPM027M128C { 83 | const WIDTH: u16 = 400; 84 | const HEIGHT: u16 = 240; 85 | 86 | type Framebuffer = FramebufferBW<{ Self::WIDTH }, { Self::HEIGHT }, JDI>; 87 | } 88 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] 2 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 3 | pub enum Error { 4 | Spi(SPIError), 5 | Gpio, 6 | } 7 | 8 | // See-also: https://stackoverflow.com/questions/37347311/how-is-there-a-conflicting-implementation-of-from-when-using-a-generic-type 9 | -------------------------------------------------------------------------------- /src/framebuffer.rs: -------------------------------------------------------------------------------- 1 | //! Framebuffer for memory displays 2 | //! 3 | //! Considerations: 4 | //! - No flip or mirror support 5 | //! - Rotation is needed 6 | //! - TODO: double buffering 7 | 8 | use core::marker::PhantomData; 9 | 10 | use embedded_graphics_core::{ 11 | pixelcolor::BinaryColor, 12 | prelude::{DrawTarget, OriginDimensions, RawData, Size}, 13 | primitives::Rectangle, 14 | Pixel, 15 | }; 16 | use embedded_hal::spi::SpiBus; 17 | 18 | use crate::pixelcolor::Rgb111; 19 | 20 | /// Display rotation. 21 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 22 | pub enum Rotation { 23 | /// No rotation. 24 | Deg0, 25 | /// 90° clockwise rotation. 26 | Deg90, 27 | /// 180° clockwise rotation. 28 | Deg180, 29 | /// 270° clockwise rotation. 30 | Deg270, 31 | } 32 | 33 | impl Rotation { 34 | #[inline] 35 | fn is_column_row_swap(self) -> bool { 36 | matches!(self, Rotation::Deg90 | Rotation::Deg270) 37 | } 38 | } 39 | 40 | pub(crate) mod sealed { 41 | use embedded_hal::spi::SpiBus; 42 | 43 | pub trait FramebufferSpiUpdate { 44 | fn update(&self, spi: &mut SPI) -> Result<(), SPI::Error>; 45 | } 46 | 47 | pub trait DriverVariant {} 48 | } 49 | 50 | pub struct JDI; 51 | pub struct Sharp; 52 | 53 | impl sealed::DriverVariant for JDI {} 54 | impl sealed::DriverVariant for Sharp {} 55 | 56 | pub trait FramebufferType: OriginDimensions + DrawTarget + Default + sealed::FramebufferSpiUpdate {} 57 | 58 | pub struct Framebuffer4Bit 59 | where 60 | [(); WIDTH as usize * HEIGHT as usize / 2]:, 61 | { 62 | data: [u8; WIDTH as usize * HEIGHT as usize / 2], 63 | rotation: Rotation, 64 | } 65 | 66 | impl Default for Framebuffer4Bit 67 | where 68 | [(); WIDTH as usize * HEIGHT as usize / 2]:, 69 | { 70 | fn default() -> Self { 71 | Self::new() 72 | } 73 | } 74 | 75 | impl sealed::FramebufferSpiUpdate for Framebuffer4Bit 76 | where 77 | [(); WIDTH as usize * HEIGHT as usize / 2]:, 78 | { 79 | // only burst update is supported 80 | fn update(&self, spi: &mut SPI) -> Result<(), SPI::Error> { 81 | for i in 0..HEIGHT { 82 | let start = (i as usize) * WIDTH as usize / 2; 83 | let end = start + WIDTH as usize / 2; 84 | let line_data = &self.data[start..end]; 85 | // NOTE: refer to manual, gate address counter is 1-based 86 | spi.write(&[crate::CMD_UPDATE_4BIT, i as u8 + 1])?; 87 | spi.write(line_data)?; 88 | } 89 | spi.write(&[0x00, 0x00])?; 90 | Ok(()) 91 | } 92 | } 93 | 94 | impl Framebuffer4Bit 95 | where 96 | [(); WIDTH as usize * HEIGHT as usize / 2]:, 97 | { 98 | pub fn new() -> Self { 99 | Self { 100 | data: [0; WIDTH as usize * HEIGHT as usize / 2], 101 | rotation: Rotation::Deg0, 102 | } 103 | } 104 | 105 | pub fn set_rotation(&mut self, rotation: Rotation) { 106 | self.rotation = rotation; 107 | } 108 | 109 | pub fn get_rotation(&self) -> Rotation { 110 | self.rotation 111 | } 112 | 113 | pub(crate) fn set_pixel(&mut self, x: u16, y: u16, color: Rgb111) { 114 | if self.rotation.is_column_row_swap() { 115 | if x >= HEIGHT || y >= WIDTH { 116 | return; 117 | } 118 | } else if y >= HEIGHT || x >= WIDTH { 119 | return; 120 | } 121 | let x = x as usize; 122 | let y = y as usize; 123 | 124 | let (x, y) = match self.rotation { 125 | Rotation::Deg0 => (x, y), 126 | Rotation::Deg90 => (y, HEIGHT as usize - x - 1), 127 | Rotation::Deg180 => (WIDTH as usize - x - 1, HEIGHT as usize - y - 1), 128 | Rotation::Deg270 => (WIDTH as usize - y - 1, x), 129 | }; 130 | 131 | let index = (y * WIDTH as usize + x) / 2; 132 | 133 | let color = color.0.into_inner(); 134 | 135 | if x % 2 == 0 { 136 | self.data[index] = (self.data[index] & 0b00001111) | (color << 4); 137 | } else { 138 | self.data[index] = (self.data[index] & 0b11110000) | color; 139 | } 140 | } 141 | } 142 | 143 | impl FramebufferType for Framebuffer4Bit where 144 | [(); WIDTH as usize * HEIGHT as usize / 2]: 145 | { 146 | } 147 | 148 | impl OriginDimensions for Framebuffer4Bit 149 | where 150 | [(); WIDTH as usize * HEIGHT as usize / 2]:, 151 | { 152 | fn size(&self) -> Size { 153 | match self.rotation { 154 | Rotation::Deg0 | Rotation::Deg180 => Size::new(WIDTH as u32, HEIGHT as u32), 155 | Rotation::Deg90 | Rotation::Deg270 => Size::new(HEIGHT as u32, WIDTH as u32), 156 | } 157 | } 158 | } 159 | 160 | impl DrawTarget for Framebuffer4Bit 161 | where 162 | [(); WIDTH as usize * HEIGHT as usize / 2]:, 163 | { 164 | type Color = Rgb111; 165 | 166 | type Error = core::convert::Infallible; 167 | 168 | fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> 169 | where 170 | I: IntoIterator>, 171 | { 172 | for Pixel(coord, color) in pixels.into_iter() { 173 | self.set_pixel(coord.x as u16, coord.y as u16, color); 174 | } 175 | Ok(()) 176 | } 177 | 178 | fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> { 179 | let raw = color.0.into_inner() << 4 | color.0.into_inner(); 180 | self.data.fill(raw); 181 | Ok(()) 182 | } 183 | 184 | fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> { 185 | if self.rotation == Rotation::Deg0 && area.top_left.x % 2 == 0 && area.size.width % 2 == 0 { 186 | let w = area.size.width as usize / 2; 187 | let x_off = area.top_left.x as usize / 2; 188 | let raw_pix = color.0.into_inner() << 4 | color.0.into_inner(); 189 | 190 | for y in area.top_left.y..area.top_left.y + area.size.height as i32 { 191 | let start = (y as usize) * WIDTH as usize / 2 + x_off; 192 | let end = start + w; 193 | self.data[start..end].fill(raw_pix); 194 | } 195 | Ok(()) 196 | } else { 197 | self.fill_contiguous(area, core::iter::repeat(color)) 198 | } 199 | } 200 | } 201 | 202 | pub struct FramebufferBW 203 | where 204 | [(); WIDTH as usize * HEIGHT as usize / 8]:, 205 | { 206 | data: [u8; WIDTH as usize * HEIGHT as usize / 8], 207 | rotation: Rotation, 208 | _type: PhantomData, 209 | } 210 | 211 | impl Default for FramebufferBW 212 | where 213 | [(); WIDTH as usize * HEIGHT as usize / 8]:, 214 | { 215 | fn default() -> Self { 216 | Self::new() 217 | } 218 | } 219 | 220 | impl FramebufferBW 221 | where 222 | [(); WIDTH as usize * HEIGHT as usize / 8]:, 223 | { 224 | pub fn new() -> Self { 225 | Self { 226 | data: [0; WIDTH as usize * HEIGHT as usize / 8], 227 | rotation: Rotation::Deg0, 228 | _type: PhantomData, 229 | } 230 | } 231 | 232 | pub fn set_rotation(&mut self, rotation: Rotation) { 233 | self.rotation = rotation; 234 | } 235 | 236 | pub fn get_rotation(&self) -> Rotation { 237 | self.rotation 238 | } 239 | 240 | pub(crate) fn set_pixel(&mut self, x: u16, y: u16, color: BinaryColor) { 241 | if self.rotation.is_column_row_swap() { 242 | if x >= HEIGHT || y >= WIDTH { 243 | return; 244 | } 245 | } else if y >= HEIGHT || x >= WIDTH { 246 | return; 247 | } 248 | 249 | let x = x as usize; 250 | let y = y as usize; 251 | 252 | let (x, y) = match self.rotation { 253 | Rotation::Deg0 => (x, y), 254 | Rotation::Deg90 => (y, HEIGHT as usize - x - 1), 255 | Rotation::Deg180 => (WIDTH as usize - x - 1, HEIGHT as usize - y - 1), 256 | Rotation::Deg270 => (WIDTH as usize - y - 1, x), 257 | }; 258 | 259 | if y >= HEIGHT as usize || x >= WIDTH as usize { 260 | return; 261 | } 262 | 263 | let index = y * WIDTH as usize + x; 264 | 265 | if color.is_on() { 266 | self.data[index / 8] |= 1 << (8 - (index % 8) - 1); 267 | } else { 268 | self.data[index / 8] &= !(1 << (8 - (index % 8) - 1)); 269 | } 270 | } 271 | } 272 | 273 | impl sealed::FramebufferSpiUpdate for FramebufferBW 274 | where 275 | [(); WIDTH as usize * HEIGHT as usize / 8]:, 276 | { 277 | fn update(&self, spi: &mut SPI) -> Result<(), SPI::Error> { 278 | for i in 0..HEIGHT { 279 | let start = (i as usize) * WIDTH as usize / 8; 280 | let end = start + WIDTH as usize / 8; 281 | let gate_line = &self.data[start..end]; 282 | // gate address is counted from 1 283 | spi.write(&[crate::CMD_UPDATE_1BIT, i as u8 + 1])?; 284 | spi.write(gate_line)?; 285 | } 286 | spi.write(&[0x00, 0x00])?; 287 | Ok(()) 288 | } 289 | } 290 | 291 | impl sealed::FramebufferSpiUpdate for FramebufferBW 292 | where 293 | [(); WIDTH as usize * HEIGHT as usize / 8]:, 294 | { 295 | fn update(&self, spi: &mut SPI) -> Result<(), SPI::Error> { 296 | for i in 0..HEIGHT { 297 | let start = (i as usize) * WIDTH as usize / 8; 298 | let end = start + WIDTH as usize / 8; 299 | let gate_line = &self.data[start..end]; 300 | // gate address is counted from 1 301 | spi.write(&[crate::CMD_UPDATE_1BIT, reverse_bits(i as u8 + 1)])?; 302 | spi.write(gate_line)?; 303 | } 304 | spi.write(&[0x00, 0x00])?; 305 | Ok(()) 306 | } 307 | } 308 | 309 | impl FramebufferType 310 | for FramebufferBW 311 | where 312 | [(); WIDTH as usize * HEIGHT as usize / 8]:, 313 | FramebufferBW: sealed::FramebufferSpiUpdate, 314 | { 315 | } 316 | 317 | impl OriginDimensions 318 | for FramebufferBW 319 | where 320 | [(); WIDTH as usize * HEIGHT as usize / 8]:, 321 | { 322 | fn size(&self) -> Size { 323 | match self.rotation { 324 | Rotation::Deg0 | Rotation::Deg180 => Size::new(WIDTH as u32, HEIGHT as u32), 325 | Rotation::Deg90 | Rotation::Deg270 => Size::new(HEIGHT as u32, WIDTH as u32), 326 | } 327 | } 328 | } 329 | 330 | impl DrawTarget for FramebufferBW 331 | where 332 | [(); WIDTH as usize * HEIGHT as usize / 8]:, 333 | { 334 | type Color = BinaryColor; 335 | 336 | type Error = core::convert::Infallible; 337 | 338 | fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> 339 | where 340 | I: IntoIterator>, 341 | { 342 | for Pixel(coord, color) in pixels.into_iter() { 343 | self.set_pixel(coord.x as u16, coord.y as u16, color); 344 | } 345 | Ok(()) 346 | } 347 | 348 | fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> { 349 | if color.is_on() { 350 | self.data.fill(0xFF); 351 | } else { 352 | self.data.fill(0x00); 353 | } 354 | Ok(()) 355 | } 356 | } 357 | 358 | // For Sharp's SPI interface 359 | fn reverse_bits(mut b: u8) -> u8 { 360 | let mut r = 0; 361 | for _ in 0..8 { 362 | r <<= 1; 363 | r |= b & 1; 364 | b >>= 1; 365 | } 366 | r 367 | } 368 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Memory LCD display driver in SPI mode 2 | 3 | #![no_std] 4 | #![allow(incomplete_features)] 5 | #![feature(generic_const_exprs)] 6 | 7 | use core::ops::{Deref, DerefMut}; 8 | 9 | use crate::error::Error; 10 | use embedded_hal::{digital::OutputPin, spi::SpiBus}; 11 | use framebuffer::FramebufferType; 12 | 13 | pub mod displays; 14 | pub mod error; 15 | pub mod framebuffer; 16 | pub mod pixelcolor; 17 | 18 | /// Specification for displays. 19 | pub trait DisplaySpec { 20 | const WIDTH: u16; 21 | const HEIGHT: u16; 22 | 23 | type Framebuffer: FramebufferType; 24 | } 25 | 26 | // NOTE: The commands are actually 6 bit, but the MSB of next 10 bit is always 0, 27 | // So it's a trick to use 8 bit to represent the command. 28 | pub const CMD_NO_UPDATE: u8 = 0x00; 29 | pub const CMD_BLINKING_BLACK: u8 = 0x10; 30 | pub const CMD_BLINKING_INVERSION: u8 = 0x14; 31 | pub const CMD_BLINKING_WHITE: u8 = 0x18; 32 | pub const CMD_ALL_CLEAR: u8 = 0x20; 33 | pub const CMD_VCOM: u8 = 0x40; 34 | // 0x90 = 0b100100_00 35 | // Use 4 bit data input 36 | pub const CMD_UPDATE_4BIT: u8 = 0x90; 37 | // Also apply to Sharp's update mode: 0b100_xxxxx 38 | pub const CMD_UPDATE_1BIT: u8 = 0x88; 39 | 40 | pub struct MemoryLCD { 41 | // cs is active high, so SpiBus is needed. 42 | spi: SPI, 43 | cs: CS, 44 | framebuffer: SPEC::Framebuffer, 45 | } 46 | 47 | impl MemoryLCD 48 | where 49 | SPI: SpiBus, 50 | CS: OutputPin, 51 | SPEC: DisplaySpec, 52 | { 53 | pub fn new(spi: SPI, cs: CS) -> Self { 54 | Self { 55 | spi, 56 | cs, 57 | framebuffer: SPEC::Framebuffer::default(), 58 | } 59 | } 60 | 61 | pub fn turn_on_display(&mut self, disp: &mut DISP) -> Result<(), Error> { 62 | disp.set_high().map_err(|_| Error::Gpio)?; 63 | Ok(()) 64 | } 65 | 66 | pub fn turn_off_display(&mut self, disp: &mut DISP) -> Result<(), Error> { 67 | disp.set_low().map_err(|_| Error::Gpio)?; 68 | Ok(()) 69 | } 70 | 71 | pub fn init(&mut self) -> Result<(), Error> { 72 | self.cs.set_high().map_err(|_| Error::Gpio)?; 73 | self.spi.write(&[CMD_ALL_CLEAR, 0x00]).map_err(Error::Spi)?; 74 | self.cs.set_low().map_err(|_| Error::Gpio)?; 75 | Ok(()) 76 | } 77 | 78 | pub fn update(&mut self) -> Result<(), Error> { 79 | use crate::framebuffer::sealed::FramebufferSpiUpdate; 80 | 81 | self.cs.set_high().map_err(|_| Error::Gpio)?; 82 | self.framebuffer.update(&mut self.spi).map_err(Error::Spi)?; 83 | self.cs.set_low().map_err(|_| Error::Gpio)?; 84 | Ok(()) 85 | } 86 | } 87 | 88 | impl Deref for MemoryLCD 89 | where 90 | SPEC: DisplaySpec, 91 | { 92 | type Target = SPEC::Framebuffer; 93 | 94 | fn deref(&self) -> &Self::Target { 95 | &self.framebuffer 96 | } 97 | } 98 | 99 | impl DerefMut for MemoryLCD 100 | where 101 | SPEC: DisplaySpec, 102 | { 103 | fn deref_mut(&mut self) -> &mut Self::Target { 104 | &mut self.framebuffer 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/pixelcolor.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics_core::{ 2 | pixelcolor::{ 3 | raw::{RawU24, RawU4}, 4 | BinaryColor, Rgb888, 5 | }, 6 | prelude::{PixelColor, RawData, RgbColor}, 7 | }; 8 | 9 | /// Rgb111 color used in color memory LCDs. Format: `0bRGB0`. 10 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default)] 11 | pub struct Rgb111(pub RawU4); 12 | 13 | impl PixelColor for Rgb111 { 14 | type Raw = RawU4; 15 | } 16 | 17 | impl Rgb111 { 18 | pub fn from_u3(raw: u8) -> Self { 19 | Self(RawU4::new((raw & 0b111) << 1)) 20 | } 21 | } 22 | 23 | impl RgbColor for Rgb111 { 24 | fn r(&self) -> u8 { 25 | (self.0.into_inner() >> 3) & 0b1 26 | } 27 | 28 | fn g(&self) -> u8 { 29 | (self.0.into_inner() >> 2) & 0b1 30 | } 31 | 32 | fn b(&self) -> u8 { 33 | (self.0.into_inner() >> 1) & 0b1 34 | } 35 | 36 | const MAX_R: u8 = 0b1; 37 | const MAX_G: u8 = 0b1; 38 | const MAX_B: u8 = 0b1; 39 | 40 | const BLACK: Self = Self(RawU4::new(0)); 41 | const RED: Self = Self(RawU4::new(0b1000)); 42 | const GREEN: Self = Self(RawU4::new(0b0100)); 43 | const BLUE: Self = Self(RawU4::new(0b0010)); 44 | const YELLOW: Self = Self(RawU4::new(0b1100)); 45 | const MAGENTA: Self = Self(RawU4::new(0b1010)); 46 | const CYAN: Self = Self(RawU4::new(0b0110)); 47 | const WHITE: Self = Self(RawU4::new(0b1110)); 48 | } 49 | 50 | impl From for Rgb111 { 51 | fn from(color: BinaryColor) -> Self { 52 | match color { 53 | BinaryColor::Off => Self::BLACK, 54 | BinaryColor::On => Self::WHITE, 55 | } 56 | } 57 | } 58 | 59 | impl From for Rgb111 { 60 | fn from(raw: RawU4) -> Self { 61 | Self(raw) 62 | } 63 | } 64 | 65 | impl From for Rgb111 { 66 | fn from(color: Rgb888) -> Self { 67 | let raw: RawU24 = color.into(); 68 | let storage = raw.into_inner(); 69 | 70 | let red = ((storage >> 16) as u8 >> 7) & 0b1; 71 | let green = ((storage >> 8) as u8 >> 7) & 0b1; 72 | let blue = (storage as u8 >> 7) & 0b1; 73 | 74 | Rgb111(RawU4::new((red << 3) | (green << 2) | (blue << 1))) 75 | } 76 | } 77 | --------------------------------------------------------------------------------