Migrate from hsmc 0.2 (local-patched) to hsmc 0.3.0 (crates.io, with embassy + trace-defmt features). Restructure radio chart and bundle several adjacent stability changes that need to land together for soak runs. Radio chart: - Booting (default, no handlers) -> Operational (default Unconfigured; super-state owning CmdPing/CmdGetInfo/Reset/WedgeDetected handlers + Recovering + CheckRecoveryBudget) -> Dead (terminal). - Cold init's during is structurally un-cancellable. - radio_task is a 30-line shim that panics if run() returns (chart is non-terminating). - Internal queue capacity bumped 8 -> 32 to cover action-emit bursts. Host chart: - Connected splits into WaitingForFirstFrame (default, no timer) + Active (chart-owned 1s inactivity timer via on(after) + self- transition on FrameReceived). Manual Timer::after + last_frame_at removed. Soak hardening: - ld/nrf52840-memory.x: app start 0x1000 -> 0x26000 to leave the factory s140 SoftDevice dormant in flash. - hal/nrf52840.rs: dump POWER.RESETREAS on boot -- WARN on DOG/LOCKUP, INFO on RESETPIN/SREQ/wake-from-OFF. - host/usb.rs: read_packet errors discriminated -- Disabled is debug (normal during USB enum), BufferOverflow stays warn. - COBS encode warns now carry type/tag/payload-length for diagnosis. - RADIO_THROTTLED static initializes to true (no proof of host drain at boot). - driver/sh1106.rs: init retries 3x with 100ms backoff (intermittent first-write race on power-up). - Cargo.toml: unexpected_cfgs allowlist for hsmc's journal/trace-* features. docs/semtech_lora_cad_lbt_guidebook.md: vendor the Semtech CAD/LBT reference for offline lookup.
150 lines
5.0 KiB
Rust
150 lines
5.0 KiB
Rust
//! 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 {
|
||
#[cfg(feature = "debug-twim")]
|
||
defmt::info!(
|
||
"[sh1106] cmd write: addr=0x{=u8:02X} bytes=[0x00, 0x{=u8:02X}]",
|
||
self.addr,
|
||
c,
|
||
);
|
||
self.i2c.write(self.addr, &[0x00, c]).await?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Initialize the display with sensible defaults.
|
||
pub async fn init(&mut self) -> Result<(), I::Error> {
|
||
#[cfg(feature = "debug-twim")]
|
||
defmt::info!("[sh1106] init: starting");
|
||
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 {
|
||
#[cfg(feature = "debug-twim")]
|
||
defmt::info!("[sh1106] flush page {=usize}: addr cmds (3 bytes)", page);
|
||
// 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]);
|
||
#[cfg(feature = "debug-twim")]
|
||
defmt::info!(
|
||
"[sh1106] flush page {=usize}: data write {=usize} bytes (0x40 prefix + 128 pixels)",
|
||
page,
|
||
page_buf.len(),
|
||
);
|
||
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> {
|
||
#[cfg(feature = "debug-twim")]
|
||
defmt::info!("[sh1106] set_brightness: 0x{=u8:02X}", value);
|
||
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)
|
||
}
|
||
}
|