Files
firmware/src/driver/sh1106.rs
T
swaits fb334118fd refactor: extract hal/ and driver/ modules
- Created hal/esp32s3.rs with MCU primitives (SPI, I2C, USB, UART, timer, MAC)
- Created hal/nrf52840.rs with shared nRF boilerplate (interrupts, bus wrapping, VBUS, MAC)
- Moved SH1106 OLED driver from display/ to driver/ (clear separation from app code)
- Board esp32s3.rs now delegates MCU init to hal, keeps only peripheral wiring (RadioParts)
- nRF board files use hal for shared StaticCell patterns, mac_address, interrupt bindings
- Board files still own pin assignments and peripheral construction
2026-04-07 09:38:56 -06:00

133 lines
4.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Minimal async SH1106 I2C OLED driver (128x64, page addressing).
//!
//! The SH1106 is very similar to the SSD1306 but only supports page-mode
//! addressing (no horizontal/vertical auto-advance). This driver sends
//! framebuffer data page-by-page with explicit page/column commands.
use embedded_graphics::draw_target::DrawTarget;
use embedded_graphics::geometry::{OriginDimensions, Size};
use embedded_graphics::pixelcolor::BinaryColor;
use embedded_graphics::Pixel;
use embedded_hal_async::i2c::I2c;
const WIDTH: usize = 128;
const HEIGHT: usize = 64;
const PAGES: usize = HEIGHT / 8;
const BUF_SIZE: usize = WIDTH * PAGES; // 1024
/// SH1106 has a 132-column RAM; visible 128 columns are centered with a
/// 2-column offset on each side.
const COL_OFFSET: u8 = 2;
pub struct Sh1106<I> {
i2c: I,
addr: u8,
buffer: [u8; BUF_SIZE],
}
impl<I: I2c> Sh1106<I> {
pub fn new(i2c: I, addr: u8) -> Self {
Self {
i2c,
addr,
buffer: [0u8; BUF_SIZE],
}
}
/// Send a sequence of commands (each prefixed with the I2C command byte 0x00).
async fn cmd(&mut self, commands: &[u8]) -> Result<(), I::Error> {
for &c in commands {
self.i2c.write(self.addr, &[0x00, c]).await?;
}
Ok(())
}
/// Initialize the display with sensible defaults.
pub async fn init(&mut self) -> Result<(), I::Error> {
self.cmd(&[
0xAE, // display off
0xD5, 0x80, // clock div: default
0xA8, 0x3F, // multiplex ratio: 64
0xD3, 0x00, // display offset: 0
0x40, // start line: 0
0x8D, 0x14, // charge pump: enable
0xA1, // segment remap: column 127 mapped to SEG0
0xC8, // COM scan direction: remapped
0xDA, 0x12, // COM pin config: alternative, no L/R remap
0x81, 0xCF, // contrast: 207
0xD9, 0xF1, // pre-charge: phase1=1, phase2=15
0xDB, 0x40, // VCOMH deselect: ~0.77×Vcc
0xA4, // entire display ON (follow RAM)
0xA6, // normal display (not inverted)
0xAF, // display on
])
.await
}
/// Flush the entire framebuffer to the display, page by page.
pub async fn flush(&mut self) -> Result<(), I::Error> {
// Each I2C data write is prefixed with 0x40 (data continuation byte).
let mut page_buf = [0u8; 1 + WIDTH]; // 0x40 + 128 data bytes
page_buf[0] = 0x40;
for page in 0..PAGES {
// Set page address and column start (with SH1106 2-column offset).
self.cmd(&[
0xB0 | page as u8, // page address
COL_OFFSET & 0x0F, // column low nibble
0x10 | (COL_OFFSET >> 4), // column high nibble
])
.await?;
let start = page * WIDTH;
page_buf[1..].copy_from_slice(&self.buffer[start..start + WIDTH]);
self.i2c.write(self.addr, &page_buf).await?;
}
Ok(())
}
/// Set display brightness (contrast).
pub async fn set_brightness(&mut self, value: u8) -> Result<(), I::Error> {
self.cmd(&[0x81, value]).await
}
}
impl<I: I2c> DrawTarget for Sh1106<I> {
type Color = BinaryColor;
type Error = core::convert::Infallible;
fn draw_iter<T>(&mut self, pixels: T) -> Result<(), Self::Error>
where
T: IntoIterator<Item = Pixel<Self::Color>>,
{
for Pixel(pos, color) in pixels {
let x = pos.x;
let y = pos.y;
if x >= 0 && x < WIDTH as i32 && y >= 0 && y < HEIGHT as i32 {
let x = x as usize;
let y = y as usize;
let idx = (y / 8) * WIDTH + x;
let bit = (y % 8) as u8;
if color.is_on() {
self.buffer[idx] |= 1 << bit;
} else {
self.buffer[idx] &= !(1 << bit);
}
}
}
Ok(())
}
fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
self.buffer.fill(if color.is_on() { 0xFF } else { 0x00 });
Ok(())
}
}
impl<I: I2c> OriginDimensions for Sh1106<I> {
fn size(&self) -> Size {
Size::new(WIDTH as u32, HEIGHT as u32)
}
}