diff --git a/ui-app/Cargo.toml b/ui-app/Cargo.toml index 360941e..819fa68 100644 --- a/ui-app/Cargo.toml +++ b/ui-app/Cargo.toml @@ -10,3 +10,6 @@ embedded-graphics = "0.8.1" embedded-graphics-core = { version = "0.4.0", default-features = false } heapless = "0.9.1" hex = { version = "0.4.3", default-features = false } + +[dev-dependencies] +tokio = { version = "1.47.1", features = ["time", "rt", "macros", "sync"] } diff --git a/ui-app/src/lib.rs b/ui-app/src/lib.rs index e6b7677..5274283 100644 --- a/ui-app/src/lib.rs +++ b/ui-app/src/lib.rs @@ -1,6 +1,7 @@ #![no_std] +#![allow(async_fn_in_trait)] -use bitmap_font::{BitmapFont, TextStyle, tamzen::FONT_10x20}; +use bitmap_font::{tamzen::FONT_10x20, BitmapFont, TextStyle}; use core::fmt::Write; use defmt::Format; use embedded_graphics::{ @@ -12,7 +13,6 @@ use embedded_graphics::{ text::Text, }; use heapless::String; -use hex::ToHex; #[derive(Copy, Clone, Debug, PartialEq, Format)] pub enum Key { @@ -73,17 +73,23 @@ pub enum Button { B, } -#[derive(Copy, Clone, Debug, PartialEq, Format)] +pub const MAX_MESSAGE_LEN: usize = 128; + +#[derive(Clone, Debug, PartialEq, Format)] pub enum FromNet { Pong, + AudioFrame(Frame), + Chat(String), } -#[derive(Copy, Clone, Debug, PartialEq, Format)] +#[derive(Clone, Debug, PartialEq, Format)] pub enum ToNet { Ping, + AudioFrame(Frame), + Chat(String), } -#[derive(Copy, Clone, Debug, PartialEq, Format)] +#[derive(Clone, Debug, PartialEq, Format)] pub enum Event { ButtonDown(Button), ButtonUp(Button), @@ -196,19 +202,79 @@ pub trait Eeprom { fn write(&mut self, data: &[u8; 256]); } +pub const FRAME_SIZE: usize = 320; + +#[derive(Clone, Debug, PartialEq, Format)] +pub struct Frame(pub [u16; FRAME_SIZE]); + +impl Default for Frame { + fn default() -> Self { + Self([0; FRAME_SIZE]) + } +} + +pub trait AudioControl { + fn start(&mut self); + fn enable_input(&mut self, enabled: bool); + fn enable_output(&mut self, enabled: bool); +} + +pub trait AudioData { + async fn start(&mut self); + async fn stop(&mut self); + async fn read(&mut self) -> Frame; + async fn write(&mut self, frame: &Frame); + + async fn write_iter(&mut self, samples: impl Iterator) { + let mut send = Frame::default(); + + let mut i_mod = 0; + for (i, sample) in samples.enumerate() { + i_mod = i % FRAME_SIZE; + send.0[i_mod] = sample; + + if i_mod == FRAME_SIZE - 1 { + self.write(&send).await; + } + } + + i_mod += 1; + if i_mod < FRAME_SIZE { + send.0[i_mod..].fill(0); + self.write(&send).await; + } + } +} + pub trait Outputs { + fn button_a_down(&self) -> bool; + fn button_b_down(&self) -> bool; fn status_led(&mut self) -> &mut impl Led; fn screen(&mut self) -> &mut impl DrawTarget; fn net_tx(&mut self) -> &mut impl NetTx; fn eeprom(&mut self) -> impl Eeprom; + fn audio_control(&mut self) -> impl AudioControl; + fn audio_data(&mut self) -> &mut impl AudioData; fn log(&mut self, message: &str); } +pub trait EventSource { + async fn receive(&mut self) -> Option; +} + +#[derive(Debug, PartialEq, Format)] +enum PttState { + Idle, + Transmitting, + Receiving, +} + #[derive(Debug)] pub struct App { a_down: bool, b_down: bool, - message_buffer: String<24>, + ptt_state: PttState, + message_buffer: String, } impl App { @@ -220,6 +286,7 @@ impl App { Self { a_down: false, b_down: false, + ptt_state: PttState::Idle, message_buffer: Default::default(), } } @@ -228,19 +295,10 @@ impl App { // Extinguish the status LED out.status_led().set_color(Color::Black); - // Read the EEPROM, XOR with 0xFF, write it, then read it back - let mut eeprom_data = [0u8; 256]; - out.eeprom().read(&mut eeprom_data); - let hex: heapless::String<1024> = eeprom_data.encode_hex(); - defmt::info!("eeprom before {}", hex); - - eeprom_data.iter_mut().for_each(|x| *x ^= 0xff); - out.eeprom().write(&eeprom_data); - - eeprom_data.fill(0); - out.eeprom().read(&mut eeprom_data); - let hex: heapless::String<1024> = eeprom_data.encode_hex(); - defmt::info!("eeprom after {}", hex); + // Start up the audio interface + out.audio_control().start(); + out.audio_control().enable_input(true); + out.audio_control().enable_output(true); // Draw a test pattern to the screen let rect = out.screen().bounding_box(); @@ -280,91 +338,138 @@ impl App { .unwrap_or_else(|_| panic!("graphics error")); } - pub fn handle(&mut self, event: Event, out: &mut impl Outputs) { - match event { - Event::ButtonDown(button) => match button { - Button::A => { - out.log("button a down"); - self.a_down = true; - out.net_tx().write(&ToNet::Ping); - } - Button::B => { - out.log("button b down"); - self.b_down = true; - } - }, + async fn handle_button_down(&mut self, button: Button, out: &mut impl Outputs) { + match button { + Button::A => { + out.log("button a down"); + self.a_down = true; + out.status_led().set(false, self.a_down, self.b_down); - Event::ButtonUp(button) => match button { - Button::A => { - out.log("button a up"); - self.a_down = false; - } - Button::B => { - out.log("button b up"); - self.b_down = false; - } - }, - - Event::KeyDown(key, value) => { - // Log the key press - let mut msg: String<32> = Default::default(); - write!(&mut msg, "key down: {:?} {:?}", key, value).unwrap(); - out.log(&msg); - - // If this key press is a return, then clear things out - if let Key::Enter = key { - let mut msg: String<64> = Default::default(); - write!(&mut msg, "sending message: {}", self.message_buffer).unwrap(); - out.log(&msg); - - self.message_buffer.clear(); - - let width = out.screen().bounding_box().size.width; - let height = Self::FONT.height(); - Rectangle::new(Point::new(0, 0), Size::new(width, height)) - .into_styled(PrimitiveStyle::with_fill(Self::BACKGROUND)) - .draw(out.screen()) - .unwrap_or_else(|_| panic!("graphics error")); - return; + out.net_tx().write(&ToNet::Ping); + } + Button::B => { + out.log("button b down"); + self.b_down = true; + out.status_led().set(false, self.a_down, self.b_down); + + self.ptt_state = PttState::Transmitting; + out.audio_data().start().await; + + while out.button_b_down() { + out.log("frame"); + let frame = out.audio_data().read().await; + out.net_tx().write(&ToNet::AudioFrame(frame)); } - // Otherwise, if it's a character, add it to the buffer and render it to the screen - let KeyValue::Char(c) = value else { - return; - }; + out.log("button b up (via down)"); + // TODO: Stop the audio buffer. If we have this line right now, it causes a + // DmaUnsynced error. Probably an error in the underlying driver code. + // out.audio_data().stop().await; + self.ptt_state = PttState::Idle; + } + } + } - if self.message_buffer.len() == self.message_buffer.capacity() { - // Ignore any characters beyond the capacity of the message buffer - return; - } + fn handle_button_up(&mut self, button: Button, out: &mut impl Outputs) { + match button { + Button::A => { + out.log("button a up"); + self.a_down = false; + } + Button::B => { + out.log("button b up"); + self.b_down = false; + } + } + + out.status_led().set(false, self.a_down, self.b_down); + } + + fn handle_key_down(&mut self, key: Key, value: KeyValue, out: &mut impl Outputs) { + // Log the key press + let mut msg: String<32> = Default::default(); + write!(&mut msg, "key down: {:?} {:?}", key, value).unwrap(); + out.log(&msg); + + // If this key press is a return, then clear things out + if let Key::Enter = key { + let mut msg: String<{ MAX_MESSAGE_LEN + 20 }> = Default::default(); + write!(&mut msg, "sending message: {}", self.message_buffer).unwrap(); + out.log(&msg); - self.message_buffer.push(c).unwrap(); + out.net_tx() + .write(&ToNet::Chat(self.message_buffer.clone())); - let text = Text::new( - &self.message_buffer, - Point { x: 0, y: 0 }, - TextStyle::new(&Self::FONT, BinaryColor::On), - ); + self.message_buffer.clear(); - let mut binary_display = - BinaryDisplay::new(Rgb565::WHITE, Rgb565::BLACK, out.screen()); - text.draw(&mut binary_display) - .unwrap_or_else(|_| panic!("graphics error")); + let width = out.screen().bounding_box().size.width; + let height = Self::FONT.height(); + Rectangle::new(Point::new(0, 0), Size::new(width, height)) + .into_styled(PrimitiveStyle::with_fill(Self::BACKGROUND)) + .draw(out.screen()) + .unwrap_or_else(|_| panic!("graphics error")); + return; + } + + // Otherwise, if it's a character, add it to the buffer and render it to the screen + let KeyValue::Char(c) = value else { + return; + }; + + if self.message_buffer.len() == self.message_buffer.capacity() { + // Ignore any characters beyond the capacity of the message buffer + return; + } + + self.message_buffer.push(c).unwrap(); + + let text = Text::new( + &self.message_buffer, + Point { x: 0, y: 0 }, + TextStyle::new(&Self::FONT, BinaryColor::On), + ); + + let mut binary_display = BinaryDisplay::new(Rgb565::WHITE, Rgb565::BLACK, out.screen()); + text.draw(&mut binary_display) + .unwrap_or_else(|_| panic!("graphics error")); + } + + fn handle_key_up(&mut self, key: Key, out: &mut impl Outputs) { + let mut msg: heapless::String<32> = Default::default(); + write!(&mut msg, "key up: {:?}", key).unwrap(); + out.log(&msg); + } + + async fn handle_from_net(&mut self, from_net: FromNet, out: &mut impl Outputs) { + match from_net { + FromNet::Pong => { + out.log("pong"); } - Event::KeyUp(key) => { - let mut msg: heapless::String<32> = Default::default(); - write!(&mut msg, "key up: {:?}", key).unwrap(); - out.log(&msg); + FromNet::AudioFrame(frame) if self.ptt_state == PttState::Receiving => { + out.audio_data().start().await; + out.audio_data().write(&frame).await } - Event::FromNet(from_net) => match from_net { - FromNet::Pong => { - out.log("pong"); - } - }, + _ => { + out.log("Dropped out-of-context message from NET chip"); + } } + } - out.status_led().set(false, self.a_down, self.b_down); + pub async fn handle(&mut self, event: Event, out: &mut impl Outputs) { + match event { + Event::ButtonDown(button) => self.handle_button_down(button, out).await, + Event::ButtonUp(button) => self.handle_button_up(button, out), + Event::KeyDown(key, value) => self.handle_key_down(key, value, out), + Event::KeyUp(key) => self.handle_key_up(key, out), + Event::FromNet(from_net) => self.handle_from_net(from_net, out).await, + } + } + + pub async fn run(&mut self, mut events: impl EventSource, mut board: impl Outputs) { + while let Some(event) = events.receive().await { + self.handle(event, &mut board).await; + } } } diff --git a/ui-app/tests/integration.rs b/ui-app/tests/integration.rs index 9885837..cd3bb2e 100644 --- a/ui-app/tests/integration.rs +++ b/ui-app/tests/integration.rs @@ -3,6 +3,8 @@ use ui_app::*; use embedded_graphics::{ draw_target::DrawTarget, pixelcolor::Rgb565, prelude::*, primitives::Rectangle, }; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc; #[derive(Default)] struct MockLed { @@ -40,12 +42,12 @@ impl DrawTarget for MockScreen { #[derive(Default)] struct MockNetTx { - last_to_net: Option, + sent: Vec, } impl NetTx for MockNetTx { fn write(&mut self, to_net: &ToNet) { - self.last_to_net = Some(*to_net); + self.sent.push(to_net.clone()); } } @@ -69,16 +71,79 @@ impl Eeprom for &mut MockEeprom { } } +#[derive(Default)] +struct MockAudioControl { + started: bool, + enable_input: bool, + enable_output: bool, +} + +impl AudioControl for &mut MockAudioControl { + fn start(&mut self) { + self.started = true; + } + + fn enable_input(&mut self, enabled: bool) { + self.enable_input = enabled; + } + + fn enable_output(&mut self, enabled: bool) { + self.enable_output = enabled; + } +} + +#[derive(Default)] +struct MockAudioData { + started: bool, + read_count: u16, + frames: Option>, + read: Vec, + written: Vec, +} + +impl AudioData for MockAudioData { + async fn start(&mut self) { + self.started = true; + } + + async fn stop(&mut self) { + self.started = true; + } + + async fn read(&mut self) -> Frame { + let frame = self.frames.as_mut().unwrap().recv().await.unwrap(); + self.read_count = self.read_count.wrapping_add(1); + self.read.push(frame.clone()); + frame + } + + async fn write(&mut self, frame: &Frame) { + self.written.push(frame.clone()); + } +} + #[derive(Default)] struct MockOutputs { + button_a_down: Arc>, + button_b_down: Arc>, status_led: MockLed, screen: MockScreen, net_tx: MockNetTx, eeprom: MockEeprom, last_message: String, + audio_control: MockAudioControl, + audio_data: MockAudioData, } impl Outputs for MockOutputs { + fn button_a_down(&self) -> bool { + *self.button_a_down.lock().unwrap() + } + + fn button_b_down(&self) -> bool { + *self.button_b_down.lock().unwrap() + } + fn status_led(&mut self) -> &mut impl Led { &mut self.status_led } @@ -95,13 +160,21 @@ impl Outputs for MockOutputs { &mut self.eeprom } + fn audio_control(&mut self) -> impl AudioControl { + &mut self.audio_control + } + + fn audio_data(&mut self) -> &mut impl AudioData { + &mut self.audio_data + } + fn log(&mut self, message: &str) { self.last_message = message.into(); } } -#[test] -fn default_black() { +#[tokio::test] +async fn default_black() { let mut outputs = MockOutputs::default(); let mut app = App::new(); app.start(&mut outputs); @@ -109,97 +182,215 @@ fn default_black() { assert_eq!(outputs.status_led.color, Some(Color::Black)); } -#[test] -fn individual_buttons() { - fn individual_button(button: Button, color: Color) { +#[tokio::test] +async fn individual_buttons() { + async fn individual_button(button: Button, color: Color) { let mut outputs = MockOutputs::default(); let mut app = App::new(); app.start(&mut outputs); assert_eq!(outputs.status_led.color, Some(Color::Black)); // Up should have no effect - app.handle(Event::ButtonUp(button), &mut outputs); + app.handle(Event::ButtonUp(button), &mut outputs).await; assert_eq!(outputs.status_led.color, Some(Color::Black)); // Pushing the button should illuminate the LED - app.handle(Event::ButtonDown(button), &mut outputs); + app.handle(Event::ButtonDown(button), &mut outputs).await; assert_eq!(outputs.status_led.color, Some(color)); // Down should be idempotent - app.handle(Event::ButtonDown(button), &mut outputs); + app.handle(Event::ButtonDown(button), &mut outputs).await; assert_eq!(outputs.status_led.color, Some(color)); // Up should extinguish the LED - app.handle(Event::ButtonUp(button), &mut outputs); + app.handle(Event::ButtonUp(button), &mut outputs).await; assert_eq!(outputs.status_led.color, Some(Color::Black)); // Up should be idempotent - app.handle(Event::ButtonUp(button), &mut outputs); + app.handle(Event::ButtonUp(button), &mut outputs).await; assert_eq!(outputs.status_led.color, Some(Color::Black)); } - individual_button(Button::A, Color::Green); - individual_button(Button::B, Color::Blue); + individual_button(Button::A, Color::Green).await; + individual_button(Button::B, Color::Blue).await; } -#[test] -fn buttons_compose() { +#[tokio::test] +async fn buttons_compose() { let mut outputs = MockOutputs::default(); let mut app = App::new(); app.start(&mut outputs); assert_eq!(outputs.status_led.color, Some(Color::Black)); - app.handle(Event::ButtonDown(Button::B), &mut outputs); + app.handle(Event::ButtonDown(Button::B), &mut outputs).await; assert_eq!(outputs.status_led.color, Some(Color::Blue)); - app.handle(Event::ButtonDown(Button::A), &mut outputs); + app.handle(Event::ButtonDown(Button::A), &mut outputs).await; assert_eq!(outputs.status_led.color, Some(Color::Cyan)); - app.handle(Event::ButtonUp(Button::B), &mut outputs); + app.handle(Event::ButtonUp(Button::B), &mut outputs).await; assert_eq!(outputs.status_led.color, Some(Color::Green)); - app.handle(Event::ButtonUp(Button::A), &mut outputs); + app.handle(Event::ButtonUp(Button::A), &mut outputs).await; assert_eq!(outputs.status_led.color, Some(Color::Black)); } -#[test] -fn key_logging() { +#[tokio::test] +async fn key_logging() { let mut outputs = MockOutputs::default(); let mut app = App::new(); app.start(&mut outputs); assert_eq!(outputs.last_message, ""); - app.handle(Event::KeyDown(Key::A, KeyValue::Char('a')), &mut outputs); + app.handle(Event::KeyDown(Key::A, KeyValue::Char('a')), &mut outputs) + .await; assert_eq!(outputs.last_message, "key down: A Char('a')"); - app.handle(Event::KeyUp(Key::A), &mut outputs); + app.handle(Event::KeyUp(Key::A), &mut outputs).await; assert_eq!(outputs.last_message, "key up: A"); } -#[test] -fn message_buffer_tolerates_overflow() { +#[tokio::test] +async fn message_buffer_tolerates_overflow() { let mut outputs = MockOutputs::default(); let mut app = App::new(); app.start(&mut outputs); for _i in 0..1000 { - app.handle(Event::KeyDown(Key::A, KeyValue::Char('a')), &mut outputs); + app.handle(Event::KeyDown(Key::A, KeyValue::Char('a')), &mut outputs) + .await; } - app.handle(Event::KeyDown(Key::Enter, KeyValue::Enter), &mut outputs); + app.handle(Event::KeyDown(Key::Enter, KeyValue::Enter), &mut outputs) + .await; assert_eq!( outputs.last_message, "sending message: aaaaaaaaaaaaaaaaaaaaaaaa" ); } -#[test] -fn button_a_sends_ping() { +#[tokio::test] +async fn button_a_sends_ping() { let mut outputs = MockOutputs::default(); let mut app = App::new(); app.start(&mut outputs); - app.handle(Event::ButtonDown(Button::A), &mut outputs); - assert_eq!(outputs.net_tx.last_to_net, Some(ToNet::Ping)); + app.handle(Event::ButtonDown(Button::A), &mut outputs).await; + assert_eq!(&outputs.net_tx.sent, &[ToNet::Ping]); +} + +#[tokio::test] +async fn receive_ptt() { + let mut outputs = MockOutputs::default(); + let mut app = App::new(); + app.start(&mut outputs); + assert_eq!(outputs.audio_control.started, true); + + // Signal the start of audio + app.handle(Event::FromNet(FromNet::AudioStart), &mut outputs) + .await; + assert_eq!(outputs.audio_control.enable_output, true); + assert_eq!(outputs.audio_data.started, true); + + // Send a few frames + let mut frames = vec![Frame::default(); 3]; + for (i, frame) in frames.iter_mut().enumerate() { + frame.0.fill(i as u16); + let from_net = Event::FromNet(FromNet::AudioFrame(frame.clone())); + app.handle(from_net, &mut outputs).await; + } + + assert_eq!(outputs.audio_data.written, frames); + + // Signal the end of audio + app.handle(Event::FromNet(FromNet::AudioEnd), &mut outputs) + .await; + assert_eq!(outputs.audio_control.started, true); + assert_eq!(outputs.audio_control.enable_output, false); + // TODO re-enable + // assert_eq!(outputs.audio_data.start, false); + + // Verify that out-of-context audio gets dropped + let from_net = Event::FromNet(FromNet::AudioFrame(frames[0].clone())); + app.handle(from_net, &mut outputs).await; + + assert_eq!(outputs.audio_data.written, frames); + assert_eq!( + outputs.last_message, + "Dropped out-of-context message from NET chip" + ); +} + +struct EventReceiver(mpsc::Receiver); + +impl EventSource for EventReceiver { + async fn receive(&mut self) -> Option { + self.0.recv().await + } +} + +#[tokio::test] +async fn transmit_ptt() { + let mut outputs = MockOutputs::default(); + let mut app = App::new(); + + let (event_send, event_recv) = mpsc::channel(5); + let (frame_send, frame_recv) = mpsc::channel(5); + + outputs.audio_data.frames = Some(frame_recv); + + // Synthesize some frames + let frames: Vec = [1, 2, 3] + .iter() + .map(|n| { + let mut frame = Frame::default(); + frame.0.fill(*n); + frame + }) + .collect(); + + // Start the app + app.start(&mut outputs); + + // Drive the app from a task + let frames_clone = frames.clone(); + let button_b_down = outputs.button_b_down.clone(); + tokio::spawn(async move { + // Push Button B + *button_b_down.lock().unwrap() = true; + event_send.send(Event::ButtonDown(Button::B)).await.unwrap(); + + // Send all but the last frame + for frame in frames_clone.iter().take(frames_clone.len() - 1) { + frame_send.send(frame.clone()).await.unwrap(); + } + + // Brief pause to make sure the frames get processed + tokio::time::sleep(core::time::Duration::from_millis(10)).await; + + // Release button B + *button_b_down.lock().unwrap() = false; + + // Send one more frame to get out of deadlock + let frame = frames_clone.last().unwrap().clone(); + frame_send.send(frame).await.unwrap(); + + // End the app + drop(event_send); + }); + + // Run the app + let mut events = EventReceiver(event_recv); + while let Some(event) = events.receive().await { + app.handle(event, &mut outputs).await; + } + + // Verify that the frames were delivered to NET, bracketed by Start/End + assert_eq!(outputs.audio_data.read, frames); + + let mut to_net: Vec = frames.into_iter().map(|f| ToNet::AudioFrame(f)).collect(); + to_net.insert(0, ToNet::AudioStart); + to_net.push(ToNet::AudioEnd); + assert_eq!(outputs.net_tx.sent, to_net); } diff --git a/ui-stm32/Cargo.toml b/ui-stm32/Cargo.toml index 84a5b2f..2096bdc 100644 --- a/ui-stm32/Cargo.toml +++ b/ui-stm32/Cargo.toml @@ -1,5 +1,5 @@ [package] -edition = "2021" +edition = "2024" name = "ui-stm32" version = "0.1.0" license = "MIT OR Apache-2.0" @@ -7,6 +7,8 @@ license = "MIT OR Apache-2.0" [features] ev12 = [] ev13 = [] +tx-demo = [] +rx-demo = [] default = ["ev13"] [dependencies] @@ -14,16 +16,20 @@ ui-app = { path = "../ui-app" } cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] } cortex-m-rt = { version = "0.7.0", features = ["paint-stack"] } +cortex-m-stack = { version = "0.1.0", path = "../cortex-m-stack" } defmt = "1.0.1" defmt-rtt = "1.0.0" - -embassy-executor = { version = "0.8.0", features = ["arch-cortex-m", "executor-thread", "executor-interrupt", "defmt"] } -embassy-futures = { version = "0.1.1", features = ["defmt"] } -embassy-stm32 = { version = "0.3.0", features = ["defmt", "stm32f405rg", "memory-x", "time-driver-tim1", "exti", "chrono"] } -embassy-time = { version = "0.5.0", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] } panic-probe = { version = "1.0.0", features = ["print-defmt"] } -embassy-sync = { version = "0.7.1", features = ["defmt"] } + +# Note: All of these must either point to a local clone or to published +# versions. If you try to mix them, weird errors occur. +embassy-executor = { path = "../../embassy/embassy-executor", version = "0.9.1", features = ["arch-cortex-m", "executor-thread", "executor-interrupt", "defmt"] } +embassy-futures = { path = "../../embassy/embassy-futures", version = "0.1.1", features = ["defmt"] } +embassy-stm32 = { path = "../../embassy/embassy-stm32", version = "0.4.0", features = ["defmt", "stm32f405rg", "memory-x", "time-driver-tim1", "exti", "chrono", "unstable-pac"] } +embassy-time = { path = "../../embassy/embassy-time", version = "0.5.0", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] } +embassy-sync = { path = "../../embassy/embassy-sync", version = "0.7.1", features = ["defmt"] } + num_enum = { version = "0.7.4", default-features = false } hex = { version = "0.4.3", default-features = false } heapless = { version = "0.9.1", features = ["defmt"] } diff --git a/ui-stm32/src/board/audio.rs b/ui-stm32/src/board/audio.rs new file mode 100644 index 0000000..d682cc2 --- /dev/null +++ b/ui-stm32/src/board/audio.rs @@ -0,0 +1,745 @@ +#![allow(dead_code)] // No need to use all of the fields on the device + +use embassy_stm32::{ + i2c::{I2c, Master}, + i2s::{self, I2S}, + mode::Blocking, +}; + +use ui_app::Frame; + +pub struct AudioData<'a> { + i2s: I2S<'a, u16>, +} + +impl<'a> From> for AudioData<'a> { + fn from(i2s: I2S<'a, u16>) -> Self { + Self { i2s } + } +} + +impl<'a> ui_app::AudioData for AudioData<'a> { + async fn start(&mut self) { + self.i2s.start(); + } + + async fn stop(&mut self) { + self.i2s.stop().await; + } + + async fn read(&mut self) -> Frame { + let zero = Frame::default(); + let mut out = Frame::default(); + + // Loop to retry on overrun failures + loop { + match self.i2s.read_write(&zero.0, &mut out.0).await { + Ok(_) => return out, + Err(i2s::Error::Overrun) => { + self.i2s.clear(); + // and retry + } + Err(e) => defmt::panic!("read error: {:?}", e), + } + } + } + + async fn write(&mut self, frame: &Frame) { + let mut ignore = Frame::default(); + self.i2s.read_write(&frame.0, &mut ignore.0).await.unwrap(); + } +} + +type I2C = I2c<'static, Blocking, Master>; +const I2C_ADDR: u8 = 0x1a; + +pub struct AudioControl<'a> { + i2c: &'a mut I2c<'static, Blocking, Master>, + regs: Registers, +} + +impl<'a> ui_app::AudioControl for AudioControl<'a> { + fn start(&mut self) { + // TODO don't enable inputs and outputs here + self.init() + } + + fn enable_input(&mut self, _enabled: bool) { + // TODO enable inputs; done in start() right now + } + + fn enable_output(&mut self, _enabled: bool) { + // TODO enable outputs; done in start() right now + } +} + +impl<'a> AudioControl<'a> { + const VALUE_MASK: u16 = 0x1ff; + + pub fn new(i2c: &'a mut I2c<'static, Blocking, Master>) -> Self { + Self { + i2c, + regs: Registers::default(), + } + } + + /// Reset the device and enable baseline devices + fn power_on(&mut self) { + // address = 0x0f, value = 0b0_0000_0000 + const RESET_SIGNAL: [u8; 2] = [0x1e, 0x00]; + self.i2c.blocking_write(I2C_ADDR, &RESET_SIGNAL).unwrap(); + + self.regs.modify(&mut self.i2c, |r| { + r.set(PowerMgmt1VrefEnable(true)); + r.set(PowerMgmt1VmidSelect(0b01)); + r.set(MicrophoneBiasEnable(true)); + }); + } + + // TODO Move all of these methods to RegisterView + /// Enable/disable the left input path + /// + /// In our configuration, on the left input path is ever connected to a microphone. So this + /// method also ensures that the right input path is disabled, and there is no parallel + /// `right_input_path()`. + pub fn left_input_path(&mut self, enable: bool) { + self.regs.modify(&mut self.i2c, |r| { + // Power on/off input devices + r.set(PowerMgmt1AinLeftEnable(enable)); + r.set(LeftMicEnable(enable)); + + // Disable left input 3 (disconnected) + r.set(Linput3Boost(0b000)); + r.set(LeftInput3ToOutputMixer(false)); + r.set(LeftInput3ToOutputMixerVolume(0b000)); + r.set(LeftInput3ToNonInverting(false)); + + // Disable the right side inputs (disconnected) + r.set(RightInputAnalogMute(true)); + r.set(Rinput2Boost(0b000)); + r.set(Rinput3Boost(0b000)); + r.set(RightInput3ToOutputMixer(false)); + r.set(RightInput3ToOutputMixerVolume(0b000)); + + // Enable the left side + r.set(LeftInput1ToInverting(true)); + r.set(LeftInput2ToNonInverting(true)); + r.set(LeftInputToBoost(true)); + r.set(LeftInputAnalogMute(false)); + + // Set volumes + r.set(Linput2Boost(0b000)); // mute + r.set(LeftBoostGain(0b00)); // 0dB + r.set(InputPgaVolumeUpdate(true)); + r.set(LeftPgaVolume(0b01_0111)); // 0dB + }); + } + + /// Enable/disable the left output path + // TODO actual enable/disable + pub fn left_output_path(&mut self, enable: bool) { + self.regs.modify(&mut self.i2c, |r| { + // Power on/off output devices + r.set(LeftOutput1Enable(enable)); + r.set(LeftOutputMixEnable(enable)); + + // Left input 3 bypass and speaker output are always disabled + r.set(LeftInput3ToOutputMixer(false)); + r.set(LeftInput3ToOutputMixerVolume(0b000)); + r.set(LeftSpeakerVolumeUpdate(true)); + r.set(LeftSpeakerVolume(0b000_0000)); + + // Set volumes + // TODO factor this out + r.set(HeadphoneOutVolumeUpdate(true)); + r.set(LeftHeadphoneVolume(0b111_1111)); // 6dB + }); + } + + /// Enable/disable the right output path + pub fn right_output_path(&mut self, enable: bool) { + self.regs.modify(&mut self.i2c, |r| { + // Power on/off output devices + r.set(RightOutput1Enable(enable)); + r.set(RightOutputMixEnable(enable)); + + // Right input 3 bypass and speaker output are always disabled + r.set(RightInput3ToOutputMixer(false)); + r.set(RightInput3ToOutputMixerVolume(0b000)); + r.set(RightSpeakerVolumeUpdate(true)); + r.set(RightSpeakerVolume(0b000_0000)); + + // Set volumes + // TODO factor this out + r.set(HeadphoneOutVolumeUpdateRight(true)); + r.set(RightHeadphoneVolume(0b111_1111)); // 6dB + }); + } + + /// Enable/disable the left analog bypass + /// + /// This method connects the left input directly to the left output, bypassing the ADC and DAC. + // TODO actually enable / disable + pub fn left_analog_bypass(&mut self, _enable: bool) { + self.regs.modify(&mut self.i2c, |r| { + // Connect left input boost to left output mix + r.set(LeftBoostToLeftOutputMix(true)); + + // Set volumes + // TODO Factor this out + r.set(LeftBoostToLeftOutputMixVolume(0b000)); // 0dB + }) + } + + /// Enable/disable digital loopback + /// + /// When this is enabled, the output of the ADCs is fed directly to the DACs. + pub fn digital_loopback(&mut self, enable: bool) { + self.regs.modify(&mut self.i2c, |r| { + // Set ADC to use the DACLRC clock + r.set(AdcLrcPinSelect(enable)); + + // Enable digital loopback + r.set(DigitalLoopback(enable)); + }) + } + + // TODO provide I2S configuration here + // XXX Do all the ADCs / DACs need to be turned on here? Or at least both channels? + /// Enable the I2S interface + pub fn enable_i2s(&mut self) { + self.regs.modify(&mut self.i2c, |r| { + // Set master mode, I2S, 16-bit words + r.set(AudioInterfaceMasterMode(true)); + r.set(AudioWordLength(0b00)); + r.set(AudioFormat(0b10)); + + // Set clocks for 8khz + // XXX This needs to be done whenever DAC or ADC is done. Maybe move to a core init + // function, or one that is shared between the DAC and ADC methods. + r.set(PllEnable(true)); + r.set(MasterClockDisable(false)); + r.set(PllN(0b1000)); + r.set(PllKMsb(0b0011_0001)); + r.set(PllKMid(0b0010_0110)); + r.set(PllKLsb(0b1110_1001)); + r.set(Adc1Divider(0b110)); + r.set(DacDivider(0b110)); + r.set(SysClkDiv(0b00)); + r.set(ClockSelect(true)); + r.set(BclkFrequency(0b1100)); + r.set(ClassDSysclkDivider(0b111)); + r.set(AdcAlcSampleRateSelect(0b101)); + }); + } + + /// Enable/disable the left DAC + pub fn left_dac(&mut self, enable: bool) { + self.regs.modify(&mut self.i2c, |r| { + // Power on and connect + r.set(LeftDacEnable(enable)); + r.set(LeftDacToOutputMixer(enable)); + + // Set volume + // TODO factor this out + r.set(DacVolumeUpdate(true)); + r.set(LeftDacDigitalVolume(0b1111_1111)); // 0dB + }); + } + + /// Enable/disable the right DAC + pub fn right_dac(&mut self, enable: bool) { + self.regs.modify(&mut self.i2c, |r| { + // Power on and connect + r.set(RightDacEnable(enable)); + r.set(RightDacToOutputMixer(enable)); + + // Set volume + // TODO factor this out + r.set(DacVolumeUpdateRight(true)); + r.set(RightDacDigitalVolume(0b1111_1111)); // 0dB + }) + } + + /// Enable/disable the left ADC + /// + /// Since the right input is always disconnected, we never turn on the right ADC + pub fn left_adc(&mut self, enable: bool) { + self.regs.modify(&mut self.i2c, |r| { + // Power on + r.set(PowerMgmt1EnableAdcLeft(enable)); + + // Disable the high pass filter + r.set(AdcHighPassDisable(true)); + }); + } + + /// Configure DAC behavior + /// + /// * `mono_mix`: Mix the left and right DAC outputs before sending them to the mixer + /// * `soft_mute_mode`: Avoid pops by making the edges of soft mute soft + /// * `mute`: Mute the DACs + // XXX(RLB) We probably want to at least factor out the mute enable/disable + pub fn configure_dac(&mut self, mono_mix: bool, soft_mute_mode: bool, mute: bool) { + self.regs.modify(&mut self.i2c, |r| { + // Mix the left and right DAC outputs before sending them to the mixer + r.set(DacMonoMix(mono_mix)); + + // Make DAC mute softer + r.set(DacSoftMuteMode(soft_mute_mode)); + + // Mute DAC outputs + r.set(DacSoftMuteEnable(mute)); + }) + } + + pub fn init(&mut self) { + self.power_on(); + + self.left_input_path(true); + self.left_output_path(true); + self.left_adc(true); + self.left_dac(true); + self.configure_dac(true, true, false); + self.enable_i2s(); + } +} + +pub type RegAddr = u8; + +trait ToFromU16 { + fn from_u16(x: u16) -> Self; + fn into_u16(self) -> u16; +} + +impl ToFromU16 for bool { + fn from_u16(x: u16) -> Self { + x != 0 + } + + fn into_u16(self) -> u16 { + if self { + 1 + } else { + 0 + } + } +} + +impl ToFromU16 for u8 { + fn from_u16(x: u16) -> Self { + x as Self + } + + fn into_u16(self) -> u16 { + self.into() + } +} + +impl ToFromU16 for u16 { + fn from_u16(x: u16) -> Self { + x + } + + fn into_u16(self) -> u16 { + self + } +} + +#[derive(Clone)] +pub struct Registers { + regs: [u16; 56], +} + +impl Default for Registers { + fn default() -> Self { + let init: [(u8, u16); 56] = [ + (0x00, 0b0_1001_0111), // R0 Left Input volume + (0x01, 0b0_1001_0111), // R1 Right Input volume + (0x02, 0b0_0000_0000), // R2 LOUT1 volume + (0x03, 0b0_0000_0000), // R3 ROUT1 volume + (0x04, 0b0_0000_0000), // R4 Clocking (1) + (0x05, 0b0_0000_1000), // R5 ADC & DAC Control (1) + (0x06, 0b0_0000_0000), // R6 ADC & DAC Control (2) + (0x07, 0b0_0000_1010), // R7 Audio Interface + (0x08, 0b1_1100_0000), // R8 Clocking (2) + (0x09, 0b0_0000_0000), // R9 Audio Interface + (0x0A, 0b0_1111_1111), // R10 Left DAC volume + (0x0B, 0b0_1111_1111), // R11 Right DAC volume + (0x0C, 0b0_0000_0000), // R12 Reserved + (0x0D, 0b0_0000_0000), // R13 Reserved + (0x0E, 0b0_0000_0000), // R14 Reserved + (0x0F, 0b0_0000_0000), // R15 Reset (not reset) + (0x10, 0b0_0000_0000), // R16 3D control + (0x11, 0b0_0111_1011), // R17 ALC1 + (0x12, 0b1_0000_0000), // R18 ALC2 + (0x13, 0b0_0011_0010), // R19 ALC3 + (0x14, 0b0_0000_0000), // R20 Noise Gate + (0x15, 0b0_1100_0011), // R21 Left ADC volume + (0x16, 0b0_1100_0011), // R22 Right ADC volume + (0x17, 0b1_1100_0000), // R23 Additional control (1) + (0x18, 0b0_0000_0000), // R24 Additional control (2) + (0x19, 0b0_0000_0000), // R25 Power Mgmt (1) + (0x1A, 0b0_0000_0000), // R26 Power Mgmt (2) + (0x1B, 0b0_0000_0000), // R27 Additional Control (3) + (0x1C, 0b0_0000_0000), // R28 Anti-pop 1 + (0x1D, 0b0_0000_0000), // R29 Anti-pop 2 + (0x1E, 0b0_0000_0000), // R30 Reserved + (0x1F, 0b0_0000_0000), // R31 Reserved + (0x20, 0b1_0000_0000), // R32 ADCL signal path + (0x21, 0b1_0000_0000), // R33 ADCR signal path + (0x22, 0b0_0101_0000), // R34 Left out Mix (1) + (0x23, 0b0_0101_0000), // R35 Reserved + (0x24, 0b0_0101_0000), // R36 Reserved + (0x25, 0b0_0101_0000), // R37 Right out Mix (2) + (0x26, 0b0_0000_0000), // R38 Mono out Mix (1) + (0x27, 0b0_0000_0000), // R39 Mono out Mix (2) + (0x28, 0b0_0000_0000), // R40 LOUT2 volume + (0x29, 0b0_0000_0000), // R41 ROUT2 volume + (0x2A, 0b0_0100_0000), // R42 MONOOUT volume + (0x2B, 0b0_0000_0000), // R43 Input boost mixer (1) + (0x2C, 0b0_0000_0000), // R44 Input boost mixer (2) + (0x2D, 0b0_0101_0000), // R45 Bypass (1) + (0x2E, 0b0_0101_0000), // R46 Bypass (2) + (0x2F, 0b0_0000_0000), // R47 Power Mgmt (3) + (0x30, 0b0_0000_0010), // R48 Additional Control (4) + (0x31, 0b0_0011_0111), // R49 Class D Control (1) + (0x32, 0b0_0100_1101), // R50 Reserved + (0x33, 0b0_1000_0000), // R51 Class D Control (3) + (0x34, 0b0_0000_1000), // R52 PLL N + (0x35, 0b0_0011_0001), // R53 PLL K1 + (0x36, 0b0_0010_0110), // R54 PLL K2 + (0x37, 0b0_1110_1001), // R55 PLL K3 + ]; + + let mut regs = [0u16; 56]; + for (i, (addr, val)) in init.iter().enumerate() { + regs[i] = ((*addr as u16) << 9) | (val & 0x01FF); + } + + Self { regs } + } +} + +impl Registers { + fn modify(&mut self, i2c: &mut I2C, f: F) + where + F: FnOnce(&mut RegisterView), + { + let mut r = RegisterView::new(&mut self.regs); + f(&mut r); + + let modified = r + .modified + .iter() + .enumerate() + .filter_map(|(i, m)| m.then_some(i)); + for i in modified { + i2c.blocking_write(I2C_ADDR, &self.regs[i].to_be_bytes()) + .unwrap(); + } + } +} + +pub struct RegisterView<'a> { + regs: &'a mut [u16; 56], + modified: [bool; 56], +} + +impl<'a> RegisterView<'a> { + pub const fn new(regs: &'a mut [u16; 56]) -> Self { + Self { + regs, + modified: [false; 56], + } + } + + // Generic getter for any defined field. + pub fn get(&self) -> F::Value { + let reg = self.regs[F::ADDR as usize]; + F::get(reg) + } + + // Generic setter for any defined field; asserts on value width and records modification. + pub fn set(&mut self, val: F) { + let idx: usize = F::ADDR.into(); + let old = self.regs[idx]; + let new = val.set(old); + if new != old { + self.regs[idx] = new; + self.modified[idx] = true; + } + } +} + +pub trait FieldAccess { + const ADDR: RegAddr; + const OFFSET: u8; + const WIDTH: u8; + const MAX: u16 = (1 << Self::WIDTH) - 1; + const MASK: u16 = Self::MAX << Self::OFFSET; + type Value; + + fn new(val: Self::Value) -> Self; + fn get(regval: u16) -> Self::Value; + fn set(&self, regval: u16) -> u16; +} + +macro_rules! define_field { + ($name:ident, $addr:expr, $offset:expr, $width:expr, $val:ty) => { + pub struct $name($val); + + impl FieldAccess for $name { + const ADDR: RegAddr = $addr; + const OFFSET: u8 = $offset; + const WIDTH: u8 = $width; + type Value = $val; + + #[inline] + fn new(val: $val) -> Self { + Self(val) + } + + #[inline] + fn get(regval: u16) -> $val { + <$val>::from_u16((regval & Self::MASK) >> Self::OFFSET) + } + + #[inline] + fn set(&self, regval: u16) -> u16 { + let val = self.0.into_u16(); + assert!( + val <= Self::MAX, + concat!(stringify!($name), ": value out of range"), + ); + let mask = ((1u16 << Self::WIDTH) - 1) << Self::OFFSET; + (regval & !mask) | ((val << Self::OFFSET) & mask) + } + } + }; +} + +// XXX(RLB) A first draft of these controls was generated by ChatGPT. I have verified their +// correctness for the registers that are touched in AudioControl::init(). If you're going to use +// any other registers, you should verify that they match the data sheet. + +// R0 (0x00) Left Input Volume +define_field!(InputPgaVolumeUpdate, 0x00, 8, 1, bool); +define_field!(LeftInputAnalogMute, 0x00, 7, 1, bool); +define_field!(LeftPgaZeroCross, 0x00, 6, 1, bool); +define_field!(LeftPgaVolume, 0x00, 0, 6, u8); + +// R1 (0x01) Right Input Volume +define_field!(InputPgaVolumeUpdateRight, 0x01, 8, 1, bool); +define_field!(RightInputAnalogMute, 0x01, 7, 1, bool); +define_field!(RightPgaZeroCross, 0x01, 6, 1, bool); +define_field!(RightPgaVolume, 0x01, 0, 6, u8); + +// R2 (0x02) LOUT1 volume +define_field!(HeadphoneOutVolumeUpdate, 0x02, 8, 1, bool); +define_field!(LeftOutZeroCross, 0x02, 7, 1, bool); +define_field!(LeftHeadphoneVolume, 0x02, 0, 7, u8); + +// R3 (0x03) ROUT1 volume +define_field!(HeadphoneOutVolumeUpdateRight, 0x03, 8, 1, bool); +define_field!(RightOutZeroCross, 0x03, 7, 1, bool); +define_field!(RightHeadphoneVolume, 0x03, 0, 7, u8); + +// R4 (0x04) Clocking (1) +define_field!(Adc1Divider, 0x04, 6, 3, u8); +define_field!(DacDivider, 0x04, 3, 3, u8); +define_field!(SysClkDiv, 0x04, 1, 2, u8); +define_field!(ClockSelect, 0x04, 0, 1, bool); + +// R5 (0x05) ADC & DAC Control (CTR1) +define_field!(Dac6dBAttenuateEnable, 0x05, 7, 1, bool); +define_field!(AdcPolarityControl, 0x05, 5, 2, u8); +define_field!(DacSoftMuteEnable, 0x05, 3, 1, bool); +define_field!(DeEmphasisControl, 0x05, 3, 2, u8); +define_field!(AdcHighPassDisable, 0x05, 0, 1, bool); + +// R6 (0x06) ADC & DAC Control (CTR2) +define_field!(DacSlopeMode, 0x06, 1, 1, bool); +define_field!(DacSoftMuteRampSlow, 0x06, 2, 1, bool); +define_field!(DacSoftMuteMode, 0x06, 3, 1, bool); + +// R7 (0x07) Audio Interface +define_field!(AdcLeftRightSwap, 0x07, 8, 1, bool); +define_field!(BclkInvert, 0x07, 7, 1, bool); +define_field!(AudioInterfaceMasterMode, 0x07, 6, 1, bool); +define_field!(DacLeftRightSwap, 0x07, 5, 1, bool); +define_field!(LrcPolarityOrDspMode, 0x07, 4, 1, bool); +define_field!(AudioWordLength, 0x07, 2, 2, u8); +define_field!(AudioFormat, 0x07, 0, 2, u8); + +// R8 (0x08) Clocking (2) +define_field!(ClassDSysclkDivider, 0x08, 6, 3, u8); +define_field!(BclkFrequency, 0x08, 0, 4, u8); + +// R9 (0x09) Audio Interface +define_field!(AdcLrcPinSelect, 0x09, 6, 1, bool); +define_field!(WordLength8, 0x09, 5, 1, bool); +define_field!(DacCompanding, 0x09, 3, 2, u8); +define_field!(AdcCompanding, 0x09, 1, 2, u8); +define_field!(DigitalLoopback, 0x09, 0, 1, bool); + +// R10 (0x0A) Left DAC Volume +define_field!(DacVolumeUpdate, 0x0A, 8, 1, bool); +define_field!(LeftDacDigitalVolume, 0x0A, 0, 8, u8); + +// R11 (0x0B) Right DAC Volume +define_field!(DacVolumeUpdateRight, 0x0B, 8, 1, bool); +define_field!(RightDacDigitalVolume, 0x0B, 0, 8, u8); + +// R12-R14 (0x0C-0x0E) Reserved + +// R15 (0x0F) Reset register (special behavior). + +// R16 (0x10) 3D control +define_field!(ThreeDEnable, 0x10, 2, 1, bool); +define_field!(ThreeDLowerCutSelect, 0x10, 1, 1, bool); +define_field!(ThreeDUpperCutSelect, 0x10, 0, 1, bool); +define_field!(ThreeDControlRaw, 0x10, 0, 9, u16); + +// R17 (0x11) ALC1 +// R18 (0x12) ALC2 +// R19 (0x12) ALC3 + +// R20 (0x14) Noise gate +define_field!(NoiseGateThreshold, 0x14, 3, 5, u8); +define_field!(NoiseGateEnable, 0x14, 0, 1, bool); + +// R21 (0x15) Left ADC volume +define_field!(LeftAdcDigitalVolume, 0x15, 0, 8, u8); +define_field!(AdcVolumeUpdateLeft, 0x15, 8, 1, bool); + +// R22 (0x16) Right ADC volume +define_field!(RightAdcDigitalVolume, 0x16, 0, 8, u8); +define_field!(AdcVolumeUpdateRight, 0x16, 8, 1, bool); + +// R23 (0x17) Additional Control (1) +define_field!(ThermalShutDownEnable, 0x17, 8, 1, bool); +define_field!(AnalogBiasOptimisation, 0x17, 6, 2, u8); +define_field!(DacMonoMix, 0x17, 4, 1, bool); +define_field!(AdcDataOutputSelect, 0x17, 2, 2, u8); +define_field!(TimeoutClockSelect, 0x17, 1, 1, bool); +define_field!(TimeoutEnable, 0x17, 0, 1, bool); + +// R24 (0x18) Additional Control (2) +define_field!(AdclrcDaclrcMode, 0x18, 2, 1, bool); +define_field!(Reg24Raw, 0x18, 0, 9, u16); + +// R25 (0x19) Power Management (1) +define_field!(PowerMgmt1VmidSelect, 0x19, 7, 2, u8); +define_field!(PowerMgmt1VrefEnable, 0x19, 6, 1, bool); +define_field!(PowerMgmt1AinLeftEnable, 0x19, 5, 1, bool); +define_field!(PowerMgmt1AinRightEnable, 0x19, 4, 1, bool); +define_field!(PowerMgmt1EnableAdcLeft, 0x19, 3, 1, bool); +define_field!(PowerMgmt1EnableAdcRight, 0x19, 2, 1, bool); +define_field!(MicrophoneBiasEnable, 0x19, 1, 1, bool); +define_field!(MasterClockDisable, 0x19, 0, 1, bool); + +// R26 (0x1A) Power Management (2) +define_field!(LeftDacEnable, 0x1A, 8, 1, bool); +define_field!(RightDacEnable, 0x1A, 7, 1, bool); +define_field!(LeftOutput1Enable, 0x1A, 6, 1, bool); +define_field!(RightOutput1Enable, 0x1A, 5, 1, bool); +define_field!(LeftSpeakerEnable, 0x1A, 4, 1, bool); +define_field!(RightSpeakerEnable, 0x1A, 3, 1, bool); +define_field!(Out3Enable, 0x1A, 1, 1, bool); +define_field!(PllEnable, 0x1A, 0, 1, bool); + +// R27 (0x1B) Additional Control (3) +define_field!(VrefToAnalogueResistance, 0x1B, 6, 1, bool); +define_field!(CaplessHeadphoneSwitchEnable, 0x1B, 3, 1, bool); +define_field!(AdcAlcSampleRateSelect, 0x1B, 0, 3, u8); + +// R28 (0x1C) Anti-pop 1 +// R29 (0x1D) Anti-pop 2 + +// R30 (0x1E) Reserved +// R31 (0x1F) Reserved + +// R32 (0x20) ADCL signal path +define_field!(LeftInput1ToInverting, 0x20, 8, 1, bool); +define_field!(LeftInput3ToNonInverting, 0x20, 7, 1, bool); +define_field!(LeftInput2ToNonInverting, 0x20, 6, 1, bool); +define_field!(LeftBoostGain, 0x20, 4, 2, u8); +define_field!(LeftInputToBoost, 0x20, 3, 1, bool); + +// R33 (0x21) ADCR signal path +define_field!(RightMicBoost, 0x21, 4, 2, u8); +define_field!(AdcrSignalPathRaw, 0x21, 0, 9, u16); + +// R34 (0x22) Left Out Mix (1) +define_field!(LeftDacToOutputMixer, 0x22, 8, 1, bool); +define_field!(LeftInput3ToOutputMixer, 0x22, 7, 1, bool); +define_field!(LeftInput3ToOutputMixerVolume, 0x22, 4, 3, u8); + +// R35 (0x23) Reserved +// R36 (0x24) Reserved + +// R37 (0x25) Right Out Mix (2) +define_field!(RightDacToOutputMixer, 0x25, 8, 1, bool); +define_field!(RightInput3ToOutputMixer, 0x25, 7, 1, bool); +define_field!(RightInput3ToOutputMixerVolume, 0x25, 4, 3, u8); + +// R38 (0x26) Mono out Mix (1) +// R39 (0x27) Mono out Mix (2) + +// R40 (0x28) LOUT2 volume +define_field!(LeftSpeakerVolumeUpdate, 0x28, 8, 1, bool); +define_field!(LeftSpeakerZeroCross, 0x28, 7, 1, bool); +define_field!(LeftSpeakerVolume, 0x28, 0, 7, u8); + +// R41 (0x29) ROUT2 volume +define_field!(RightSpeakerVolumeUpdate, 0x28, 8, 1, bool); +define_field!(RightSpeakerZeroCross, 0x28, 7, 1, bool); +define_field!(RightSpeakerVolume, 0x28, 0, 7, u8); + +// R42 (0x2A) MONOOUT volume +define_field!(MonoOutVolume, 0x2A, 6, 1, bool); + +// R43 (0x2B) Input Boost Mixer (1) +define_field!(Linput3Boost, 0x2B, 4, 3, u8); +define_field!(Linput2Boost, 0x2B, 1, 3, u8); + +// R44 (0x2C) Input Boost Mixer (2) +define_field!(Rinput3Boost, 0x2C, 4, 3, u8); +define_field!(Rinput2Boost, 0x2C, 1, 3, u8); + +// R45 (0x2D) Bypass (1) +define_field!(LeftBoostToLeftOutputMix, 0x2D, 7, 1, bool); +define_field!(LeftBoostToLeftOutputMixVolume, 0x2D, 4, 3, u8); + +// R46 (0x2E) Bypass (2) +define_field!(RightBoostToRightOutputMix, 0x2E, 7, 1, bool); +define_field!(RightBoostToRightOutputMixVolume, 0x2E, 4, 3, u8); + +// R47 (0x2F) Power Management (3) +define_field!(LeftMicEnable, 0x2F, 5, 1, bool); +define_field!(RightMicEnable, 0x2F, 4, 1, bool); +define_field!(LeftOutputMixEnable, 0x2F, 3, 1, bool); +define_field!(RightOutputMixEnable, 0x2F, 2, 1, bool); + +// R48 (0x30) Additional Control (4) + +// R49 (0x31) Class D Control (1) +define_field!(ClassDSpeakerOutputEnable, 0x31, 6, 2, u8); + +// R50 (0x32) Reserved + +// R51 (0x33) Class D Control (3) +define_field!(SpeakerDcGain, 0x33, 3, 3, u8); +define_field!(SpeakerAcGain, 0x33, 0, 3, u8); + +// R52 (0x34) PLL N +define_field!(OpClockDivider, 0x34, 6, 3, u8); +define_field!(IntegerModeEnable, 0x34, 5, 1, bool); +define_field!(PllRescale, 0x34, 4, 1, bool); +define_field!(PllN, 0x34, 0, 4, u8); + +// R53, R54, R55 (0x35, 0x36, 0x37) PLL K +define_field!(PllKMsb, 0x35, 0, 8, u8); +define_field!(PllKMid, 0x36, 0, 8, u8); +define_field!(PllKLsb, 0x37, 0, 8, u8); diff --git a/ui-stm32/src/board/ev13.rs b/ui-stm32/src/board/ev13.rs index 1bb6d28..041adf9 100644 --- a/ui-stm32/src/board/ev13.rs +++ b/ui-stm32/src/board/ev13.rs @@ -1,17 +1,21 @@ -use super::{Button, Eeprom, Keyboard, NetTx, StatusLed}; +use super::{AudioControl, AudioData, Button, Eeprom, Keyboard, NetTx, StatusLed}; +use core::sync::atomic::{AtomicBool, Ordering}; use display_interface::{DataFormat, DisplayError, WriteOnlyDataCommand}; use embassy_stm32::{ bind_interrupts, exti::ExtiInput, gpio::{Input, Level, Output, Pull, Speed}, i2c::{mode::Master, I2c}, + i2s::I2S, mode::{Async, Blocking}, peripherals, spi::{Spi, Word}, - usart::{self, UartRx, UartTx}, + usart::{self, RingBufferedUartRx, UartTx}, }; use embassy_time::Delay; use embedded_graphics::{pixelcolor::Rgb565, prelude::*}; +use heapless::String; +use hex::ToHex; use ili9341::{Ili9341, Orientation}; use ui_app::{Led, Outputs}; @@ -100,23 +104,50 @@ impl WriteOnlyDataCommand for DisplayData { } } +struct LogTx(W); + +impl embedded_io::ErrorType for LogTx { + type Error = W::Error; +} + +impl embedded_io::Write for LogTx { + fn write(&mut self, buf: &[u8]) -> Result { + let hex: String<1024> = buf.encode_hex(); + defmt::info!("write: {}", hex); + + self.0.write(buf) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + self.0.flush() + } +} + pub struct Board { status_led: StatusLed, screen: Ili9341>, - net_tx: NetTx>, + net_tx: NetTx>>, i2c: I2c<'static, Blocking, Master>, + audio_data: AudioData<'static>, pub button_a: Option