net-esp-hosted: add Interface trait.

This commit is contained in:
Dario Nieuwenhuis
2025-10-29 17:49:55 +01:00
parent 1bbf35bdf4
commit 98de11e5e3
4 changed files with 89 additions and 51 deletions

View File

@@ -0,0 +1,62 @@
use embassy_time::Timer;
use embedded_hal::digital::InputPin;
use embedded_hal_async::digital::Wait;
use embedded_hal_async::spi::SpiDevice;
/// Physical interface trait for communicating with the ESP chip.
pub trait Interface {
/// Wait for the HANDSHAKE signal indicating the ESP is ready for a new transaction.
async fn wait_for_handshake(&mut self);
/// Wait for the READY signal indicating the ESP has data to send.
async fn wait_for_ready(&mut self);
/// Perform a SPI transfer, exchanging data with the ESP chip.
async fn transfer(&mut self, rx: &mut [u8], tx: &[u8]);
}
/// Standard SPI interface.
///
/// This interface is what's implemented in the upstream `esp-hosted-fg` firmware. It uses:
/// - An `SpiDevice` for SPI communication (CS is handled by the device)
/// - A handshake pin that signals when the ESP is ready for a new transaction
/// - A ready pin that indicates when the ESP has data to send
pub struct SpiInterface<SPI, IN> {
spi: SPI,
handshake: IN,
ready: IN,
}
impl<SPI, IN> SpiInterface<SPI, IN>
where
SPI: SpiDevice,
IN: InputPin + Wait,
{
/// Create a new SpiInterface.
pub fn new(spi: SPI, handshake: IN, ready: IN) -> Self {
Self { spi, handshake, ready }
}
}
impl<SPI, IN> Interface for SpiInterface<SPI, IN>
where
SPI: SpiDevice,
IN: InputPin + Wait,
{
async fn wait_for_handshake(&mut self) {
self.handshake.wait_for_high().await.unwrap();
}
async fn wait_for_ready(&mut self) {
self.ready.wait_for_high().await.unwrap();
}
async fn transfer(&mut self, rx: &mut [u8], tx: &[u8]) {
self.spi.transfer(rx, tx).await.unwrap();
// The esp-hosted firmware deasserts the HANDSHAKE pin a few us AFTER ending the SPI transfer
// If we check it again too fast, we'll see it's high from the previous transfer, and if we send it
// data it will get lost.
Timer::after_micros(100).await;
}
}

View File

@@ -1,14 +1,13 @@
#![no_std]
#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
#![allow(async_fn_in_trait)]
use embassy_futures::select::{Either4, select4};
use embassy_net_driver_channel as ch;
use embassy_net_driver_channel::driver::LinkState;
use embassy_time::{Duration, Instant, Timer};
use embedded_hal::digital::{InputPin, OutputPin};
use embedded_hal_async::digital::Wait;
use embedded_hal_async::spi::SpiDevice;
use embedded_hal::digital::OutputPin;
use crate::ioctl::{PendingIoctl, Shared};
use crate::proto::{CtrlMsg, CtrlMsgPayload};
@@ -19,9 +18,11 @@ mod proto;
mod fmt;
mod control;
mod iface;
mod ioctl;
pub use control::*;
pub use iface::*;
const MTU: usize = 1514;
@@ -118,20 +119,17 @@ impl State {
/// Type alias for network driver.
pub type NetDriver<'a> = ch::Device<'a, MTU>;
/// Create a new esp-hosted driver using the provided state, SPI peripheral and pins.
/// Create a new esp-hosted driver using the provided state, interface, and reset pin.
///
/// Returns a device handle for interfacing with embassy-net, a control handle for
/// interacting with the driver, and a runner for communicating with the WiFi device.
pub async fn new<'a, SPI, IN, OUT>(
pub async fn new<'a, I, OUT>(
state: &'a mut State,
spi: SPI,
handshake: IN,
ready: IN,
iface: I,
reset: OUT,
) -> (NetDriver<'a>, Control<'a>, Runner<'a, SPI, IN, OUT>)
) -> (NetDriver<'a>, Control<'a>, Runner<'a, I, OUT>)
where
SPI: SpiDevice,
IN: InputPin + Wait,
I: Interface,
OUT: OutputPin,
{
let (ch_runner, device) = ch::new(&mut state.ch, ch::driver::HardwareAddress::Ethernet([0; 6]));
@@ -142,10 +140,8 @@ where
state_ch,
shared: &state.shared,
next_seq: 1,
handshake,
ready,
reset,
spi,
iface,
heartbeat_deadline: Instant::now() + HEARTBEAT_MAX_GAP,
};
@@ -153,7 +149,7 @@ where
}
/// Runner for communicating with the WiFi device.
pub struct Runner<'a, SPI, IN, OUT> {
pub struct Runner<'a, I, OUT> {
ch: ch::Runner<'a, MTU>,
state_ch: ch::StateRunner<'a>,
shared: &'a Shared,
@@ -161,16 +157,13 @@ pub struct Runner<'a, SPI, IN, OUT> {
next_seq: u16,
heartbeat_deadline: Instant,
spi: SPI,
handshake: IN,
ready: IN,
iface: I,
reset: OUT,
}
impl<'a, SPI, IN, OUT> Runner<'a, SPI, IN, OUT>
impl<'a, I, OUT> Runner<'a, I, OUT>
where
SPI: SpiDevice,
IN: InputPin + Wait,
I: Interface,
OUT: OutputPin,
{
/// Run the packet processing.
@@ -185,11 +178,11 @@ where
let mut rx_buf = [0u8; MAX_SPI_BUFFER_SIZE];
loop {
self.handshake.wait_for_high().await.unwrap();
self.iface.wait_for_handshake().await;
let ioctl = self.shared.ioctl_wait_pending();
let tx = self.ch.tx_buf();
let ev = async { self.ready.wait_for_high().await.unwrap() };
let ev = async { self.iface.wait_for_ready().await };
let hb = Timer::at(self.heartbeat_deadline);
match select4(ioctl, tx, ev, hb).await {
@@ -243,15 +236,9 @@ where
trace!("tx: {:02x}", &tx_buf[..40]);
}
self.spi.transfer(&mut rx_buf, &tx_buf).await.unwrap();
self.iface.transfer(&mut rx_buf, &tx_buf).await;
// The esp-hosted firmware deasserts the HANSHAKE pin a few us AFTER ending the SPI transfer
// If we check it again too fast, we'll see it's high from the previous transfer, and if we send it
// data it will get lost.
// Make sure we check it after 100us at minimum.
let delay_until = Instant::now() + Duration::from_micros(100);
self.handle_rx(&mut rx_buf);
Timer::at(delay_until).await;
}
}

