From c4962c8b9207e9474659526e91a1cf387d2a7b8f Mon Sep 17 00:00:00 2001 From: Ian McIntyre Date: Sun, 9 Aug 2026 12:09:22 -0400 Subject: Integrate ENET, GPIO, PHY drivers for 1170EVK --- Cargo.lock | 186 +++++++-- Cargo.toml | 16 + README.md | 17 +- crates/imxrt/Cargo.toml | 19 + crates/imxrt/build.rs | 17 + crates/imxrt/src/drivers/enet.rs | 849 +++++++++++++++++++++++++++++++++++++++ crates/imxrt/src/drivers/gpio.rs | 265 ++++++++++++ crates/imxrt/src/lib.rs | 40 ++ crates/imxrt1170evk/Cargo.toml | 4 +- crates/imxrt1170evk/src/lib.rs | 329 ++++++++++++--- crates/imxrt1170evk/src/main.rs | 28 ++ crates/net-phys/Cargo.toml | 8 + crates/net-phys/src/ksz8081.rs | 130 ++++++ crates/net-phys/src/lib.rs | 62 +++ 14 files changed, 1884 insertions(+), 86 deletions(-) create mode 100644 crates/imxrt/Cargo.toml create mode 100644 crates/imxrt/build.rs create mode 100644 crates/imxrt/src/drivers/enet.rs create mode 100644 crates/imxrt/src/drivers/gpio.rs create mode 100644 crates/imxrt/src/lib.rs create mode 100644 crates/net-phys/Cargo.toml create mode 100644 crates/net-phys/src/ksz8081.rs create mode 100644 crates/net-phys/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 7868e5d..108de34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,7 +56,7 @@ source = "git+https://github.com/rust-embedded/cortex-m?rev=8c62cc47314445346851 dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -76,7 +76,7 @@ checksum = "061a75f3e7f3f01d649668d68e02d0ef92c7041a9c97b8fe6bc354ddbf40822d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -124,9 +124,9 @@ checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" [[package]] name = "futures" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -138,9 +138,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -148,44 +148,44 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-macro", @@ -226,7 +226,87 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fd44f861bfa8cb39efcf6461461ed3c91c63f629c3811abc00c4310f7629ef" dependencies = [ "critical-section", - "ral-registers", + "ral-registers 0.1.3", +] + +[[package]] +name = "imxrt-drivers-ccm-11xx" +version = "0.1.0" +source = "git+https://git.mciantyre.dev/imxrt-drivers#725ce1d24529e3911dc35d08db4cf5acb9ffd06d" +dependencies = [ + "ral-registers 0.2.0", +] + +[[package]] +name = "imxrt-drivers-enet" +version = "0.1.0" +source = "git+https://git.mciantyre.dev/imxrt-drivers#725ce1d24529e3911dc35d08db4cf5acb9ffd06d" +dependencies = [ + "ral-registers 0.2.0", +] + +[[package]] +name = "imxrt-drivers-flexspi" +version = "0.1.0" +source = "git+https://git.mciantyre.dev/imxrt-drivers#725ce1d24529e3911dc35d08db4cf5acb9ffd06d" +dependencies = [ + "ral-registers 0.2.0", +] + +[[package]] +name = "imxrt-drivers-gpc-11xx" +version = "0.1.0" +source = "git+https://git.mciantyre.dev/imxrt-drivers#725ce1d24529e3911dc35d08db4cf5acb9ffd06d" +dependencies = [ + "ral-registers 0.2.0", +] + +[[package]] +name = "imxrt-drivers-gpio" +version = "0.1.0" +source = "git+https://git.mciantyre.dev/imxrt-drivers#725ce1d24529e3911dc35d08db4cf5acb9ffd06d" +dependencies = [ + "ral-registers 0.2.0", +] + +[[package]] +name = "imxrt-drivers-iomuxc-11xx" +version = "0.1.0" +source = "git+https://git.mciantyre.dev/imxrt-drivers#725ce1d24529e3911dc35d08db4cf5acb9ffd06d" +dependencies = [ + "ral-registers 0.2.0", +] + +[[package]] +name = "imxrt-drivers-lpspi" +version = "0.1.0" +source = "git+https://git.mciantyre.dev/imxrt-drivers#725ce1d24529e3911dc35d08db4cf5acb9ffd06d" +dependencies = [ + "ral-registers 0.2.0", +] + +[[package]] +name = "imxrt-drivers-pit" +version = "0.1.0" +source = "git+https://git.mciantyre.dev/imxrt-drivers#725ce1d24529e3911dc35d08db4cf5acb9ffd06d" +dependencies = [ + "ral-registers 0.2.0", +] + +[[package]] +name = "imxrt-drivers-pmu-11xx" +version = "0.1.0" +source = "git+https://git.mciantyre.dev/imxrt-drivers#725ce1d24529e3911dc35d08db4cf5acb9ffd06d" +dependencies = [ + "ral-registers 0.2.0", +] + +[[package]] +name = "imxrt-drivers-rtwdog" +version = "0.1.0" +source = "git+https://git.mciantyre.dev/imxrt-drivers#725ce1d24529e3911dc35d08db4cf5acb9ffd06d" +dependencies = [ + "ral-registers 0.2.0", ] [[package]] @@ -246,7 +326,7 @@ dependencies = [ "imxrt-iomuxc", "imxrt-ral", "nb 1.1.0", - "ral-registers", + "ral-registers 0.1.3", ] [[package]] @@ -261,7 +341,7 @@ version = "0.6.2" source = "git+https://github.com/mciantyre/imxrt-ral?branch=rust-threadx#83e17a2759d2ee82625d783b79ab0d2af8ee1460" dependencies = [ "cortex-m", - "ral-registers", + "ral-registers 0.1.3", ] [[package]] @@ -285,6 +365,25 @@ dependencies = [ "usb-device", ] +[[package]] +name = "imxrt1170" +version = "0.1.0" +source = "git+https://git.mciantyre.dev/imxrt-drivers#725ce1d24529e3911dc35d08db4cf5acb9ffd06d" +dependencies = [ + "cortex-m", + "imxrt-drivers-ccm-11xx", + "imxrt-drivers-enet", + "imxrt-drivers-flexspi", + "imxrt-drivers-gpc-11xx", + "imxrt-drivers-gpio", + "imxrt-drivers-iomuxc-11xx", + "imxrt-drivers-lpspi", + "imxrt-drivers-pit", + "imxrt-drivers-pmu-11xx", + "imxrt-drivers-rtwdog", + "ral-registers 0.2.0", +] + [[package]] name = "imxrt1170evk-fcb" version = "0.2.0" @@ -337,9 +436,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "portable-atomic" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "proc-macro2" @@ -365,22 +464,48 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46b71a9d9206e8b46714c74255adcaea8b11e0350c1d8456165073c3f75fc81a" +[[package]] +name = "ral-registers" +version = "0.2.0" +source = "git+https://git.mciantyre.dev/ral-registers?branch=v0.2#495fbc58b6f4d2e751b16883a2c5cee3c598fd79" + [[package]] name = "rust-threadx-cortex-m" version = "0.1.0" +[[package]] +name = "rust-threadx-imxrt" +version = "0.1.0" +dependencies = [ + "cortex-m", + "imxrt-drivers-enet", + "imxrt-drivers-gpio", + "imxrt-rt", + "ral-registers 0.2.0", + "rust-threadx-net-phys", +] + [[package]] name = "rust-threadx-imxrt1170evk" version = "0.1.0" dependencies = [ "cortex-m", - "imxrt-ral", "imxrt-rt", + "imxrt1170", "imxrt1170evk-fcb", "rust-threadx-cortex-m", + "rust-threadx-imxrt", + "rust-threadx-net-phys", "rust-threadx-stdio-semihosting", ] +[[package]] +name = "rust-threadx-net-phys" +version = "0.1.0" +dependencies = [ + "bitflags", +] + [[package]] name = "rust-threadx-qemu" version = "0.1.0" @@ -463,6 +588,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "teensy4-bsp" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index ab25a71..d42b128 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ members = ["crates/*"] [workspace.dependencies] rust-threadx-cortex-m = { path = "crates/cortex-m" } +rust-threadx-imxrt = { path = "crates/imxrt" } +rust-threadx-net-phys = { path = "crates/net-phys" } rust-threadx-qemu = { path = "crates/qemu" } rust-threadx-stdio-imxrt-usbd = { path = "crates/stdio-imxrt-usbd" } rust-threadx-stdio-semihosting = { path = "crates/stdio-semihosting" } @@ -16,6 +18,12 @@ lm3s6965 = "0.2" imxrt-ral = { version = "0.6.2", features = ["rt"] } imxrt-rt = { version = "0.1.8", features = ["device"] } +imxrt-drivers-enet = { git = "https://git.mciantyre.dev/imxrt-drivers" } +imxrt-drivers-gpio = { git = "https://git.mciantyre.dev/imxrt-drivers" } +imxrt1170 = { git = "https://git.mciantyre.dev/imxrt-drivers" } + +ral-registers = { version = "0.2" } + [patch.crates-io] cortex-m = { git = "https://github.com/rust-embedded/cortex-m", rev = "8c62cc47314445346851b0d0664f9cc34c50b24d" } imxrt-ral = { @@ -34,3 +42,11 @@ teensy4-bsp = { git = "https://github.com/mciantyre/teensy4-rs", branch = "rust-threadx", } + +[patch.crates-io.ral-registers-0x2] +package = "ral-registers" +git = "https://git.mciantyre.dev/ral-registers" +branch = "v0.2" + +[profile.release] +opt-level = "s" diff --git a/README.md b/README.md index d037d41..bdfadc1 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ They work in / on - a Teensy 4.0 - an iMXRT1170EVK -Right now, they blink LEDs and / or print `"hello world!"`. +Right now, most apps blink LEDs and / or print `"hello world!"`. +The iMXRT1170EVK can demonstrate a basic UDP loopback, and you can `ping` this board. These demos are for [my RustConf 2026 talk][project-landing]! @@ -51,7 +52,11 @@ The board's stdio uses USB serial for stdout; stdin is not (yet) implemented. ### iMXRT1170EVK -You'll need the board and a recent build of [probe-rs]. +You'll need the board, a recent build of [probe-rs], and a way to connect to the board's Ethernet network. +Connect your development host to the 100M ENET interface. +The board thinks its IP address is `192.168.5.1`. +Assign your host an address on that `/24` network. + Run the following to compile and flash the example: [probe-rs]: https://probe.rs @@ -62,3 +67,11 @@ cargo +stage1 --config .cargo/imxrt1170evk.toml run --release --package=rust-thr You should see the LED blinking. The board's stdio uses semihosting for input and output; probe-rs should automatically detect this. + +If you `ping` the board, it responds. + +Connect to the UDP loopback port `5678` and talk with yourself: + +``` +printf 'hello world' | nc -u -w1 192.168.5.1 5678 +``` diff --git a/crates/imxrt/Cargo.toml b/crates/imxrt/Cargo.toml new file mode 100644 index 0000000..a5b7a15 --- /dev/null +++ b/crates/imxrt/Cargo.toml @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: Copyright 2025 Ian McIntyre + +[package] +name = "rust-threadx-imxrt" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +cortex-m = { workspace = true } +imxrt-rt = { workspace = true, optional = true } + +rust-threadx-net-phys = { workspace = true } + +imxrt-drivers-enet = { workspace = true } +imxrt-drivers-gpio = { workspace = true } + +ral-registers = { workspace = true } diff --git a/crates/imxrt/build.rs b/crates/imxrt/build.rs new file mode 100644 index 0000000..5bf94e7 --- /dev/null +++ b/crates/imxrt/build.rs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: Copyright 2025 Ian McIntyre + +use std::env; + +fn main() { + let out_dir = std::path::PathBuf::from(env::var("OUT_DIR").unwrap()); + std::fs::write( + out_dir.join("weak-symbols.x"), + r#" +PROVIDE(PendSV = PendSV_Handler); +PROVIDE(SysTick = _tx_timer_interrupt); +"#, + ) + .unwrap(); + println!("cargo::rustc-link-search={}", out_dir.display()); +} diff --git a/crates/imxrt/src/drivers/enet.rs b/crates/imxrt/src/drivers/enet.rs new file mode 100644 index 0000000..611fa51 --- /dev/null +++ b/crates/imxrt/src/drivers/enet.rs @@ -0,0 +1,849 @@ +use crate::Instance; +use crate::ral::enet; +use core::{ + num::NonZeroU32, + pin::Pin, + sync::atomic::{self, Ordering}, +}; + +use fusible::{ + event_flags::{EventFlags, EventFlagsContext, GetOption, SetOption}, + netx_duo::{ + driver, + interface::{Capability, Interface}, + ip::Ip, + packet::{self, PacketChainer, PacketList, PacketListChainView, PacketPool}, + }, +}; + +use rust_threadx_net_phys::{Mdio, Phy}; + +use super::gpio::GpioOutput; + +pub use enet::{RxRing, TxRing, rx_bd::RxBD, tx_bd::TxBD}; + +pub struct EnetContext { + flags: EventFlagsContext<'static>, +} + +const MII_EVENT: NonZeroU32 = NonZeroU32::new(enet::EIR::MII::mask).unwrap(); + +impl EnetContext { + pub const fn new() -> Self { + Self { + flags: EventFlags::context(), + } + } + + #[inline(always)] + pub unsafe fn on_interrupt( + &'static self, + enet: Instance, + ip: &'static Ip, + ) { + unsafe { + let eir = crate::read_reg!(enet, enet, EIR); + crate::write_reg!(enet, enet, EIR, eir); + + if eir & DEFERRED_INTERRUPTS.get() != 0 { + ip.deferred_processing(); + } + + Pin::static_ref(&self.flags) + .assume_created() + .set(eir, SetOption::Or); + } + } + + pub unsafe fn create>( + &'static self, + enet: Instance, + tx_ring: &'static [TxBD], + rx_ring: &'static [RxBD], + phy: P, + rst: GpioOutput, + mdio_source_clock_hz: u32, + ) -> Enet

