aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIan McIntyre <me@mciantyre.dev>2026-08-09 12:09:22 -0400
committerIan McIntyre <me@mciantyre.dev>2026-08-22 08:37:40 -0400
commitc4962c8b9207e9474659526e91a1cf387d2a7b8f (patch)
treec0d448ae6459d49de6d15579152c11cde3683cd3
parent7ffc62ab8613b097485b2c664093e649907dc2a1 (diff)
Integrate ENET, GPIO, PHY drivers for 1170EVKHEADmain
-rw-r--r--Cargo.lock186
-rw-r--r--Cargo.toml16
-rw-r--r--README.md17
-rw-r--r--crates/imxrt/Cargo.toml19
-rw-r--r--crates/imxrt/build.rs17
-rw-r--r--crates/imxrt/src/drivers/enet.rs849
-rw-r--r--crates/imxrt/src/drivers/gpio.rs265
-rw-r--r--crates/imxrt/src/lib.rs40
-rw-r--r--crates/imxrt1170evk/Cargo.toml4
-rw-r--r--crates/imxrt1170evk/src/lib.rs329
-rw-r--r--crates/imxrt1170evk/src/main.rs28
-rw-r--r--crates/net-phys/Cargo.toml8
-rw-r--r--crates/net-phys/src/ksz8081.rs130
-rw-r--r--crates/net-phys/src/lib.rs62
14 files changed, 1884 insertions, 86 deletions
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]]
@@ -286,6 +366,25 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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"
@@ -366,22 +465,48 @@ 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"
dependencies = [
@@ -464,6 +589,17 @@ dependencies = [
]
[[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"
source = "git+https://github.com/mciantyre/teensy4-rs?branch=rust-threadx#c2dc3d0beab8d3a9d6f5eda39311eb68fda82053"
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<enet::RegisterBlock>,
+ 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<P: Phy<EnetMdio>>(
+ &'static self,
+ enet: Instance<enet::RegisterBlock>,
+ tx_ring: &'static [TxBD],
+ rx_ring: &'static [RxBD],
+ phy: P,
+ rst: GpioOutput,
+ mdio_source_clock_hz: u32,
+ ) -> Enet<P> {
+ 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<enet::RegisterBlock>,
+ flags: &'static EventFlags,
+}
+
+pub struct Enet<P: Phy<EnetMdio>> {
+ enet: Instance<enet::RegisterBlock>,
+ phy: P,
+ rst: GpioOutput,
+ flags: &'static EventFlags,
+
+ tx_ring: TransmitRing,
+ rx_ring: ReceiveRing,
+}
+
+unsafe impl<P: Phy<EnetMdio>> Send for Enet<P> {}
+
+impl Mdio for EnetMdio {
+ type Error = core::convert::Infallible;
+
+ fn read(&mut self, ctrl: u16) -> Result<u16, Self::Error> {
+ 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<P: Phy<EnetMdio>> driver::Driver<'static> for Enet<P> {
+ 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<enet::RegisterBlock>,
+ 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(&ethertype)
+ {
+ 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<packet::Packet<'static>> {
+ // 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<packet::Packet<'static>> {
+ // 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<gpio::RegisterBlock>) {
+ // 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<gpio::RegisterBlock>) -> 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<gpio::RegisterBlock>,
+ 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<Mask> {
+ 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<u32>;
+
+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<rust_threadx_net_phys::ksz8081::Ksz8081>;
-#[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<Enet> = 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<E> {
+ Phy(E),
+ NotFound,
+}
+
+impl<E: core::fmt::Display> core::fmt::Display for Error<E> {
+ 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<E> From<E> for Error<E> {
+ fn from(value: E) -> Self {
+ Self::Phy(value)
+ }
+}
+
+impl<E: core::error::Error> core::error::Error for Error<E> {
+ // TODO
+}
+
+fn init<M: super::Miim>(miim: &mut M) -> Result<(), Error<M::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<M: super::Miim> super::Phy<M> for Ksz8081 {
+ type Error = Error<M::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<u16, Self::Error>;
+
+ 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<u16, Self::Error>;
+
+ fn write(&mut self, phy: u8, reg: u8, data: u16) -> Result<(), Self::Error>;
+}
+
+impl<T> Miim for T
+where
+ T: Mdio,
+{
+ type Error = <T as Mdio>::Error;
+
+ fn read(&mut self, phy: u8, reg: u8) -> Result<u16, Self::Error> {
+ 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<M: Miim> {
+ type Error: core::error::Error;
+
+ fn initialize(&mut self, miim: &mut M) -> Result<(), Self::Error>;
+}