A fork of pulp-os for the xteink4 adding custom apps
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

add: custom display driver

hansmrtn 4894fefb 133aae52

+548 -258
+78 -43
src/bin/main.rs
··· 2 2 #![no_main] 3 3 #![deny( 4 4 clippy::mem_forget, 5 - reason = "mem::forget is generally not safe to do with esp_hal types, especially those \ 6 - holding buffers for the duration of a data transfer." 5 + reason = "mem::forget is generally not safe to do with esp_hal types" 7 6 )] 8 7 #![deny(clippy::large_stack_frames)] 9 8 ··· 16 15 mono_font::{ascii::FONT_10X20, MonoTextStyle}, 17 16 pixelcolor::BinaryColor, 18 17 prelude::*, 19 - primitives::{Circle, PrimitiveStyle, Rectangle, Line}, 18 + primitives::{Circle, Line, PrimitiveStyle, Rectangle}, 20 19 text::Text, 21 20 }; 22 21 23 22 use pulp_os::board::Board; 24 - use pulp_os::drivers::display::DisplayDriver; 25 - use pulp_os::drivers::input::{Event, InputDriver}; 23 + use pulp_os::drivers::input::InputDriver; 24 + 26 25 27 26 extern crate alloc; 28 27 29 28 esp_bootloader_esp_idf::esp_app_desc!(); 29 + 30 + /// The rectangle that will flash on button events. 31 + /// X must be 8-pixel aligned for efficient partial refresh. 32 + const FLASH_RECT_X: u16 = 24; // Aligned to 8 pixels 33 + const FLASH_RECT_Y: u16 = 70; 34 + const FLASH_RECT_W: u16 = 120; // Multiple of 8 35 + const FLASH_RECT_H: u16 = 60; 30 36 31 37 #[allow( 32 38 clippy::large_stack_frames, ··· 39 45 let peripherals = esp_hal::init(config); 40 46 esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 66320); 41 47 42 - info!("pulp-os booting..."); 48 + info!("booting pulp-os..."); 43 49 44 50 // ---- Hardware init ---- 45 - let Board { input, display } = Board::init(peripherals); 46 - let mut input = InputDriver::new(input); 47 - let mut display = DisplayDriver::new(display); 51 + let mut board = Board::init(peripherals); 52 + let mut delay = Delay::new(); 48 53 49 - // ---- Draw test pattern ---- 50 - let sz = display.size(); 54 + // Initialize display 55 + board.display.epd.init(&mut delay); 56 + 57 + let sz = board.display.epd.size(); 51 58 let w = sz.width as i32; 52 59 let h = sz.height as i32; 53 60 info!("display: {}x{}", w, h); 54 61 55 - display.clear_white(); 62 + // Fill framebuffer with white (no display update yet) 63 + board.display.epd.fill_white(); 56 64 57 - let black = PrimitiveStyle::with_stroke(BinaryColor::On, 2); 58 - let filled = PrimitiveStyle::with_fill(BinaryColor::On); 65 + // Draw initial content using embedded-graphics 66 + let black_stroke = PrimitiveStyle::with_stroke(BinaryColor::On, 2); 59 67 let text_style = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); 60 68 61 69 // 1. Border 62 70 Rectangle::new(Point::new(2, 2), Size::new(sz.width - 4, sz.height - 4)) 63 - .into_styled(black) 64 - .draw(&mut display) 71 + .into_styled(black_stroke) 72 + .draw(&mut board.display.epd) 65 73 .unwrap(); 66 74 67 75 // 2. Title 68 76 Text::new("pulp-os", Point::new(20, 40), text_style) 69 - .draw(&mut display) 77 + .draw(&mut board.display.epd) 70 78 .unwrap(); 71 79 72 80 // 3. Crosshair at center 73 81 let cx = w / 2; 74 82 let cy = h / 2; 75 83 Line::new(Point::new(cx - 20, cy), Point::new(cx + 20, cy)) 76 - .into_styled(black) 77 - .draw(&mut display) 84 + .into_styled(black_stroke) 85 + .draw(&mut board.display.epd) 78 86 .unwrap(); 79 87 Line::new(Point::new(cx, cy - 20), Point::new(cx, cy + 20)) 80 - .into_styled(black) 81 - .draw(&mut display) 88 + .into_styled(black_stroke) 89 + .draw(&mut board.display.epd) 82 90 .unwrap(); 83 91 84 - // 4. Filled rectangle 85 - Rectangle::new(Point::new(20, 70), Size::new(120, 60)) 86 - .into_styled(filled) 87 - .draw(&mut display) 88 - .unwrap(); 92 + // 4. The flash rectangle (starts black) 93 + let mut rect_is_black = true; 94 + Rectangle::new( 95 + Point::new(FLASH_RECT_X as i32, FLASH_RECT_Y as i32), 96 + Size::new(FLASH_RECT_W as u32, FLASH_RECT_H as u32), 97 + ) 98 + .into_styled(PrimitiveStyle::with_fill(BinaryColor::On)) 99 + .draw(&mut board.display.epd) 100 + .unwrap(); 89 101 90 102 // 5. Circle 91 103 Circle::new(Point::new(180, 70), 60) 92 - .into_styled(black) 93 - .draw(&mut display) 104 + .into_styled(black_stroke) 105 + .draw(&mut board.display.epd) 94 106 .unwrap(); 95 107 96 - // 6. Label at bottom 97 - Text::new("draw test OK", Point::new(20, h - 20), text_style) 98 - .draw(&mut display) 108 + // 6. Instructions 109 + Text::new("display OK", Point::new(20, h - 20), text_style) 110 + .draw(&mut board.display.epd) 99 111 .unwrap(); 100 112 101 - display.flush_full(&mut Delay::new()); 102 - info!("draw test flushed"); 113 + // Initial full refresh 114 + board.display.epd.refresh_full(&mut delay); 115 + info!("Initial draw complete"); 103 116 104 - // ---- Event loop ---- 105 - let delay = Delay::new(); 117 + // ---- Input polling and event loop ---- 118 + let mut input = InputDriver::new(board.input); 106 119 107 120 loop { 108 - while let Some(ev) = input.poll() { 109 - match ev { 110 - Event::Press(btn) => info!("[BTN] {} pressed", btn), 111 - Event::Release(btn) => info!("[BTN] {} released", btn), 112 - Event::LongPress(btn) => info!("[BTN] {} long-press", btn), 113 - Event::Repeat(btn) => info!("[BTN] {} repeat", btn), 114 - } 121 + if let Some(btn) = input.poll() { 122 + info!("[BTN] {:?} pressed", btn); 123 + 124 + // Toggle rectangle 125 + rect_is_black = !rect_is_black; 126 + let color = if rect_is_black { 127 + BinaryColor::On 128 + } else { 129 + BinaryColor::Off 130 + }; 131 + 132 + // Draw to framebuffer 133 + Rectangle::new( 134 + Point::new(FLASH_RECT_X as i32, FLASH_RECT_Y as i32), 135 + Size::new(FLASH_RECT_W as u32, FLASH_RECT_H as u32), 136 + ) 137 + .into_styled(PrimitiveStyle::with_fill(color)) 138 + .draw(&mut board.display.epd) 139 + .unwrap(); 140 + 141 + // Partial refresh - only the rectangle region 142 + board.display.epd.refresh_partial( 143 + FLASH_RECT_X, 144 + FLASH_RECT_Y, 145 + FLASH_RECT_W, 146 + FLASH_RECT_H, 147 + &mut delay, 148 + ); 149 + 115 150 } 116 151 117 - delay.delay_millis(20); 152 + delay.delay_millis(10); 118 153 } 119 154 }
+464 -5
src/board/display.rs
··· 1 - //! Display hardware constants for XTEink X4 1 + //! SSD1677 E-Paper Display Driver for XTEink X4 2 2 //! 3 - //! The X4 uses an 800x480 e-paper display driven by an SSD1677 controller. 3 + //! Based on GxEPD2_426_GDEQ0426T82.cpp by Jean-Marc Zingg 4 + //! <https://github.com/ZinggJM/GxEPD2> 5 + use embedded_graphics_core::{ 6 + draw_target::DrawTarget, 7 + geometry::{OriginDimensions, Size}, 8 + pixelcolor::BinaryColor, 9 + Pixel, 10 + }; 11 + use embedded_hal::digital::{InputPin, OutputPin}; 12 + use embedded_hal::spi::SpiDevice; 13 + use esp_hal::delay::Delay; 4 14 15 + // Display dimensions 5 16 pub const WIDTH: u16 = 800; 6 17 pub const HEIGHT: u16 = 480; 7 - 8 - /// Framebuffer size in bytes (1 bit per pixel, packed). 9 18 pub const FRAMEBUFFER_SIZE: usize = (WIDTH as usize * HEIGHT as usize) / 8; 10 19 11 - /// The SSD1677 typically supports up to 20MHz, but 10MHz seems fine for now 20 + // SPI frequency (10MHz as per GxEPD2) 12 21 pub const SPI_FREQ_MHZ: u32 = 10; 22 + 23 + // Timing constants from GxEPD2 24 + #[allow(dead_code)] 25 + const POWER_ON_TIME_MS: u32 = 100; 26 + const POWER_OFF_TIME_MS: u32 = 200; 27 + const FULL_REFRESH_TIME_MS: u32 = 1600; 28 + const PARTIAL_REFRESH_TIME_MS: u32 = 600; 29 + 30 + // SSD1677 Commands (matching GxEPD2 exactly) 31 + mod cmd { 32 + pub const DRIVER_OUTPUT_CONTROL: u8 = 0x01; 33 + pub const BOOSTER_SOFT_START: u8 = 0x0C; 34 + pub const DEEP_SLEEP: u8 = 0x10; 35 + pub const DATA_ENTRY_MODE: u8 = 0x11; 36 + pub const SW_RESET: u8 = 0x12; 37 + pub const TEMPERATURE_SENSOR: u8 = 0x18; 38 + pub const WRITE_TEMP_REGISTER: u8 = 0x1A; 39 + pub const MASTER_ACTIVATION: u8 = 0x20; 40 + pub const DISPLAY_UPDATE_CONTROL_1: u8 = 0x21; 41 + pub const DISPLAY_UPDATE_CONTROL_2: u8 = 0x22; 42 + pub const WRITE_RAM_BW: u8 = 0x24; // Current/New buffer 43 + pub const WRITE_RAM_RED: u8 = 0x26; // Previous buffer (for differential) 44 + pub const BORDER_WAVEFORM: u8 = 0x3C; 45 + pub const SET_RAM_X_RANGE: u8 = 0x44; 46 + pub const SET_RAM_Y_RANGE: u8 = 0x45; 47 + pub const SET_RAM_X_COUNTER: u8 = 0x4E; 48 + pub const SET_RAM_Y_COUNTER: u8 = 0x4F; 49 + } 50 + 51 + /// Display driver for SSD1677-based e-paper (GDEQ0426T82) 52 + pub struct DisplayDriver<SPI, DC, RST, BUSY> { 53 + spi: SPI, 54 + dc: DC, 55 + rst: RST, 56 + busy: BUSY, 57 + framebuffer: [u8; FRAMEBUFFER_SIZE], 58 + power_is_on: bool, 59 + init_done: bool, 60 + initial_refresh: bool, 61 + initial_write: bool, 62 + } 63 + 64 + impl<SPI, DC, RST, BUSY, E> DisplayDriver<SPI, DC, RST, BUSY> 65 + where 66 + SPI: SpiDevice<Error = E>, 67 + DC: OutputPin, 68 + RST: OutputPin, 69 + BUSY: InputPin, 70 + { 71 + /// Create a new display driver 72 + pub fn new(spi: SPI, dc: DC, rst: RST, busy: BUSY) -> Self { 73 + Self { 74 + spi, 75 + dc, 76 + rst, 77 + busy, 78 + framebuffer: [0xFF; FRAMEBUFFER_SIZE], // White 79 + power_is_on: false, 80 + init_done: false, 81 + initial_refresh: true, 82 + initial_write: true, 83 + } 84 + } 85 + 86 + /// Hardware reset 87 + pub fn reset(&mut self, delay: &mut Delay) { 88 + let _ = self.rst.set_high(); 89 + delay.delay_millis(20); 90 + let _ = self.rst.set_low(); 91 + delay.delay_millis(2); 92 + let _ = self.rst.set_high(); 93 + delay.delay_millis(20); 94 + } 95 + 96 + /// Initialize the display 97 + pub fn init(&mut self, delay: &mut Delay) { 98 + self.reset(delay); 99 + self.init_display(delay); 100 + } 101 + 102 + /// Clear the entire screen to white 103 + pub fn clear(&mut self, delay: &mut Delay) { 104 + self.framebuffer.fill(0xFF); 105 + self.clear_screen(delay); 106 + } 107 + 108 + /// Fill framebuffer only (no display update) 109 + pub fn fill_white(&mut self) { 110 + self.framebuffer.fill(0xFF); 111 + } 112 + 113 + /// Fill framebuffer with black (no display update) 114 + pub fn fill_black(&mut self) { 115 + self.framebuffer.fill(0x00); 116 + } 117 + 118 + /// Set a pixel in the framebuffer (0,0 is top-left) 119 + pub fn set_pixel(&mut self, x: u16, y: u16, black: bool) { 120 + if x >= WIDTH || y >= HEIGHT { 121 + return; 122 + } 123 + let idx = (x as usize / 8) + (y as usize * (WIDTH as usize / 8)); 124 + let bit = 7 - (x % 8); 125 + if black { 126 + self.framebuffer[idx] &= !(1 << bit); 127 + } else { 128 + self.framebuffer[idx] |= 1 << bit; 129 + } 130 + } 131 + 132 + /// Fill framebuffer with color (true = black, false = white) 133 + pub fn fill(&mut self, black: bool) { 134 + self.framebuffer.fill(if black { 0x00 } else { 0xFF }); 135 + } 136 + 137 + /// Full screen refresh (use after initial setup or to clear ghosting) 138 + pub fn refresh_full(&mut self, delay: &mut Delay) { 139 + if !self.init_done { 140 + self.init_display(delay); 141 + } 142 + 143 + // Small delay to ensure display is ready 144 + delay.delay_millis(10); 145 + 146 + // Write to both buffers for full refresh 147 + self.set_partial_ram_area(0, 0, WIDTH, HEIGHT); 148 + self.write_full_buffer(cmd::WRITE_RAM_RED); // Previous 149 + 150 + delay.delay_millis(1); // Yield between large transfers 151 + 152 + self.set_partial_ram_area(0, 0, WIDTH, HEIGHT); 153 + self.write_full_buffer(cmd::WRITE_RAM_BW); // Current 154 + 155 + self.update_full(delay); 156 + self.initial_refresh = false; 157 + self.initial_write = false; 158 + } 159 + 160 + /// Partial screen refresh (fast, for small updates) 161 + /// Matches GxEPD2's drawImage() sequence exactly: 162 + /// 1. writeImage to 0x24 (current buffer only) 163 + /// 2. refresh (partial update compares 0x24 vs 0x26) 164 + /// 3. writeImageAgain to BOTH 0x26 and 0x24 (sync buffers for next update) 165 + pub fn refresh_partial(&mut self, x: u16, y: u16, w: u16, h: u16, delay: &mut Delay) { 166 + // Initial refresh must be full 167 + if self.initial_refresh { 168 + return self.refresh_full(delay); 169 + } 170 + 171 + if !self.init_done { 172 + self.init_display(delay); 173 + } 174 + 175 + // Ensure x and w are multiples of 8 (byte boundary requirement) 176 + let x = x & !7; 177 + let mut w = w + (x & 7); // Add back what we subtracted from x 178 + if w % 8 > 0 { 179 + w += 8 - (w % 8); 180 + } 181 + 182 + // Clamp to screen bounds 183 + let x = x.min(WIDTH); 184 + let y = y.min(HEIGHT); 185 + let w = w.min(WIDTH - x); 186 + let h = h.min(HEIGHT - y); 187 + 188 + if w == 0 || h == 0 { 189 + return; 190 + } 191 + 192 + // Step 1: Write to current buffer (0x24) only 193 + self.write_image_partial(cmd::WRITE_RAM_BW, x, y, w, h); 194 + 195 + // Step 2: Partial refresh 196 + self.set_partial_ram_area(x, y, w, h); 197 + self.update_partial(delay); 198 + 199 + // Step 3: Sync buffers - write to BOTH previous (0x26) AND current (0x24) 200 + // This is writeImageAgain() in GxEPD2 201 + self.write_image_partial(cmd::WRITE_RAM_RED, x, y, w, h); 202 + self.write_image_partial(cmd::WRITE_RAM_BW, x, y, w, h); 203 + } 204 + 205 + /// Refresh a rectangular window (convenience method) 206 + pub fn refresh_window(&mut self, x: u16, y: u16, w: u16, h: u16, delay: &mut Delay) { 207 + self.refresh_partial(x, y, w, h, delay); 208 + } 209 + 210 + /// Power off the display (reduces power consumption, prevents fading) 211 + pub fn power_off(&mut self, delay: &mut Delay) { 212 + if self.power_is_on { 213 + self.send_command(cmd::DISPLAY_UPDATE_CONTROL_2); 214 + self.send_data(&[0x83]); 215 + self.send_command(cmd::MASTER_ACTIVATION); 216 + self.wait_busy(delay, POWER_OFF_TIME_MS); 217 + self.power_is_on = false; 218 + } 219 + } 220 + 221 + /// Put display into deep sleep (minimum power, needs reset to wake) 222 + pub fn hibernate(&mut self, delay: &mut Delay) { 223 + self.power_off(delay); 224 + self.send_command(cmd::DEEP_SLEEP); 225 + self.send_data(&[0x01]); 226 + self.init_done = false; 227 + } 228 + 229 + // ========== Private methods ========== 230 + 231 + fn init_display(&mut self, delay: &mut Delay) { 232 + // Software reset 233 + self.send_command(cmd::SW_RESET); 234 + delay.delay_millis(10); 235 + 236 + // Temperature sensor: internal 237 + self.send_command(cmd::TEMPERATURE_SENSOR); 238 + self.send_data(&[0x80]); 239 + 240 + // Booster soft start 241 + self.send_command(cmd::BOOSTER_SOFT_START); 242 + self.send_data(&[0xAE, 0xC7, 0xC3, 0xC0, 0x80]); 243 + 244 + // Driver output control 245 + self.send_command(cmd::DRIVER_OUTPUT_CONTROL); 246 + self.send_data(&[ 247 + ((HEIGHT - 1) & 0xFF) as u8, // A[7:0] 248 + ((HEIGHT - 1) >> 8) as u8, // A[9:8] 249 + 0x02, // SM = interlaced 250 + ]); 251 + 252 + // Border waveform 253 + self.send_command(cmd::BORDER_WAVEFORM); 254 + self.send_data(&[0x01]); 255 + 256 + // Set initial RAM area 257 + self.set_partial_ram_area(0, 0, WIDTH, HEIGHT); 258 + 259 + self.init_done = true; 260 + } 261 + 262 + fn set_partial_ram_area(&mut self, x: u16, y: u16, w: u16, h: u16) { 263 + // Gates are reversed on this display - flip Y 264 + let y_flipped = HEIGHT - y - h; 265 + 266 + // Data entry mode: X increase, Y decrease (for gate reversal) 267 + self.send_command(cmd::DATA_ENTRY_MODE); 268 + self.send_data(&[0x01]); 269 + 270 + // Set RAM X address start/end 271 + self.send_command(cmd::SET_RAM_X_RANGE); 272 + self.send_data(&[ 273 + (x & 0xFF) as u8, 274 + (x >> 8) as u8, 275 + ((x + w - 1) & 0xFF) as u8, 276 + ((x + w - 1) >> 8) as u8, 277 + ]); 278 + 279 + // Set RAM Y address start/end (reversed) 280 + self.send_command(cmd::SET_RAM_Y_RANGE); 281 + self.send_data(&[ 282 + ((y_flipped + h - 1) & 0xFF) as u8, 283 + ((y_flipped + h - 1) >> 8) as u8, 284 + (y_flipped & 0xFF) as u8, 285 + (y_flipped >> 8) as u8, 286 + ]); 287 + 288 + // Set RAM X counter 289 + self.send_command(cmd::SET_RAM_X_COUNTER); 290 + self.send_data(&[(x & 0xFF) as u8, (x >> 8) as u8]); 291 + 292 + // Set RAM Y counter 293 + self.send_command(cmd::SET_RAM_Y_COUNTER); 294 + self.send_data(&[ 295 + ((y_flipped + h - 1) & 0xFF) as u8, 296 + ((y_flipped + h - 1) >> 8) as u8, 297 + ]); 298 + } 299 + 300 + fn clear_screen(&mut self, delay: &mut Delay) { 301 + if !self.init_done { 302 + self.init_display(delay); 303 + } 304 + 305 + // Write white to both buffers 306 + self.set_partial_ram_area(0, 0, WIDTH, HEIGHT); 307 + self.write_screen_buffer(cmd::WRITE_RAM_RED, 0xFF); 308 + self.set_partial_ram_area(0, 0, WIDTH, HEIGHT); 309 + self.write_screen_buffer(cmd::WRITE_RAM_BW, 0xFF); 310 + 311 + self.update_full(delay); 312 + self.initial_write = false; 313 + self.initial_refresh = false; 314 + } 315 + 316 + fn write_screen_buffer(&mut self, command: u8, value: u8) { 317 + self.send_command(command); 318 + // Write in chunks to avoid watchdog issues 319 + let total = (WIDTH as u32 * HEIGHT as u32 / 8) as usize; 320 + let chunk_size = 256; 321 + let chunk = [value; 256]; 322 + for i in (0..total).step_by(chunk_size) { 323 + let len = (total - i).min(chunk_size); 324 + self.send_data(&chunk[..len]); 325 + } 326 + } 327 + 328 + fn write_full_buffer(&mut self, command: u8) { 329 + self.send_command(command); 330 + // Write in normal row order - gate reversal is handled by RAM address setup 331 + // (Y-decrease mode in _setPartialRamArea), NOT by reversing rows here 332 + let bytes_per_row = (WIDTH / 8) as usize; 333 + 334 + // Use a temporary buffer to avoid borrow checker issues 335 + let mut row_buf = [0u8; 256]; 336 + 337 + for row in 0..HEIGHT as usize { 338 + let start = row * bytes_per_row; 339 + // Write row in chunks to avoid issues 340 + for chunk_start in (0..bytes_per_row).step_by(256) { 341 + let chunk_end = (chunk_start + 256).min(bytes_per_row); 342 + let chunk_len = chunk_end - chunk_start; 343 + 344 + // Copy to temp buffer 345 + row_buf[..chunk_len].copy_from_slice( 346 + &self.framebuffer[start + chunk_start..start + chunk_end] 347 + ); 348 + self.send_data(&row_buf[..chunk_len]); 349 + } 350 + } 351 + } 352 + 353 + fn write_image_partial(&mut self, command: u8, x: u16, y: u16, w: u16, h: u16) { 354 + self.set_partial_ram_area(x, y, w, h); 355 + self.send_command(command); 356 + 357 + let bytes_per_row = (WIDTH / 8) as usize; 358 + let window_bytes = (w / 8) as usize; 359 + let x_byte = (x / 8) as usize; 360 + 361 + // Use a temporary buffer to avoid borrow checker issues 362 + // Max window width is 800/8 = 100 bytes per row 363 + let mut row_buf = [0u8; 100]; 364 + 365 + // Write rows in NORMAL order (row 0, 1, 2, ...) 366 + // Gate reversal is handled by the RAM address setup, not here! 367 + for row in 0..h as usize { 368 + let src_row = y as usize + row; 369 + let src_start = src_row * bytes_per_row + x_byte; 370 + 371 + // Copy to temp buffer 372 + row_buf[..window_bytes].copy_from_slice( 373 + &self.framebuffer[src_start..src_start + window_bytes] 374 + ); 375 + self.send_data(&row_buf[..window_bytes]); 376 + } 377 + } 378 + 379 + fn update_full(&mut self, delay: &mut Delay) { 380 + // Display Update Control 1: bypass RED as 0 381 + self.send_command(cmd::DISPLAY_UPDATE_CONTROL_1); 382 + self.send_data(&[0x40, 0x00]); 383 + 384 + // Use standard full refresh (0xF7) - more reliable than fast mode (0xD7) 385 + // Fast mode requires temperature register which may not work on all panels 386 + self.send_command(cmd::DISPLAY_UPDATE_CONTROL_2); 387 + self.send_data(&[0xF7]); 388 + 389 + self.send_command(cmd::MASTER_ACTIVATION); 390 + self.wait_busy(delay, FULL_REFRESH_TIME_MS); 391 + 392 + self.power_is_on = false; 393 + } 394 + 395 + fn update_partial(&mut self, delay: &mut Delay) { 396 + // Display Update Control 1: RED normal 397 + self.send_command(cmd::DISPLAY_UPDATE_CONTROL_1); 398 + self.send_data(&[0x00, 0x00]); 399 + 400 + self.send_command(cmd::DISPLAY_UPDATE_CONTROL_2); 401 + self.send_data(&[0xFC]); // Partial refresh (uses OTP LUT) 402 + 403 + self.send_command(cmd::MASTER_ACTIVATION); 404 + self.wait_busy(delay, PARTIAL_REFRESH_TIME_MS); 405 + 406 + self.power_is_on = true; 407 + } 408 + 409 + fn wait_busy(&mut self, delay: &mut Delay, timeout_ms: u32) { 410 + let mut elapsed = 0u32; 411 + while elapsed < timeout_ms { 412 + if self.busy.is_low().unwrap_or(true) { 413 + return; 414 + } 415 + delay.delay_millis(1); 416 + elapsed += 1; 417 + } 418 + } 419 + 420 + fn send_command(&mut self, cmd: u8) { 421 + let _ = self.dc.set_low(); 422 + let _ = self.spi.write(&[cmd]); 423 + let _ = self.dc.set_high(); 424 + } 425 + 426 + fn send_data(&mut self, data: &[u8]) { 427 + let _ = self.dc.set_high(); 428 + let _ = self.spi.write(data); 429 + } 430 + } 431 + 432 + // ========== embedded-graphics integration ========== 433 + 434 + impl<SPI, DC, RST, BUSY, E> OriginDimensions for DisplayDriver<SPI, DC, RST, BUSY> 435 + where 436 + SPI: SpiDevice<Error = E>, 437 + DC: OutputPin, 438 + RST: OutputPin, 439 + BUSY: InputPin, 440 + { 441 + fn size(&self) -> Size { 442 + Size::new(WIDTH as u32, HEIGHT as u32) 443 + } 444 + } 445 + 446 + impl<SPI, DC, RST, BUSY, E> DrawTarget for DisplayDriver<SPI, DC, RST, BUSY> 447 + where 448 + SPI: SpiDevice<Error = E>, 449 + DC: OutputPin, 450 + RST: OutputPin, 451 + BUSY: InputPin, 452 + { 453 + type Color = BinaryColor; 454 + type Error = core::convert::Infallible; 455 + 456 + fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error> 457 + where 458 + I: IntoIterator<Item = Pixel<Self::Color>>, 459 + { 460 + for Pixel(coord, color) in pixels { 461 + if coord.x >= 0 && coord.x < WIDTH as i32 && coord.y >= 0 && coord.y < HEIGHT as i32 { 462 + self.set_pixel( 463 + coord.x as u16, 464 + coord.y as u16, 465 + color == BinaryColor::On, // On = black (foreground), Off = white (background) 466 + ); 467 + } 468 + } 469 + Ok(()) 470 + } 471 + }
+5 -16
src/board/mod.rs
··· 9 9 pub mod pins; 10 10 11 11 pub use button::{decode_ladder, Button, ROW1_THRESHOLDS, ROW2_THRESHOLDS}; 12 + pub use display::{DisplayDriver, HEIGHT, WIDTH, FRAMEBUFFER_SIZE, SPI_FREQ_MHZ}; 12 13 13 14 use embedded_hal_bus::spi::ExclusiveDevice; 14 15 use esp_hal::{ ··· 20 21 time::Rate, 21 22 Blocking, 22 23 }; 23 - use ssd1677::{Builder, Dimensions, Display, Interface, Rotation}; 24 24 25 25 // Type Aliases 26 26 pub type SpiBus = spi::master::Spi<'static, Blocking>; 27 27 pub type SpiDevice = ExclusiveDevice<SpiBus, Output<'static>, Delay>; 28 - pub type EpdInterface = Interface<SpiDevice, Output<'static>, Output<'static>, Input<'static>>; 29 - pub type Epd = Display<EpdInterface>; 28 + pub type Epd = DisplayDriver<SpiDevice, Output<'static>, Output<'static>, Input<'static>>; 30 29 31 30 // Hardware Bundles 32 31 /// Input subsystem hardware: ADC for button ladders + power button. ··· 93 92 94 93 // SPI bus 95 94 let spi_cfg = 96 - spi::master::Config::default().with_frequency(Rate::from_mhz(display::SPI_FREQ_MHZ)); 95 + spi::master::Config::default().with_frequency(Rate::from_mhz(SPI_FREQ_MHZ)); 97 96 let spi_bus = spi::master::Spi::new(p.SPI2, spi_cfg) 98 97 .unwrap() 99 98 .with_sck(p.GPIO8) ··· 101 100 102 101 let spi_dev = ExclusiveDevice::new(spi_bus, cs, Delay::new()).unwrap(); 103 102 104 - // Display controller 105 - let interface = Interface::new(spi_dev, dc, rst, busy); 106 - 107 - let dims = Dimensions::new(display::HEIGHT, display::WIDTH).unwrap(); 108 - let cfg = Builder::new() 109 - .dimensions(dims) 110 - .rotation(Rotation::Rotate270) 111 - .build() 112 - .unwrap(); 113 - 114 - let mut epd = Display::new(interface, cfg); 115 - epd.reset(&mut Delay::new()); 103 + // Create display driver (our custom GxEPD2-based driver) 104 + let epd = DisplayDriver::new(spi_dev, dc, rst, busy); 116 105 117 106 DisplayHw { epd } 118 107 }
-193
src/drivers/display.rs
··· 1 - //! Display driver for XTEink X4 e-paper 2 - //! 3 - //! Wraps the SSD1677-based e-paper display with a framebuffer and 4 - //! implements `embedded_graphics::DrawTarget` for easy rendering. 5 - 6 - extern crate alloc; 7 - 8 - use alloc::vec; 9 - use alloc::vec::Vec; 10 - use core::convert::Infallible; 11 - 12 - use embedded_graphics_core::{ 13 - draw_target::DrawTarget, 14 - geometry::{OriginDimensions, Point, Size}, 15 - pixelcolor::BinaryColor, 16 - prelude::Pixel, 17 - }; 18 - use esp_hal::delay::Delay; 19 - use log::info; 20 - use ssd1677::rotation::apply_rotation; 21 - use ssd1677::{RefreshMode, Region, UpdateRegion}; 22 - 23 - use crate::board::display::{FRAMEBUFFER_SIZE, HEIGHT, WIDTH}; 24 - use crate::board::{DisplayHw, Epd}; 25 - 26 - /// Number of fast/partial refreshes before forcing a full refresh. 27 - /// 28 - /// E-paper displays accumulate "ghosting" artifacts with partial updates. 29 - /// A periodic full refresh clears these artifacts. 30 - const FULL_REFRESH_INTERVAL: u32 = 10; 31 - 32 - /// High-level display driver with framebuffer management. 33 - pub struct DisplayDriver { 34 - epd: Epd, 35 - buf: Vec<u8>, 36 - fast_count: u32, 37 - dirty: bool, 38 - } 39 - 40 - impl DisplayDriver { 41 - /// Create a new display driver from initialized hardware. 42 - pub fn new(hw: DisplayHw) -> Self { 43 - // 0xFF = all white (bit set = white pixel for this controller) 44 - let buf = vec![0xFFu8; FRAMEBUFFER_SIZE]; 45 - 46 - Self { 47 - epd: hw.epd, 48 - buf, 49 - fast_count: 0, 50 - dirty: false, 51 - } 52 - } 53 - 54 - /// Clear the framebuffer to white. 55 - pub fn clear_white(&mut self) { 56 - self.buf.fill(0xFF); 57 - self.dirty = true; 58 - } 59 - 60 - /// Clear the framebuffer to black. 61 - pub fn clear_black(&mut self) { 62 - self.buf.fill(0x00); 63 - self.dirty = true; 64 - } 65 - 66 - /// Flush the framebuffer to the display. 67 - /// 68 - /// Automatically chooses full vs. fast refresh based on the 69 - /// number of fast refreshes since the last full refresh. 70 - pub fn flush(&mut self, delay: &mut Delay) { 71 - if !self.dirty { 72 - return; 73 - } 74 - 75 - let mode = if self.fast_count >= FULL_REFRESH_INTERVAL { 76 - info!("[EPD] full refresh (ghost cleanup)"); 77 - RefreshMode::Full 78 - } else { 79 - RefreshMode::Fast 80 - }; 81 - 82 - self.flush_inner(mode, delay); 83 - } 84 - 85 - /// Flush with a specific refresh mode. 86 - pub fn flush_with_mode(&mut self, mode: RefreshMode, delay: &mut Delay) { 87 - self.flush_inner(mode, delay); 88 - } 89 - 90 - /// Force a full refresh (clears ghosting artifacts). 91 - pub fn flush_full(&mut self, delay: &mut Delay) { 92 - self.flush_inner(RefreshMode::Full, delay); 93 - } 94 - 95 - /// Returns `true` if the framebuffer has been modified since the last flush. 96 - pub fn is_dirty(&self) -> bool { 97 - self.dirty 98 - } 99 - 100 - /// Number of fast refreshes since the last full refresh. 101 - pub fn fast_count(&self) -> u32 { 102 - self.fast_count 103 - } 104 - 105 - /// Access the underlying EPD driver (for advanced use). 106 - pub fn epd(&mut self) -> &mut Epd { 107 - &mut self.epd 108 - } 109 - 110 - /// Read-only access to the framebuffer. 111 - pub fn buffer(&self) -> &[u8] { 112 - &self.buf 113 - } 114 - 115 - fn flush_inner(&mut self, mode: RefreshMode, delay: &mut Delay) { 116 - let region = Region::new(0, 0, WIDTH, HEIGHT); 117 - 118 - let update = UpdateRegion { 119 - region, 120 - black_buffer: &self.buf, 121 - red_buffer: &[], 122 - mode, 123 - }; 124 - 125 - if let Err(_e) = self.epd.update_region(update, delay) { 126 - info!("[EPD] flush error"); 127 - } 128 - 129 - match mode { 130 - RefreshMode::Full => self.fast_count = 0, 131 - _ => self.fast_count += 1, 132 - } 133 - 134 - self.dirty = false; 135 - } 136 - 137 - fn set_pixel(&mut self, x: u32, y: u32, on: bool) { 138 - let rotation = self.epd.rotation(); 139 - 140 - // Physical (unrotated) dimensions 141 - let dims = self.epd.dimensions(); 142 - let width = dims.cols as u32; 143 - let height = dims.rows as u32; 144 - 145 - let (index, bit) = apply_rotation(x, y, width, height, rotation); 146 - 147 - if index >= self.buf.len() { 148 - return; 149 - } 150 - 151 - if on { 152 - // "On" = black = clear bit 153 - self.buf[index] &= !bit; 154 - } else { 155 - // "Off" = white = set bit 156 - self.buf[index] |= bit; 157 - } 158 - } 159 - } 160 - 161 - impl DrawTarget for DisplayDriver { 162 - type Color = BinaryColor; 163 - type Error = Infallible; 164 - 165 - fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error> 166 - where 167 - I: IntoIterator<Item = Pixel<Self::Color>>, 168 - { 169 - let sz = self.size(); 170 - 171 - for Pixel(Point { x, y }, color) in pixels { 172 - if x < 0 || y < 0 { 173 - continue; 174 - } 175 - let x = x as u32; 176 - let y = y as u32; 177 - if x >= sz.width || y >= sz.height { 178 - continue; 179 - } 180 - self.set_pixel(x, y, color.is_on()); 181 - self.dirty = true; 182 - } 183 - 184 - Ok(()) 185 - } 186 - } 187 - 188 - impl OriginDimensions for DisplayDriver { 189 - fn size(&self) -> Size { 190 - let rotated = self.epd.config().rotated_dimensions(); 191 - Size::new(rotated.cols as u32, rotated.rows as u32) 192 - } 193 - }
+1 -1
src/drivers/mod.rs
··· 1 1 2 2 3 3 pub mod input; 4 - pub mod display; 4 + // pub mod display_driver;