A custom OS for the xteink x4 ebook reader
at main 246 lines 9.3 kB view raw
1// board support for the XTEink X4 (ESP32-C3, SSD1677 800x480, SD over SPI2) 2// DMA-backed SPI (GDMA CH0); CriticalSectionDevice arbitrates bus 3 4pub mod action; 5pub mod battery; 6pub mod button; 7pub mod layout; 8pub mod raw_gpio; 9 10pub use crate::drivers::sdcard::{SdStorage, SyncSdCard}; 11pub use crate::drivers::ssd1677::{DisplayDriver, HEIGHT, SPI_FREQ_MHZ, WIDTH}; 12pub use crate::drivers::strip::StripBuffer; 13pub use button::{Button, ROW1_THRESHOLDS, ROW2_THRESHOLDS}; 14 15// logical screen size (portrait mode via 270-degree rotation of 800x480 panel) 16pub const SCREEN_W: u16 = HEIGHT; // 480 17pub const SCREEN_H: u16 = WIDTH; // 800 18 19use core::cell::RefCell; 20 21use critical_section::Mutex; 22use embedded_hal_bus::spi::CriticalSectionDevice; 23use esp_hal::{ 24 Blocking, 25 analog::adc::{Adc, AdcCalCurve, AdcConfig, AdcPin, Attenuation}, 26 delay::Delay, 27 dma::{DmaRxBuf, DmaTxBuf}, 28 gpio::{Event, Input, InputConfig, Io, Level, Output, OutputConfig, Pull}, 29 peripherals::{ADC1, GPIO0, GPIO1, GPIO2, Peripherals}, 30 spi, 31 time::Rate, 32}; 33use log::info; 34use static_cell::StaticCell; 35 36pub type SpiBus = spi::master::SpiDmaBus<'static, Blocking>; 37pub type SharedSpiDevice = CriticalSectionDevice<'static, SpiBus, Output<'static>, Delay>; 38pub type SdSpiDevice = CriticalSectionDevice<'static, SpiBus, raw_gpio::RawOutputPin, Delay>; 39pub type Epd = DisplayDriver<SharedSpiDevice, Output<'static>, Output<'static>, Input<'static>>; 40 41static SPI_BUS: StaticCell<Mutex<RefCell<SpiBus>>> = StaticCell::new(); 42 43// cached ref to the SPI bus mutex, set once in Board::init 44// cached ref to the SPI bus mutex; pub(crate) so scheduler can 45// access the bus in sd_card_sleep before deep sleep 46pub(crate) static SPI_BUS_REF: Mutex<core::cell::Cell<Option<&'static Mutex<RefCell<SpiBus>>>>> = 47 Mutex::new(core::cell::Cell::new(None)); 48 49// sd cs clone; only used in enter_sleep to send cmd0 50// safety: same clone_unchecked pattern as gpio0/1/2/3 in init_input; 51// only accessed after all normal sd i/o has stopped and before mcu halts 52pub(crate) static SD_CS_SLEEP: Mutex<RefCell<Option<raw_gpio::RawOutputPin>>> = 53 Mutex::new(RefCell::new(None)); 54 55static POWER_BTN: Mutex<RefCell<Option<Input<'static>>>> = Mutex::new(RefCell::new(None)); 56 57#[esp_hal::handler] 58fn gpio_handler() { 59 critical_section::with(|cs| { 60 if let Some(btn) = POWER_BTN.borrow_ref_mut(cs).as_mut() 61 && btn.is_interrupt_set() 62 { 63 btn.clear_interrupt(); 64 } 65 }); 66} 67 68pub fn power_button_is_low() -> bool { 69 critical_section::with(|cs| { 70 POWER_BTN 71 .borrow_ref_mut(cs) 72 .as_mut() 73 .map(|btn| btn.is_low()) 74 .unwrap_or(false) 75 }) 76} 77 78pub struct InputHw { 79 pub adc: Adc<'static, ADC1<'static>, Blocking>, 80 pub row1: AdcPin<GPIO1<'static>, ADC1<'static>, AdcCalCurve<ADC1<'static>>>, 81 pub row2: AdcPin<GPIO2<'static>, ADC1<'static>, AdcCalCurve<ADC1<'static>>>, 82 pub battery: AdcPin<GPIO0<'static>, ADC1<'static>, AdcCalCurve<ADC1<'static>>>, 83} 84 85pub struct DisplayHw { 86 pub epd: Epd, 87} 88 89pub struct StorageHw { 90 // sd card, initialised at 400 kHz before EPD touches the bus 91 pub sd_card: Option<SyncSdCard>, 92} 93 94pub struct Board { 95 pub input: InputHw, 96 pub display: DisplayHw, 97 pub storage: StorageHw, 98} 99 100impl Board { 101 pub fn init(p: Peripherals) -> Self { 102 let input = Self::init_input(&p); 103 let (display, storage) = Self::init_spi_peripherals(p); 104 Board { 105 input, 106 display, 107 storage, 108 } 109 } 110 111 // gpio / peripheral ownership: 112 // 113 // init_input (clone_unchecked) init_spi_peripherals (move/clone) 114 // --- --- 115 // GPIO0 battery ADC GPIO4 EPD DC 116 // GPIO1 button row 1 ADC GPIO5 EPD RST 117 // GPIO2 button row 2 ADC GPIO6 EPD BUSY 118 // GPIO3 power button GPIO7 SPI MISO 119 // ADC1 GPIO8 SPI SCK 120 // IO_MUX GPIO10 SPI MOSI 121 // GPIO12 SD CS (raw register) 122 // GPIO21 EPD CS 123 // SPI2, DMA_CH0 124 125 // Safety for all clone_unchecked calls below: 126 // 127 // init_input borrows Peripherals immutably and clones the pins it 128 // needs. init_spi_peripherals later takes ownership of the full 129 // Peripherals struct but only touches a disjoint set of GPIOs 130 // (GPIO4-8, GPIO10, GPIO21, SPI2, DMA_CH0). See the ownership 131 // table above for the complete split. Each peripheral listed here 132 // is used exclusively by InputHw and never touched again. 133 fn init_input(p: &Peripherals) -> InputHw { 134 let mut adc_cfg = AdcConfig::new(); 135 136 // Safety: GPIO1 is used only here (button row 1 ADC). 137 let row1 = adc_cfg.enable_pin_with_cal::<_, AdcCalCurve<ADC1>>( 138 unsafe { p.GPIO1.clone_unchecked() }, 139 Attenuation::_11dB, 140 ); 141 142 // Safety: GPIO2 is used only here (button row 2 ADC). 143 let row2 = adc_cfg.enable_pin_with_cal::<_, AdcCalCurve<ADC1>>( 144 unsafe { p.GPIO2.clone_unchecked() }, 145 Attenuation::_11dB, 146 ); 147 148 // Safety: GPIO0 is used only here (battery voltage ADC). 149 let battery = adc_cfg.enable_pin_with_cal::<_, AdcCalCurve<ADC1>>( 150 unsafe { p.GPIO0.clone_unchecked() }, 151 Attenuation::_11dB, 152 ); 153 154 // Safety: ADC1 is used only here; init_spi_peripherals does not use ADC. 155 let adc = Adc::new(unsafe { p.ADC1.clone_unchecked() }, adc_cfg); 156 157 // Safety: IO_MUX is used only here for the GPIO interrupt handler. 158 let mut io = Io::new(unsafe { p.IO_MUX.clone_unchecked() }); 159 io.set_interrupt_handler(gpio_handler); 160 161 // Safety: GPIO3 is used only here (power button input with IRQ). 162 let mut power = Input::new( 163 unsafe { p.GPIO3.clone_unchecked() }, 164 InputConfig::default().with_pull(Pull::Up), 165 ); 166 power.listen(Event::FallingEdge); 167 168 critical_section::with(|cs| { 169 POWER_BTN.borrow_ref_mut(cs).replace(power); 170 }); 171 info!("power button: GPIO3 interrupt armed (FallingEdge)"); 172 173 InputHw { 174 adc, 175 row1, 176 row2, 177 battery, 178 } 179 } 180 181 // 400 kHz for SD probe, then 20 MHz; DMA-backed 182 fn init_spi_peripherals(p: Peripherals) -> (DisplayHw, StorageHw) { 183 let epd_cs = Output::new(p.GPIO21, Level::High, OutputConfig::default()); 184 let dc = Output::new(p.GPIO4, Level::High, OutputConfig::default()); 185 let rst = Output::new(p.GPIO5, Level::High, OutputConfig::default()); 186 let busy = Input::new(p.GPIO6, InputConfig::default().with_pull(Pull::None)); 187 188 // GPIO12 free in DIO mode; no esp-hal type, use raw registers 189 let sd_cs = unsafe { raw_gpio::RawOutputPin::new(12) }; 190 191 // second handle to GPIO12 for sending cmd0 before deep sleep 192 let sd_cs_sleep = unsafe { raw_gpio::RawOutputPin::new(12) }; 193 critical_section::with(|cs| { 194 SD_CS_SLEEP.borrow_ref_mut(cs).replace(sd_cs_sleep); 195 }); 196 197 let slow_cfg = spi::master::Config::default().with_frequency(Rate::from_khz(400)); 198 199 let mut spi_raw = spi::master::Spi::new(p.SPI2, slow_cfg) 200 .unwrap() 201 .with_sck(p.GPIO8) 202 .with_mosi(p.GPIO10) 203 .with_miso(p.GPIO7); 204 205 // 80 clocks with CS high before DMA conversion (SD spec init) 206 let _ = spi_raw.write(&[0xFF; 10]); 207 208 // 4096B each direction: strip max ~4000B, SD sectors 512B 209 let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = esp_hal::dma_buffers!(4096); 210 let dma_rx_buf = DmaRxBuf::new(rx_descriptors, rx_buffer).unwrap(); 211 let dma_tx_buf = DmaTxBuf::new(tx_descriptors, tx_buffer).unwrap(); 212 213 let spi_dma_bus = spi_raw 214 .with_dma(p.DMA_CH0) 215 .with_buffers(dma_rx_buf, dma_tx_buf); 216 217 let spi_ref: &'static Mutex<RefCell<SpiBus>> = 218 SPI_BUS.init(Mutex::new(RefCell::new(spi_dma_bus))); 219 info!("SPI bus: DMA enabled (CH0, 4096B TX+RX)"); 220 221 critical_section::with(|cs| SPI_BUS_REF.borrow(cs).set(Some(spi_ref))); 222 223 let sd_spi = CriticalSectionDevice::new(spi_ref, sd_cs, Delay::new()).unwrap(); 224 225 // init SD card now, at 400 kHz on a pristine bus, before EPD 226 // traffic -- SD spec requires CMD0 on a clean bus 227 let sd_card = SdStorage::init_card(sd_spi); 228 229 let epd_spi = CriticalSectionDevice::new(spi_ref, epd_cs, Delay::new()).unwrap(); 230 let epd = DisplayDriver::new(epd_spi, dc, rst, busy); 231 232 (DisplayHw { epd }, StorageHw { sd_card }) 233 } 234} 235 236// switch SPI bus from 400 kHz to operational frequency (20 MHz) 237// call after Board::init and before first EPD render 238pub fn speed_up_spi() { 239 let fast_cfg = spi::master::Config::default().with_frequency(Rate::from_mhz(SPI_FREQ_MHZ)); 240 critical_section::with(|cs| { 241 if let Some(bus) = SPI_BUS_REF.borrow(cs).get() { 242 bus.borrow(cs).borrow_mut().apply_config(&fast_cfg).unwrap(); 243 info!("SPI bus: 400kHz -> {}MHz", SPI_FREQ_MHZ); 244 } 245 }); 246}