From 4ae71ea3c179eb5470c5176fc17c6f617ef0d958 Mon Sep 17 00:00:00 2001 From: James Waples Date: Sat, 18 Nov 2023 21:09:07 +0000 Subject: [PATCH] Add RTICv2 example skeleton --- examples/rtic-stm32/.cargo/config | 10 + examples/rtic-stm32/.gitignore | 2 + examples/rtic-stm32/Cargo.toml | 28 +++ examples/rtic-stm32/rust-toolchain.toml | 4 + examples/rtic-stm32/src/common.rs | 284 ++++++++++++++++++++++++ examples/rtic-stm32/src/main.rs | 261 ++++++++++++++++++++++ 6 files changed, 589 insertions(+) create mode 100644 examples/rtic-stm32/.cargo/config create mode 100644 examples/rtic-stm32/.gitignore create mode 100644 examples/rtic-stm32/Cargo.toml create mode 100644 examples/rtic-stm32/rust-toolchain.toml create mode 100644 examples/rtic-stm32/src/common.rs create mode 100644 examples/rtic-stm32/src/main.rs diff --git a/examples/rtic-stm32/.cargo/config b/examples/rtic-stm32/.cargo/config new file mode 100644 index 00000000..617fbf74 --- /dev/null +++ b/examples/rtic-stm32/.cargo/config @@ -0,0 +1,10 @@ +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +runner = "probe-rs run --chip STM32F429ZITx" +rustflags = [ + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", +] + + +[build] +target = "thumbv7em-none-eabi" diff --git a/examples/rtic-stm32/.gitignore b/examples/rtic-stm32/.gitignore new file mode 100644 index 00000000..2c96eb1b --- /dev/null +++ b/examples/rtic-stm32/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/examples/rtic-stm32/Cargo.toml b/examples/rtic-stm32/Cargo.toml new file mode 100644 index 00000000..0ef2919d --- /dev/null +++ b/examples/rtic-stm32/Cargo.toml @@ -0,0 +1,28 @@ +[package] +version = "0.1.0" +name = "stm32-rtic" +edition = "2021" +resolver = "2" + +[dependencies] +volatile-register = "0.2" +aligned = "0.4" +stm32f4xx-hal = { version = "0.14", features = ["stm32f429"] } +stm32f4 = { version = "0.15" } +ieee802_3_miim = "0.8" +defmt = { version = "0.3" } +futures = { version = "0.3", default-features = false, features = [ + "async-await", +] } +cortex-m = { version = "0.7", features = ["critical-section-single-core"] } +cortex-m-rt = "0.7" +fugit = "0.3" +defmt-rtt = "0.4" +panic-probe = { version = "0.3", features = ["print-defmt"] } +systick-monotonic = "1.0" +rtic = { version = "2.0.1", features = ["thumbv7-backend"] } +stm32-eth = { version = "0.5.2", default-features = false, features = ["defmt", "stm32f429", "async-await"] } + +[profile.release] +debug = 2 +lto = true diff --git a/examples/rtic-stm32/rust-toolchain.toml b/examples/rtic-stm32/rust-toolchain.toml new file mode 100644 index 00000000..2fefb806 --- /dev/null +++ b/examples/rtic-stm32/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly" +components = ["llvm-tools"] +targets = ["thumbv7em-none-eabi"] diff --git a/examples/rtic-stm32/src/common.rs b/examples/rtic-stm32/src/common.rs new file mode 100644 index 00000000..fde8288e --- /dev/null +++ b/examples/rtic-stm32/src/common.rs @@ -0,0 +1,284 @@ +//! Common features used in examples. +//! +//! Note that this module isn't an example by itself. + +use defmt_rtt as _; +use panic_probe as _; + +use stm32_eth::{ + hal::{ + gpio::{GpioExt, *}, + rcc::Clocks, + }, + EthPins, PartsIn, +}; + +use fugit::RateExtU32; +use stm32_eth::hal::rcc::RccExt; + +/// Setup the clocks and return clocks and a GPIO struct that +/// can be used to set up all of the pins. +/// +/// This configures HCLK to be at least 25 MHz, which is the minimum required +/// for ethernet operation to be valid. +pub fn setup_peripherals(p: stm32_eth::stm32::Peripherals) -> (Clocks, Gpio, PartsIn) { + let ethernet = PartsIn { + dma: p.ETHERNET_DMA, + mac: p.ETHERNET_MAC, + mmc: p.ETHERNET_MMC, + #[cfg(feature = "ptp")] + ptp: p.ETHERNET_PTP, + }; + + let rcc = p.RCC.constrain(); + + let clocks = rcc.cfgr.sysclk(96.MHz()).hclk(96.MHz()); + + #[cfg(feature = "stm32f4xx-hal")] + let clocks = { + if cfg!(hse = "bypass") { + clocks.use_hse(8.MHz()).bypass_hse_oscillator() + } else if cfg!(hse = "oscillator") { + clocks.use_hse(8.MHz()) + } else { + clocks + } + }; + + #[cfg(feature = "stm32f7xx-hal")] + let clocks = { + if cfg!(hse = "bypass") { + clocks.hse(stm32_eth::hal::rcc::HSEClock::new( + 8.MHz(), + stm32_eth::hal::rcc::HSEClockMode::Bypass, + )) + } else if cfg!(hse = "oscillator") { + clocks.hse(stm32_eth::hal::rcc::HSEClock::new( + 8.MHz(), + stm32_eth::hal::rcc::HSEClockMode::Oscillator, + )) + } else { + clocks + } + }; + + let clocks = clocks.freeze(); + + let gpio = Gpio { + gpioa: p.GPIOA.split(), + gpiob: p.GPIOB.split(), + gpioc: p.GPIOC.split(), + gpiog: p.GPIOG.split(), + }; + + (clocks, gpio, ethernet) +} + +pub struct Gpio { + pub gpioa: gpioa::Parts, + pub gpiob: gpiob::Parts, + pub gpioc: gpioc::Parts, + pub gpiog: gpiog::Parts, +} + +pub type RefClk = PA1; +pub type Crs = PA7; +pub type TxD1 = PB13; +pub type RxD0 = PC4; +pub type RxD1 = PC5; + +#[cfg(not(pins = "nucleo"))] +pub type TxEn = PB11; +#[cfg(not(pins = "nucleo"))] +pub type TxD0 = PB12; + +#[cfg(pins = "nucleo")] +pub type TxEn = PG11; +#[cfg(pins = "nucleo")] +pub type TxD0 = PG13; + +pub type Mdio = PA2>; +pub type Mdc = PC1>; + +#[cfg(not(pps = "alternate"))] +pub type Pps = PB5>; + +#[cfg(pps = "alternate")] +pub type Pps = PG8>; + +pub fn setup_pins( + gpio: Gpio, +) -> ( + EthPins, + Mdio, + Mdc, + Pps, +) { + #[allow(unused_variables)] + let Gpio { + gpioa, + gpiob, + gpioc, + gpiog, + } = gpio; + + let ref_clk = gpioa.pa1.into_floating_input(); + let crs = gpioa.pa7.into_floating_input(); + let tx_d1 = gpiob.pb13.into_floating_input(); + let rx_d0 = gpioc.pc4.into_floating_input(); + let rx_d1 = gpioc.pc5.into_floating_input(); + + #[cfg(not(pins = "nucleo"))] + let (tx_en, tx_d0) = ( + gpiob.pb11.into_floating_input(), + gpiob.pb12.into_floating_input(), + ); + + #[cfg(pins = "nucleo")] + let (tx_en, tx_d0) = { + ( + gpiog.pg11.into_floating_input(), + gpiog.pg13.into_floating_input(), + ) + }; + + #[cfg(feature = "stm32f4xx-hal")] + let (mdio, mdc) = { + let mut mdio = gpioa.pa2.into_alternate(); + mdio.set_speed(Speed::VeryHigh); + let mut mdc = gpioc.pc1.into_alternate(); + mdc.set_speed(Speed::VeryHigh); + (mdio, mdc) + }; + + #[cfg(any(feature = "stm32f7xx-hal"))] + let (mdio, mdc) = ( + gpioa.pa2.into_alternate().set_speed(Speed::VeryHigh), + gpioc.pc1.into_alternate().set_speed(Speed::VeryHigh), + ); + + #[cfg(not(pps = "alternate"))] + let pps = gpiob.pb5.into_push_pull_output(); + #[cfg(pps = "alternate")] + let pps = gpiog.pg8.into_push_pull_output(); + + ( + EthPins { + ref_clk, + crs, + tx_en, + tx_d0, + tx_d1, + rx_d0, + rx_d1, + }, + mdio, + mdc, + pps, + ) +} + +use ieee802_3_miim::{ + phy::{ + lan87xxa::{LAN8720A, LAN8742A}, + BarePhy, KSZ8081R, + }, + Miim, Pause, Phy, +}; + +/// An ethernet PHY +pub enum EthernetPhy { + /// LAN8720A + LAN8720A(LAN8720A), + /// LAN8742A + LAN8742A(LAN8742A), + /// KSZ8081R + KSZ8081R(KSZ8081R), +} + +impl Phy for EthernetPhy { + fn best_supported_advertisement(&self) -> ieee802_3_miim::AutoNegotiationAdvertisement { + match self { + EthernetPhy::LAN8720A(phy) => phy.best_supported_advertisement(), + EthernetPhy::LAN8742A(phy) => phy.best_supported_advertisement(), + EthernetPhy::KSZ8081R(phy) => phy.best_supported_advertisement(), + } + } + + fn get_miim(&mut self) -> &mut M { + match self { + EthernetPhy::LAN8720A(phy) => phy.get_miim(), + EthernetPhy::LAN8742A(phy) => phy.get_miim(), + EthernetPhy::KSZ8081R(phy) => phy.get_miim(), + } + } + + fn get_phy_addr(&self) -> u8 { + match self { + EthernetPhy::LAN8720A(phy) => phy.get_phy_addr(), + EthernetPhy::LAN8742A(phy) => phy.get_phy_addr(), + EthernetPhy::KSZ8081R(phy) => phy.get_phy_addr(), + } + } +} + +impl EthernetPhy { + /// Attempt to create one of the known PHYs from the given + /// MIIM. + /// + /// Returns an error if the PHY does not support the extended register + /// set, or if the PHY's identifier does not correspond to a known PHY. + pub fn from_miim(miim: M, phy_addr: u8) -> Result { + let mut bare = BarePhy::new(miim, phy_addr, Pause::NoPause); + let phy_ident = if let Some(id) = bare.phy_ident() { + id.raw_u32() + } else { + return Err(bare.release()); + }; + let miim = bare.release(); + match phy_ident & 0xFFFFFFF0 { + 0x0007C0F0 => Ok(Self::LAN8720A(LAN8720A::new(miim, phy_addr))), + 0x0007C130 => Ok(Self::LAN8742A(LAN8742A::new(miim, phy_addr))), + 0x00221560 => Ok(Self::KSZ8081R(KSZ8081R::new(miim, phy_addr))), + _ => Err(miim), + } + } + + /// Get a string describing the type of PHY + pub const fn ident_string(&self) -> &'static str { + match self { + EthernetPhy::LAN8720A(_) => "LAN8720A", + EthernetPhy::LAN8742A(_) => "LAN8742A", + EthernetPhy::KSZ8081R(_) => "KSZ8081R", + } + } + + /// Initialize the PHY + pub fn phy_init(&mut self) { + match self { + EthernetPhy::LAN8720A(phy) => phy.phy_init(), + EthernetPhy::LAN8742A(phy) => phy.phy_init(), + EthernetPhy::KSZ8081R(phy) => { + phy.set_autonegotiation_advertisement(phy.best_supported_advertisement()); + } + } + } + + #[allow(dead_code)] + pub fn speed(&mut self) -> Option { + match self { + EthernetPhy::LAN8720A(phy) => phy.link_speed(), + EthernetPhy::LAN8742A(phy) => phy.link_speed(), + EthernetPhy::KSZ8081R(phy) => phy.link_speed(), + } + } + + #[allow(dead_code)] + pub fn release(self) -> M { + match self { + EthernetPhy::LAN8720A(phy) => phy.release(), + EthernetPhy::LAN8742A(phy) => phy.release(), + EthernetPhy::KSZ8081R(phy) => phy.release(), + } + } +} diff --git a/examples/rtic-stm32/src/main.rs b/examples/rtic-stm32/src/main.rs new file mode 100644 index 00000000..062f626e --- /dev/null +++ b/examples/rtic-stm32/src/main.rs @@ -0,0 +1,261 @@ +#![no_std] +#![no_main] + +use defmt_rtt as _; +use panic_probe as _; + +mod common; + +#[rtic::app(device = stm32_eth::stm32, dispatchers = [SPI1])] +mod app { + use crate::common::EthernetPhy; + use core::task::Poll; + use fugit::ExtU64; + use ieee802_3_miim::{phy::PhySpeed, Phy}; + use stm32_eth::{ + dma::{EthernetDMA, PacketId, RxRingEntry, TxRingEntry}, + mac::Speed, + ptp::{EthernetPTP, Timestamp}, + Parts, + }; + use systick_monotonic::Systick; + + #[local] + struct Local {} + + #[shared] + struct Shared { + dma: EthernetDMA<'static, 'static>, + ptp: EthernetPTP, + tx_id: Option<(u32, Timestamp)>, + scheduled_time: Option, + } + + #[monotonic(binds = SysTick, default = true)] + type Monotonic = Systick<1000>; + + #[init(local = [ + rx_ring: [RxRingEntry; 2] = [RxRingEntry::new(),RxRingEntry::new()], + tx_ring: [TxRingEntry; 2] = [TxRingEntry::new(),TxRingEntry::new()], + ])] + fn init(cx: init::Context) -> (Shared, Local) { + defmt::info!("Pre-init"); + let core = cx.core; + let p = cx.device; + + let rx_ring = cx.local.rx_ring; + let tx_ring = cx.local.tx_ring; + + let (clocks, gpio, ethernet) = crate::common::setup_peripherals(p); + + let rcc = device_peripherals.RCC; + let rcc = rcc.constrain(); + let _clocks = rcc.cfgr.sysclk(100.MHz()).pclk1(36.MHz()).freeze(); + + let systick_mono_token = rtic_monotonics::create_systick_token!(); + Systick::start(cx.core.SYST, 100_000_000, systick_mono_token); + + defmt::info!("Setting up pins"); + let (pins, mdio, mdc, pps) = crate::common::setup_pins(gpio); + + defmt::info!("Configuring ethernet"); + + let Parts { dma, mac, mut ptp } = + stm32_eth::new_with_mii(ethernet, rx_ring, tx_ring, clocks, pins, mdio, mdc).unwrap(); + + ptp.enable_pps(pps); + + defmt::info!("Enabling interrupts"); + dma.enable_interrupt(); + + match EthernetPhy::from_miim(mac, 0) { + Ok(mut phy) => { + defmt::info!( + "Resetting PHY as an extra step. Type: {}", + phy.ident_string() + ); + + phy.phy_init(); + + defmt::info!("Waiting for link up."); + + while !phy.phy_link_up() {} + + defmt::info!("Link up."); + + if let Some(speed) = phy.speed().map(|s| match s { + PhySpeed::HalfDuplexBase10T => Speed::HalfDuplexBase10T, + PhySpeed::FullDuplexBase10T => Speed::FullDuplexBase10T, + PhySpeed::HalfDuplexBase100Tx => Speed::HalfDuplexBase100Tx, + PhySpeed::FullDuplexBase100Tx => Speed::FullDuplexBase100Tx, + }) { + phy.get_miim().set_speed(speed); + defmt::info!("Detected link speed: {}", speed); + } else { + defmt::warn!("Failed to detect link speed."); + } + } + Err(_) => { + defmt::info!("Not resetting unsupported PHY. Cannot detect link speed."); + } + }; + + sender::spawn().ok(); + + ( + Shared { + dma, + tx_id: None, + scheduled_time: None, + ptp, + }, + Local {}, + ) + } + + #[task(shared = [dma, tx_id, ptp, scheduled_time], local = [tx_id_ctr: u32 = 0x8000_0000])] + async fn sender(cx: sender::Context) { + sender::spawn_after(1u64.secs()).ok(); + + const SIZE: usize = 42; + + // Obtain the current time to use as the "TX time" of our frame. It is clearly + // incorrect, but works well enough in low-activity systems (such as this example). + let now = (cx.shared.ptp, cx.shared.scheduled_time).lock(|ptp, sched_time| { + let now = EthernetPTP::get_time(); + #[cfg(not(feature = "stm32f107"))] + { + let in_half_sec = now + + Timestamp::new( + false, + 0, + stm32_eth::ptp::Subseconds::new_from_nanos(500_000_000).unwrap(), + ); + ptp.configure_target_time_interrupt(in_half_sec); + } + *sched_time = Some(now); + + now + }); + + const DST_MAC: [u8; 6] = [0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56]; + const SRC_MAC: [u8; 6] = [0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF]; + const ETH_TYPE: [u8; 2] = [0xFF, 0xFF]; // Custom/unknown ethertype + + let tx_id_ctr = cx.local.tx_id_ctr; + (cx.shared.dma, cx.shared.tx_id).lock(|dma, tx_id| { + let tx_id_val = *tx_id_ctr; + dma.send(SIZE, Some(PacketId(tx_id_val)), |buf| { + // Write the Ethernet Header and the current timestamp value to + // the frame. + buf[0..6].copy_from_slice(&DST_MAC); + buf[6..12].copy_from_slice(&SRC_MAC); + buf[12..14].copy_from_slice(Ð_TYPE); + buf[14..22].copy_from_slice(&now.raw().to_be_bytes()); + }) + .ok(); + *tx_id = Some((tx_id_val, now)); + *tx_id_ctr += 1; + *tx_id_ctr |= 0x8000_0000; + }); + } + + #[task(binds = ETH, shared = [dma, tx_id, ptp, scheduled_time], local = [pkt_id_ctr: u32 = 0], priority = 2)] + fn eth_interrupt(cx: eth_interrupt::Context) { + let packet_id_ctr = cx.local.pkt_id_ctr; + + ( + cx.shared.dma, + cx.shared.tx_id, + cx.shared.ptp, + cx.shared.scheduled_time, + ) + .lock(|dma, tx_id, ptp, _sched_time| { + let int_reason = stm32_eth::eth_interrupt_handler(); + + #[cfg(not(feature = "stm32f107"))] + { + if int_reason.time_passed { + if let Some(sched_time) = _sched_time.take() { + let now = EthernetPTP::get_time(); + defmt::info!( + "Got a timestamp interrupt {} seconds after scheduling", + now - sched_time + ); + } + } + } + + loop { + let packet_id = PacketId(*packet_id_ctr); + let rx_timestamp = if let Ok(packet) = dma.recv_next(Some(packet_id.clone())) { + let mut dst_mac = [0u8; 6]; + dst_mac.copy_from_slice(&packet[..6]); + + let ts = if let Some(timestamp) = packet.timestamp() { + timestamp + } else { + continue; + }; + + defmt::debug!("RX timestamp: {}", ts); + + if dst_mac == [0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56] { + let mut timestamp_data = [0u8; 8]; + timestamp_data.copy_from_slice(&packet[14..22]); + let raw = i64::from_be_bytes(timestamp_data); + + let timestamp = Timestamp::new_raw(raw); + + defmt::debug!("Contained TX timestamp: {}", ts); + + let diff = timestamp - ts; + + defmt::info!("Difference between TX and RX time: {}", diff); + + let addend = ptp.addend(); + let nanos = diff.nanos() as u64; + + if nanos <= 20_000 { + let p1 = ((nanos * addend as u64) / 1_000_000_000) as u32; + + defmt::debug!("Addend correction value: {}", p1); + + if diff.is_negative() { + ptp.set_addend(addend - p1 / 2); + } else { + ptp.set_addend(addend + p1 / 2); + }; + } else { + defmt::warn!("Updated time."); + ptp.update_time(diff); + } + } + + ts + } else { + break; + }; + + let polled_ts = dma.poll_timestamp(&packet_id); + + assert_eq!(polled_ts, Poll::Ready(Ok(Some(rx_timestamp)))); + + *packet_id_ctr += 1; + *packet_id_ctr &= !0x8000_0000; + } + + if let Some((tx_id, sent_time)) = tx_id.take() { + if let Poll::Ready(Ok(Some(ts))) = dma.poll_timestamp(&PacketId(tx_id)) { + defmt::info!("TX timestamp: {}", ts); + defmt::debug!( + "Diff between TX timestamp and the time that was put into the packet: {}", + ts - sent_time + ); + } else { + defmt::warn!("Failed to retrieve TX timestamp"); + } + } + }); + } +}