{ + let flags = EventFlags::create(Pin::static_ref(&self.flags), &Default::default()).unwrap(); + + crate::write_reg!(enet, enet, ECR, RESET: 1); + + const SMI_MDC_FREQUENCY_HZ: u32 = 2_500_000; + let mii_speed = mdio_source_clock_hz.div_ceil(2 * SMI_MDC_FREQUENCY_HZ) - 1; + let hold_time = 10_u32.div_ceil(1_000_000_000 / mdio_source_clock_hz) - 1; + crate::modify_reg!(enet, enet, MSCR, HOLDTIME: hold_time, MII_SPEED: mii_speed); + + Enet { + enet, + phy, + rst, + flags, + + tx_ring: TransmitRing::new(tx_ring), + rx_ring: ReceiveRing::new(rx_ring), + } + } +} + +pub struct EnetMdio { + enet: Instance, + flags: &'static EventFlags, +} + +pub struct Enet> { + enet: Instance, + phy: P, + rst: GpioOutput, + flags: &'static EventFlags, + + tx_ring: TransmitRing, + rx_ring: ReceiveRing, +} + +unsafe impl> Send for Enet

{} + +impl Mdio for EnetMdio { + type Error = core::convert::Infallible; + + fn read(&mut self, ctrl: u16) -> Result { + let mmfr = (ctrl as u32) << 16; + crate::write_reg!(enet, self.enet, MMFR, mmfr); + + self.flags.get(MII_EVENT, GetOption::AndClear).unwrap(); + + // Automatically discards control bits. + let data = crate::read_reg!(enet, self.enet, MMFR, DATA) as u16; + Ok(data) + } + + fn write(&mut self, ctrl: u16, data: u16) -> Result<(), Self::Error> { + let mmfr = (ctrl as u32) << 16 | data as u32; + crate::write_reg!(enet, self.enet, MMFR, mmfr); + + self.flags.get(MII_EVENT, GetOption::AndClear).unwrap(); + + Ok(()) + } +} + +impl> driver::Driver<'static> for Enet

{ + fn attach_interface( + self: Pin<&'static mut Self>, + mut extras: driver::DriverExtras<'static>, + ) -> Result<(), driver::DriverError> { + extras + .interface + .set_capability( + extras.ip, + Capability::IPV4_RX_CHECKSUM + | Capability::IPV4_TX_CHECKSUM + | Capability::UDP_RX_CHECKSUM + | Capability::UDP_TX_CHECKSUM + | Capability::TCP_RX_CHECKSUM + | Capability::TCP_TX_CHECKSUM, + ) + .unwrap(); + + // TODO: this may need to be bound by MRBR. Otherwise, + // we're lying to the upper levels. + extras.interface.set_mtu(extras.ip, 1500); + extras.interface.driver_needs_mapping(extras.ip); + + Ok(()) + } + + fn initialize_link( + self: Pin<&'static mut Self>, + _: driver::DriverExtras<'static>, + ) -> Result<(), driver::DriverError> { + // Safety: not relocating anything in memory. + let this = unsafe { self.get_unchecked_mut() }; + initialize_enet( + this.enet, + this.tx_ring.buffer_descriptors, + this.rx_ring.buffer_descriptors, + true, + ); + + this.rst.clear(); + fusible::thread::sleep(10); + this.rst.set(); + fusible::thread::sleep(10); + + let mut mdio = EnetMdio { + enet: this.enet.clone(), + flags: this.flags, + }; + + this.phy.initialize(&mut mdio).unwrap(); + + Ok(()) + } + + fn enable_link( + self: Pin<&'static mut Self>, + mut extras: driver::DriverExtras<'static>, + ) -> Result<(), driver::DriverError> { + // Safety: not moving out of self. + let this = unsafe { self.get_unchecked_mut() }; + let mrbr = schedule_receives(&mut this.rx_ring, extras.ip.default_packet_pool()); + assert_ne!(mrbr, 0); + + crate::write_reg!(enet, this.enet, MRBR, mrbr as u32); + crate::write_reg!(enet, this.enet, RDAR, RDAR: 1); + + extras.interface.set_link_up(true); + + Ok(()) + } + + fn set_physical_address( + self: Pin<&'static mut Self>, + extras: driver::DriverExtras<'static>, + ) -> Result<(), driver::DriverError> { + crate::write_reg!(enet, self.enet, PALR, extras.msw << 16 | (extras.lsw >> 16)); + crate::write_reg!(enet, self.enet, PAUR, extras.lsw << 16); + Ok(()) + } + + fn deferred_processing( + self: Pin<&'static mut Self>, + extras: driver::DriverExtras<'static>, + ) -> Result<(), driver::DriverError> { + // Safety: not moving out of this object. + let this = unsafe { self.get_unchecked_mut() }; + let eir = this.flags.try_get(DEFERRED_INTERRUPTS, GetOption::OrClear); + let eir = eir.map_or(0, NonZeroU32::get); + + if eir & TRANSMIT_INTERRUPT_EVENTS != 0 { + this.tx_ring.deallocate_completions(); + } + + if eir & RECEIVE_INTERRUPT_EVENTS != 0 { + while let Some(packet) = this.rx_ring.try_frame_receive() { + enqueue_rx_packet_to_ip(packet, extras.ip, extras.interface); + } + } + + // Make sure receive operations are primed. Otherwise, + // we drop data. Once all descriptors are saturated, + // try transmitting pending packets. + if 0 != schedule_receives(&mut this.rx_ring, extras.ip.default_packet_pool()) { + crate::write_reg!(enet, this.enet, RDAR, RDAR: 1); + } + + this.tx_ring.schedule_transmits(); + crate::write_reg!(enet, this.enet, TDAR, TDAR: 1); + + Ok(()) + } + + fn send_packet( + self: Pin<&'static mut Self>, + extras: driver::DriverExtras<'static>, + ethertype: u16, + ) -> Result<(), driver::DriverError> { + let Some(mut packet) = extras.packet else { + return Err(driver::DriverError::Unhandled); + }; + + prepare_transmit_head(&mut packet, ethertype, extras.msw, extras.lsw); + + // Safety: we're not moving out of this object. + let this = unsafe { self.get_unchecked_mut() }; + this.tx_ring.fifo_pending.push_back(packet); + this.tx_ring.schedule_transmits(); + crate::write_reg!(enet, this.enet, TDAR, TDAR: 1); + + Ok(()) + } +} + +const RECEIVE_INTERRUPT_EVENTS: u32 = enet::EIR::RXB::mask | enet::EIR::RXF::mask; +const TRANSMIT_INTERRUPT_EVENTS: u32 = enet::EIR::TXB::mask | enet::EIR::TXF::mask; + +const DEFERRED_INTERRUPTS: NonZeroU32 = + NonZeroU32::new(RECEIVE_INTERRUPT_EVENTS | TRANSMIT_INTERRUPT_EVENTS).unwrap(); + +/// Initialize the ENET IP block. +/// +/// This assumes that you've already reset the IP block. When this +/// call returns, the ENET block is enabled. This is required for +/// MDIO interrupt activation. Although the IP block is enabled, +/// the call does not initialize any I/O. +fn initialize_enet( + enet: Instance, + tx_ring: &'static [TxBD], + rx_ring: &'static [RxBD], + rmii: bool, +) { + crate::modify_reg!(enet, enet, ECR, + DBSWP: 1, // Swap data for this little endian device. + EN1588: 1, // Use enhanced buffer descriptors. + RESET: 0, // I think this auto-clears, but just in case... + DBGEN: 0, // Keep running the MAC in debug mode. + ); + + // Clear all interrupt flags. + crate::write_reg!(enet, enet, EIR, u32::MAX); + + // Unmask interrupts. + // + // Make sure to update DEFERRED_INTERRUPTS if the event + // needs to signal the IP thread. + crate::write_reg!(enet, enet, EIMR, + // MDIO completions. + MII: 1, + // Receive buffer complete. + RXB: 1, + // Receive frame complete. + RXF: 1, + // Transmit buffer complete. + TXB: 1, + // Transmit frame complete. + TXF: 1, + ); + + // Tell the DMA engine which descriptors are the last + // ones in the ring. + if let Some(tx_tail) = tx_ring.last() { + tx_tail + .flags + .fetch_or(enet::tx_bd::FLAGS_WRAP, Ordering::Relaxed); + } + if let Some(rx_tail) = rx_ring.last() { + rx_tail + .flags + .fetch_or(enet::rx_bd::FLAGS_WRAP, Ordering::Relaxed); + } + + // Establish the ring starting addresses. + crate::write_reg!(enet, enet, TDSR, tx_ring.as_ptr() as u32); + crate::write_reg!(enet, enet, RDSR, rx_ring.as_ptr() as u32); + + crate::modify_reg!(enet, enet, RCR, + // Default max frame length without VLAN tags. + MAX_FL: 1518, + // Disable loopback by default. If we expose half-duplex to + // the user, we wouldn't be able to support this. + LOOP: 0, + // No need to snoop. + PROM: 0, + // Do not reject broadcast frames; we might be interested + // in these. + BC_REJ: 0, + // The MAC doesn't supply pause frames to the application. + PAUFWD: 0, + // Drop padding, along with the CRC, when supplying frames + // to our software. This configuration implicitly includes + // the CRC, so the CRCFWD below has no effect. + PADEN: 1, + // Drop the CRC in received frames. This doesn't turn off + // CRC checking at the hardware level. + // + // If PADEN is set, this configuration does nothing. + CRCFWD: 1, + // Check the payload length based on the expected frame type / + // frame length (encoded in the frame). + NLC: 1, + // Enable flow control; react to pause frames by pausing the data + // transmit paths. + FCE: 1, + // MII or RMII mode; must be set. + MII_MODE: 1, + // Is this an RMII interface? + RMII_MODE: rmii as u32, + // Default to 100Mbit/sec. + RMII_10T: 0, + // For prototyping purposes, we're strictly a full-duplex MAC. + DRT: 0, + ); + + crate::modify_reg!(enet, enet, TCR, + // We told the IP thread to skip software CRCs. Let + // the hardware handle it. + CRCFWD: 0, + // We'll program our source MAC into the PADDR registers. + // It's our job to set up the frame, so we can choose to + // have the hardware inject the source address. + ADDINS: 1, + // For prototyping purposes, we're strictly + // a full-duplex MAC. + FDEN: 1, + ); + + // Enable store-and-forward: start transmitting once you have a complete + // frame in the FIFO. + crate::modify_reg!(enet, enet, TFWR, STRFWD: 1); + // Maintain store-and-forward on the receive path: use the receive queue + // as a buffer until an entire frame is received. + crate::write_reg!(enet, enet, RSFL, 0); + + // These accelerator options assume store-and-forward operations on both + // data paths. See above. + crate::modify_reg!(enet, enet, RACC, + // Discard frames with MAC errors (checksumming, length, PHY errors). + LINEDIS: 1, + // Discard frames with the wrong checksums for the protocol and headers. + PRODIS: 1, + IPDIS: 1, + // Discard any padding within a short IP datagram. + PADREM: 1, + // Insert two extra bytes so that the data section is four byte aligned. + SHIFT16: 1, + ); + crate::modify_reg!(enet, enet, TACC, + // Enable protocol checksums. Assumes that the netx-duo sets these fields + // to zero on our behalf. + PROCHK: 1, + // Enable IP checksum injection into the IPv4 header. Assumes that netx-duo + // sets these fields to zero on our behalf. + IPCHK: 1, + // Expect two extra bytes when transmitting data. + SHIFT16: 1, + ); + + // Enable the IP block. + crate::modify_reg!(enet, enet, ECR, ETHEREN: 1); +} + +/// Set up receive DMA operations for packet reception. +fn schedule_receives(rx_ring: &mut ReceiveRing, packet_pool: &'static PacketPool) -> usize { + let mut data_capacity = 0; + while rx_ring.is_schedulable() + && let Some(packet) = packet_pool + .try_allocate(packet::PacketType::Receive) + .unwrap() + { + data_capacity = packet.data_capacity(); + assert_ne!(data_capacity, 0); + assert!(data_capacity % 64 == 0); + + rx_ring.schedule_next(packet); + } + + data_capacity +} + +/// Give the received packet to the IP instance. +fn enqueue_rx_packet_to_ip( + mut packet: packet::Packet<'static>, + ip: &'static Ip, + interface: Interface<'static>, +) { + // Safety: Interface and packet have same lifetime. + unsafe { packet.set_ip_interface(interface) }; + + // Safety: Assuming no 802.1 tag, the Ethertype enum + // starts at 6 + 6 bytes from the start of the data. + // It uses network byte order. + let ethertype = unsafe { + let prepend_ptr = packet.prepend_ptr().add(12); + (prepend_ptr.read() as u16) << 8 | prepend_ptr.add(1).read() as u16 + }; + + if ![ + driver::ETHERTYPE_ARP, + driver::ETHERTYPE_IPV4, + driver::ETHERTYPE_IPV6, + driver::ETHERTYPE_RARP, + ] + .contains(ðertype) + { + return; // Packet drop deallocates the packet. + } + + // We know how to handle this! Hide the Ethernet header from + // the network stack. + // + // Safety: we know that we just recieved this data and that the + // RX ring removes the two byte padding that starts this data. + // + // Safety: data remains in bounds. We're removing pointers and + // info to access valid data. + unsafe { + packet.set_prepend_ptr(packet.prepend_ptr().add(driver::ETHERNET_FRAME_SIZE)); + packet.set_len(packet.len() - driver::ETHERNET_FRAME_SIZE); + } + + if ethertype == driver::ETHERTYPE_IPV4 || ethertype == driver::ETHERTYPE_IPV6 { + ip.defer_ip_receive(packet); + } else if ethertype == driver::ETHERTYPE_ARP { + ip.defer_arp_receive(packet); + } else if ethertype == driver::ETHERTYPE_RARP { + ip.defer_rarp_receive(packet); + } else { + unreachable!(); + } +} + +fn prepare_transmit_head(packet: &mut packet::Packet<'_>, ethertype: u16, msw: u32, lsw: u32) { + // Make space for an Ethernet header. + // + // Safety: user expected to have allocated this packet + // appropriately. + unsafe { + packet.set_prepend_ptr(packet.prepend_ptr().sub(driver::ETHERNET_FRAME_SIZE)); + packet.set_len(packet.len() + driver::ETHERNET_FRAME_SIZE); + } + + // Fill in the destination MAC and ethertype. + // + // Safety: pointer is in range for all accesses. + unsafe { + let prepend_ptr = packet.prepend_ptr(); + + prepend_ptr.add(0).write((msw >> 8) as u8); + prepend_ptr.add(1).write(msw as u8); + prepend_ptr.add(2).write((lsw >> 24) as u8); + prepend_ptr.add(3).write((lsw >> 16) as u8); + prepend_ptr.add(4).write((lsw >> 8) as u8); + prepend_ptr.add(5).write(lsw as u8); + + // MAC fills in the source address. + + prepend_ptr.add(12).write((ethertype >> 8) as u8); + prepend_ptr.add(13).write(ethertype as u8); + } + + // Make space for our two byte padding. + // + // Safety: NetX assumes a 16 byte allocation for the Ethernet + // header. Therefore, this remains in range. + unsafe { + packet.set_prepend_ptr(packet.prepend_ptr().sub(2)); + packet.set_len(packet.len() + 2); + } +} + +/// Ring state for managing receive operations. +struct ReceiveRing { + buffer_descriptors: &'static [RxBD], + idx_in_flight: usize, + idx_schedulable: usize, + + fifo_in_flight: PacketList<'static>, + fifo_chaining: PacketChainer<'static>, +} + +impl ReceiveRing { + const fn new(buffer_descriptors: &'static [RxBD]) -> Self { + Self { + buffer_descriptors, + idx_in_flight: 0, + idx_schedulable: 0, + fifo_in_flight: PacketList::empty(), + fifo_chaining: PacketChainer::empty(), + } + } + + /// Do we have a descriptor for scheduling a receive? + fn is_schedulable(&self) -> bool { + let rx_bd = &self.buffer_descriptors[self.idx_schedulable]; + let flags = rx_bd.flags.load(Ordering::Relaxed); + flags & enet::rx_bd::FLAGS_EMPTY == 0 && flags & enet::rx_bd::FLAGS_RECEIVE_OWNERSHP_1 == 0 + } + + /// Schedule a receive into the given packet. + /// + /// Assumes we have a schedulable descriptor. This updates + /// the next schedulable descriptor. + fn schedule_next(&mut self, packet: packet::Packet<'static>) { + let rx_bd = &self.buffer_descriptors[self.idx_schedulable]; + + rx_bd.data_length.store(0, Ordering::Relaxed); + rx_bd + .data_buffer_pointer + .store(packet.prepend_ptr() as u32, Ordering::Relaxed); + rx_bd + .control + .fetch_or(enet::rx_bd::CONTROL_INT, Ordering::Relaxed); + rx_bd.flags.fetch_or( + enet::rx_bd::FLAGS_EMPTY | enet::rx_bd::FLAGS_RECEIVE_OWNERSHP_1, + Ordering::Relaxed, + ); + + atomic::fence(Ordering::Release); + + self.fifo_in_flight.push_back(packet); + + self.idx_schedulable = (self.idx_schedulable + 1) % self.buffer_descriptors.len(); + } + + /// Try to receive a frame with one or more packets. + /// + /// If this returns a packet, you should try calling it + /// again; it may be able to produce another packet. + fn try_frame_receive(&mut self) -> Option> { + // Show the borrow checker what we're doing. + let Self { + buffer_descriptors, + idx_in_flight, + fifo_in_flight, + fifo_chaining, + .. + } = self; + + // Which receive operations have complete? Pop those packets so we can + // start to form a packet chain. + let completions = core::iter::from_fn(|| { + let rx_bd = &buffer_descriptors[*idx_in_flight]; + let flags = rx_bd.flags.load(Ordering::Relaxed); + let ready = !fifo_in_flight.is_empty() + && flags & enet::rx_bd::FLAGS_EMPTY == 0 + && flags & enet::rx_bd::FLAGS_RECEIVE_OWNERSHP_1 != 0; + + ready.then(|| { + rx_bd + .flags + .fetch_and(!enet::rx_bd::FLAGS_RECEIVE_OWNERSHP_1, Ordering::Relaxed); + + // Panic unlikely. We checked if the FIFO is empty + // when deciding if it's ready. + let packet = fifo_in_flight.pop_front().unwrap(); + *idx_in_flight = (*idx_in_flight + 1) % buffer_descriptors.len(); + + let is_last = flags & enet::rx_bd::FLAGS_LAST != 0; + let total_data_length: usize = rx_bd.data_length.load(Ordering::Relaxed).into(); + + (total_data_length, packet, is_last) + }) + }) + // Loop bound by the number of receive descriptors that could + // possibly be filled. Without this, we could enter the loop, + // observe that all descriptors are filled, and loop forever. + // We must eventually break to prime another batch of descriptors. + .take(buffer_descriptors.len()); + + for (total_data_length, mut packet, is_last) in completions { + // Per the ENET docs, when the last flag is set, the data length describes + // the total length of the entire frame. We only use this when figuring out + // information for the last packet. Note that this includes the two bytes + // of padding. + + if is_last { + // The number of other packets in the chain, + // besides this one. This packet hasn't been + // inserted into the chain, yet, so this can + // be zero, signaling "only packet." + let other_packets = fifo_chaining.len(); + + // If there are other packets, they're saturated + // to capacity. + let data_in_other_packets = other_packets * packet.data_capacity(); + + // The data in the final (only) packet is the total length + // without the data in other packets. This includes the + // two bytes of padding. + let our_data_len = total_data_length - data_in_other_packets; + + // Safety: computation of data in the final packet maintains + // an in-bounds offset into the packet's data. It's known that + // a receive packet is allocated with its data start and prepend + // pointers pointing at the same place. + // + // If this is an only packet, then the offset already accounts + // for the two byte padding that we'll strip from the prepend + // pointer, later. If this is the tail of the packet chain, then + // we already need to handle the extra two bytes that aren't in + // the chain's head packet. + unsafe { + packet.set_append_ptr(packet.prepend_ptr().add(our_data_len)); + } + } else { + // Safety: This is an intermediate packet in the chain. + // Since it's not a last packet, it's not an only packet. + // Therefore, it's been filled to capacity by the DMA + // engine. + // + // Since it's been filled to capacity, the data end pointer + // represents valid data. Module inspection shows that the + // maximum buffer size is the packet capacity. It's known + // that a receive type pointer has its data start and prepend + // pointers pointing at the same address. + unsafe { + packet.set_append_ptr(packet.data_end()); + } + } + + // Insert all packets into the current chain. + // Once we see the last packet, we'll pop the + // chains head, clean it up, and hand it off. + fifo_chaining.push_back(packet); + if is_last { + // Panic unlikely. We just pushed a packet into the FIFO, before + // the branch. + let mut packet = fifo_chaining.pop().unwrap(); + + // Safety: We're trusting the hardware to represent the total + // frame size in this descriptor. We remove the two bytes of + // padding inserted as data. + unsafe { packet.set_len(total_data_length - 2) }; + + // Safety: prepend pointer and its two byte offset are part of + // the same head packet. We're simply telling the netstack + // to skip the invalid two bytes in the front of the packet. + unsafe { packet.set_prepend_ptr(packet.prepend_ptr().add(2)) }; + + // We formed a chained packet. Return it to the user. + // + // If there's more data to process, the user can keep + // calling us. + return Some(packet); + } + } + + // Nothing was ready. + None + } +} + +/// Ring state for managing transmits. +struct TransmitRing { + buffer_descriptors: &'static [TxBD], + + idx_schedulable: usize, + idx_in_flight: usize, + + /// Packets that need to be assigned + /// to a descriptor. + fifo_pending: PacketListChainView<'static>, + /// Packets that are in flight. + fifo_in_flight: PacketList<'static>, +} + +impl TransmitRing { + const fn new(buffer_descriptors: &'static [TxBD]) -> Self { + Self { + buffer_descriptors, + idx_schedulable: 0, + idx_in_flight: 0, + fifo_pending: PacketListChainView::empty(), + fifo_in_flight: PacketList::empty(), + } + } + + /// Try scheduling transmits for a chain of packets. + /// + /// Returns the packet once all packets in its chain + /// have been scheduled. Otherwise, returns `None` if + /// there are packets in the chain that still need + /// scheduling. Keep calling this in a loop to drive + /// packet scheduling. + fn try_schedule_packet_chain(&mut self) -> Option> { + // Flags set for the last packet in the frame. + const LAST_FRAME_FLAGS: u16 = + enet::tx_bd::FLAGS_LAST_IN_FRAME | enet::tx_bd::FLAGS_TRANSMIT_CRC; + + let Self { + buffer_descriptors, + idx_schedulable, + fifo_pending, + .. + } = self; + + // True while we're iterating over + // a chain of packets. + while fifo_pending.has_chain_link() { + let tx_bd = &buffer_descriptors[*idx_schedulable]; + + if tx_bd.flags.load(Ordering::Relaxed) & enet::tx_bd::FLAGS_READY != 0 { + // Descriptor is waiting to transmit. + return None; + } + + // Schedule a DMA transfer from the chained packet we're + // looking at. + fifo_pending.with_chain_link(|packet| { + if packet.has_chained_packet() { + // Clear any "last packet" flags set by the prior transfer. + tx_bd.flags.fetch_and(!LAST_FRAME_FLAGS, Ordering::Relaxed); + } else { + // This is the final packet in the chain. + tx_bd.flags.fetch_or(LAST_FRAME_FLAGS, Ordering::Relaxed); + } + + tx_bd + .data_buffer_pointer + .store(packet.prepend_ptr() as u32, Ordering::Relaxed); + tx_bd + .data_length + .store(packet.data_length() as u16, Ordering::Relaxed); + tx_bd + .control + .fetch_or(enet::tx_bd::CONTROL_INT, Ordering::Relaxed); + tx_bd + .flags + .fetch_or(enet::tx_bd::FLAGS_READY, Ordering::Relaxed); + + atomic::fence(Ordering::Release); + }); + + // We used a buffer descriptor. Go to the next one. + *idx_schedulable = (*idx_schedulable + 1) % buffer_descriptors.len(); + + // If this was the last packet in the chain, + // we'll break the loop. Otherwise, we'll keep + // looping while we have ready descriptors. + fifo_pending.advance_chain_link(); + } + + // Either pops the packet chain that's been + // scheduled. Or, it pops None when there's + // nothing in the FIFO. + return fifo_pending.pop_front(); + } + + /// Try to schedule transmit operations on pending packets. + fn schedule_transmits(&mut self) { + while let Some(packet) = self.try_schedule_packet_chain() { + self.fifo_in_flight.push_back(packet); + } + } + + /// Release descriptors and packets that have completed transmission. + fn deallocate_completions(&mut self) { + let completions = core::iter::from_fn(|| { + let tx_bd = &self.buffer_descriptors[self.idx_in_flight]; + let flags = tx_bd.flags.load(Ordering::Relaxed); + + let complete = !self.fifo_in_flight.is_empty() && flags & enet::tx_bd::FLAGS_READY == 0; + + complete.then(|| { + self.idx_in_flight = (self.idx_in_flight + 1) % self.buffer_descriptors.len(); + + let last = flags & enet::tx_bd::FLAGS_LAST_IN_FRAME != 0; + last.then(|| self.fifo_in_flight.pop_front().unwrap()) + }) + }) + // Bound the number of descriptors that could possibly + // complete in one evaluation. Without this, there's a + // chance to loop endlessly when the number of packets + // exceeds the number of available descriptors. Break + // the loop to continue packet scheduling. + .take(self.buffer_descriptors.len()); + + // Loop runs for every complete packet, which may + // not be the last packet. Drive the completions as + // far as possible so we can deallocate the packet + // chain ASAP. + for packet in completions { + if let Some(mut packet) = packet { + // Strip the Ethernet frame and padding before deallocation. + // + // Safety: pointer remains in bounds of an allocation. + unsafe { + packet + .set_prepend_ptr(packet.prepend_ptr().add(driver::ETHERNET_FRAME_SIZE + 2)); + packet.set_len(packet.len() - driver::ETHERNET_FRAME_SIZE - 2); + } + + // Try to release a packet that may need retransmission. + // + // If the release didn't happen, we'll be given back the + // packet through another send call. + packet.transmit_release(); + } + } + } +} diff --git a/crates/imxrt/src/drivers/gpio.rs b/crates/imxrt/src/drivers/gpio.rs new file mode 100644 index 0000000..b1f883b --- /dev/null +++ b/crates/imxrt/src/drivers/gpio.rs @@ -0,0 +1,265 @@ +use crate::Instance; +use core::num::NonZero; +use core::pin::Pin; + +use fusible::event_flags::{EventFlagsContext, GetError, GetOption, SetOption}; + +use crate::ral::gpio; +use fusible::{event_flags::EventFlags, interrupt_control}; + +pub struct GpioContext { + flags: EventFlagsContext<'static>, +} + +unsafe impl Sync for GpioPort {} + +impl GpioContext { + /// # Safety + /// + /// You must call this in the interrupt associated with this GPIO context. + /// You must have already created the context. + #[inline(always)] + pub unsafe fn on_interrupt(&'static self, port: Instance) { + // Safety: caller claims that we've already created the context. + // We are the "owners" of the GPIO port we're pointing at, and + // we control interrupts when accessing the IMR and ISR registers. + unsafe { + let imr = crate::read_reg!(gpio, port, IMR); + let isr = crate::read_reg!(gpio, port, ISR); + + crate::write_reg!(gpio, port, IMR, imr & !isr); + crate::write_reg!(gpio, port, ISR, isr); + + Pin::static_ref(&self.flags) + .assume_created() + .set(isr, SetOption::Or) + } + } + + /// # Panics + /// + /// Panics if you've already created the context. Also panics if + /// called in an interrupt context, or if called before the kernel + /// is entered. + pub unsafe fn create(&'static self, port: Instance) -> GpioPort { + let flags = EventFlags::create(Pin::static_ref(&self.flags), &Default::default()).unwrap(); + GpioPort { flags, port } + } + + pub const fn new() -> Self { + Self { + flags: EventFlags::context(), + } + } +} + +#[derive(Clone)] +pub struct GpioPort { + port: Instance, + flags: &'static EventFlags, +} + +unsafe impl Send for GpioPort {} + +impl GpioPort { + pub fn configure_multiple_outputs(&self, mask: Mask) { + interrupt_control::with_disabled(|| { + crate::modify_reg!(gpio, self.port, GDIR, |gdir| gdir | mask.get()) + }) + } + + pub fn configure_multiple_inputs(&self, mask: Mask) { + interrupt_control::with_disabled(|| { + crate::modify_reg!(gpio, self.port, GDIR, |gdir| gdir & !mask.get()) + }); + } + + #[inline] + pub fn set_multiple(&self, mask: Mask) { + crate::write_reg!(gpio, self.port, DR_SET, mask.get()); + } + + #[inline] + pub fn clear_multiple(&self, mask: Mask) { + crate::write_reg!(gpio, self.port, DR_CLEAR, mask.get()); + } + + #[inline] + pub fn toggle_multiple(&self, mask: Mask) { + crate::write_reg!(gpio, self.port, DR_TOGGLE, mask.get()); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Interrupt { + LowLevel = 0, + HighLevel = 1, + RisingEdge = 2, + FallingEdge = 3, + EitherEdge = 4, +} + +impl GpioPort { + pub fn configure_interrupt(&self, pin: IO, interrupt: Interrupt) { + let pin = pin as u32; + + let clear_edge_sel = !(1 << pin); + let set_edge_sel = ((interrupt == Interrupt::EitherEdge) as u32) << pin; + + let icr_offset = (pin % 16) * 2; + let set_icr = (interrupt as u32 & 0b11) << icr_offset; + let clear_icr = !(0b11 << icr_offset); + + let icr1 = pin < 16; + interrupt_control::with_disabled(|| { + let mut icr = if icr1 { + crate::read_reg!(gpio, self.port, ICR1) + } else { + crate::read_reg!(gpio, self.port, ICR2) + }; + + icr &= clear_icr; + icr |= set_icr; + + if icr1 { + crate::write_reg!(gpio, self.port, ICR1, icr); + } else { + crate::write_reg!(gpio, self.port, ICR2, icr); + } + + let mut edge_sel = crate::read_reg!(gpio, self.port, EDGE_SEL); + edge_sel &= clear_edge_sel; + edge_sel |= set_edge_sel; + crate::write_reg!(gpio, self.port, EDGE_SEL, edge_sel); + }); + } + + pub fn block_on_interrupts(&self, mask: Mask) -> Option { + interrupt_control::with_disabled(|| { + crate::modify_reg!(gpio, self.port, IMR, |imr| imr | mask.get()); + }); + match self.flags.get(mask, GetOption::OrClear) { + Ok(mask) => Some(mask), + Err(GetError::WaitAborted) => None, + Err(GetError::InvalidWait) => panic!(), + } + } +} + +impl GpioPort { + pub fn make_output(&self, io: IO) -> GpioOutput { + self.configure_multiple_outputs(io.mask()); + GpioOutput { + port: self.clone(), + io, + } + } + pub fn make_input(&self, io: IO) -> GpioInput { + self.configure_multiple_inputs(io.mask()); + GpioInput { + port: self.clone(), + io, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum IO { + IO00, + IO01, + IO02, + IO03, + IO04, + IO05, + IO06, + IO07, + IO08, + IO09, + IO10, + IO11, + IO12, + IO13, + IO14, + IO15, + IO16, + IO17, + IO18, + IO19, + IO20, + IO21, + IO22, + IO23, + IO24, + IO25, + IO26, + IO27, + IO28, + IO29, + IO30, + IO31, +} + +impl IO { + #[inline] + #[must_use] + pub const fn mask(self) -> Mask { + // Safety: the value is at least 1. + unsafe { NonZero::new_unchecked(1 << self.offset()) } + } + + #[inline] + #[must_use] + pub const fn offset(self) -> u32 { + self as u32 + } +} + +pub type Mask = NonZero; + +pub struct GpioOutput { + port: GpioPort, + io: IO, +} + +impl GpioOutput { + #[inline] + pub fn set(&self) { + self.port.set_multiple(self.io.mask()); + } + #[inline] + pub fn clear(&self) { + self.port.clear_multiple(self.io.mask()); + } + #[inline] + pub fn toggle(&self) { + self.port.toggle_multiple(self.io.mask()); + } +} + +impl super::ToggleOutput for GpioOutput { + fn toggle(&self) { + GpioOutput::toggle(&self); + } +} + +pub struct GpioInput { + port: GpioPort, + io: IO, +} + +impl GpioInput { + pub fn configure_interrupt(&self, interrupt: Interrupt) { + self.port.configure_interrupt(self.io, interrupt); + } + pub fn block_on_interrupt(&self) { + self.port.block_on_interrupts(self.io.mask()); + } +} + +impl super::BlockingInput for GpioInput { + fn block_on_interrupt(&self) { + GpioInput::block_on_interrupt(&self); + } +} diff --git a/crates/imxrt/src/lib.rs b/crates/imxrt/src/lib.rs new file mode 100644 index 0000000..781fc50 --- /dev/null +++ b/crates/imxrt/src/lib.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: Copyright 2025 Ian McIntyre + +#![no_std] +#![feature(rustc_private)] +extern crate fusible; + +pub use ral_registers::{Instance, modify_reg, read_reg, write_reg}; + +pub mod ral { + pub use imxrt_drivers_enet as enet; + pub use imxrt_drivers_gpio as gpio; +} + +pub mod drivers { + pub mod enet; + pub mod gpio; + + /// TODO: remove this, use embedded-hal or something else. + pub trait ToggleOutput: Send + 'static { + fn toggle(&self); + } + + /// TODO: remove this, use embedded-hal or something else. + pub trait BlockingInput: Send + 'static { + fn block_on_interrupt(&self); + } +} + +/// TODO: A REAL RANDOM NUMBER GENERATOR. +/// +/// This is needed by NetX Duo for port allocation. +/// I'm being very lazy and spoofing the required +/// implementation. +#[unsafe(no_mangle)] +pub extern "C" fn rand() -> core::ffi::c_int { + use core::sync::atomic::{AtomicI32, Ordering}; + static SOME_RANDOM_NUMBER: AtomicI32 = AtomicI32::new(0); + SOME_RANDOM_NUMBER.fetch_add(1, Ordering::Relaxed) +} diff --git a/crates/imxrt1170evk/Cargo.toml b/crates/imxrt1170evk/Cargo.toml index 995eff0..f442508 100644 --- a/crates/imxrt1170evk/Cargo.toml +++ b/crates/imxrt1170evk/Cargo.toml @@ -5,12 +5,14 @@ edition = "2024" publish = false [dependencies] -imxrt-ral = { workspace = true, features = ["imxrt1176_cm7"] } +imxrt1170 = { workspace = true } imxrt-rt = { workspace = true } imxrt1170evk-fcb = { version = "0.2" } cortex-m = { workspace = true } rust-threadx-cortex-m = { workspace = true } +rust-threadx-imxrt = { workspace = true } +rust-threadx-net-phys = { workspace = true } rust-threadx-stdio-semihosting = { workspace = true } [build-dependencies] diff --git a/crates/imxrt1170evk/src/lib.rs b/crates/imxrt1170evk/src/lib.rs index b626843..1b2aad7 100644 --- a/crates/imxrt1170evk/src/lib.rs +++ b/crates/imxrt1170evk/src/lib.rs @@ -2,14 +2,25 @@ #![feature(rustc_private)] extern crate fusible; +use core::net::Ipv4Addr; +use core::num::NonZeroU8; use core::pin::Pin; -use imxrt_ral as ral; use imxrt_rt as _; use imxrt1170evk_fcb as _; use rust_threadx_cortex_m as _; +use rust_threadx_imxrt::*; use rust_threadx_stdio_semihosting as _; +use drivers::{enet, gpio}; + +use imxrt1170::interrupt; + +use fusible::netx_duo::{ + ip::{Ip, IpContext}, + packet::{PacketPool, PacketPoolContext, StaticPacketStorage, packet_storage_size}, +}; + #[doc(hidden)] #[unsafe(no_mangle)] pub unsafe extern "C" fn _tx_initialize_low_level() { @@ -35,83 +46,285 @@ pub unsafe extern "C" fn _tx_initialize_low_level() { SYST.enable_interrupt(); SYST.enable_counter(); } +} - unsafe { - use ral::ccm; +use fusible::thread::{StaticStack, Thread, ThreadContext}; +static STACK: StaticStack<512> = StaticStack::new(); +static THREAD: ThreadContext = Thread::context(); - let ccm = ccm::CCM::instance(); - let clock_root = &ccm.CLOCK_ROOT[8]; - ral::modify_reg!(ccm::clockroot, clock_root, CLOCK_ROOT_CONTROL, MUX: 1, DIV: 240 - 1); - } +#[unsafe(no_mangle)] +#[doc(hidden)] +pub extern "C" fn __rust_threadx_app_define() { + configure_clocks(); + configure_pins(); unsafe { - use ral::iomuxc; - let iomuxc = iomuxc::IOMUXC::instance(); + cortex_m::peripheral::NVIC::unmask(interrupt::ENET); - ral::write_reg!(iomuxc, iomuxc, SW_MUX_CTL_PAD_GPIO_AD_04, MUX_MODE: 5, SION: 0); + let gpio3 = GPIO3.create(const { imxrt1170::instances::gpio3() }); + let gpio6 = GPIO6.create(const { imxrt1170::instances::gpio6() }); - ral::write_reg!(iomuxc, iomuxc, SW_PAD_CTL_PAD_GPIO_AD_04, PUE: 0); - } + let led = gpio3.make_output(gpio::IO::IO03); + let phy_rst = gpio6.make_output(gpio::IO::IO12); - unsafe { - use ral::gpio; - let gpio3 = gpio::GPIO3::instance(); + let enet = ENET.create( + imxrt1170::instances::enet(), + ENET_TX_RING.as_slice(), + ENET_RX_RING.as_slice(), + rust_threadx_net_phys::ksz8081::Ksz8081::new(), + phy_rst, + BUS_FREQUENCY_HZ, + ); + + let pool = PacketPool::create( + Pin::static_ref(&PACKET_POOL), + PACKET_STORAGE.0.take().unwrap(), + 1536 / 12, + &Default::default(), + ) + .unwrap(); - const GPIO3_OUTPUTS: u32 = led::MASK; + let ip = Ip::create( + Pin::static_ref(&IP), + Ipv4Addr::new(192, 168, 5, 1), + 0xffff_ff00, + pool, + enet, + IP_THREAD_STACK.take().unwrap(), + &Default::default(), + ) + .unwrap(); - ral::write_reg!(gpio, gpio3, GDIR, GPIO3_OUTPUTS); + ip.set_physical_address(0x0011, 0x22334456); + ip.enable_arp(); + ip.enable_udp(); + ip.enable_icmp(); + + Thread::create( + Pin::static_ref(&THREAD), + STACK.take().unwrap(), + &Default::default(), + move || loop { + fusible::thread::sleep(25); + led.toggle(); + }, + ) + .unwrap(); } } -/// Controls the LED. -pub mod led { - use crate::ral::gpio; +#[unsafe(no_mangle)] +#[doc(hidden)] +pub extern "C" fn __rust_threadx_ip() -> Option<&'static Ip> { + Pin::static_ref(&IP).try_created() +} - pub(crate) const MASK: u32 = 1 << 3; +fn configure_clocks() { + use imxrt1170::{ccm, gpc_cpu_mode_ctrl as gpc_cpu, pmu}; - /// Turn on the LED. - #[inline] - pub fn set() { - unsafe { crate::ral::write_reg!(gpio, gpio::GPIO3, DR_SET, MASK) }; - } + let ccm = unsafe { imxrt1170::instances::ccm() }; + let pmu = unsafe { imxrt1170::instances::pmu() }; + let gpc_cpu = unsafe { imxrt1170::instances::gpc_cpu_mode_ctrl0() }; + let pll = unsafe { imxrt1170::instances::pll() }; - /// Turn off the LED. - #[inline] - pub fn clear() { - unsafe { crate::ral::write_reg!(gpio, gpio::GPIO3, DR_CLEAR, MASK) }; - } + // Switch the core to something stable before we + // start changing upstream sources. + ccm::set_clock_root( + ccm, + ccm::ClockRoot::M7, + const { ccm::mux(ccm::ClockRoot::M7, ccm::ClockSource::Xtal) }, + NO_DIVIDER, + ); + ccm::set_clock_root( + ccm, + ccm::ClockRoot::Bus, + const { ccm::mux(ccm::ClockRoot::Bus, ccm::ClockSource::Xtal) }, + NO_DIVIDER, + ); + ccm::set_clock_root( + ccm, + ccm::ClockRoot::BusLpsr, + const { ccm::mux(ccm::ClockRoot::BusLpsr, ccm::ClockSource::Xtal) }, + NO_DIVIDER, + ); + ccm::set_clock_root( + ccm, + ccm::ClockRoot::M7Systick, + const { ccm::mux(ccm::ClockRoot::M7Systick, ccm::ClockSource::Xtal) }, + const { NonZeroU8::new(240).unwrap() }, + ); + + pmu::enable_pll_reference_voltage(pmu, true); + pmu::set_phy_ldo_setpoints(pmu, u16::MAX); + pmu::enable_phy_ldo_setpoints(pmu); + pmu::enable_pll_reference_setpoints(pmu); - /// Toggle the LED. - /// - /// This is achieved in hardware, without reading the current state - /// of the LED. It's usually more efficient. - #[inline] - pub fn toggle() { - unsafe { crate::ral::write_reg!(gpio, gpio::GPIO3, DR_TOGGLE, MASK) }; + for clock_source in { + use ccm::ClockSource::*; + [ + Pll1, Pll1Clk, Pll1Div2, Pll1Div5, ArmPll, ArmPllClk, // + Pll2, Pll2Clk, Pll2Pfd0, Pll2Pfd1, Pll2Pfd2, Pll2Pfd3, // + Pll3, Pll3Clk, Pll3Div2, Pll3Pfd0, Pll3Pfd1, Pll3Pfd2, Pll3Pfd3, // + ] + } { + ccm::set_source_setpoints(ccm, clock_source, u16::MAX, 0); + ccm::enable_source_setpoints(ccm, clock_source).unwrap(); } - /// Drive the LED on or off. - #[inline] - pub fn drive(value: bool) { - if value { set() } else { clear() } + ccm::pll::enable_sys_pll1_setpoints(pll); + ccm::pll::enable_arm_pll_setpoints(pll, ARM_PLL_POST_DIV, ARM_PLL_DIV_SELECT); + ccm::pll::enable_sys_pll2_setpoints(pll); + ccm::pll::enable_sys_pll3_setpoints(pll); + + gpc_cpu::request_setpoint_transition(gpc_cpu, 1).unwrap(); + + ccm::pll::set_pll3_pfd_fracs( + pll, + [ + SYS_PLL3_PFD0_DIV, + SYS_PLL3_PFD1_DIV, + SYS_PLL3_PFD2_DIV, + SYS_PLL3_PFD3_DIV, + ], + ); + ccm::pll::update_pll3_pfd_fracs(pll, [true, true, true, true]); + + ccm::set_clock_root( + ccm, + ccm::ClockRoot::M7, + const { ccm::mux(ccm::ClockRoot::M7, ccm::ClockSource::ArmPll) }, + M7_DIVIDER, + ); + + ccm::set_clock_root( + ccm, + ccm::ClockRoot::Bus, + const { ccm::mux(ccm::ClockRoot::Bus, ccm::ClockSource::Pll1Div5) }, + BUS_DIVIDER, + ); + + ccm::set_clock_root( + ccm, + ccm::ClockRoot::Enet, + const { ccm::mux(ccm::ClockRoot::Enet, ccm::ClockSource::Pll1Div2) }, + const { NonZeroU8::new((SYS_PLL1_DIV2_FREQUENCY_HZ / ENET_FREQUENCY_HZ) as u8).unwrap() }, + ); +} + +const NO_DIVIDER: NonZeroU8 = NonZeroU8::new(1).unwrap(); +const M7_DIVIDER: NonZeroU8 = NonZeroU8::new(1).unwrap(); +const BUS_DIVIDER: NonZeroU8 = NonZeroU8::new(1).unwrap(); +const FLEXSPI1_DIVIDER: NonZeroU8 = NonZeroU8::new(2).unwrap(); + +pub const XTAL_FREQUENCY_HZ: u32 = 24_000_000; +pub const BUS_FREQUENCY_HZ: u32 = SYS_PLL1_DIV5_FREQUENCY_HZ / BUS_DIVIDER.get() as u32; +pub const ENET_FREQUENCY_HZ: u32 = 50_000_000; +pub const SYS_PLL1_FREQUENCY_HZ: u32 = 1_000_000_000; +pub const SYS_PLL1_DIV2_FREQUENCY_HZ: u32 = SYS_PLL1_FREQUENCY_HZ / 2; +pub const SYS_PLL1_DIV5_FREQUENCY_HZ: u32 = SYS_PLL1_FREQUENCY_HZ / 5; +pub const M7_FREQUENCY_HZ: u32 = ARM_PLL_FREQUENCY_HZ / M7_DIVIDER.get() as u32; +pub const SYS_PLL2_FREQUENCY_HZ: u32 = 528_000_000; +pub const SYS_PLL3_FREQUENCY_HZ: u32 = 480_000_000; +pub const FLEXSPI1_FREQUENCY_HZ: u32 = + SYS_PLL3_FREQUENCY_HZ / SYS_PLL3_PFD0_DIV.get() as u32 / FLEXSPI1_DIVIDER.get() as u32 * 18; + +const ARM_PLL_DIV_SELECT: imxrt1170::ccm::pll::ArmPllDivSelect = + imxrt1170::ccm::pll::ArmPllDivSelect::new(200).unwrap(); +const ARM_PLL_POST_DIV: imxrt1170::ccm::pll::ArmPllPostDiv = + imxrt1170::ccm::pll::ArmPllPostDiv::Div4; +const ARM_PLL_FREQUENCY_HZ: u32 = + imxrt1170::ccm::pll::arm_pll_frequency(ARM_PLL_POST_DIV, ARM_PLL_DIV_SELECT); +const SYS_PLL3_PFD0_DIV: imxrt1170::ccm::pll::PfdFrac = + imxrt1170::ccm::pll::PfdFrac::new(33).unwrap(); +const SYS_PLL3_PFD1_DIV: imxrt1170::ccm::pll::PfdFrac = + imxrt1170::ccm::pll::PfdFrac::new(27).unwrap(); +const SYS_PLL3_PFD2_DIV: imxrt1170::ccm::pll::PfdFrac = + imxrt1170::ccm::pll::PfdFrac::new(21).unwrap(); +const SYS_PLL3_PFD3_DIV: imxrt1170::ccm::pll::PfdFrac = + imxrt1170::ccm::pll::PfdFrac::new(17).unwrap(); + +fn configure_pins() { + use imxrt1170::{ + iomuxc::{self, select_input}, + iomuxc_gpr, iomuxc_lpsr, + }; + + let iomuxc = unsafe { imxrt1170::instances::iomuxc() }; + let iomuxc_gpr = unsafe { imxrt1170::instances::iomuxc_gpr() }; + let iomuxc_lpsr = unsafe { imxrt1170::instances::iomuxc_lpsr() }; + + // ENET_REF_CLK driven as output. + modify_reg!(iomuxc_gpr, iomuxc_gpr, GPR[4], |gpr| gpr | (1 << 1)); + // ERR050396 handling. + modify_reg!(iomuxc_gpr, iomuxc_gpr, GPR[28], |gpr| gpr + | (1 << 5) + | (1 << 7)); + + // LED. + write_reg!(iomuxc, iomuxc, SW_MUX_CTL_PAD.GPIO_AD[4], MUX_MODE: 5); + write_reg!(iomuxc, iomuxc, SW_PAD_CTL_PAD.GPIO_AD[4], PUE: 0); + + // ENET PHY_RST. + write_reg!(iomuxc_lpsr, iomuxc_lpsr, SW_MUX_CTL_PAD[12], MUX_MODE: 5); + + // ENET RMII. + write_reg!(iomuxc, iomuxc, SW_MUX_CTL_PAD.GPIO_AD[32], MUX_MODE: 3, SION: 0); // MDC + write_reg!(iomuxc, iomuxc, SW_MUX_CTL_PAD.GPIO_AD[33], MUX_MODE: 3, SION: 0); // MDIO + write_reg!(iomuxc, iomuxc, SW_MUX_CTL_PAD.GPIO_DISP_B2[2], MUX_MODE: 1, SION: 0); // TX_DATA0 + write_reg!(iomuxc, iomuxc, SW_MUX_CTL_PAD.GPIO_DISP_B2[3], MUX_MODE: 1, SION: 0); // TX_DATA1 + write_reg!(iomuxc, iomuxc, SW_MUX_CTL_PAD.GPIO_DISP_B2[4], MUX_MODE: 1, SION: 0); // TX_EN + write_reg!(iomuxc, iomuxc, SW_MUX_CTL_PAD.GPIO_DISP_B2[5], MUX_MODE: 2, SION: 1); // REF_CLK + write_reg!(iomuxc, iomuxc, SW_MUX_CTL_PAD.GPIO_DISP_B2[6], MUX_MODE: 1, SION: 1); // RX_DATA0 + write_reg!(iomuxc, iomuxc, SW_MUX_CTL_PAD.GPIO_DISP_B2[7], MUX_MODE: 1, SION: 1); // RX_DATA1 + write_reg!(iomuxc, iomuxc, SW_MUX_CTL_PAD.GPIO_DISP_B2[8], MUX_MODE: 1, SION: 0); // RX_EN + write_reg!(iomuxc, iomuxc, SW_MUX_CTL_PAD.GPIO_DISP_B2[9], MUX_MODE: 1, SION: 0); // RX_ER + + write_reg!(iomuxc, iomuxc, SW_PAD_CTL_PAD.GPIO_AD[32], PUE: 1, PUS: PULL_UP, ODE: 0); // MDC + write_reg!(iomuxc, iomuxc, SW_PAD_CTL_PAD.GPIO_AD[33], PUE: 1, PUS: PULL_UP, ODE: 1); // MDIO + write_reg!(iomuxc, iomuxc, SW_PAD_CTL_PAD.GPIO_DISP_B2[2], PUE: 0, SRE: SLOW, DSE: HIGH, ODE: 0); // TX_DATA0 + write_reg!(iomuxc, iomuxc, SW_PAD_CTL_PAD.GPIO_DISP_B2[3], PUE: 0, SRE: SLOW, DSE: HIGH, ODE: 0); // TX_DATA1 + write_reg!(iomuxc, iomuxc, SW_PAD_CTL_PAD.GPIO_DISP_B2[4], PUE: 0, SRE: SLOW, DSE: HIGH, ODE: 0); // TX_EN + write_reg!(iomuxc, iomuxc, SW_PAD_CTL_PAD.GPIO_DISP_B2[5], PUE: 0, SRE: SLOW, DSE: HIGH, ODE: 0); // REF_CLK + write_reg!(iomuxc, iomuxc, SW_PAD_CTL_PAD.GPIO_DISP_B2[6], PUE: 0, SRE: SLOW, DSE: HIGH, ODE: 0); // RX_DATA0 + write_reg!(iomuxc, iomuxc, SW_PAD_CTL_PAD.GPIO_DISP_B2[7], PUE: 0, SRE: SLOW, DSE: HIGH, ODE: 0); // RX_DATA1 + write_reg!(iomuxc, iomuxc, SW_PAD_CTL_PAD.GPIO_DISP_B2[8], PUE: 0, SRE: SLOW, DSE: HIGH, ODE: 0); // RX_EN + write_reg!(iomuxc, iomuxc, SW_PAD_CTL_PAD.GPIO_DISP_B2[9], PUE: 0, SRE: SLOW, DSE: HIGH, ODE: 0); // RX_ER + + for select_input in [ + select_input::ENET_IPG_CLK_RMII, + select_input::ENET_MAC0_MDIO, + select_input::ENET_MAC0_RXDATA_0, + select_input::ENET_MAC0_RXDATA_1, + select_input::ENET_MAC0_RXEN, + select_input::ENET_MAC0_RXERR, + ] { + write_reg!(iomuxc, iomuxc, SELECT_INPUT[select_input], 1); } } -use fusible::thread::{StaticStack, Thread, ThreadContext}; -static STACK: StaticStack<512> = StaticStack::new(); -static THREAD: ThreadContext = Thread::context(); +type Enet = drivers::enet::Enet; -#[unsafe(no_mangle)] -#[doc(hidden)] -pub extern "C" fn __rust_threadx_app_define() { - Thread::create( - Pin::static_ref(&THREAD), - STACK.take().unwrap(), - &Default::default(), - || loop { - fusible::thread::sleep(25); - led::toggle(); - }, - ) - .unwrap(); +static ENET: enet::EnetContext = enet::EnetContext::new(); +static ENET_TX_RING: enet::TxRing<16> = enet::TxRing::ZEROED; +static ENET_RX_RING: enet::RxRing<16> = enet::RxRing::ZEROED; +static IP: IpContext = Ip::context(); + +#[repr(align(64))] +pub struct AlignedPacketStorage(StaticPacketStorage<{ packet_storage_size(8 * 12, 1536 / 12) }>); + +static PACKET_POOL: PacketPoolContext = PacketPool::context(); +static PACKET_STORAGE: AlignedPacketStorage = AlignedPacketStorage(StaticPacketStorage::new()); +static IP_THREAD_STACK: StaticStack<4096> = StaticStack::new(); + +static GPIO3: gpio::GpioContext = gpio::GpioContext::new(); +static GPIO6: gpio::GpioContext = gpio::GpioContext::new(); + +#[imxrt_rt::interrupt] +fn ENET() { + unsafe { + ENET.on_interrupt( + const { imxrt1170::instances::enet() }, + Pin::static_ref(&IP).assume_created(), + ) + } } diff --git a/crates/imxrt1170evk/src/main.rs b/crates/imxrt1170evk/src/main.rs index 737a6cc..c2db594 100644 --- a/crates/imxrt1170evk/src/main.rs +++ b/crates/imxrt1170evk/src/main.rs @@ -1,6 +1,19 @@ use rust_threadx_imxrt1170evk as _; +use std::{net::UdpSocket, thread}; + fn main() { + thread::scope(|scope| { + thread::Builder::new() + .stack_size(512) + .spawn_scoped(scope, count) + .unwrap(); + + udp_loopback(); + }) +} + +fn count() -> ! { let mut count = 0_usize; loop { println!("Hello world! The count is {count}"); @@ -8,3 +21,18 @@ fn main() { std::thread::sleep(std::time::Duration::from_millis(500)) } } + +fn udp_loopback() -> ! { + let mut buffer = [0_u8; 1024]; + let socket = UdpSocket::bind("192.168.5.1:5678").unwrap(); + loop { + let (amt, src) = socket.recv_from(&mut buffer).unwrap(); + let msg = &buffer[..amt]; + println!( + "Received {amt} bytes from {src}: \"{}\"", + str::from_utf8(msg).unwrap() + ); + + socket.send_to(msg, &src).unwrap(); + } +} diff --git a/crates/net-phys/Cargo.toml b/crates/net-phys/Cargo.toml new file mode 100644 index 0000000..d7b4d3e --- /dev/null +++ b/crates/net-phys/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rust-threadx-net-phys" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +bitflags = { version = "2.13.1" } diff --git a/crates/net-phys/src/ksz8081.rs b/crates/net-phys/src/ksz8081.rs new file mode 100644 index 0000000..476997e --- /dev/null +++ b/crates/net-phys/src/ksz8081.rs @@ -0,0 +1,130 @@ +const PHY_ID1_REG: u8 = 0x02; +const PHY_BASICCONTROL_REG: u8 = 0x00; +const PHY_BASICSTATUS_REG: u8 = 0x01; +const PHY_AUTONEG_ADVERTISE_REG: u8 = 0x04; + +const KSZ8081_PHY_ADDR: u8 = 0x02; // Arbitrary. +const KSZ8081_PHY_CONTROL2_REG: u8 = 0x1F; +const KSZ8081_PHY_CTL2_REFCLK_SELECT_MASK: u16 = 0x0080; + +bitflags::bitflags! { + struct BasicControl : u16 { + const RESET = 0x8000; + const AUTONEG = 0x1000; + const RESTART_AUTONEG = 0x0200; + } +} + +bitflags::bitflags! { + struct BasicStatus : u16 { + const LINKSTATUS = 0x0004; + const AUTONEG_COMP = 0x0020; + } +} + +bitflags::bitflags! { + struct AutoNegotiation : u16 { + const BASET4_100_ABILITY = 0x0200; + const BASETX_100_FULL_DUPLEX = 0x0100; + const BASETX_100_HALF_DUPLEX = 0x0080; + const BASETX_10_FULL_DUPLEX = 0x0040; + const BASETX_10_HALF_DUPLEX = 0x0020; + const IEEE802_3_SELECTOR = 0x0001; + } +} + +const KSZ8081_ID: u16 = 0x22; + +#[derive(Debug)] +pub enum Error { + Phy(E), + NotFound, +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Phy(err) => write!(f, "PHY error: {err}"), + Self::NotFound => write!(f, "KSZ8081 not found"), + } + } +} + +impl From for Error { + fn from(value: E) -> Self { + Self::Phy(value) + } +} + +impl core::error::Error for Error { + // TODO +} + +fn init(miim: &mut M) -> Result<(), Error> { + let mut retries = 100000usize; + while retries > 0 { + let address = miim.read(KSZ8081_PHY_ADDR, PHY_ID1_REG)?; + if address == KSZ8081_ID { + break; + } + retries -= 1; + fusible::thread::sleep(1); + } + + if retries == 0 { + return Err(Error::NotFound); + } + + miim.write( + KSZ8081_PHY_ADDR, + PHY_BASICCONTROL_REG, + BasicControl::RESET.bits(), + )?; + + // Use RMII50M (KSZ8081-specific configuration). + let ctrl2 = miim.read(KSZ8081_PHY_ADDR, KSZ8081_PHY_CONTROL2_REG)?; + miim.write( + KSZ8081_PHY_ADDR, + KSZ8081_PHY_CONTROL2_REG, + ctrl2 | KSZ8081_PHY_CTL2_REFCLK_SELECT_MASK, + )?; + + // Enable auto-negotiation. + miim.write( + KSZ8081_PHY_ADDR, + PHY_AUTONEG_ADVERTISE_REG, + (AutoNegotiation::BASETX_100_FULL_DUPLEX | AutoNegotiation::IEEE802_3_SELECTOR).bits(), + )?; + miim.write( + KSZ8081_PHY_ADDR, + PHY_BASICCONTROL_REG, + (BasicControl::AUTONEG | BasicControl::RESTART_AUTONEG).bits(), + )?; + + // Wait for auto-negotiation, and for the link to be available. + // + // This blocks while there's no link. + while !BasicStatus::from_bits_truncate(miim.read(KSZ8081_PHY_ADDR, PHY_BASICSTATUS_REG)?) + .contains(BasicStatus::AUTONEG_COMP | BasicStatus::LINKSTATUS) + { + fusible::thread::sleep(1); + } + + Ok(()) +} + +pub struct Ksz8081(()); + +impl Ksz8081 { + pub fn new() -> Self { + Self(()) + } +} + +impl super::Phy for Ksz8081 { + type Error = Error; + + fn initialize(&mut self, miim: &mut M) -> Result<(), Self::Error> { + init(miim) + } +} diff --git a/crates/net-phys/src/lib.rs b/crates/net-phys/src/lib.rs new file mode 100644 index 0000000..c476799 --- /dev/null +++ b/crates/net-phys/src/lib.rs @@ -0,0 +1,62 @@ +#![no_std] +#![feature(rustc_private)] +extern crate fusible; + +pub mod ksz8081; + +pub trait Mdio { + type Error: core::error::Error; + + fn read(&mut self, ctrl: u16) -> Result; + + fn write(&mut self, ctrl: u16, data: u16) -> Result<(), Self::Error>; +} + +pub trait Miim { + type Error: core::error::Error; + + fn read(&mut self, phy: u8, reg: u8) -> Result; + + fn write(&mut self, phy: u8, reg: u8, data: u16) -> Result<(), Self::Error>; +} + +impl Miim for T +where + T: Mdio, +{ + type Error = ::Error; + + fn read(&mut self, phy: u8, reg: u8) -> Result { + Mdio::read(self, read_ctrl_bits(phy, reg)) + } + + fn write(&mut self, phy: u8, reg: u8, data: u16) -> Result<(), Self::Error> { + Mdio::write(self, write_ctrl_bits(phy, reg), data) + } +} + +const fn phy_addr_ctrl_bits(phy_addr: u8) -> u16 { + const PHY_ADDR_OFFSET: u16 = 7; + ((phy_addr & 0b00011111) as u16) << PHY_ADDR_OFFSET +} + +const fn reg_addr_ctrl_bits(reg_addr: u8) -> u16 { + const REG_ADDR_OFFSET: u16 = 2; + ((reg_addr & 0b00011111) as u16) << REG_ADDR_OFFSET +} + +pub const fn read_ctrl_bits(phy_addr: u8, reg_addr: u8) -> u16 { + const READ_CTRL_BITS: u16 = 0b0110_00000_00000_00; + READ_CTRL_BITS | phy_addr_ctrl_bits(phy_addr) | reg_addr_ctrl_bits(reg_addr) +} + +pub const fn write_ctrl_bits(phy_addr: u8, reg_addr: u8) -> u16 { + const WRITE_CTRL_BITS: u16 = 0b0101_00000_00000_10; + WRITE_CTRL_BITS | phy_addr_ctrl_bits(phy_addr) | reg_addr_ctrl_bits(reg_addr) +} + +pub trait Phy { + type Error: core::error::Error; + + fn initialize(&mut self, miim: &mut M) -> Result<(), Self::Error>; +} -- cgit v1.2.3