feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
Vendored
+370
@@ -0,0 +1,370 @@
|
||||
//! # USB On-The-Go (USB OTG)
|
||||
//!
|
||||
//! ## Overview
|
||||
//! The USB OTG Full-speed (FS) peripheral allows communication
|
||||
//! with USB devices using either blocking (usb-device) or asynchronous
|
||||
//! (embassy-usb) APIs.
|
||||
//!
|
||||
//! It can operate as either a USB Host or Device, and supports full-speed (FS)
|
||||
//! and low-speed (LS) data rates of the USB 2.0 specification.
|
||||
//!
|
||||
//! The blocking driver uses the `esp_synopsys_usb_otg` crate, which provides
|
||||
//! the `USB bus` implementation and `USB peripheral traits`.
|
||||
//!
|
||||
//! The asynchronous driver uses the `embassy_usb_synopsys_otg` crate, which
|
||||
//! provides the `USB bus` and `USB device` implementations.
|
||||
//!
|
||||
//! The module also relies on other peripheral modules, such as `GPIO`,
|
||||
//! `system`, and `clock control`, to configure and enable the `USB` peripheral.
|
||||
//!
|
||||
//! ## Configuration
|
||||
//! To use the USB OTG Full-speed peripheral driver, you need to initialize the
|
||||
//! peripheral and configure its settings. The [`Usb`] struct represents the USB
|
||||
//! peripheral and requires the GPIO pins that implement [`UsbDp`], and
|
||||
//! [`UsbDm`], which define the specific types used for USB pin selection.
|
||||
//!
|
||||
//! The returned `Usb` instance can be used with the `usb-device` crate, or it
|
||||
//! can be further configured with [`asynch::Driver`] to be used with the
|
||||
//! `embassy-usb` crate.
|
||||
//!
|
||||
//! ## Examples
|
||||
//! Visit the [USB Serial] example for an example of using the USB OTG
|
||||
//! peripheral.
|
||||
//!
|
||||
//! [USB Serial]: https://github.com/esp-rs/esp-hal/blob/main/examples/src/bin/usb_serial.rs
|
||||
//!
|
||||
//! ## Implementation State
|
||||
//! - Low-speed (LS) is not supported.
|
||||
|
||||
pub use esp_synopsys_usb_otg::UsbBus;
|
||||
use esp_synopsys_usb_otg::UsbPeripheral;
|
||||
|
||||
use crate::{
|
||||
gpio::InputSignal,
|
||||
peripheral::{Peripheral, PeripheralRef},
|
||||
peripherals,
|
||||
system::{Peripheral as PeripheralEnable, PeripheralClockControl},
|
||||
};
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Trait representing the USB D+ (data plus) pin.
|
||||
pub trait UsbDp: crate::private::Sealed {}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Trait representing the USB D- (data minus) pin.
|
||||
pub trait UsbDm: crate::private::Sealed {}
|
||||
|
||||
/// USB peripheral.
|
||||
pub struct Usb<'d> {
|
||||
_usb0: PeripheralRef<'d, peripherals::USB0>,
|
||||
}
|
||||
|
||||
impl<'d> Usb<'d> {
|
||||
/// Creates a new `Usb` instance.
|
||||
pub fn new<P, M>(
|
||||
usb0: impl Peripheral<P = peripherals::USB0> + 'd,
|
||||
_usb_dp: impl Peripheral<P = P> + 'd,
|
||||
_usb_dm: impl Peripheral<P = M> + 'd,
|
||||
) -> Self
|
||||
where
|
||||
P: UsbDp + Send + Sync,
|
||||
M: UsbDm + Send + Sync,
|
||||
{
|
||||
PeripheralClockControl::reset(PeripheralEnable::Usb);
|
||||
PeripheralClockControl::enable(PeripheralEnable::Usb);
|
||||
|
||||
Self {
|
||||
_usb0: usb0.into_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
fn _enable() {
|
||||
unsafe {
|
||||
let usb_wrap = &*peripherals::USB_WRAP::PTR;
|
||||
usb_wrap.otg_conf().modify(|_, w| {
|
||||
w.usb_pad_enable().set_bit();
|
||||
w.phy_sel().clear_bit();
|
||||
w.clk_en().set_bit();
|
||||
w.ahb_clk_force_on().set_bit();
|
||||
w.phy_clk_force_on().set_bit()
|
||||
});
|
||||
|
||||
#[cfg(esp32s3)]
|
||||
{
|
||||
let rtc = &*peripherals::LPWR::PTR;
|
||||
rtc.usb_conf()
|
||||
.modify(|_, w| w.sw_hw_usb_phy_sel().set_bit().sw_usb_phy_sel().set_bit());
|
||||
}
|
||||
|
||||
use crate::gpio::Level;
|
||||
|
||||
InputSignal::USB_OTG_IDDIG.connect_to(Level::High); // connected connector is mini-B side
|
||||
InputSignal::USB_SRP_BVALID.connect_to(Level::High); // HIGH to force USB device mode
|
||||
InputSignal::USB_OTG_VBUSVALID.connect_to(Level::High); // receiving a valid Vbus from device
|
||||
InputSignal::USB_OTG_AVALID.connect_to(Level::Low);
|
||||
}
|
||||
}
|
||||
|
||||
fn _disable() {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<'d> Sync for Usb<'d> {}
|
||||
|
||||
unsafe impl<'d> UsbPeripheral for Usb<'d> {
|
||||
const REGISTERS: *const () = peripherals::USB0::ptr() as *const ();
|
||||
|
||||
const HIGH_SPEED: bool = false;
|
||||
const FIFO_DEPTH_WORDS: usize = 256;
|
||||
const ENDPOINT_COUNT: usize = 5;
|
||||
|
||||
fn enable() {
|
||||
Self::_enable();
|
||||
}
|
||||
|
||||
fn ahb_frequency_hz(&self) -> u32 {
|
||||
// unused
|
||||
80_000_000
|
||||
}
|
||||
}
|
||||
/// Async functionality
|
||||
pub mod asynch {
|
||||
use embassy_usb_driver::{
|
||||
EndpointAddress,
|
||||
EndpointAllocError,
|
||||
EndpointType,
|
||||
Event,
|
||||
Unsupported,
|
||||
};
|
||||
pub use embassy_usb_synopsys_otg::Config;
|
||||
use embassy_usb_synopsys_otg::{
|
||||
on_interrupt,
|
||||
otg_v1::Otg,
|
||||
Bus as OtgBus,
|
||||
ControlPipe,
|
||||
Driver as OtgDriver,
|
||||
Endpoint,
|
||||
In,
|
||||
OtgInstance,
|
||||
Out,
|
||||
PhyType,
|
||||
State,
|
||||
};
|
||||
use procmacros::handler;
|
||||
|
||||
use super::*;
|
||||
use crate::Cpu;
|
||||
|
||||
// From ESP32-S3 TRM:
|
||||
// Six additional endpoints (endpoint numbers 1 to 6), configurable as IN or OUT
|
||||
const MAX_EP_COUNT: usize = 7;
|
||||
|
||||
static STATE: State<MAX_EP_COUNT> = State::new();
|
||||
|
||||
/// Asynchronous USB driver.
|
||||
pub struct Driver<'d> {
|
||||
inner: OtgDriver<'d, MAX_EP_COUNT>,
|
||||
}
|
||||
|
||||
impl<'d> Driver<'d> {
|
||||
const REGISTERS: Otg = unsafe { Otg::from_ptr(Usb::REGISTERS.cast_mut()) };
|
||||
|
||||
/// Initializes USB OTG peripheral with internal Full-Speed PHY, for
|
||||
/// asynchronous operation.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `ep_out_buffer` - An internal buffer used to temporarily store
|
||||
/// received packets.
|
||||
///
|
||||
/// Must be large enough to fit all OUT endpoint max packet sizes.
|
||||
/// Endpoint allocation will fail if it is too small.
|
||||
pub fn new(_peri: Usb<'d>, ep_out_buffer: &'d mut [u8], config: Config) -> Self {
|
||||
// From `synopsys-usb-otg` crate:
|
||||
// This calculation doesn't correspond to one in a Reference Manual.
|
||||
// In fact, the required number of words is higher than indicated in RM.
|
||||
// The following numbers are pessimistic and were figured out empirically.
|
||||
const RX_FIFO_EXTRA_SIZE_WORDS: u16 = 30;
|
||||
|
||||
let instance = OtgInstance {
|
||||
regs: Self::REGISTERS,
|
||||
state: &STATE,
|
||||
fifo_depth_words: Usb::FIFO_DEPTH_WORDS as u16,
|
||||
extra_rx_fifo_words: RX_FIFO_EXTRA_SIZE_WORDS,
|
||||
endpoint_count: Usb::ENDPOINT_COUNT,
|
||||
phy_type: PhyType::InternalFullSpeed,
|
||||
quirk_setup_late_cnak: quirk_setup_late_cnak(),
|
||||
calculate_trdt_fn: |_| 5,
|
||||
};
|
||||
Self {
|
||||
inner: OtgDriver::new(ep_out_buffer, instance, config),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d> embassy_usb_driver::Driver<'d> for Driver<'d> {
|
||||
type EndpointOut = Endpoint<'d, Out>;
|
||||
type EndpointIn = Endpoint<'d, In>;
|
||||
type ControlPipe = ControlPipe<'d>;
|
||||
type Bus = Bus<'d>;
|
||||
|
||||
fn alloc_endpoint_in(
|
||||
&mut self,
|
||||
ep_type: EndpointType,
|
||||
max_packet_size: u16,
|
||||
interval_ms: u8,
|
||||
) -> Result<Self::EndpointIn, EndpointAllocError> {
|
||||
self.inner
|
||||
.alloc_endpoint_in(ep_type, max_packet_size, interval_ms)
|
||||
}
|
||||
|
||||
fn alloc_endpoint_out(
|
||||
&mut self,
|
||||
ep_type: EndpointType,
|
||||
max_packet_size: u16,
|
||||
interval_ms: u8,
|
||||
) -> Result<Self::EndpointOut, EndpointAllocError> {
|
||||
self.inner
|
||||
.alloc_endpoint_out(ep_type, max_packet_size, interval_ms)
|
||||
}
|
||||
|
||||
fn start(self, control_max_packet_size: u16) -> (Self::Bus, Self::ControlPipe) {
|
||||
let (bus, cp) = self.inner.start(control_max_packet_size);
|
||||
|
||||
(
|
||||
Bus {
|
||||
inner: bus,
|
||||
inited: false,
|
||||
},
|
||||
cp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Asynchronous USB bus mainly used internally by `embassy-usb`.
|
||||
// We need a custom wrapper implementation to handle custom initialization.
|
||||
pub struct Bus<'d> {
|
||||
inner: OtgBus<'d, MAX_EP_COUNT>,
|
||||
inited: bool,
|
||||
}
|
||||
|
||||
impl<'d> Bus<'d> {
|
||||
fn init(&mut self) {
|
||||
Usb::_enable();
|
||||
|
||||
let r = Driver::REGISTERS;
|
||||
|
||||
// Wait for AHB ready.
|
||||
while !r.grstctl().read().ahbidl() {}
|
||||
|
||||
// Configure as device.
|
||||
r.gusbcfg().write(|w| {
|
||||
// Force device mode
|
||||
w.set_fdmod(true);
|
||||
w.set_srpcap(false);
|
||||
// Enable internal full-speed PHY
|
||||
w.set_physel(true);
|
||||
});
|
||||
self.inner.config_v1();
|
||||
|
||||
// Perform core soft-reset
|
||||
r.grstctl().modify(|w| w.set_csrst(true));
|
||||
while r.grstctl().read().csrst() {}
|
||||
|
||||
r.pcgcctl().modify(|w| {
|
||||
// Disable power down
|
||||
w.set_stppclk(false);
|
||||
});
|
||||
|
||||
unsafe {
|
||||
crate::interrupt::bind_interrupt(
|
||||
crate::peripherals::Interrupt::USB,
|
||||
interrupt_handler.handler(),
|
||||
);
|
||||
crate::interrupt::enable(
|
||||
crate::peripherals::Interrupt::USB,
|
||||
interrupt_handler.priority(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn disable(&mut self) {
|
||||
crate::interrupt::disable(Cpu::ProCpu, crate::peripherals::Interrupt::USB);
|
||||
|
||||
#[cfg(multi_core)]
|
||||
crate::interrupt::disable(Cpu::AppCpu, crate::peripherals::Interrupt::USB);
|
||||
|
||||
Usb::_disable();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d> embassy_usb_driver::Bus for Bus<'d> {
|
||||
async fn poll(&mut self) -> Event {
|
||||
if !self.inited {
|
||||
self.init();
|
||||
self.inited = true;
|
||||
}
|
||||
|
||||
self.inner.poll().await
|
||||
}
|
||||
|
||||
fn endpoint_set_stalled(&mut self, ep_addr: EndpointAddress, stalled: bool) {
|
||||
self.inner.endpoint_set_stalled(ep_addr, stalled)
|
||||
}
|
||||
|
||||
fn endpoint_is_stalled(&mut self, ep_addr: EndpointAddress) -> bool {
|
||||
self.inner.endpoint_is_stalled(ep_addr)
|
||||
}
|
||||
|
||||
fn endpoint_set_enabled(&mut self, ep_addr: EndpointAddress, enabled: bool) {
|
||||
self.inner.endpoint_set_enabled(ep_addr, enabled)
|
||||
}
|
||||
|
||||
async fn enable(&mut self) {
|
||||
self.inner.enable().await
|
||||
}
|
||||
|
||||
async fn disable(&mut self) {
|
||||
self.inner.disable().await
|
||||
}
|
||||
|
||||
async fn remote_wakeup(&mut self) -> Result<(), Unsupported> {
|
||||
self.inner.remote_wakeup().await
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d> Drop for Bus<'d> {
|
||||
fn drop(&mut self) {
|
||||
Bus::disable(self);
|
||||
}
|
||||
}
|
||||
|
||||
fn quirk_setup_late_cnak() -> bool {
|
||||
// Our CID register is 4 bytes offset from what's in embassy-usb-synopsys-otg
|
||||
let cid = unsafe {
|
||||
Driver::REGISTERS
|
||||
.as_ptr()
|
||||
.cast::<u32>()
|
||||
.add(0x40)
|
||||
.read_volatile()
|
||||
};
|
||||
// ESP32-Sx has a different CID register value, too
|
||||
cid == 0x4f54_400a || cid & 0xf000 == 0x1000
|
||||
}
|
||||
|
||||
#[handler(priority = crate::interrupt::Priority::max())]
|
||||
fn interrupt_handler() {
|
||||
let setup_late_cnak = quirk_setup_late_cnak();
|
||||
|
||||
unsafe {
|
||||
on_interrupt(
|
||||
Driver::REGISTERS,
|
||||
&STATE,
|
||||
Usb::ENDPOINT_COUNT,
|
||||
setup_late_cnak,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user