From 94951762919bffe9f7f0e5e29c55a6aff3b0b476 Mon Sep 17 00:00:00 2001 From: Claas Date: Sun, 25 Jan 2026 20:25:50 +0100 Subject: [PATCH] Add broken code for display initialization --- Cargo.lock | 49 ++++---- Cargo.toml | 7 +- README.md | 1 + src/bin/main.rs | 71 ------------ src/eink_display.rs | 277 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 - src/main.rs | 105 +++++++++++++++++ 7 files changed, 412 insertions(+), 99 deletions(-) delete mode 100644 src/bin/main.rs create mode 100644 src/eink_display.rs delete mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/Cargo.lock b/Cargo.lock index f9ff025..505ab02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,31 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crustpoint" +version = "0.1.0" +dependencies = [ + "bt-hci", + "critical-section", + "defmt 1.0.1", + "embassy-executor", + "embassy-net", + "embassy-time", + "embedded-hal 1.0.0", + "embedded-io 0.7.1", + "embedded-io-async 0.7.0", + "esp-alloc", + "esp-backtrace", + "esp-bootloader-esp-idf", + "esp-hal", + "esp-println", + "esp-radio", + "esp-rtos", + "smoltcp", + "static_cell", + "trouble-host", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -1231,30 +1256,6 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" -[[package]] -name = "my-esp-project" -version = "0.1.0" -dependencies = [ - "bt-hci", - "critical-section", - "defmt 1.0.1", - "embassy-executor", - "embassy-net", - "embassy-time", - "embedded-io 0.7.1", - "embedded-io-async 0.7.0", - "esp-alloc", - "esp-backtrace", - "esp-bootloader-esp-idf", - "esp-hal", - "esp-println", - "esp-radio", - "esp-rtos", - "smoltcp", - "static_cell", - "trouble-host", -] - [[package]] name = "nb" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index cd473df..cbbb43f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] edition = "2024" -name = "my-esp-project" +name = "crustpoint" rust-version = "1.88" version = "0.1.0" [[bin]] -name = "my-esp-project" -path = "./src/bin/main.rs" +name = "crustpoint" +path = "./src/main.rs" [dependencies] esp-hal = { version = "~1.0", features = ["defmt", "esp32c3", "unstable"] } @@ -69,6 +69,7 @@ trouble-host = { version = "0.5.0", features = ["gatt"] } critical-section = "1.2.0" static_cell = "2.1.1" +embedded-hal = "1.0.0" [profile.dev] diff --git a/README.md b/README.md index d8925f4..a936117 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Experimenting with the xteink 4 ereader and rust - Repositories - [CrossPoint Reader](https://github.com/daveallie/crosspoint-reader) - [Xteink X4 Sample](https://github.com/CidVonHighwind/xteink-x4-sample) + - [Ariel-OS fork x4 example](https://github.com/juicecultus/ariel-os/blob/a2816b156b632b2633801df45d69c8fe9dde500c/examples/x4-launcher/src/ssd1677.rs) - Tools - [Xteink Flash Tools](https://xteink.dve.al/) - [ESPConnect](https://thelastoutpostworkshop.github.io/ESPConnect/) diff --git a/src/bin/main.rs b/src/bin/main.rs deleted file mode 100644 index 22e216e..0000000 --- a/src/bin/main.rs +++ /dev/null @@ -1,71 +0,0 @@ -#![no_std] -#![no_main] -#![deny( - clippy::mem_forget, - reason = "mem::forget is generally not safe to do with esp_hal types, especially those \ - holding buffers for the duration of a data transfer." -)] -#![deny(clippy::large_stack_frames)] - -use bt_hci::controller::ExternalController; -use defmt::info; -use embassy_executor::Spawner; -use embassy_time::{Duration, Timer}; -use esp_hal::clock::CpuClock; -use esp_hal::timer::timg::TimerGroup; -use esp_radio::ble::controller::BleConnector; -use trouble_host::prelude::*; -use {esp_backtrace as _, esp_println as _}; - -extern crate alloc; - -const CONNECTIONS_MAX: usize = 1; -const L2CAP_CHANNELS_MAX: usize = 1; - -// This creates a default app-descriptor required by the esp-idf bootloader. -// For more information see: -esp_bootloader_esp_idf::esp_app_desc!(); - -#[allow( - clippy::large_stack_frames, - reason = "it's not unusual to allocate larger buffers etc. in main" -)] -#[esp_rtos::main] -async fn main(spawner: Spawner) -> ! { - // generator version: 1.2.0 - - let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); - let peripherals = esp_hal::init(config); - - esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 66320); - // COEX needs more RAM - so we've added some more - esp_alloc::heap_allocator!(size: 64 * 1024); - - let timg0 = TimerGroup::new(peripherals.TIMG0); - let sw_interrupt = - esp_hal::interrupt::software::SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); - esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0); - - info!("Embassy initialized!"); - - let radio_init = esp_radio::init().expect("Failed to initialize Wi-Fi/BLE controller"); - let (mut _wifi_controller, _interfaces) = - esp_radio::wifi::new(&radio_init, peripherals.WIFI, Default::default()) - .expect("Failed to initialize Wi-Fi controller"); - // find more examples https://github.com/embassy-rs/trouble/tree/main/examples/esp32 - let transport = BleConnector::new(&radio_init, peripherals.BT, Default::default()).unwrap(); - let ble_controller = ExternalController::<_, 1>::new(transport); - let mut resources: HostResources = - HostResources::new(); - let _stack = trouble_host::new(ble_controller, &mut resources); - - // TODO: Spawn some tasks - let _ = spawner; - - loop { - info!("Hello world!"); - Timer::after(Duration::from_secs(1)).await; - } - - // for inspiration have a look at the examples at https://github.com/esp-rs/esp-hal/tree/esp-hal-v1.0.0/examples -} diff --git a/src/eink_display.rs b/src/eink_display.rs new file mode 100644 index 0000000..3496acd --- /dev/null +++ b/src/eink_display.rs @@ -0,0 +1,277 @@ +use bt_hci::cmd::info; +use defmt::{error, info}; +use embassy_time::{Duration, TimeoutError, Timer, with_timeout}; +use esp_hal::{ + Async, + dma::{DmaChannelFor, DmaRxBuf, DmaTxBuf}, + dma_buffers, + gpio::{ + Input, InputConfig, InputPin, Level, Output, OutputConfig, OutputPin, + interconnect::PeripheralOutput, + }, + spi::master::{AnySpi, Config, Instance, Spi, SpiDmaBus}, + time::Rate, +}; + +const DISPLAY_WIDTH: u16 = 800; +const DISPLAY_HEIGHT: u16 = 480; +const DISPLAY_WIDTH_BYTES: usize = { + // There is no div_exact yet + assert!( + DISPLAY_WIDTH % 8 == 0, + "Display width must be a multiple of 8" + ); + + DISPLAY_WIDTH.strict_div(8) as usize +}; + +const BUFFER_SIZE: usize = DISPLAY_WIDTH_BYTES.strict_mul(DISPLAY_HEIGHT as usize); + +#[derive(Debug, defmt::Format)] +#[repr(u8)] +enum Command { + // Initialization and reset + SoftReset = 0x12, + TemperatureSensorControl = 0x18, + BoosterSoftStart = 0x0C, + DriverOutputControl = 0x01, + BorderWaveformControl = 0x3C, + + // RAM and buffer management + DataEntryMode = 0x11, + SetRamXRange = 0x44, + SetRamYRange = 0x45, + SetRamXCounter = 0x4E, + SetRamYCounter = 0x4F, + AutoWriteBwRam = 0x46, +} + +pub(super) struct EinkDisplay<'d> { + spi: SpiDmaBus<'d, Async>, + reset: Output<'d>, + data_command: Output<'d>, + busy: Input<'d>, +} + +impl<'d> EinkDisplay<'d> { + fn new( + spi: impl Instance + 'd, + serial_clock: impl PeripheralOutput<'d>, + master_in_slave_out: impl PeripheralOutput<'d>, + chip_select: impl PeripheralOutput<'d>, + direct_memory_access_channel: impl DmaChannelFor>, + reset: impl OutputPin + 'd, + data_command: impl OutputPin + 'd, + busy: impl InputPin + 'd, + ) -> Self { + // DMA = Direct Memory Access + let (receive_buffer, receive_descriptor, transmit_buffer, transmit_descriptors) = + dma_buffers!(BUFFER_SIZE); + let direct_memory_access_receive_buffer = DmaRxBuf::new(receive_descriptor, receive_buffer) + .expect("Expected direct memory access (DMA) receive channel buffer to be created"); + let direct_memory_access_transmit_buffer = + DmaTxBuf::new(transmit_descriptors, transmit_buffer).expect( + "Expected direct memory access (DMA) transmit channel buffer to be created", + ); + + // Initialize SPI with custom pins + let spi = Spi::new( + spi, + Config::default() + .with_frequency(Rate::from_mhz(40)) + .with_mode(esp_hal::spi::Mode::_0) + .with_read_bit_order(esp_hal::spi::BitOrder::MsbFirst), // .with_write_bit_order(esp_hal::spi::BitOrder::MsbFirst) + ) + .expect("Failed to create SPI bus") + .with_sck(serial_clock) + .with_mosi(master_in_slave_out) + // .with_miso(todo!("Not defined in XteinkX4 screen spec")) + .with_cs(chip_select) + .with_dma(direct_memory_access_channel) + .with_buffers( + direct_memory_access_receive_buffer, + direct_memory_access_transmit_buffer, + ) + .into_async(); + + // Set up GPIO pins + let reset = Output::new(reset, Level::Low, OutputConfig::default()); + let data_command = Output::new(data_command, Level::High, OutputConfig::default()); + let busy = Input::new(busy, InputConfig::default()); + + Self { + spi, + reset, + data_command, + busy, + } + } + + async fn reset(&mut self) { + info!("Resetting display"); + self.reset.set_high(); + // Might need to be blocking if it needs to be the exact time + Timer::after_millis(20).await; + self.reset.set_low(); + Timer::after_millis(2).await; + self.reset.set_high(); + Timer::after_millis(20).await; + info!("Display reset completed"); + } + + async fn send_command(&mut self, command: Command) { + info!("Sending command: {:?}", command); + // Set into command mode + self.data_command.set_low(); + self.spi + .write_async(&[command as u8]) + .await + .expect_err("Expected to write command"); + info!("Command sent"); + } + + async fn send_data(&mut self, data: &[u8]) { + info!("Sending data: {:?}", data); + // Set into data mode + self.data_command.set_high(); + self.spi + .write_async(data) + .await + .expect("Expected to write data"); + info!("Data sent"); + } + + async fn set_ram_area(&mut self, x: u16, y: u16, width: u16, height: u16) { + // Data entry x increment y decrement??? + const DATA_ENTRY_X_INC_Y_DEC: u8 = 0x01; + + //TODO overflow safety + // Reverse Y coordinate (gates are reversed on this display) + let y = DISPLAY_HEIGHT - y - height; + + self.send_command(Command::DataEntryMode).await; + self.send_data(&[DATA_ENTRY_X_INC_Y_DEC]).await; + + // Set RAM X address range (start, end) - X is in PIXELS + self.send_command(Command::SetRamXRange).await; + //TODO safe arithmetic and casting + // Start low byte + self.send_data(&[(x % 256) as u8]).await; + // Start high byte + self.send_data(&[(x / 256) as u8]).await; + // End low byte + self.send_data(&[((x + width - 1) % 256) as u8]).await; + // End high byte + self.send_data(&[((x + width - 1) / 256) as u8]).await; + + // Set RAM Y address range (start, end) - Y is in PIXELS + self.send_command(Command::SetRamYRange).await; + // Start low byte + self.send_data(&[((y + height - 1) % 256) as u8]).await; + // Start high byte + self.send_data(&[((y + height - 1) / 256) as u8]).await; + // End low byte + self.send_data(&[(y % 256) as u8]).await; + // End high byte + self.send_data(&[(y / 256) as u8]).await; + + // Set RAM X address counter - X is in PIXELS + self.send_command(Command::SetRamXCounter).await; + // Low byte + self.send_data(&[(x % 256) as u8]).await; + // High byte + self.send_data(&[(x / 256) as u8]).await; + + // Set RAM Y address counter - Y is in PIXELS + self.send_command(Command::SetRamYCounter).await; + // Low byte + self.send_data(&[((y + height - 1) % 256) as u8]).await; + // High byte + self.send_data(&[((y + height - 1) / 256) as u8]).await; + } + + pub(super) async fn initialize_controller(&mut self) { + info!("Initializing SSD1677 controller"); + + // Soft reset + self.send_command(Command::SoftReset).await; + let result = with_timeout(Duration::from_millis(10_000), self.busy.wait_for_low()).await; + if let Err(TimeoutError) = result { + error!("Timeout waiting for busy"); + return; + } + + info!("Busy wait completed"); + + // Temperature sensor control (internal) + const TEMPERATURE_SENSOR_INTERNAL: u8 = 0x80; + self.send_command(Command::TemperatureSensorControl).await; + self.send_data(&[TEMPERATURE_SENSOR_INTERNAL]).await; + + // Booster soft-start control (GDEQ0426T82 specific values) + self.send_command(Command::BoosterSoftStart).await; + self.send_data(&[0xAE]).await; + self.send_data(&[0xC7]).await; + self.send_data(&[0xC3]).await; + self.send_data(&[0xC0]).await; + self.send_data(&[0xC0]).await; + self.send_data(&[0x40]).await; + + // Driver output control: set display height (480) and scan direction + self.send_command(Command::DriverOutputControl).await; + //TODO safer casting + self.send_data(&[((DISPLAY_HEIGHT - 1) % 256) as u8]).await; + self.send_data(&[((DISPLAY_HEIGHT - 1) / 256) as u8]).await; + self.send_data(&[0x02]).await; + + // Border waveform control + self.send_command(Command::BorderWaveformControl).await; + self.send_data(&[0x01]).await; + + // Set up full screen RAM area + self.set_ram_area(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT).await; + + info!("Clearing RAM buffers"); + // Auto write BW RAM + self.send_command(Command::AutoWriteBwRam).await; + self.send_data(&[0xF7]).await; + let result = with_timeout(Duration::from_millis(10_000), self.busy.wait_for_low()).await; + if let Err(TimeoutError) = result { + error!("Timeout waiting for busy"); + return; + } + + info!("SSD1677 controller initialized"); + } + + pub(super) async fn initialize( + spi: impl Instance + 'd, + serial_clock: impl PeripheralOutput<'d>, + master_in_slave_out: impl PeripheralOutput<'d>, + chip_select: impl PeripheralOutput<'d>, + direct_memory_access_channel: impl DmaChannelFor>, + reset: impl OutputPin + 'd, + data_command: impl OutputPin + 'd, + busy: impl InputPin + 'd, + ) -> Self { + info!("Initializing e-ink display driver"); + let mut this = Self::new( + spi, + serial_clock, + master_in_slave_out, + chip_select, + direct_memory_access_channel, + reset, + data_command, + busy, + ); + + this.reset().await; + + this.initialize_controller().await; + + info!("E-ink display driver initialized"); + + this + } +} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 0c9ac1a..0000000 --- a/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -#![no_std] diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..bf013f3 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,105 @@ +#![no_std] +#![no_main] +#![deny( + clippy::mem_forget, + reason = "mem::forget is generally not safe to do with esp_hal types, especially those \ + holding buffers for the duration of a data transfer." +)] +#![deny(clippy::large_stack_frames)] + +mod eink_display; + +use defmt::info; +use embassy_executor::Spawner; +use embassy_time::{Duration, Timer}; +use esp_hal::clock::CpuClock; +use esp_hal::gpio::{self, InputConfig, Level, OutputConfig}; +use esp_hal::timer::timg::TimerGroup; +use {esp_backtrace as _, esp_println as _}; + +use crate::eink_display::EinkDisplay; + +extern crate alloc; + +const CONNECTIONS_MAX: usize = 1; +const L2CAP_CHANNELS_MAX: usize = 1; + +// This creates a default app-descriptor required by the esp-idf bootloader. +// For more information see: +esp_bootloader_esp_idf::esp_app_desc!(); + +#[embassy_executor::task] +async fn hello_world() { + loop { + info!("Hello world!"); + Timer::after(Duration::from_secs(1)).await; + } +} + +#[allow( + clippy::large_stack_frames, + reason = "it's not unusual to allocate larger buffers etc. in main" +)] +#[esp_rtos::main] +async fn main(spawner: Spawner) { + // generator version: 1.2.0 + + let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); + let peripherals = esp_hal::init(config); + + esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 66320); + // COEX needs more RAM - so we've added some more + esp_alloc::heap_allocator!(size: 64 * 1024); + + let timer_group_0 = TimerGroup::new(peripherals.TIMG0); + let software_interrupt = + esp_hal::interrupt::software::SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); + esp_rtos::start(timer_group_0.timer0, software_interrupt.software_interrupt0); + + info!("Embassy initialized!"); + + // Radio setup + // let radio_init = esp_radio::init().expect("Failed to initialize Wi-Fi/BLE controller"); + // let (mut _wifi_controller, _interfaces) = + // esp_radio::wifi::new(&radio_init, peripherals.WIFI, Default::default()) + // .expect("Failed to initialize Wi-Fi controller"); + // // find more examples https://github.com/embassy-rs/trouble/tree/main/examples/esp32 + // let transport = BleConnector::new(&radio_init, peripherals.BT, Default::default()).unwrap(); + // let ble_controller = ExternalController::<_, 1>::new(transport); + // let mut resources: HostResources = + // HostResources::new(); + // let _stack = trouble_host::new(ble_controller, &mut resources); + + // Set up epaper display + // Custom pins for XteinkX4, not hardware SPI defaults + // SPI Clock (SCLK = serial clock) + let serial_clock = peripherals.GPIO8; + // SPI Master Out Slave In (MOSI) + let master_in_slave_out = peripherals.GPIO10; + // Chip Select (CS) + let chip_select = peripherals.GPIO21; + // Data/Command (DC) + let data_command = peripherals.GPIO4; + // Reset (RST) + let reset = peripherals.GPIO5; + // Busy + let busy = peripherals.GPIO6; + + let direct_memory_access_channel = peripherals.DMA_CH0; + let mut display = EinkDisplay::initialize( + peripherals.SPI2, + serial_clock, + master_in_slave_out, + chip_select, + direct_memory_access_channel, + reset, + data_command, + busy, + ) + .await; + + // Task test + spawner.must_spawn(hello_world()); + + // for inspiration have a look at the examples at https://github.com/esp-rs/esp-hal/tree/esp-hal-v1.0.0/examples +} -- 2.51.2