Initial commit

Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
This commit is contained in:
Thomas Klaehn
2026-03-12 13:49:07 +01:00
commit 2301274850
17 changed files with 1834 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
[build]
target = "riscv32imac-unknown-none-elf"
[target.riscv32imac-unknown-none-elf]
runner = "espflash flash --monitor"
[env]
ESP_LOG = "info"
[unstable]
build-std = ["alloc", "core"]
+14
View File
@@ -0,0 +1,14 @@
{
"permissions": {
"allow": [
"WebFetch(domain:crates.io)",
"WebFetch(domain:docs.esp-rs.org)",
"WebFetch(domain:github.com)",
"WebFetch(domain:raw.githubusercontent.com)",
"WebFetch(domain:docs.rs)",
"WebFetch(domain:docs.espressif.com)",
"WebFetch(domain:esp32.implrust.com)",
"WebSearch"
]
}
}
+49
View File
@@ -0,0 +1,49 @@
# Base image
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
# Arguments
ARG CONTAINER_USER=esp
ARG CONTAINER_GROUP=esp
ARG NIGHTLY_VERSION=nightly-2025-11-01
# Install dependencies
RUN apt-get update \
&& apt-get install -y \
curl unzip git pkg-config gcc \
libssl-dev libusb-1.0-0 libusb-1.0-0-dev libudev-dev \
sudo \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN adduser --disabled-password --gecos "" ${CONTAINER_USER}
# Allow esp to sync the dialout group GID at container start
RUN echo "${CONTAINER_USER} ALL=(root) NOPASSWD: /usr/sbin/groupadd, /usr/sbin/groupmod, /usr/sbin/usermod, /usr/bin/chmod" \
>> /etc/sudoers.d/esp-dialout
USER ${CONTAINER_USER}
WORKDIR /home/${CONTAINER_USER}
# Install rustup + nightly toolchain with RISC-V target and rust-src component
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- \
--default-toolchain ${NIGHTLY_VERSION} -y --profile minimal \
--component rust-src,clippy,rustfmt \
--target riscv32imac-unknown-none-elf
ENV PATH=${PATH}:$HOME/.cargo/bin
# Install espflash (flash + monitor tool)
RUN ARCH=$($HOME/.cargo/bin/rustup show | grep "Default host" | sed -e 's/.* //') && \
curl -L "https://github.com/esp-rs/espflash/releases/latest/download/espflash-${ARCH}.zip" \
-o "${HOME}/.cargo/bin/espflash.zip" && \
unzip "${HOME}/.cargo/bin/espflash.zip" -d "${HOME}/.cargo/bin/" && \
rm "${HOME}/.cargo/bin/espflash.zip" && \
chmod u+x "${HOME}/.cargo/bin/espflash"
# Install probe-rs (on-chip debugger, flashing, RTT)
RUN $HOME/.cargo/bin/cargo install probe-rs-tools --locked
CMD ["/bin/bash"]
+45
View File
@@ -0,0 +1,45 @@
{
"name": "esp32c6-dev",
"build": {
"dockerfile": "Dockerfile",
"args": {
"NIGHTLY_VERSION": "nightly-2025-11-01"
}
},
// Privileged container to access /dev
"privileged": true,
// Mount USB bus so espflash can reach the connected board
"mounts": [
"source=/dev/bus/usb/,target=/dev/bus/usb/,type=bind"
],
"runArgs": [
"--device=/dev/ttyACM0",
"--device=/dev/ttyACM1"
],
"customizations": {
"vscode": {
"settings": {
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "modifications",
"files.watcherExclude": {
"**/target/**": true
},
"rust-analyzer.check.command": "clippy",
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
}
},
"extensions": [
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"mutantdino.resourcemonitor",
"probe-rs.probe-rs-debugger"
]
}
},
"postCreateCommand": "DIALOUT_GID=$(stat -c '%g' /dev/ttyACM0 2>/dev/null || stat -c '%g' /dev/ttyACM1 2>/dev/null || echo 20) && (getent group dialout > /dev/null && sudo groupmod -g $DIALOUT_GID dialout || sudo groupadd -g $DIALOUT_GID dialout) && sudo usermod -aG dialout esp && USB_GID=$(stat -c '%g' /dev/bus/usb/*/* 2>/dev/null | sort -u | head -1 || echo 46) && (getent group plugdev > /dev/null && sudo groupmod -g $USB_GID plugdev || sudo groupadd -g $USB_GID plugdev) && sudo usermod -aG plugdev esp || true",
"postStartCommand": "sudo chmod a+rw /dev/bus/usb/*/* 2>/dev/null; sudo chmod a+rw /dev/ttyACM* 2>/dev/null; true",
"remoteUser": "esp",
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached",
"workspaceFolder": "/workspace"
}
+2
View File
@@ -0,0 +1,2 @@
/target
*.rs.bk
+15
View File
@@ -0,0 +1,15 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "probe-rs-debug",
"request": "launch",
"name": "Debug ESP32-C6",
"chip": "esp32c6",
"flashingConfig": { "flashingEnabled": true },
"coreConfigs": [{
"programBinary": "target/riscv32imac-unknown-none-elf/debug/esp32c6-display"
}]
}
]
}
Generated
+1404
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
[package]
name = "esp32c6-display"
version = "0.1.0"
edition = "2024"
license = "MIT OR Apache-2.0"
publish = false
[dependencies]
# ESP32-C6 hardware abstraction layer
esp-hal = { version = "1.0.0", features = ["esp32c6", "unstable"] }
# Panic handler + backtrace over serial
esp-backtrace = { version = "0.18", features = ["esp32c6", "panic-handler", "println"] }
# Serial output / log backend
esp-println = { version = "0.16", features = ["esp32c6", "log-04"] }
# Heap allocator
esp-alloc = "0.9"
# Logging facade
log = "0.4"
# ESP-IDF app descriptor required by the on-chip bootloader (and probe-rs)
esp-bootloader-esp-idf = { version = "0.2", features = ["esp32c6"] }
# embedded-hal 1.0 traits + bus utilities
embedded-hal = "1.0"
embedded-hal-bus = "0.2"
# MIPI DSI display driver (supports ST7789)
mipidsi = "0.10"
# 2D graphics primitives and text rendering
embedded-graphics = "0.8"
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false
incremental = false
opt-level = "s"
lto = "fat"
overflow-checks = false
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("cargo:rustc-link-arg=-Tlinkall.x");
}
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
[toolchain]
channel = "nightly"
components = ["rust-src"]
targets = ["riscv32imac-unknown-none-elf"]
+138
View File
@@ -0,0 +1,138 @@
use embedded_graphics::pixelcolor::Rgb565;
use embedded_hal::delay::DelayNs;
use mipidsi::{
ConfigurationError,
dcs::{
BitsPerPixel, ExitSleepMode, InterfaceExt, PixelFormat, SetAddressMode, SetDisplayOn,
SetInvertMode, SetPixelFormat, SetTearingEffect,
},
interface::{Interface, InterfaceKind},
models::{Model, ModelInitError},
options::{ColorInversion, ModelOptions, TearingEffect},
};
/// JD9853 display driver in Rgb565 color mode.
///
/// The JD9853 has a 240-column internal framebuffer but the visible panel is
/// 172×320, starting at column offset 34. Configure the [`Builder`] as:
///
/// ```ignore
/// Builder::new(JD9853, di)
/// .display_size(172, 320)
/// .display_offset(34, 0)
/// ```
///
/// This panel requires colour inversion; the driver always enables it.
pub struct JD9853;
impl Model for JD9853 {
type ColorFormat = Rgb565;
// Full internal framebuffer of the JD9853 controller.
// Visible area is 172×320 at column offset 34 set in the Builder.
const FRAMEBUFFER_SIZE: (u16, u16) = (240, 320);
fn init<DELAY, DI>(
&mut self,
di: &mut DI,
delay: &mut DELAY,
options: &ModelOptions,
) -> Result<SetAddressMode, ModelInitError<DI::Error>>
where
DELAY: DelayNs,
DI: Interface,
{
if !matches!(
DI::KIND,
InterfaceKind::Serial4Line
| InterfaceKind::Parallel8Bit
| InterfaceKind::Parallel16Bit
) {
return Err(ModelInitError::InvalidConfiguration(
ConfigurationError::UnsupportedInterface,
));
}
let madctl = SetAddressMode::from(options);
// --- Exit sleep and wait for internal oscillator ---
di.write_command(ExitSleepMode)?;
delay.delay_us(120_000);
// --- Unlock manufacturer commands ---
di.write_raw(0xDF, &[0x98, 0x53])?;
// --- Bank 0: panel / power / gamma settings ---
di.write_raw(0xB2, &[0x23])?;
di.write_raw(0xB7, &[0x00, 0x47, 0x00, 0x6F])?;
di.write_raw(0xBB, &[0x1C, 0x1A, 0x55, 0x73, 0x63, 0xF0])?;
di.write_raw(0xC0, &[0x44, 0xA4])?;
di.write_raw(0xC1, &[0x16])?;
di.write_raw(0xC3, &[0x7D, 0x07, 0x14, 0x06, 0xCF, 0x71, 0x72, 0x77])?;
di.write_raw(
0xC4,
&[
0x00, 0x00, 0xA0, 0x79, 0x0B, 0x0A, 0x16, 0x79, 0x0B, 0x0A, 0x16, 0x82,
],
)?;
// Gamma table (positive + negative, 16 bytes each)
di.write_raw(
0xC8,
&[
0x3F, 0x32, 0x29, 0x29, 0x27, 0x2B, 0x27, 0x28,
0x28, 0x26, 0x25, 0x17, 0x12, 0x0D, 0x04, 0x00,
0x3F, 0x32, 0x29, 0x29, 0x27, 0x2B, 0x27, 0x28,
0x28, 0x26, 0x25, 0x17, 0x12, 0x0D, 0x04, 0x00,
],
)?;
di.write_raw(0xD0, &[0x04, 0x06, 0x6B, 0x0F, 0x00])?;
di.write_raw(0xD7, &[0x00, 0x30])?;
di.write_raw(0xE6, &[0x14])?;
// --- Bank 1: power settings ---
di.write_raw(0xDE, &[0x01])?;
di.write_raw(0xB7, &[0x03, 0x13, 0xEF, 0x35, 0x35])?;
di.write_raw(0xC1, &[0x14, 0x15, 0xC0])?;
di.write_raw(0xC2, &[0x06, 0x3A])?;
di.write_raw(0xC4, &[0x72, 0x12])?;
di.write_raw(0xBE, &[0x00])?;
// --- Bank 2: internal settings (first pass) ---
di.write_raw(0xDE, &[0x02])?;
di.write_raw(0xE5, &[0x00, 0x02, 0x00])?;
di.write_raw(0xE5, &[0x01, 0x02, 0x00])?;
// --- Return to bank 0 ---
di.write_raw(0xDE, &[0x00])?;
// --- Standard DCS: tearing effect + pixel format ---
di.write_command(SetTearingEffect::new(TearingEffect::Vertical))?;
let pf = PixelFormat::with_all(BitsPerPixel::from_rgb_color::<Self::ColorFormat>());
di.write_command(SetPixelFormat::new(pf))?;
// Seed the column / page address window (mipidsi will update this before
// each draw, but the second bank-2 block below requires these to have been
// written first to latch internal timing registers).
di.write_raw(0x2A, &[0x00, 0x22, 0x00, 0xCD])?; // cols 34..205
di.write_raw(0x2B, &[0x00, 0x00, 0x01, 0x3F])?; // rows 0..319
// --- Bank 2: internal settings (second pass, after window init) ---
di.write_raw(0xDE, &[0x02])?;
di.write_raw(0xE5, &[0x00, 0x02, 0x00])?;
di.write_raw(0xDE, &[0x00])?;
// --- Address mode + colour inversion ---
di.write_command(madctl)?;
// This panel always requires colour inversion to display correct colours.
di.write_command(SetInvertMode::new(ColorInversion::Inverted))?;
// 10 ms settle time before display-on (matches reference init ordering).
delay.delay_us(10_000);
di.write_command(SetDisplayOn)?;
Ok(madctl)
}
}
+110
View File
@@ -0,0 +1,110 @@
#![no_std]
#![no_main]
use esp_backtrace as _;
esp_bootloader_esp_idf::esp_app_desc!();
use esp_hal::{
delay::Delay,
gpio::{Level, Output, OutputConfig},
main,
spi::master::{Config, Spi},
time::Rate,
};
use esp_println::println;
mod jd9853;
use jd9853::JD9853;
use mipidsi::{interface::SpiInterface, Builder};
use embedded_graphics::{
mono_font::{ascii::FONT_10X20, MonoTextStyle},
pixelcolor::Rgb565,
prelude::*,
primitives::{PrimitiveStyleBuilder, Rectangle},
text::Text,
};
// TODO: verify pin assignments against your board schematic
//
// Typical wiring for a 1.47" JD9853 breakout on ESP32-C6:
//
// Display pin │ ESP32-C6 GPIO
// ─────────────┼───────────────
// SCL / SCK │ GPIO6
// SDA / MOSI │ GPIO7
// CS │ GPIO10
// DC │ GPIO4
// RST │ GPIO5
// BLK / BL │ GPIO22 (set high to enable backlight)
//
const LCD_W: u16 = 172;
const LCD_H: u16 = 320;
#[main]
fn main() -> ! {
let peripherals = esp_hal::init(esp_hal::Config::default());
// 72 KB heap for mipidsi internal buffers, alloc, etc.
esp_alloc::heap_allocator!(size: 72 * 1024);
println!("ESP32-C6 display demo starting…");
// --- SPI bus -----------------------------------------------------------
let spi = Spi::new(
peripherals.SPI2,
Config::default()
.with_frequency(Rate::from_mhz(40))
.with_mode(esp_hal::spi::Mode::_0),
)
.unwrap()
.with_sck(peripherals.GPIO1)
.with_mosi(peripherals.GPIO2);
// Manual chip-select via GPIO
let cs = Output::new(peripherals.GPIO14, Level::High, OutputConfig::default());
// Wrap bus + CS into an embedded-hal SpiDevice
let spi_device = embedded_hal_bus::spi::ExclusiveDevice::new(spi, cs, Delay::new()).unwrap();
// --- Display -----------------------------------------------------------
let dc = Output::new(peripherals.GPIO15, Level::Low, OutputConfig::default());
let rst = Output::new(peripherals.GPIO22, Level::High, OutputConfig::default());
// Backlight — drive high to enable
let _bl = Output::new(peripherals.GPIO23, Level::High, OutputConfig::default());
// mipidsi 0.10 requires a small scratch buffer owned by the interface
let mut buf = [0u8; 512];
let di = SpiInterface::new(spi_device, dc, &mut buf);
// JD9853 has a 240-wide internal framebuffer; the 172-pixel panel starts at column 34.
let mut display = Builder::new(JD9853, di)
.reset_pin(rst)
.display_size(LCD_W, LCD_H)
.display_offset(34, 0)
.init(&mut Delay::new())
.expect("display init failed");
// --- Drawing -----------------------------------------------------------
display.clear(Rgb565::BLACK).unwrap();
// Blue rectangle as background banner
let bg_style = PrimitiveStyleBuilder::new()
.fill_color(Rgb565::new(0, 0, 20))
.build();
Rectangle::new(Point::new(0, 0), Size::new(LCD_W as u32, 40))
.into_styled(bg_style)
.draw(&mut display)
.unwrap();
// White text
let text_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
Text::new("ESP32-C6", Point::new(10, 28), text_style)
.draw(&mut display)
.unwrap();
println!("Display ready.");
loop {}
}