View File

@@ -27,14 +27,12 @@ bind_interrupts!(struct Irqs {
async fn wifi_task(
runner: hosted::Runner<
'static,
ExclusiveDevice<Spim<'static>, Output<'static>, Delay>,
Input<'static>,
hosted::SpiInterface<ExclusiveDevice<Spim<'static>, Output<'static>, Delay>, Input<'static>>,
Output<'static>,
>,
) -> ! {
runner.run().await
}
#[embassy_executor::task]
async fn net_task(mut runner: embassy_net::Runner<'static, hosted::NetDriver<'static>>) -> ! {
runner.run().await
@@ -60,15 +58,11 @@ async fn main(spawner: Spawner) {
let spi = spim::Spim::new(p.SPI3, Irqs, sck, miso, mosi, config);
let spi = ExclusiveDevice::new(spi, cs, Delay);
let iface = hosted::SpiInterface::new(spi, handshake, ready);
static ESP_STATE: StaticCell<embassy_net_esp_hosted::State> = StaticCell::new();
let (device, mut control, runner) = embassy_net_esp_hosted::new(
ESP_STATE.init(embassy_net_esp_hosted::State::new()),
spi,
handshake,
ready,
reset,
)
.await;
let (device, mut control, runner) =
embassy_net_esp_hosted::new(ESP_STATE.init(embassy_net_esp_hosted::State::new()), iface, reset).await;
spawner.spawn(unwrap!(wifi_task(runner)));

View File

@@ -29,8 +29,7 @@ const WIFI_PASSWORD: &str = "V8YxhKt5CdIAJFud";
async fn wifi_task(
runner: hosted::Runner<
'static,
ExclusiveDevice<Spim<'static>, Output<'static>, Delay>,
Input<'static>,
hosted::SpiInterface<ExclusiveDevice<Spim<'static>, Output<'static>, Delay>, Input<'static>>,
Output<'static>,
>,
) -> ! {
@@ -64,15 +63,11 @@ async fn main(spawner: Spawner) {
let spi = spim::Spim::new(p.SPI3, Irqs, sck, miso, mosi, config);
let spi = ExclusiveDevice::new(spi, cs, Delay);
let iface = hosted::SpiInterface::new(spi, handshake, ready);
static STATE: StaticCell<embassy_net_esp_hosted::State> = StaticCell::new();
let (device, mut control, runner) = embassy_net_esp_hosted::new(
STATE.init(embassy_net_esp_hosted::State::new()),
spi,
handshake,
ready,
reset,
)
.await;
let (device, mut control, runner) =
embassy_net_esp_hosted::new(STATE.init(embassy_net_esp_hosted::State::new()), iface, reset).await;
spawner.spawn(unwrap!(wifi_task(runner)));