···11-//! XTEink X4 BSP
22-//!
33-//! This module is to map the X4's physical hardware to named subsystems.
44-//! All the pin assignments, bus configs, and calibration consts are here.
55-//! Goal: have no need for another part of pulp-os to know GPIO
66-//!
77-//! Pin Map:
88-//! GPIO | Function | Notes
99-//! 1 | ADC1 - Button 2 | Resistance ladder button for Right/Left/Confirm/Back
1010-//! 2 | ADC2 - Button 1 | Resistance ladder (for consistency): Volume Up/Down
1111-//! 3 | Digital - Power | Active LOW, internal pullup
1212-//! 5 | EPD RST | Reset (active low)
1313-//! 6 | EPD BUSY | Busy signal from display
1414-//! 8 | SPI2 SCK | Shared SPI clock
1515-//! 10 | SPI2 MOSI | Shared SPI data out
1616-//! 21 | EPD CS | Display chip select
1717-1818-use esp_hal::{
1919- analog::adc::{ Adc, AdcCalCurve, AdcPin, AdcConfig, Attenuation},
2020- gpio::{Output, Input, Pull, InputConfig, OutputConfig, Level },
2121- peripherals::{ Peripherals, ADC1, GPIO1, GPIO2 },
2222- time::Rate,
2323- delay::Delay,
2424- spi,
2525- Blocking,
2626-};
2727-use embedded_hal_bus::spi::ExclusiveDevice;
2828-use ssd1677::{Interface, Display, Rotation, Builder, Dimensions};
2929-3030-// Display
3131-pub const DISPLAY_WIDTH: u16 = 800;
3232-pub const DISPLAY_HEIGHT: u16 = 480;
3333-3434-pub const FB_SIZE: usize = (DISPLAY_WIDTH as usize * DISPLAY_HEIGHT as usize) / 8;
3535-3636-// SPI clock rate for the epaper dispaly
3737-pub const EPD_SPI_FREQ: u32 = 10;
3838-3939-// Buttons
4040-pub const BTN_TOLERANCE: u16 = 150;
4141-4242-// Calibrated(?) tolerance band for the resistence ladder btns
4343-pub const ROW1_THRESHOLDS: &[(u16, u16, Button)] = &[
4444- // center (mV), move, button
4545- (3, 50, Button::Right),
4646- (1113, BTN_TOLERANCE, Button::Left),
4747- (1984, BTN_TOLERANCE, Button::Back),
4848- (2556, BTN_TOLERANCE, Button::Confirm),
4949-];
5050-5151-pub const ROW2_THRESHOLDS: &[(u16, u16, Button)] = &[
5252- (3, 50, Button::VolDown),
5353- (1659, BTN_TOLERANCE, Button::VolUp),
5454-];
5555-5656-pub type SpiBus = spi::master::Spi<'static, Blocking>;
5757-5858-pub type SpiDev = ExclusiveDevice<SpiBus, Output<'static>, Delay>;
5959-6060-pub type EpdInterface = Interface<SpiDev, Output<'static>, Output<'static>, Input<'static>>;
6161-6262-pub type Epd = Display<EpdInterface>;
6363-6464-pub type EpdCs = Output<'static>;
6565-pub type EpdDc = Output<'static>;
6666-pub type EpdRst = Output<'static>;
6767-pub type EpdBusy = Input<'static>;
6868-pub type PowerButton = Input<'static>;
6969-7070-// pub type AdcRow1 = AdcPin<P, ADC1<'static>, AdcCalCurve<ADC1<'static>>>;
7171-// pub type AdcRow2 = AdcPin<P, ADC1<'static>, AdcCalCurve<ADC1<'static>>>;
7272-7373-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7474-pub enum Button {
7575- Right,
7676- Left,
7777- Confirm,
7878- Back,
7979- VolUp,
8080- VolDown,
8181- Power,
8282-}
8383-8484-impl Button {
8585- pub fn name(self) -> &'static str {
8686- match self {
8787- Button::Right => "Right",
8888- Button::Left => "Left",
8989- Button::Confirm => "Confirm",
9090- Button::Back => "Back",
9191- Button::VolUp => "Vol Up",
9292- Button::VolDown => "Vol Down",
9393- Button::Power => "Power",
9494- }
9595- }
9696-}
9797-9898-impl core::fmt::Display for Button {
9999- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
100100- f.write_str(self.name())
101101- }
102102-}
103103-104104-pub struct InputHw {
105105- pub adc: Adc<'static, ADC1<'static>, Blocking>,
106106- pub row1: AdcPin<GPIO1<'static>, ADC1<'static>, AdcCalCurve<ADC1<'static>>>,
107107- pub row2: AdcPin<GPIO2<'static>, ADC1<'static>, AdcCalCurve<ADC1<'static>>>,
108108- pub power: Input<'static>,
109109-}
110110-111111-pub struct DisplayHw {
112112- pub epd: Epd, // type alias chain from before
113113-}
114114-115115-pub struct Board {
116116- pub input: InputHw,
117117- pub display: DisplayHw,
118118-}
119119-120120-impl Board{
121121- pub fn init(p: Peripherals) -> Self {
122122- let mut adc_cfg = AdcConfig::new();
123123-124124- let row1 = adc_cfg.enable_pin_with_cal::<_, AdcCalCurve<ADC1>>(
125125- p.GPIO1,
126126- Attenuation::_11dB,
127127- );
128128-129129- let row2 = adc_cfg.enable_pin_with_cal::<_, AdcCalCurve<ADC1>>(
130130- p.GPIO2,
131131- Attenuation::_11dB,
132132- );
133133-134134- let adc = Adc::new(p.ADC1, adc_cfg);
135135-136136- let power = Input::new(p.GPIO3, InputConfig::default().with_pull(Pull::Up));
137137-138138- let input = InputHw { adc, row1, row2, power };
139139-140140- // Display: SPI bus + EPD
141141- let cs = Output::new(p.GPIO21,Level::High, OutputConfig::default());
142142- let dc= Output::new(p.GPIO4,Level::High, OutputConfig::default());
143143- let rst= Output::new(p.GPIO5,Level::High, OutputConfig::default());
144144- let busy= Input::new(p.GPIO6, InputConfig::default().with_pull(Pull::None));
145145-146146- let spi_cfg = spi::master::Config::default().with_frequency(Rate::from_mhz(EPD_SPI_FREQ));
147147- let spi_bus = spi::master::Spi::new(p.SPI2, spi_cfg).unwrap().with_sck(p.GPIO8).with_mosi(p.GPIO10);
148148-149149- let spi_dev = ExclusiveDevice::new(spi_bus, cs, Delay::new()).unwrap();
150150-151151- let interface = Interface::new(spi_dev, dc, rst, busy);
152152-153153- let dims = Dimensions::new(DISPLAY_HEIGHT, DISPLAY_WIDTH).unwrap();
154154- let cfg = Builder::new()
155155- .dimensions(dims)
156156- .rotation(Rotation::Rotate270)
157157- .build()
158158- .unwrap();
159159-160160- let mut epd = Display::new(interface, cfg);
161161- epd.reset(&mut Delay::new());
162162-163163- let display = DisplayHw { epd };
164164-165165- Board { input, display }
166166- }
167167-}
168168-169169-170170-/// Decode a millivolt reading against a threshold table.
171171-/// Used by the input subsystem to map ADC readings to buttons.
172172-pub fn decode_ladder(mv: u16, thresholds: &[(u16, u16, Button)]) -> Option<Button> {
173173- for &(center, tol, button) in thresholds {
174174- if mv >= center.saturating_sub(tol) && mv <= center.saturating_add(tol) {
175175- return Some(button);
176176- }
177177- }
178178- None
179179-}
+69
src/board/button.rs
···11+//! Button definitions and ADC decoding for XTEink X4
22+//!
33+//! The X4 uses resistance ladder circuits for most buttons.
44+//! Each ladder is read via ADC and decoded by comparing the
55+//! millivolt reading against known thresholds.
66+77+/// All physical buttons on the device.
88+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99+pub enum Button {
1010+ // Navigation cluster (Row 1 - GPIO1)
1111+ Right,
1212+ Left,
1313+ Confirm,
1414+ Back,
1515+ // Volume buttons (Row 2 - GPIO2)
1616+ VolUp,
1717+ VolDown,
1818+ // Discrete digital button
1919+ Power,
2020+}
2121+2222+impl Button {
2323+ pub const fn name(self) -> &'static str {
2424+ match self {
2525+ Button::Right => "Right",
2626+ Button::Left => "Left",
2727+ Button::Confirm => "Confirm",
2828+ Button::Back => "Back",
2929+ Button::VolUp => "Vol Up",
3030+ Button::VolDown => "Vol Down",
3131+ Button::Power => "Power",
3232+ }
3333+ }
3434+}
3535+3636+impl core::fmt::Display for Button {
3737+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3838+ f.write_str(self.name())
3939+ }
4040+}
4141+4242+// ADC Threshold Tables
4343+// Each entry: (center_mv, tolerance_mv, button)
4444+// A reading matches if: center - tolerance <= reading <= center + tolerance
4545+pub const DEFAULT_TOLERANCE: u16 = 150;
4646+4747+pub const ROW1_THRESHOLDS: &[(u16, u16, Button)] = &[
4848+ (3, 50, Button::Right), // Near ground
4949+ (1113, DEFAULT_TOLERANCE, Button::Left),
5050+ (1984, DEFAULT_TOLERANCE, Button::Back),
5151+ (2556, DEFAULT_TOLERANCE, Button::Confirm),
5252+];
5353+5454+pub const ROW2_THRESHOLDS: &[(u16, u16, Button)] = &[
5555+ (3, 50, Button::VolDown), // Near ground
5656+ (1659, DEFAULT_TOLERANCE, Button::VolUp),
5757+];
5858+5959+pub fn decode_ladder(mv: u16, thresholds: &[(u16, u16, Button)]) -> Option<Button> {
6060+ for &(center, tolerance, button) in thresholds {
6161+ let low = center.saturating_sub(tolerance);
6262+ let high = center.saturating_add(tolerance);
6363+ if mv >= low && mv <= high {
6464+ return Some(button);
6565+ }
6666+ }
6767+ None
6868+}
6969+
+12
src/board/display.rs
···11+//! Display hardware constants for XTEink X4
22+//!
33+//! The X4 uses an 800x480 e-paper display driven by an SSD1677 controller.
44+55+pub const WIDTH: u16 = 800;
66+pub const HEIGHT: u16 = 480;
77+88+/// Framebuffer size in bytes (1 bit per pixel, packed).
99+pub const FRAMEBUFFER_SIZE: usize = (WIDTH as usize * HEIGHT as usize) / 8;
1010+1111+/// The SSD1677 typically supports up to 20MHz, but 10MHz seems fine for now
1212+pub const SPI_FREQ_MHZ: u32 = 10;
+119
src/board/mod.rs
···11+//! XTEink X4 Board Support Package (BSP)
22+//!
33+//! This module provides hardware abstraction for the XTEink X4 e-reader.
44+//! It maps physical hardware to named subsystems so that application code
55+//! doesn't need to know GPIO numbers or peripheral details.
66+77+pub mod button;
88+pub mod display;
99+pub mod pins;
1010+1111+pub use button::{decode_ladder, Button, ROW1_THRESHOLDS, ROW2_THRESHOLDS};
1212+1313+use embedded_hal_bus::spi::ExclusiveDevice;
1414+use esp_hal::{
1515+ analog::adc::{Adc, AdcCalCurve, AdcConfig, AdcPin, Attenuation},
1616+ delay::Delay,
1717+ gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull},
1818+ peripherals::{Peripherals, ADC1, GPIO1, GPIO2},
1919+ spi,
2020+ time::Rate,
2121+ Blocking,
2222+};
2323+use ssd1677::{Builder, Dimensions, Display, Interface, Rotation};
2424+2525+// Type Aliases
2626+pub type SpiBus = spi::master::Spi<'static, Blocking>;
2727+pub type SpiDevice = ExclusiveDevice<SpiBus, Output<'static>, Delay>;
2828+pub type EpdInterface = Interface<SpiDevice, Output<'static>, Output<'static>, Input<'static>>;
2929+pub type Epd = Display<EpdInterface>;
3030+3131+// Hardware Bundles
3232+/// Input subsystem hardware: ADC for button ladders + power button.
3333+pub struct InputHw {
3434+ pub adc: Adc<'static, ADC1<'static>, Blocking>,
3535+ pub row1: AdcPin<GPIO1<'static>, ADC1<'static>, AdcCalCurve<ADC1<'static>>>,
3636+ pub row2: AdcPin<GPIO2<'static>, ADC1<'static>, AdcCalCurve<ADC1<'static>>>,
3737+ pub power: Input<'static>,
3838+}
3939+4040+/// Display subsystem hardware: initialized e-paper display.
4141+pub struct DisplayHw {
4242+ pub epd: Epd,
4343+}
4444+4545+/// Complete board hardware, ready for driver initialization.
4646+pub struct Board {
4747+ pub input: InputHw,
4848+ pub display: DisplayHw,
4949+}
5050+5151+impl Board {
5252+ pub fn init(p: Peripherals) -> Self {
5353+ let input = Self::init_input(&p);
5454+ let display = Self::init_display(p);
5555+ Board { input, display }
5656+ }
5757+5858+ fn init_input(p: &Peripherals) -> InputHw {
5959+ let mut adc_cfg = AdcConfig::new();
6060+6161+ // Configure both ADC channels with 11dB attenuation for full 0-3.3V range
6262+ let row1 = adc_cfg.enable_pin_with_cal::<_, AdcCalCurve<ADC1>>(
6363+ unsafe { p.GPIO1.clone_unchecked() },
6464+ Attenuation::_11dB,
6565+ );
6666+6767+ let row2 = adc_cfg.enable_pin_with_cal::<_, AdcCalCurve<ADC1>>(
6868+ unsafe { p.GPIO2.clone_unchecked() },
6969+ Attenuation::_11dB,
7070+ );
7171+7272+ let adc = Adc::new(unsafe { p.ADC1.clone_unchecked() }, adc_cfg);
7373+7474+ let power = Input::new(
7575+ unsafe { p.GPIO3.clone_unchecked() },
7676+ InputConfig::default().with_pull(Pull::Up),
7777+ );
7878+7979+ InputHw {
8080+ adc,
8181+ row1,
8282+ row2,
8383+ power,
8484+ }
8585+ }
8686+8787+ fn init_display(p: Peripherals) -> DisplayHw {
8888+ // GPIO setup
8989+ let cs = Output::new(p.GPIO21, Level::High, OutputConfig::default());
9090+ let dc = Output::new(p.GPIO4, Level::High, OutputConfig::default());
9191+ let rst = Output::new(p.GPIO5, Level::High, OutputConfig::default());
9292+ let busy = Input::new(p.GPIO6, InputConfig::default().with_pull(Pull::None));
9393+9494+ // SPI bus
9595+ let spi_cfg =
9696+ spi::master::Config::default().with_frequency(Rate::from_mhz(display::SPI_FREQ_MHZ));
9797+ let spi_bus = spi::master::Spi::new(p.SPI2, spi_cfg)
9898+ .unwrap()
9999+ .with_sck(p.GPIO8)
100100+ .with_mosi(p.GPIO10);
101101+102102+ let spi_dev = ExclusiveDevice::new(spi_bus, cs, Delay::new()).unwrap();
103103+104104+ // Display controller
105105+ let interface = Interface::new(spi_dev, dc, rst, busy);
106106+107107+ let dims = Dimensions::new(display::HEIGHT, display::WIDTH).unwrap();
108108+ let cfg = Builder::new()
109109+ .dimensions(dims)
110110+ .rotation(Rotation::Rotate270)
111111+ .build()
112112+ .unwrap();
113113+114114+ let mut epd = Display::new(interface, cfg);
115115+ epd.reset(&mut Delay::new());
116116+117117+ DisplayHw { epd }
118118+ }
119119+}
···11-//! Input event driver
22-//!
33-//! The XTEink X4 has three physical input sources that all funnel
44-//! into a single "one button at a time" model:
55-//! Because each resistance ladder can only report one press at a
66-//! time, and the power button is digital, we collapse everything
77-//! into `Option<Button>` per poll cycle.
88-//!
99-1010-use esp_hal::time::{Duration, Instant};
1111-1212-use crate::board::{self, Button, InputHw, ROW1_THRESHOLDS, ROW2_THRESHOLDS};
1313-1414-const DEBOUNCE_MS: u64 = 30;
1515-const LONG_PRESS_MS: u64 = 600;
1616-const REPEAT_MS: u64 = 150;
1717-1818-/// Events are returned one at a time from InputDriver::Poll
1919-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2020-pub enum Event {
2121- Press(Button),
2222- Release(Button),
2323- LongPress(Button),
2424- Repeat(Button),
2525-}
2626-2727-struct EventQueue {
2828- buf: [Option<Event>; 2],
2929- read: u8,
3030-}
3131-3232-impl EventQueue {
3333- const fn new() -> Self {
3434- Self {
3535- buf: [None; 2],
3636- read: 0,
3737- }
3838- }
3939-4040- fn push(&mut self, ev: Event) {
4141- for slot in self.buf.iter_mut() {
4242- if slot.is_none() {
4343- *slot = Some(ev);
4444- return;
4545- }
4646- }
4747- // NOTE: If both slots are full something is wrong with our logic,
4848- // silently dropping is safer than panic.
4949- }
5050-5151- fn pop(&mut self) -> Option<Event> {
5252- if (self.read as usize) < self.buf.len() {
5353- let idx = self.read as usize;
5454- if let Some(ev) = self.buf[idx].take() {
5555- self.read += 1;
5656- return Some(ev);
5757- }
5858- }
5959- // Reset for next cycle.
6060- self.read = 0;
6161- None
6262- }
6363-6464- fn is_empty(&self) -> bool {
6565- self.buf.iter().all(|s| s.is_none())
6666- }
6767-}
6868-6969-pub struct InputDriver {
7070- hw: InputHw,
7171- stable: Option<Button>,
7272- candidate: Option<Button>,
7373- candidate_since: Instant,
7474- press_since: Instant,
7575- long_press_fired: bool,
7676- last_repeat: Instant,
7777- queue: EventQueue,
7878-}
7979-8080-impl InputDriver {
8181- pub fn new(hw: InputHw) -> Self {
8282- let now = Instant::now();
8383- Self {
8484- hw,
8585- stable: None,
8686- candidate: None,
8787- candidate_since: now,
8888- press_since: now,
8989- long_press_fired: false,
9090- last_repeat: now,
9191- queue: EventQueue::new(),
9292- }
9393- }
9494-9595- /// Poll for the next input event.
9696- pub fn poll(&mut self) -> Option<Event> {
9797- if !self.queue.is_empty() {
9898- return self.queue.pop();
9999- }
100100-101101- let raw = self.read_raw();
102102- let now = Instant::now();
103103-104104- if raw != self.candidate {
105105- self.candidate = raw;
106106- self.candidate_since = now;
107107- }
108108-109109- let debounced = if now - self.candidate_since >= Duration::from_millis(DEBOUNCE_MS) {
110110- self.candidate
111111- } else {
112112- self.stable
113113- };
114114-115115- if debounced != self.stable {
116116- if let Some(old) = self.stable {
117117- self.queue.push(Event::Release(old));
118118- }
119119- if let Some(new) = debounced {
120120- self.queue.push(Event::Press(new));
121121- self.press_since = now;
122122- self.long_press_fired = false;
123123- self.last_repeat = now;
124124- }
125125- self.stable = debounced;
126126- return self.queue.pop();
127127- }
128128-129129- if let Some(btn) = self.stable {
130130- let held = now - self.press_since;
131131-132132- if !self.long_press_fired && held >= Duration::from_millis(LONG_PRESS_MS) {
133133- self.long_press_fired = true;
134134- self.last_repeat = now;
135135- return Some(Event::LongPress(btn));
136136- }
137137-138138- if self.long_press_fired && (now - self.last_repeat) >= Duration::from_millis(REPEAT_MS)
139139- {
140140- self.last_repeat = now;
141141- return Some(Event::Repeat(btn));
142142- }
143143- }
144144-145145- None
146146- }
147147-148148- fn read_raw(&mut self) -> Option<Button> {
149149- if self.hw.power.is_low() {
150150- return Some(Button::Power);
151151- }
152152-153153- let mv1: u16 = nb::block!(self.hw.adc.read_oneshot(&mut self.hw.row1)).unwrap();
154154- let mv2: u16 = nb::block!(self.hw.adc.read_oneshot(&mut self.hw.row2)).unwrap();
155155-156156- board::decode_ladder(mv1, ROW1_THRESHOLDS)
157157- .or_else(|| board::decode_ladder(mv2, ROW2_THRESHOLDS))
158158- }
159159-}
+4-3
src/lib.rs
···11#![no_std]
2233-pub mod board;
44-pub mod input;
55-pub mod display;
33+// pub mod input;
44+pub mod board;
55+pub mod drivers;
66+// pub mod display;