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: sdcard support

hansmrtn e45589da 449fb1e3

+382 -24
+14
Cargo.lock
··· 347 347 ] 348 348 349 349 [[package]] 350 + name = "embedded-sdmmc" 351 + version = "0.9.0" 352 + source = "registry+https://github.com/rust-lang/crates.io-index" 353 + checksum = "ce3c7f9ea039eeafc4a49597b7bd5ae3a1c8e51b2803a381cb0f29ce90fe1ec6" 354 + dependencies = [ 355 + "byteorder", 356 + "embedded-hal 1.0.0", 357 + "embedded-io 0.6.1", 358 + "heapless 0.8.0", 359 + "log", 360 + ] 361 + 362 + [[package]] 350 363 name = "embedded-storage" 351 364 version = "0.3.1" 352 365 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 936 949 "embedded-graphics-core", 937 950 "embedded-hal 1.0.0", 938 951 "embedded-hal-bus", 952 + "embedded-sdmmc", 939 953 "esp-alloc", 940 954 "esp-backtrace", 941 955 "esp-bootloader-esp-idf",
+1
Cargo.toml
··· 29 29 nb = "1.1.0" 30 30 embedded-graphics-core = "0.4.1" 31 31 embedded-graphics = "0.8.2" 32 + embedded-sdmmc = "0.9.0" 32 33 33 34 34 35 [profile.dev]
+79 -23
src/board/mod.rs
··· 1 1 //! XTEink X4 Board Support Package (BSP) 2 2 //! 3 - //! This module provides hardware abstraction for the XTEink X4 e-reader. 4 - //! It maps physical hardware to named subsystems so that application code 5 - //! doesn't need to know GPIO numbers or peripheral details. 3 + //! ## SPI Bus Sharing 4 + //! 5 + //! The e-paper display and SD card share SPI2 (SCK=GPIO8, MOSI=GPIO10). 6 + //! SD also uses MISO=GPIO7 (display is write-only, ignores MISO). 7 + //! Bus arbitration uses `RefCellDevice` from embedded-hal-bus — safe 8 + //! because we're single-threaded bare-metal and ISRs don't touch SPI. 6 9 7 10 pub mod button; 8 11 pub mod display; 9 12 pub mod pins; 13 + pub mod raw_gpio; 14 + pub mod sdcard; 10 15 pub mod strip; 11 16 12 17 pub use button::{Button, ROW1_THRESHOLDS, ROW2_THRESHOLDS, decode_ladder}; 13 18 pub use display::{DisplayDriver, HEIGHT, SPI_FREQ_MHZ, WIDTH}; 19 + pub use sdcard::SdStorage; 14 20 pub use strip::StripBuffer; 15 21 16 - use embedded_hal_bus::spi::ExclusiveDevice; 22 + use core::cell::RefCell; 23 + 24 + use embedded_hal_bus::spi::RefCellDevice; 17 25 use esp_hal::{ 18 26 Blocking, 19 27 analog::adc::{Adc, AdcCalCurve, AdcConfig, AdcPin, Attenuation}, ··· 23 31 spi, 24 32 time::Rate, 25 33 }; 34 + use log::info; 35 + use static_cell::StaticCell; 26 36 27 37 // Type Aliases 28 38 pub type SpiBus = spi::master::Spi<'static, Blocking>; 29 - pub type SpiDevice = ExclusiveDevice<SpiBus, Output<'static>, Delay>; 30 - pub type Epd = DisplayDriver<SpiDevice, Output<'static>, Output<'static>, Input<'static>>; 39 + pub type SharedSpiDevice = RefCellDevice<'static, SpiBus, Output<'static>, Delay>; 40 + pub type SdSpiDevice = RefCellDevice<'static, SpiBus, raw_gpio::RawOutputPin, Delay>; 41 + pub type Epd = DisplayDriver<SharedSpiDevice, Output<'static>, Output<'static>, Input<'static>>; 42 + 43 + // Static SPI bus — shared between display and SD card. 44 + static SPI_BUS: StaticCell<RefCell<SpiBus>> = StaticCell::new(); 31 45 32 46 // Hardware Bundles 33 - /// Input subsystem hardware: ADC for button ladders + power button. 47 + 48 + /// Input subsystem: ADC for button ladders + power button. 34 49 pub struct InputHw { 35 50 pub adc: Adc<'static, ADC1<'static>, Blocking>, 36 51 pub row1: AdcPin<GPIO1<'static>, ADC1<'static>, AdcCalCurve<ADC1<'static>>>, ··· 38 53 pub power: Input<'static>, 39 54 } 40 55 41 - /// Display subsystem hardware: initialized e-paper display. 56 + /// Display subsystem hardware. 42 57 pub struct DisplayHw { 43 58 pub epd: Epd, 44 59 } 45 60 46 - /// Complete board hardware, ready for driver initialization. 61 + /// SD card storage hardware. 62 + pub struct StorageHw { 63 + pub sd: SdStorage<SdSpiDevice>, 64 + } 65 + 66 + /// Complete board hardware. 47 67 pub struct Board { 48 68 pub input: InputHw, 49 69 pub display: DisplayHw, 70 + pub storage: StorageHw, 50 71 } 51 72 52 73 impl Board { 53 74 pub fn init(p: Peripherals) -> Self { 54 75 let input = Self::init_input(&p); 55 - let display = Self::init_display(p); 56 - Board { input, display } 76 + let (display, storage) = Self::init_spi_peripherals(p); 77 + Board { 78 + input, 79 + display, 80 + storage, 81 + } 57 82 } 58 83 59 84 fn init_input(p: &Peripherals) -> InputHw { 60 85 let mut adc_cfg = AdcConfig::new(); 61 86 62 - // Configure both ADC channels with 11dB attenuation for full 0-3.3V range 63 87 let row1 = adc_cfg.enable_pin_with_cal::<_, AdcCalCurve<ADC1>>( 64 88 unsafe { p.GPIO1.clone_unchecked() }, 65 89 Attenuation::_11dB, ··· 85 109 } 86 110 } 87 111 88 - fn init_display(p: Peripherals) -> DisplayHw { 89 - // GPIO setup 90 - let cs = Output::new(p.GPIO21, Level::High, OutputConfig::default()); 112 + /// Initialize SPI bus and all SPI peripherals (display + SD card). 113 + /// 114 + /// Three-phase init: 115 + /// 1. Create bus at 400kHz, send 74-clock preamble 116 + /// 2. Create SD device, probe card (triggers SD init at 400kHz) 117 + /// 3. Speed up to 20MHz, create display device 118 + fn init_spi_peripherals(p: Peripherals) -> (DisplayHw, StorageHw) { 119 + // Display GPIO 120 + let epd_cs = Output::new(p.GPIO21, Level::High, OutputConfig::default()); 91 121 let dc = Output::new(p.GPIO4, Level::High, OutputConfig::default()); 92 122 let rst = Output::new(p.GPIO5, Level::High, OutputConfig::default()); 93 123 let busy = Input::new(p.GPIO6, InputConfig::default().with_pull(Pull::None)); 94 124 95 - // SPI bus 96 - let spi_cfg = spi::master::Config::default().with_frequency(Rate::from_mhz(SPI_FREQ_MHZ)); 97 - let spi_bus = spi::master::Spi::new(p.SPI2, spi_cfg) 125 + // SD card CS on GPIO12 (SPIHD). The X4 uses DIO flash mode so 126 + // GPIO12 is physically free, but esp-hal doesn't expose GPIO12-17 127 + // for ESP32-C3. Drive it via direct register access. 128 + let sd_cs = unsafe { raw_gpio::RawOutputPin::new(12) }; 129 + 130 + // Phase 1: SPI bus at 400kHz for SD card identification. 131 + let slow_cfg = spi::master::Config::default() 132 + .with_frequency(Rate::from_khz(400)); 133 + 134 + let mut spi_bus = spi::master::Spi::new(p.SPI2, slow_cfg) 98 135 .unwrap() 99 136 .with_sck(p.GPIO8) 100 - .with_mosi(p.GPIO10); 137 + .with_mosi(p.GPIO10) 138 + .with_miso(p.GPIO7); 139 + 140 + // 74+ clock cycles with CS deasserted (SD spec requirement). 141 + // 10 bytes × 8 bits = 80 clocks. 142 + let _ = spi_bus.write(&[0xFF; 10]); 143 + 144 + // Place bus in static RefCell for shared access. 145 + let spi_ref: &'static RefCell<SpiBus> = SPI_BUS.init(RefCell::new(spi_bus)); 146 + 147 + // Phase 2: SD card init at 400kHz. 148 + // RefCellDevice::new() returns Result<_, Infallible>, always safe. 149 + let sd_spi = RefCellDevice::new(spi_ref, sd_cs, Delay::new()).unwrap(); 150 + // SdStorage::new() probes the card internally (calls num_bytes()). 151 + let sd = SdStorage::new(sd_spi); 101 152 102 - let spi_dev = ExclusiveDevice::new(spi_bus, cs, Delay::new()).unwrap(); 153 + // Phase 3: Speed up to 20MHz for display + normal SD operations. 154 + let fast_cfg = spi::master::Config::default() 155 + .with_frequency(Rate::from_mhz(SPI_FREQ_MHZ)); 156 + spi_ref.borrow_mut().apply_config(&fast_cfg).unwrap(); 157 + info!("SPI bus: 400kHz -> {}MHz", SPI_FREQ_MHZ); 103 158 104 - // Create display driver (our custom GxEPD2-based driver) 105 - let epd = DisplayDriver::new(spi_dev, dc, rst, busy); 159 + // Create display device on the shared bus. 160 + let epd_spi = RefCellDevice::new(spi_ref, epd_cs, Delay::new()).unwrap(); 161 + let epd = DisplayDriver::new(epd_spi, dc, rst, busy); 106 162 107 - DisplayHw { epd } 163 + (DisplayHw { epd }, StorageHw { sd }) 108 164 } 109 165 }
+12 -1
src/board/pins.rs
··· 1 1 //! GPIO | Function | Notes 2 2 //! -----+-----------------+---------------------------------- 3 + //! 0 | ADC - Battery | Voltage divider (2x10K), reads 1/2 actual voltage 3 4 //! 1 | ADC1 - Button 2 | Resistance ladder: Right/Left/Confirm/Back 4 5 //! 2 | ADC2 - Button 1 | Resistance ladder: Volume Up/Down 5 6 //! 3 | Digital - Power | Active LOW, internal pullup 6 7 //! 4 | EPD DC | Data/Command select 7 8 //! 5 | EPD RST | Reset (active low) 8 9 //! 6 | EPD BUSY | Busy signal from display 10 + //! 7 | SPI2 MISO | SD card data out (display is write-only) 9 11 //! 8 | SPI2 SCK | Shared SPI clock 10 12 //! 10 | SPI2 MOSI | Shared SPI data out 13 + //! 12 | SD CS | SD card chip select 14 + //! 20 | USB detect | UART0_RXD, can detect USB connection 11 15 //! 21 | EPD CS | Display chip select 12 16 13 17 // ----- E-Paper Display ----- ··· 16 20 pub const EPD_RST: u8 = 5; 17 21 pub const EPD_BUSY: u8 = 6; 18 22 19 - // ----- SPI Bus ----- 23 + // ----- SD Card ----- 24 + pub const SD_CS: u8 = 12; 25 + 26 + // ----- SPI Bus (shared: EPD + SD) ----- 20 27 pub const SPI_SCK: u8 = 8; 21 28 pub const SPI_MOSI: u8 = 10; 29 + pub const SPI_MISO: u8 = 7; // SD card read; display doesn't use MISO 22 30 23 31 // ----- Buttons (ADC) ----- 24 32 pub const BTN_ROW1_ADC: u8 = 1; // GPIO1 - Right/Left/Confirm/Back ··· 26 34 27 35 // ----- Power Button ----- 28 36 pub const BTN_POWER: u8 = 3; // Digital, active LOW 37 + 38 + // ----- Battery ----- 39 + pub const BATTERY_ADC: u8 = 0; // GPIO0 - voltage divider, 1/2 of battery voltage
+73
src/board/raw_gpio.rs
··· 1 + //! Raw GPIO output for pins not exposed by esp-hal (e.g. flash pins on ESP32-C3). 2 + //! 3 + //! The XTEink X4 uses DIO flash mode, freeing GPIO12 (SPIHD) and GPIO13 (SPIWP) 4 + //! for general use. esp-hal 1.0 doesn't generate peripheral types for GPIO12-17 5 + //! on ESP32-C3, so we drive the pin via direct register writes. 6 + const GPIO_OUT_W1TS: u32 = 0x6000_4008; // Set output high (write-1-to-set) 7 + const GPIO_OUT_W1TC: u32 = 0x6000_400C; // Set output low (write-1-to-clear) 8 + const GPIO_ENABLE_W1TS: u32 = 0x6000_4024; // Enable output (write-1-to-set) 9 + const IO_MUX_BASE: u32 = 0x6000_9000; // IO_MUX register base 10 + const IO_MUX_PIN_STRIDE: u32 = 0x04; // Each pin has a 4-byte register 11 + 12 + // Minimal output-only GPIO driver using direct register access. 13 + pub struct RawOutputPin { 14 + mask: u32, // Bit mask for this pin (1 << pin_number) 15 + } 16 + 17 + impl RawOutputPin { 18 + // Configure a GPIO as push-pull output, initially HIGH. 19 + // 20 + // Safety: Caller must ensure 21 + // - The pin is physically available (not connected to active flash lines) 22 + // - No other driver is controlling the same pin 23 + pub unsafe fn new(pin: u8) -> Self { 24 + let mask = 1u32 << pin; 25 + 26 + // Configure IO_MUX: select GPIO function (function 1), enable output 27 + let mux_reg = (IO_MUX_BASE + pin as u32 * IO_MUX_PIN_STRIDE) as *mut u32; 28 + // Bits [14:12] = FUN_DRV (drive strength, default 2) 29 + // Bits [11:10] = 0 (no pull-up/down) 30 + // Bit [9] = FUN_IE (input enable) = 0 31 + // Bits [2:0] = MCU_SEL (function select) = 1 (GPIO) 32 + // 33 + // read-modify-write to preserve reserved bits, but set function to GPIO. 34 + let val = mux_reg.read_volatile(); 35 + let val = (val & !0b111) | 1; // MCU_SEL = 1 (GPIO function) 36 + mux_reg.write_volatile(val); 37 + 38 + // enable output for this pin 39 + // GPIO_FUNCn_OUT_SEL_CFG register (base 0x60004554, stride 4) 40 + let out_sel = (0x6000_4554 + pin as u32 * 4) as *mut u32; 41 + out_sel.write_volatile(0x80); // SIG_OUT = 128 (simple GPIO output) 42 + 43 + // Enable output 44 + (GPIO_ENABLE_W1TS as *mut u32).write_volatile(mask); 45 + 46 + // Drive HIGH initially (CS deasserted) 47 + (GPIO_OUT_W1TS as *mut u32).write_volatile(mask); 48 + 49 + Self { mask } 50 + } 51 + } 52 + 53 + impl embedded_hal::digital::ErrorType for RawOutputPin { 54 + type Error = core::convert::Infallible; 55 + } 56 + 57 + impl embedded_hal::digital::OutputPin for RawOutputPin { 58 + #[inline] 59 + fn set_high(&mut self) -> Result<(), Self::Error> { 60 + unsafe { 61 + (GPIO_OUT_W1TS as *mut u32).write_volatile(self.mask); 62 + } 63 + Ok(()) 64 + } 65 + 66 + #[inline] 67 + fn set_low(&mut self) -> Result<(), Self::Error> { 68 + unsafe { 69 + (GPIO_OUT_W1TC as *mut u32).write_volatile(self.mask); 70 + } 71 + Ok(()) 72 + } 73 + }
+58
src/board/sdcard.rs
··· 1 + //! SD Card support for XTEink X4 2 + //! 3 + //! The SD card shares the SPI2 bus with the e-paper display. 4 + //! Bus arbitration is handled at the board level using RefCellDevice. 5 + use embedded_sdmmc::{SdCard, TimeSource, Timestamp, VolumeManager}; 6 + use log::info; 7 + 8 + // Dummy time source for FAT timestamps (X4 has no RTC). 9 + #[derive(Default, Clone, Copy)] 10 + pub struct DummyTimeSource; 11 + 12 + impl TimeSource for DummyTimeSource { 13 + fn get_timestamp(&self) -> Timestamp { 14 + Timestamp { 15 + year_since_1970: 55, // 2025 16 + zero_indexed_month: 0, 17 + zero_indexed_day: 0, 18 + hours: 0, 19 + minutes: 0, 20 + seconds: 0, 21 + } 22 + } 23 + } 24 + 25 + // sd card initialization frequency (Hz). 26 + pub const SD_INIT_FREQ_HZ: u32 = 400_000; 27 + 28 + // Normal operating frequency after init 29 + // TODO: Put this somewhere else? 30 + pub const SD_NORMAL_FREQ_HZ: u32 = 20_000_000; 31 + 32 + // Wrapper that holds the SdCard + VolumeManager together. 33 + pub struct SdStorage<SPI> 34 + where 35 + SPI: embedded_hal::spi::SpiDevice, 36 + { 37 + pub volume_mgr: VolumeManager<SdCard<SPI, esp_hal::delay::Delay>, DummyTimeSource>, 38 + } 39 + 40 + impl<SPI> SdStorage<SPI> 41 + where 42 + SPI: embedded_hal::spi::SpiDevice, 43 + { 44 + // Create SD storage, probing the card during construction. 45 + pub fn new(spi: SPI) -> Self { 46 + let sdcard = SdCard::new(spi, esp_hal::delay::Delay::new()); 47 + 48 + // Probe card before handing ownership to VolumeManager. 49 + // This triggers the SD init sequence (CMD0, CMD8, ACMD41, etc). 50 + match sdcard.num_bytes() { 51 + Ok(bytes) => info!("SD card: {} bytes ({} MB)", bytes, bytes / 1024 / 1024), 52 + Err(e) => info!("SD card probe failed: {:?}", e), 53 + } 54 + 55 + let volume_mgr = VolumeManager::new(sdcard, DummyTimeSource); 56 + Self { volume_mgr } 57 + } 58 + }
+1
src/drivers/mod.rs
··· 1 1 pub mod input; 2 + pub mod storage; 2 3 // pub mod display_driver;
+144
src/drivers/storage.rs
··· 1 + //! High-level storage operations for reading files from SD card. 2 + use embedded_sdmmc::{Error, Mode, SdCardError, VolumeIdx}; 3 + 4 + use crate::board::sdcard::SdStorage; 5 + 6 + pub fn list_root_dir<SPI>( 7 + sd: &mut SdStorage<SPI>, 8 + mut f: impl FnMut(&str, u32, bool), 9 + ) -> Result<u32, Error<SdCardError>> 10 + where 11 + SPI: embedded_hal::spi::SpiDevice, 12 + { 13 + let volume = sd.volume_mgr.open_volume(VolumeIdx(0))?; 14 + let root = volume.open_root_dir()?; 15 + 16 + let mut count = 0u32; 17 + root.iterate_dir(|entry| { 18 + let name = entry.name.base_name(); 19 + let ext = entry.name.extension(); 20 + let is_dir = entry.attributes.is_directory(); 21 + let size = entry.size; 22 + 23 + // Format "NAME.EXT" into a stack buffer (8.3 = max 12 chars) 24 + let mut buf = [0u8; 13]; 25 + let mut pos = 0; 26 + 27 + for &b in name { 28 + if b == b' ' { 29 + break; 30 + } 31 + if pos < buf.len() { 32 + buf[pos] = b; 33 + pos += 1; 34 + } 35 + } 36 + 37 + if ext[0] != b' ' { 38 + if pos < buf.len() { 39 + buf[pos] = b'.'; 40 + pos += 1; 41 + } 42 + for &b in ext { 43 + if b == b' ' { 44 + break; 45 + } 46 + if pos < buf.len() { 47 + buf[pos] = b; 48 + pos += 1; 49 + } 50 + } 51 + } 52 + 53 + if let Ok(formatted) = core::str::from_utf8(&buf[..pos]) { 54 + f(formatted, size, is_dir); 55 + } 56 + 57 + count += 1; 58 + })?; 59 + 60 + Ok(count) 61 + } 62 + 63 + pub fn file_size<SPI>( 64 + sd: &mut SdStorage<SPI>, 65 + name: &str, 66 + ) -> Result<u32, Error<SdCardError>> 67 + where 68 + SPI: embedded_hal::spi::SpiDevice, 69 + { 70 + let volume = sd.volume_mgr.open_volume(VolumeIdx(0))?; 71 + let root = volume.open_root_dir()?; 72 + let file = root.open_file_in_dir(name, Mode::ReadOnly)?; 73 + Ok(file.length()) 74 + } 75 + 76 + /// Read an entire file into a buffer. Returns bytes read. 77 + pub fn read_file<SPI>( 78 + sd: &mut SdStorage<SPI>, 79 + name: &str, 80 + buf: &mut [u8], 81 + ) -> Result<usize, Error<SdCardError>> 82 + where 83 + SPI: embedded_hal::spi::SpiDevice, 84 + { 85 + let volume = sd.volume_mgr.open_volume(VolumeIdx(0))?; 86 + let root = volume.open_root_dir()?; 87 + let file = root.open_file_in_dir(name, Mode::ReadOnly)?; 88 + 89 + let mut total = 0; 90 + while !file.is_eof() && total < buf.len() { 91 + let n = file.read(&mut buf[total..])?; 92 + if n == 0 { 93 + break; 94 + } 95 + total += n; 96 + } 97 + 98 + Ok(total) 99 + } 100 + 101 + /// Read a chunk of a file starting at `offset`. Returns bytes read. 102 + pub fn read_file_chunk<SPI>( 103 + sd: &mut SdStorage<SPI>, 104 + name: &str, 105 + offset: u32, 106 + buf: &mut [u8], 107 + ) -> Result<usize, Error<SdCardError>> 108 + where 109 + SPI: embedded_hal::spi::SpiDevice, 110 + { 111 + let volume = sd.volume_mgr.open_volume(VolumeIdx(0))?; 112 + let root = volume.open_root_dir()?; 113 + let file = root.open_file_in_dir(name, Mode::ReadOnly)?; 114 + file.seek_from_start(offset)?; 115 + 116 + let mut total = 0; 117 + while !file.is_eof() && total < buf.len() { 118 + let n = file.read(&mut buf[total..])?; 119 + if n == 0 { 120 + break; 121 + } 122 + total += n; 123 + } 124 + 125 + Ok(total) 126 + } 127 + 128 + /// Write data to a file (create or truncate). 129 + /// File name must be in 8.3 format. 130 + pub fn write_file<SPI>( 131 + sd: &mut SdStorage<SPI>, 132 + name: &str, 133 + data: &[u8], 134 + ) -> Result<(), Error<SdCardError>> 135 + where 136 + SPI: embedded_hal::spi::SpiDevice, 137 + { 138 + let volume = sd.volume_mgr.open_volume(VolumeIdx(0))?; 139 + let root = volume.open_root_dir()?; 140 + let file = root.open_file_in_dir(name, Mode::ReadWriteCreateOrTruncate)?; 141 + file.write(data)?; 142 + file.flush()?; 143 + Ok(()) 144 + }