esp-p4-eth/examples/embassy_static_ping.rs

275 lines
11 KiB
Rust
Raw Normal View History

2026-10-05 01:16:55 +08:00
#![no_std]
#![no_main]
//! End-to-end smoke: SYSTIMER-backed embassy-time + EMAC + embassy-net with
//! a static IPv4 address. embassy-net's `auto-icmp-echo-reply` feature
//! handles ICMP echo replies automatically — so once link is up the host
//! should be able to `ping 169.254.144.50` and get responses.
//!
//! Use case: in this lab the P4 is direct-attached to the host's Ethernet
//! NIC (no DHCP server). Host autoconf'd to 169.254.144.33/16 (Windows
//! Automatic Private IP). We pick 169.254.144.50/16 in the same link-local
//! subnet so the host can reach us without any additional config.
use core::fmt::{self, Write};
use core::hint::spin_loop;
use core::ptr::{read_volatile, write_volatile};
use core::sync::atomic::Ordering;
use embassy_executor::{Executor, Spawner};
use embassy_net::{Config, Ipv4Address, Ipv4Cidr, Runner as NetRunner, Stack, StackResources, StaticConfigV4};
use embassy_time::{Duration, Timer};
use panic_halt as _;
use riscv_rt::entry;
use esp_p4_eth::{
diag::{
RX_ARP, RX_FRAMES, RX_ICMP, RX_IPV4, RX_LAST_DST_MAC_HI, RX_LAST_ETHERTYPE, TX_FRAMES,
TX_LAST_BUF_ADDR, TX_LAST_DESC_ADDR, TX_LAST_DST_MAC_HI, TX_LAST_ETHERTYPE, TX_LAST_LEN,
TX_LAST_SRC_MAC_HI, TX_LAST_TDES0, TX_LAST_TDES1,
},
ethernet_task, new_from_static_resources as emac_new, release_waveshare_phy_reset, systimer,
time_driver::time_polling_task, Device, Dma, StaticDmaResources,
};
use static_cell::{ConstStaticCell, StaticCell};
const UART0_BASE: usize = 0x500C_A000;
const UART_FIFO_REG: *mut u32 = UART0_BASE as *mut u32;
const UART_STATUS_REG: *const u32 = (UART0_BASE + 0x1C) as *const u32;
const UART_TXFIFO_CNT_SHIFT: u32 = 16;
const UART_TXFIFO_CNT_MASK: u32 = 0xFF << UART_TXFIFO_CNT_SHIFT;
const UART_TXFIFO_CAPACITY: u32 = 128;
const MAC_ADDR: [u8; 6] = [0xE8, 0xF6, 0x0A, 0xE0, 0x93, 0xF5];
const NET_SEED: u64 = 0x5A17_2026_CAFE_BEEF;
// LAN topology: P4 plugged into router (192.168.0.0/24), host on Wi-Fi.
const SELF_IP: Ipv4Address = Ipv4Address::new(192, 168, 0, 50);
const SELF_PREFIX: u8 = 24;
const GATEWAY: Ipv4Address = Ipv4Address::new(192, 168, 0, 1);
static EXECUTOR: StaticCell<Executor> = StaticCell::new();
// DMA descriptors and packet buffers MUST live below 0x4FF80000 — the upper
// 256 KB of HP SRAM is the L2 cache backing region, not accessible to the
// EMAC DMA. See memory.x .dma_bss section.
#[link_section = ".dma_bss"]
static DMA_RESOURCES: ConstStaticCell<StaticDmaResources> =
ConstStaticCell::new(StaticDmaResources::new());
static NET_RESOURCES: ConstStaticCell<StackResources<4>> =
ConstStaticCell::new(StackResources::<4>::new());
struct Uart0;
impl Uart0 {
fn write_byte(&mut self, byte: u8) {
while txfifo_count() >= UART_TXFIFO_CAPACITY {
spin_loop();
}
unsafe {
write_volatile(UART_FIFO_REG, byte as u32);
}
}
}
impl Write for Uart0 {
fn write_str(&mut self, s: &str) -> fmt::Result {
for byte in s.bytes() {
if byte == b'\n' {
self.write_byte(b'\r');
}
self.write_byte(byte);
}
Ok(())
}
}
fn txfifo_count() -> u32 {
let status = unsafe { read_volatile(UART_STATUS_REG) };
(status & UART_TXFIFO_CNT_MASK) >> UART_TXFIFO_CNT_SHIFT
}
#[embassy_executor::task]
async fn net_task(mut runner: NetRunner<'static, Device<'static>>) -> ! {
runner.run().await
}
/// Periodic DMA RX maintenance — drains DMA_STATUS so sticky RU/RBU bits get
/// acked and `handle_overflow` reissues the poll demand. Without this, RX
/// engine wedges in Suspended state after ~140 frames and silently drops all
/// further packets (including ARP replies and retransmitted ICMP echoes).
#[embassy_executor::task]
async fn dma_recovery_task() -> ! {
loop {
Timer::after(Duration::from_millis(1)).await;
let dma_status = unsafe { read_volatile(0x5009_9014 as *const u32) };
let rps = (dma_status >> 17) & 0b111;
// Only kick when actually suspended/stopped — running state shouldn't
// be touched. RPS=0 stopped, RPS=4 suspended (RBU). Sticky RU bit at
// 7 also indicates we missed an unavailable event.
if rps == 0 || rps == 4 || dma_status & (1 << 7) != 0 {
// Clear sticky status bits (W1C) and re-issue demand. This is the
// minimal safe recovery — we don't reset the descriptor list since
// CPU writes to descriptors are working fine on this build.
unsafe {
write_volatile(0x5009_9014 as *mut u32, dma_status & 0x0001E7FF);
write_volatile(0x5009_9008 as *mut u32, 1); // RX_POLL_DEMAND
}
}
}
}
#[embassy_executor::task]
async fn app_task(stack: Stack<'static>) -> ! {
let mut uart = Uart0;
let _ = writeln!(uart, "app: started, self IP = 192.168.0.50/24 gw 192.168.0.1");
// Wait for link state up.
let t0 = systimer::now_us();
while !stack.is_link_up() {
Timer::after(Duration::from_millis(100)).await;
}
let _ = writeln!(uart, "app: link up after {} ms", (systimer::now_us() - t0) / 1000);
// Force promiscuous so unicast ICMP requests addressed to our MAC pass —
// the default PM-only filter requires the primary MAC address slot to be
// matched, and ICMP from the host comes via ARP-resolved unicast.
// 0x80000011 = RA | PR | PM. RA (Receive All, bit 31) bypasses every
// remaining filter — pass even malformed frames so smoltcp sees them.
unsafe {
write_volatile(0x5009_8004 as *mut u32, 0x80000011);
}
let _ = writeln!(
uart,
"app: MAC_FRAME_FILTER = 0x{:08X} (RA+PR+PM)",
unsafe { read_volatile(0x5009_8004 as *const u32) },
);
// Synopsys GMAC drops frames < 64 bytes (runt) by default. Windows ARP
// replies are 42 bytes on the wire — they get filtered out, smoltcp never
// sees them, and we keep re-ARP'ing forever. Set FUF (DMA_OP_MODE bit 6)
// so the DMA forwards undersized frames to the host.
unsafe {
let dma_op_mode = 0x5009_9018 as *mut u32;
let cur = read_volatile(dma_op_mode);
write_volatile(dma_op_mode, cur | (1 << 6));
let _ = writeln!(
uart,
"app: DMA_OP_MODE 0x{:08X} -> 0x{:08X} (FUF set)",
cur,
read_volatile(dma_op_mode),
);
}
// Match IDF MAC_CONFIG (0x0000CC0C) more closely — clear ACS (Auto CRC
// Strip, bit 25). Our default 0x0200C88C has ACS=1, IDF baseline ACS=0.
// ACS=1 may interact badly with short frames after stripping.
unsafe {
let mac_cfg = 0x5009_8000 as *mut u32;
let cur = read_volatile(mac_cfg);
write_volatile(mac_cfg, cur & !(1 << 25));
let _ = writeln!(
uart,
"app: MAC_CONFIG 0x{:08X} -> 0x{:08X} (ACS cleared)",
cur,
read_volatile(mac_cfg),
);
}
if let Some(cfg) = stack.config_v4() {
let _ = writeln!(uart, "app: stack live — {}", cfg.address);
} else {
let _ = writeln!(uart, "app: WARNING — stack.config_v4() is None");
}
let _ = writeln!(uart, "app: ready for ping 192.168.0.50");
let _ = writeln!(uart, " (run from host: `ping 192.168.0.50`)");
let mut tick = 0u64;
loop {
Timer::after(Duration::from_secs(2)).await;
tick += 1;
let _ = writeln!(
uart,
" t={}s rx={} tx={} arp={} icmp={} rx_eth=0x{:04X} tx_len={} tx_eth=0x{:04X} tx_dst=0x{:08X} tx_src=0x{:08X}",
tick * 2,
RX_FRAMES.load(Ordering::Relaxed),
TX_FRAMES.load(Ordering::Relaxed),
RX_ARP.load(Ordering::Relaxed),
RX_ICMP.load(Ordering::Relaxed),
RX_LAST_ETHERTYPE.load(Ordering::Relaxed),
TX_LAST_LEN.load(Ordering::Relaxed),
TX_LAST_ETHERTYPE.load(Ordering::Relaxed),
TX_LAST_DST_MAC_HI.load(Ordering::Relaxed),
TX_LAST_SRC_MAC_HI.load(Ordering::Relaxed),
);
// TX descriptor + buffer post-mortem. Compare CPU snapshot of buf
// contents (cached) to memory view (after invalidate). If they differ,
// CPU writes to TX buffer didn't make it to memory and DMA shipped
// garbage on the wire.
let buf_addr = TX_LAST_BUF_ADDR.load(Ordering::Relaxed);
let desc_addr = TX_LAST_DESC_ADDR.load(Ordering::Relaxed);
if buf_addr != 0 {
let mem_buf = unsafe { Dma::peek_rdes(buf_addr) };
let cached_buf = unsafe { Dma::peek_rdes_cached(buf_addr) };
let _ = writeln!(
uart,
" desc@0x{:08X} tdes0=0x{:08X} tdes1=0x{:08X} buf@0x{:08X}",
desc_addr,
TX_LAST_TDES0.load(Ordering::Relaxed),
TX_LAST_TDES1.load(Ordering::Relaxed),
buf_addr,
);
let _ = writeln!(
uart,
" buf cached: {:08X} {:08X} {:08X} {:08X}",
cached_buf[0], cached_buf[1], cached_buf[2], cached_buf[3],
);
let _ = writeln!(
uart,
" buf mem : {:08X} {:08X} {:08X} {:08X}",
mem_buf[0], mem_buf[1], mem_buf[2], mem_buf[3],
);
}
let _ = RX_LAST_DST_MAC_HI.load(Ordering::Relaxed);
let _ = RX_IPV4.load(Ordering::Relaxed);
}
}
#[entry]
fn main() -> ! {
let mut uart = Uart0;
let _ = writeln!(uart, "\n=== embassy_static_ping alive ===");
release_waveshare_phy_reset();
for _ in 0..5_000_000 {
spin_loop();
}
let _ = writeln!(uart, "main: PHY reset deasserted, waited 5M cycles");
let executor = EXECUTOR.init(Executor::new());
executor.run(init);
}
fn init(spawner: Spawner) {
let mut uart = Uart0;
let _ = writeln!(uart, "init: emac_new...");
let dma_resources = DMA_RESOURCES.take();
let (device, emac_runner, eth) = emac_new(MAC_ADDR, dma_resources);
let _ = writeln!(uart, "init: emac_new returned");
let config = Config::ipv4_static(StaticConfigV4 {
address: Ipv4Cidr::new(SELF_IP, SELF_PREFIX),
gateway: Some(GATEWAY),
dns_servers: heapless::Vec::new(),
});
let resources = NET_RESOURCES.take();
let (stack, net_runner) = embassy_net::new(device, config, resources, NET_SEED);
let _ = writeln!(uart, "init: embassy_net::new returned");
spawner.spawn(time_polling_task()).unwrap();
spawner.spawn(ethernet_task(emac_runner, eth)).unwrap();
spawner.spawn(net_task(net_runner)).unwrap();
spawner.spawn(dma_recovery_task()).unwrap();
spawner.spawn(app_task(stack)).unwrap();
let _ = writeln!(uart, "init: all tasks spawned (incl. dma_recovery)");
}