#![no_std]
#![warn(missing_docs)]
#![expect(
clippy::future_not_send,
reason = "esp-hal's async drivers aren't Send; embassy polls a future on the executor that \
made it"
)]
use core::{cell::RefCell, convert::Infallible, fmt};
pub use cardputer_adv_keyboard as keys;
use critical_section::Mutex;
use embassy_futures::select::{Either, select};
use embassy_time::{Duration, Timer};
use embedded_graphics_core::{
Pixel,
draw_target::DrawTarget,
geometry::{OriginDimensions, Size},
pixelcolor::{IntoStorage, Rgb565},
primitives::Rectangle,
};
use embedded_hal::digital::OutputPin;
use embedded_hal_bus::{
i2c::CriticalSectionDevice,
spi::{ExclusiveDevice, NoDelay},
};
use esp_hal::{
Async, Blocking,
analog::adc::{Adc, AdcCalCurve, AdcConfig, AdcPin, Attenuation},
delay::Delay,
dma::DmaTxStreamBuf,
dma_tx_stream_buffer,
gpio::{DriveMode, Event, Input, InputConfig, Level, Output, OutputConfig, Pull},
i2c::master::{Config as I2cConfig, I2c},
i2s::master::{Channels, DataFormat, I2s, I2sTxDmaTransfer, TdmConfig},
ledc::{
LSGlobalClkSource, Ledc, LowSpeed,
channel::{self, ChannelIFace},
timer::{self, TimerIFace},
},
peripherals::{
ADC1, DMA_CH0, GPIO8, GPIO9, GPIO10, GPIO11, GPIO33, GPIO34, GPIO35, GPIO36, GPIO37,
GPIO38, GPIO41, GPIO42, GPIO43, I2C0, I2S0, LEDC, SPI2,
},
spi::{
Mode,
master::{AnySpi, Config as SpiConfig, Spi, SpiDma, SpiMasterDmaChannel},
},
time::Rate,
};
pub use mipidsi;
use mipidsi::{
Builder,
interface::{Interface, InterfacePixelFormat, SpiInterface},
models::ST7789,
options::{ColorInversion, Orientation, Rotation},
};
use static_cell::StaticCell;
pub const WIDTH: u16 = 240;
pub const HEIGHT: u16 = 135;
type DisplaySpi = ExclusiveDevice<Spi<'static, Blocking>, Output<'static>, NoDelay>;
pub type Display =
mipidsi::Display<SpiInterface<'static, DisplaySpi, Output<'static>>, ST7789, Output<'static>>;
#[expect(missing_docs, reason = "the fields are the pins they're named after")]
pub struct DisplayPins {
pub spi: SPI2<'static>,
pub sck: GPIO36<'static>,
pub mosi: GPIO35<'static>,
pub cs: GPIO37<'static>,
pub dc: GPIO34<'static>,
pub reset: GPIO33<'static>,
pub backlight: GPIO38<'static>,
pub ledc: LEDC<'static>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
Spi,
I2c,
DisplayInit,
Keyboard,
Backlight,
Imu,
Speaker,
Codec,
DisplayWrite,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Spi => "LCD SPI bus setup failed",
Self::I2c => "I2C bus setup failed",
Self::DisplayInit => "LCD initialisation failed",
Self::Keyboard => "keyboard controller (TCA8418) not answering",
Self::Backlight => "backlight PWM setup failed",
Self::Imu => "motion sensor (BMI270) not found or not starting",
Self::Speaker => "speaker I2S stream failed",
Self::Codec => "audio codec (ES8311) not answering",
Self::DisplayWrite => "LCD frame write failed",
})
}
}
impl core::error::Error for Error {}
pub struct Backlight {
channel: channel::Channel<'static, LowSpeed>,
}
impl Backlight {
pub fn set_percent(&mut self, percent: u8) -> Result<(), Error> {
self.channel
.set_duty(percent.min(100))
.map_err(|_| Error::Backlight)
}
}
fn backlight(ledc: LEDC<'static>, pin: GPIO38<'static>) -> Result<Backlight, Error> {
static TIMER: StaticCell<esp_hal::ledc::timer::Timer<'static, LowSpeed>> = StaticCell::new();
let mut ledc = Ledc::new(ledc);
ledc.set_global_slow_clock(LSGlobalClkSource::APBClk);
let timer = TIMER.init(ledc.timer::<LowSpeed>(timer::Number::Timer0));
timer
.configure(timer::config::Config {
duty: timer::config::Duty::Duty10Bit,
clock_source: timer::LSClockSource::APBClk,
frequency: Rate::from_hz(256),
})
.map_err(|_| Error::Backlight)?;
let mut channel = ledc.channel(channel::Number::Channel0, pin);
channel
.configure(channel::config::Config {
timer,
duty_pct: 100,
drive_mode: DriveMode::PushPull,
})
.map_err(|_| Error::Backlight)?;
Ok(Backlight { channel })
}
pub fn display(pins: DisplayPins) -> Result<(Display, Backlight), Error> {
static BUFFER: StaticCell<[u8; 512]> = StaticCell::new();
let backlight = backlight(pins.ledc, pins.backlight)?;
let dc = Output::new(pins.dc, Level::Low, OutputConfig::default());
let reset = Output::new(pins.reset, Level::High, OutputConfig::default());
let cs = Output::new(pins.cs, Level::High, OutputConfig::default());
let spi = Spi::new(pins.spi, spi_config())
.map_err(|_| Error::Spi)?
.with_sck(pins.sck)
.with_mosi(pins.mosi);
let device = ExclusiveDevice::new_no_delay(spi, cs).map_err(|_| Error::Spi)?;
let interface = SpiInterface::new(device, dc, BUFFER.init([0; 512]));
let display = init_panel(interface, reset)?;
Ok((display, backlight))
}
fn spi_config() -> SpiConfig {
SpiConfig::default()
.with_frequency(Rate::from_mhz(40))
.with_mode(Mode::_0)
}
const PANEL_OFFSET: (u16, u16) = (52, 40);
fn init_panel<DI, RST>(
interface: DI,
reset: RST,
) -> Result<mipidsi::Display<DI, ST7789, RST>, Error>
where
DI: Interface,
Rgb565: InterfacePixelFormat<DI::Word>,
RST: OutputPin,
{
Builder::new(ST7789, interface)
.reset_pin(reset)
.display_size(HEIGHT, WIDTH)
.display_offset(PANEL_OFFSET.0, PANEL_OFFSET.1)
.orientation(Orientation::new().rotate(Rotation::Deg90))
.invert_colors(ColorInversion::Inverted)
.init(&mut Delay::new())
.map_err(|_| Error::DisplayInit)
}
const W: usize = WIDTH as usize;
const H: usize = HEIGHT as usize;
#[derive(Clone, Copy)]
struct Span {
first: u8,
last: u8,
}
impl Span {
const CLEAN: Self = Self {
first: u8::MAX,
last: 0,
};
#[expect(clippy::cast_possible_truncation, reason = "the width (240) fits")]
const FULL: Self = Self {
first: 0,
last: (W - 1) as u8,
};
const fn is_clean(self) -> bool {
self.first > self.last
}
#[expect(clippy::cast_possible_truncation, reason = "x < WIDTH (240)")]
fn with(self, x: usize) -> Self {
let x = x as u8;
Self {
first: self.first.min(x),
last: self.last.max(x),
}
}
fn union(self, other: Self) -> Self {
Self {
first: self.first.min(other.first),
last: self.last.max(other.last),
}
}
}
fn row_hash(row: &[u16]) -> u32 {
let (pairs, _) = row.as_chunks::<2>();
pairs.iter().fold(0x811C_9DC5, |hash, &[a, b]| {
(hash ^ (u32::from(a) | u32::from(b) << 16))
.wrapping_mul(0x0100_0193)
.rotate_left(5)
})
}
pub struct Frame {
pixels: [u16; W * H],
written: [Span; H],
sent: [u32; H],
stale: bool,
}
impl Frame {
#[must_use]
#[expect(
clippy::large_stack_arrays,
reason = "made at compile time, into a static (see above)"
)]
pub const fn new() -> Self {
Self {
pixels: [0; W * H],
written: [Span::FULL; H],
sent: [0; H],
stale: true,
}
}
pub const fn invalidate(&mut self) {
self.written = [Span::FULL; H];
self.stale = true;
}
fn changed(&mut self, y: usize) -> Option<u32> {
if self.written[y].is_clean() {
return None;
}
let hash = row_hash(&self.pixels[y * W..(y + 1) * W]);
if !self.stale && hash == self.sent[y] {
self.written[y] = Span::CLEAN;
return None;
}
Some(hash)
}
fn raw(color: Rgb565) -> u16 {
u16::from_ne_bytes(color.into_storage().to_be_bytes())
}
fn fill_row(&mut self, y: usize, x0: usize, x1: usize, raw: u16) {
let row = &mut self.pixels[y * W + x0..=y * W + x1];
let (head, words, tail) = bytemuck::pod_align_to_mut::<u16, u32>(row);
head.fill(raw);
words.fill(u32::from(raw) * 0x0001_0001);
tail.fill(raw);
self.written[y] = self.written[y].with(x0).with(x1);
}
}
impl Default for Frame {
fn default() -> Self {
Self::new()
}
}
impl OriginDimensions for Frame {
fn size(&self) -> Size {
Size::new(WIDTH.into(), HEIGHT.into())
}
}
impl DrawTarget for Frame {
type Color = Rgb565;
type Error = Infallible;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Infallible>
where
I: IntoIterator<Item = Pixel<Rgb565>>,
{
for Pixel(point, color) in pixels {
if let (Ok(x), Ok(y)) = (usize::try_from(point.x), usize::try_from(point.y))
&& x < W
&& y < H
{
self.pixels[y * W + x] = Self::raw(color);
self.written[y] = self.written[y].with(x);
}
}
Ok(())
}
fn fill_solid(&mut self, area: &Rectangle, color: Rgb565) -> Result<(), Infallible> {
let clip = |start: i32, length: u32, limit: usize| {
let end = start.saturating_add_unsigned(length);
let clamp = |v: i32| usize::try_from(v).unwrap_or(0).min(limit);
(clamp(start), clamp(end))
};
let (x0, x1) = clip(area.top_left.x, area.size.width, W);
let (y0, y1) = clip(area.top_left.y, area.size.height, H);
if x0 >= x1 {
return Ok(());
}
let raw = Self::raw(color);
for y in y0..y1 {
self.fill_row(y, x0, x1 - 1, raw);
}
Ok(())
}
}
pub struct FrameDisplay {
spi: SpiDma<'static, Async>,
cs: Output<'static>,
dc: Output<'static>,
_reset: Output<'static>,
}
const MIN_DMA_TRANSFER: usize = 64;
const SLPIN: u8 = 0x10;
const SLPOUT: u8 = 0x11;
const CASET: u8 = 0x2A;
const RASET: u8 = 0x2B;
const RAMWR: u8 = 0x2C;
const COLUMN_OFFSET: u16 = PANEL_OFFSET.1;
const ROW_OFFSET: u16 = 240 - (HEIGHT + PANEL_OFFSET.0);
impl FrameDisplay {
pub async fn flush(&mut self, frame: &mut Frame) -> Result<(), Error> {
self.cs.set_low();
let sent = self.send(frame).await;
self.cs.set_high();
sent
}
async fn send(&mut self, frame: &mut Frame) -> Result<(), Error> {
let mut hashes = [0; H];
let mut y = 0;
while y < H {
let top = y;
let mut span = Span::CLEAN;
while y < H
&& let Some(hash) = frame.changed(y)
{
hashes[y] = hash;
span = span.union(frame.written[y]);
y += 1;
}
if span.is_clean() {
y += 1;
continue;
}
let (first, last) = (usize::from(span.first), usize::from(span.last));
self.window(first, top, last, y - 1).await?;
if first == 0 && last == W - 1 {
self.write(bytemuck::cast_slice(&frame.pixels[top * W..y * W]))
.await?;
} else {
for row in top..y {
let pixels = &frame.pixels[row * W + first..=row * W + last];
self.write(bytemuck::cast_slice(pixels)).await?;
}
}
frame.sent[top..y].copy_from_slice(&hashes[top..y]);
frame.written[top..y].fill(Span::CLEAN);
}
frame.stale = false;
Ok(())
}
pub async fn set_asleep(&mut self, asleep: bool) -> Result<(), Error> {
self.cs.set_low();
let sent = self.command(if asleep { SLPIN } else { SLPOUT }, &[]).await;
self.cs.set_high();
Timer::after(Duration::from_millis(5)).await;
sent
}
#[expect(clippy::cast_possible_truncation, reason = "inside the 240×135 frame")]
async fn window(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) -> Result<(), Error> {
let range = |from: usize, to: usize, offset: u16| {
let [a, b] = (from as u16 + offset).to_be_bytes();
let [c, d] = (to as u16 + offset).to_be_bytes();
[a, b, c, d]
};
self.command(CASET, &range(x0, x1, COLUMN_OFFSET)).await?;
self.command(RASET, &range(y0, y1, ROW_OFFSET)).await?;
self.command(RAMWR, &[]).await
}
async fn command(&mut self, command: u8, args: &[u8]) -> Result<(), Error> {
self.dc.set_low();
self.write(&[command]).await?;
self.dc.set_high();
self.write(args).await
}
async fn write(&mut self, bytes: &[u8]) -> Result<(), Error> {
self.spi
.write_async(bytes)
.await
.map_err(|_| Error::DisplayWrite)
}
}
pub fn frame_display(
pins: DisplayPins,
dma: impl SpiMasterDmaChannel<'static, AnySpi<'static>>,
) -> Result<(FrameDisplay, Backlight), Error> {
let backlight = backlight(pins.ledc, pins.backlight)?;
let mut dc = Output::new(pins.dc, Level::Low, OutputConfig::default());
let mut reset = Output::new(pins.reset, Level::High, OutputConfig::default());
let mut cs = Output::new(pins.cs, Level::High, OutputConfig::default());
let mut spi = Spi::new(
pins.spi,
spi_config().with_min_async_transfer_size(MIN_DMA_TRANSFER),
)
.map_err(|_| Error::Spi)?
.with_sck(pins.sck)
.with_mosi(pins.mosi)
.with_dma(dma);
let mut buffer = [0; 64];
let device = ExclusiveDevice::new_no_delay(&mut spi, &mut cs).map_err(|_| Error::Spi)?;
init_panel(SpiInterface::new(device, &mut dc, &mut buffer), &mut reset)?;
Ok((
FrameDisplay {
spi: spi.into_async(),
cs,
dc,
_reset: reset,
},
backlight,
))
}
pub type I2cBus = Mutex<RefCell<I2c<'static, Blocking>>>;
pub type I2cDevice = CriticalSectionDevice<'static, I2c<'static, Blocking>>;
#[expect(missing_docs, reason = "the fields are the pins they're named after")]
pub struct I2cPins {
pub i2c: I2C0<'static>,
pub sda: GPIO8<'static>,
pub scl: GPIO9<'static>,
}
pub fn i2c(pins: I2cPins) -> Result<&'static I2cBus, Error> {
static BUS: StaticCell<I2cBus> = StaticCell::new();
let i2c = I2c::new(
pins.i2c,
I2cConfig::default().with_frequency(Rate::from_khz(400)),
)
.map_err(|_| Error::I2c)?
.with_sda(pins.sda)
.with_scl(pins.scl);
Ok(BUS.init(Mutex::new(RefCell::new(i2c))))
}
pub struct KeyboardPins {
pub i2c: I2C0<'static>,
pub sda: GPIO8<'static>,
pub scl: GPIO9<'static>,
pub interrupt: GPIO11<'static>,
}
const FALLBACK_POLL: Duration = Duration::from_secs(1);
const SPURIOUS_BACKOFF: Duration = Duration::from_millis(10);
const MAX_INPUTS: usize = 10;
pub struct Keyboard {
keys: keys::Keyboard<I2cDevice>,
interrupt: Input<'static>,
pending: bool,
missed: u32,
}
enum Wake {
Interrupt,
Fallback,
Pending,
}
impl Keyboard {
pub async fn wait_inputs(&mut self) -> Result<impl Iterator<Item = keys::KeyInput>, Error> {
loop {
let wake = if core::mem::take(&mut self.pending) {
Wake::Pending
} else {
match select(self.interrupt.wait_for_low(), Timer::after(FALLBACK_POLL)).await {
Either::First(()) => Wake::Interrupt,
Either::Second(()) => Wake::Fallback,
}
};
let events = self.keys.events().map_err(|_| Error::Keyboard)?;
let cleared = self.keys.clear_interrupts().is_ok();
self.pending = !cleared || self.keys.events_available().unwrap_or(true);
let mut events = events.peekable();
let any_event = events.peek().is_some();
let mut inputs = [None; MAX_INPUTS];
for (slot, input) in inputs
.iter_mut()
.zip(events.filter_map(|event| event.input))
{
*slot = Some(input);
}
if !any_event {
if matches!(wake, Wake::Interrupt) {
Timer::after(SPURIOUS_BACKOFF).await;
}
continue;
}
if matches!(wake, Wake::Fallback) {
self.missed = self.missed.saturating_add(1);
}
if inputs[0].is_none() {
continue;
}
return Ok(inputs.into_iter().flatten());
}
}
pub fn listen_for_wake(&mut self) {
self.interrupt.listen(Event::LowLevel);
}
#[must_use]
pub const fn missed_interrupts(&self) -> u32 {
self.missed
}
pub const fn driver(&mut self) -> &mut keys::Keyboard<I2cDevice> {
&mut self.keys
}
}
pub fn keyboard(pins: KeyboardPins) -> Result<Keyboard, Error> {
let bus = i2c(I2cPins {
i2c: pins.i2c,
sda: pins.sda,
scl: pins.scl,
})?;
keyboard_on(bus, pins.interrupt)
}
const KEYBOARD_TRIES: u32 = 5;
const KEYBOARD_RETRY_MS: u32 = 20;
pub fn keyboard_on(bus: &'static I2cBus, interrupt: GPIO11<'static>) -> Result<Keyboard, Error> {
let set_up = || -> Result<keys::Keyboard<I2cDevice>, Error> {
let mut keys =
keys::Keyboard::new(CriticalSectionDevice::new(bus)).map_err(|_| Error::Keyboard)?;
keys.enable_keyboard_interrupt()
.map_err(|_| Error::Keyboard)?;
keys.clear_interrupts().map_err(|_| Error::Keyboard)?;
Ok(keys)
};
let keys = (0..KEYBOARD_TRIES)
.find_map(|attempt| {
if attempt > 0 {
Delay::new().delay_millis(KEYBOARD_RETRY_MS);
}
set_up().ok()
})
.ok_or(Error::Keyboard)?;
let interrupt = Input::new(interrupt, InputConfig::default().with_pull(Pull::Up));
Ok(Keyboard {
keys,
interrupt,
pending: false,
missed: 0,
})
}
const IMU_BURST: u16 = 128;
type Bmi270 = bmi2::Bmi2<bmi2::interface::I2cInterface<I2cDevice>, Delay, { IMU_BURST as usize }>;
pub struct Imu {
bmi: Bmi270,
}
impl Imu {
pub fn acceleration(&mut self) -> Result<[i32; 3], Error> {
let data = self.bmi.get_acc_data().map_err(|_| Error::Imu)?;
let mg = |raw: i16| i32::from(raw) * 1000 / 4096;
Ok([mg(data.x), mg(data.y), mg(data.z)])
}
}
pub fn imu(bus: &'static I2cBus) -> Result<Imu, Error> {
use bmi2::{
I2cAddr, config,
types::{AccBwp, AccConf, AccRange, Burst, Odr, PerfMode, PwrCtrl},
};
let mut bmi = [I2cAddr::Default, I2cAddr::Alternative]
.into_iter()
.find_map(|address| {
let mut bmi = Bmi270::new_i2c(
CriticalSectionDevice::new(bus),
Delay::new(),
address,
Burst::new(IMU_BURST),
);
bmi.get_chip_id().is_ok_and(|id| id == 0x24).then_some(bmi)
})
.ok_or(Error::Imu)?;
bmi.init(&config::BMI270_CONFIG_FILE)
.map_err(|_| Error::Imu)?;
bmi.set_acc_range(AccRange::Range8g)
.map_err(|_| Error::Imu)?;
bmi.set_acc_conf(AccConf {
odr: Odr::Odr100,
bwp: AccBwp::NormAvg4,
filter_perf: PerfMode::Perf,
})
.map_err(|_| Error::Imu)?;
bmi.set_pwr_ctrl(PwrCtrl {
aux_en: false,
gyr_en: false,
acc_en: true,
temp_en: false,
})
.map_err(|_| Error::Imu)?;
Ok(Imu { bmi })
}
pub const SAMPLE_RATE: u32 = 44_100;
const SPEAKER_BUFFER: usize = 4096;
const SPEAKER_CHUNK: usize = 512;
#[expect(missing_docs, reason = "the fields are the pins they're named after")]
pub struct SpeakerPins {
pub i2s: I2S0<'static>,
pub dma: DMA_CH0<'static>,
pub bclk: GPIO41<'static>,
pub ws: GPIO43<'static>,
pub dout: GPIO42<'static>,
}
type SpeakerTransfer = I2sTxDmaTransfer<'static, Async, DmaTxStreamBuf>;
const SILENCE: [u8; 64] = [0; 64];
pub struct Speaker {
transfer: Option<SpeakerTransfer>,
}
impl Speaker {
pub async fn run(&mut self, mut fill: impl FnMut(&mut [i16])) -> Result<Infallible, Error> {
let mut mono = [0i16; 256];
loop {
let transfer = self.transfer.as_mut().ok_or(Error::Speaker)?;
if transfer.wait_for_available_async().await.is_err() {
self.restart()?;
continue;
}
loop {
let pushed = transfer.push_with(|bytes| {
debug_assert!(bytes.is_empty() || bytes.len() >= 4);
let frames = (bytes.len() / 4).min(mono.len());
fill(&mut mono[..frames]);
for (frame, sample) in bytes.chunks_exact_mut(4).zip(&mono[..frames]) {
let [lo, hi] = sample.to_le_bytes();
frame.copy_from_slice(&[lo, hi, lo, hi]);
}
frames * 4
});
if pushed == 0 {
break;
}
}
}
}
fn restart(&mut self) -> Result<(), Error> {
let transfer = self.transfer.take().ok_or(Error::Speaker)?;
let (tx, buffer) = transfer.stop();
let (descriptors, bytes) = buffer.split();
let mut buffer = DmaTxStreamBuf::new(descriptors, bytes).map_err(|_| Error::Speaker)?;
while buffer.push(&SILENCE) > 0 {}
self.transfer = Some(tx.write(buffer).map_err(|_| Error::Speaker)?);
Ok(())
}
}
pub fn speaker(pins: SpeakerPins) -> Result<Speaker, Error> {
let i2s = I2s::new(
pins.i2s,
pins.dma,
TdmConfig::new_tdm_philips()
.with_sample_rate(Rate::from_hz(SAMPLE_RATE))
.with_data_format(DataFormat::Data16Channel16)
.with_channels(Channels::STEREO),
)
.map_err(|_| Error::Speaker)?
.into_async();
let tx = i2s
.i2s_tx
.with_bclk(pins.bclk)
.with_ws(pins.ws)
.with_dout(pins.dout)
.build();
let mut buffer = dma_tx_stream_buffer!(SPEAKER_BUFFER, SPEAKER_CHUNK);
while buffer.push(&SILENCE) > 0 {}
let transfer = tx.write(buffer).map_err(|_| Error::Speaker)?;
Ok(Speaker {
transfer: Some(transfer),
})
}
const ES8311: u8 = 0x18;
const ES8311_DAC_VOLUME: u8 = 0x32;
pub struct Codec {
device: I2cDevice,
}
impl Codec {
pub fn set_volume(&mut self, percent: u8) -> Result<(), Error> {
let step = u16::from(percent.min(100)) * 0xBF / 100;
let register = u8::try_from(step).unwrap_or(0xBF);
self.write(&[[ES8311_DAC_VOLUME, register]])
}
pub fn set_output(&mut self, on: bool) -> Result<(), Error> {
if on {
self.write(&[[0x12, 0x00], [0x13, 0x10]])
} else {
self.write(&[[0x13, 0x00], [0x12, 0x02]])
}
}
fn write(&mut self, writes: &[[u8; 2]]) -> Result<(), Error> {
use embedded_hal::i2c::I2c as _;
writes
.iter()
.try_for_each(|write| self.device.write(ES8311, write).map_err(|_| Error::Codec))
}
}
pub fn enable_codec(bus: &'static I2cBus) -> Result<Codec, Error> {
let mut codec = Codec {
device: CriticalSectionDevice::new(bus),
};
codec.write(&[
[0x00, 0x80], [0x01, 0xB5], [0x02, 0x18], [0x0D, 0x01], [0x12, 0x00], [0x13, 0x10], [ES8311_DAC_VOLUME, 0xBF], [0x37, 0x08], ])?;
Ok(codec)
}
pub struct BatteryPins {
pub adc: ADC1<'static>,
pub pin: GPIO10<'static>,
}
const BATTERY_SAMPLES: u32 = 16;
pub struct Battery {
adc: Adc<'static, ADC1<'static>, Blocking>,
pin: AdcPin<GPIO10<'static>, ADC1<'static>, AdcCalCurve<ADC1<'static>>>,
}
impl Battery {
pub fn millivolts(&mut self) -> u16 {
let sum: u32 = (0..BATTERY_SAMPLES)
.map(|_| u32::from(self.adc.read_blocking(&mut self.pin)))
.sum();
u16::try_from(sum / BATTERY_SAMPLES * 2).unwrap_or(u16::MAX)
}
pub fn percent(&mut self) -> u8 {
battery_percent(self.millivolts())
}
}
#[must_use]
pub fn battery_percent(millivolts: u16) -> u8 {
let level = (i32::from(millivolts) - 3300) * 100 / 800;
u8::try_from(level.clamp(0, 100)).unwrap_or(100)
}
#[must_use]
pub fn battery(pins: BatteryPins) -> Battery {
let mut config = AdcConfig::new();
let pin =
config.enable_pin_with_cal::<_, AdcCalCurve<ADC1<'static>>>(pins.pin, Attenuation::_11dB);
Battery {
adc: Adc::new(pins.adc, config),
pin,
}
}