A custom OS for the xteink x4 ebook reader
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}