[PATCH v4 7/7] samples: rust: add EDU PCI driver sample
From: Maurice Hieronymus
Date: Wed Nov 04 2026 - 18:24:29 EST
Add a driver for QEMU's EDU device to demonstrate and exercise Rust PCI
driver abstractions without physical hardware. Its probe-time selftest
reads PCI config space, checks BAR MMIO and factorial computation, tests
MSI delivery through an IRQ handler, and performs a coherent DMA roundtrip.
The sample uses `pci::Driver`, `DeviceEnableGuard`, typed I/O registers,
PCI IRQ vectors, `irq::Registration`, `dma::Coherent` and `Completion` to
manage resources and wait for interrupts.
Signed-off-by: Maurice Hieronymus <mhi@xxxxxxxxxxx>
---
MAINTAINERS | 1 +
samples/rust/Kconfig | 15 ++
samples/rust/Makefile | 1 +
samples/rust/rust_driver_edu.rs | 475 ++++++++++++++++++++++++++++++++++++++++
4 files changed, 492 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 8f7f1e14b31e..ec4d2cb59662 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21595,6 +21595,7 @@ T: git git://git.kernel.org/pub/scm/linux/kernel/git/pci/pci.git
F: rust/helpers/pci.c
F: rust/kernel/pci.rs
F: rust/kernel/pci/
+F: samples/rust/rust_driver_edu.rs
PCIE BANDWIDTH CONTROLLER
M: Ilpo Järvinen <ilpo.jarvinen@xxxxxxxxxxxxxxx>
diff --git a/samples/rust/Kconfig b/samples/rust/Kconfig
index 7fbd49df30ce..8226a21fc5e4 100644
--- a/samples/rust/Kconfig
+++ b/samples/rust/Kconfig
@@ -107,6 +107,21 @@ config SAMPLE_RUST_I2C_CLIENT
If unsure, say N.
+config SAMPLE_RUST_DRIVER_EDU
+ tristate "EDU Driver"
+ depends on PCI && 64BIT
+ help
+ This option builds the Rust PCI driver sample for QEMU's EDU device.
+ Start QEMU with -device edu to use this sample.
+ The sample requires 64BIT because it accesses the device's DMA
+ registers using 64-bit MMIO operations, which the Rust I/O
+ abstractions only support on 64-bit kernels.
+
+ To compile this as a module, choose M here:
+ the module will be called rust_driver_edu.
+
+ If unsure, say N.
+
config SAMPLE_RUST_DRIVER_PLATFORM
tristate "Platform Driver"
help
diff --git a/samples/rust/Makefile b/samples/rust/Makefile
index 20b55e0bdbb7..9b0b3d7f8cc2 100644
--- a/samples/rust/Makefile
+++ b/samples/rust/Makefile
@@ -9,6 +9,7 @@ obj-$(CONFIG_SAMPLE_RUST_DEBUGFS_SCOPED) += rust_debugfs_scoped.o
obj-$(CONFIG_SAMPLE_RUST_DMA) += rust_dma.o
obj-$(CONFIG_SAMPLE_RUST_DRIVER_I2C) += rust_driver_i2c.o
obj-$(CONFIG_SAMPLE_RUST_I2C_CLIENT) += rust_i2c_client.o
+obj-$(CONFIG_SAMPLE_RUST_DRIVER_EDU) += rust_driver_edu.o
obj-$(CONFIG_SAMPLE_RUST_DRIVER_PLATFORM) += rust_driver_platform.o
obj-$(CONFIG_SAMPLE_RUST_DRIVER_USB) += rust_driver_usb.o
obj-$(CONFIG_SAMPLE_RUST_DRIVER_FAUX) += rust_driver_faux.o
diff --git a/samples/rust/rust_driver_edu.rs b/samples/rust/rust_driver_edu.rs
new file mode 100644
index 000000000000..a83095d2a988
--- /dev/null
+++ b/samples/rust/rust_driver_edu.rs
@@ -0,0 +1,475 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Rust EDU driver sample (based on QEMU's `edu`).
+//!
+//! To make this driver probe, QEMU must be run with `-device edu`.
+
+use kernel::{
+ bitfield,
+ device::Bound,
+ dma::{
+ Coherent,
+ Device,
+ DmaMask, //
+ },
+ io::{
+ io_project,
+ io_write,
+ poll::read_poll_timeout,
+ register,
+ register::Array,
+ Io, //
+ },
+ irq::{
+ self,
+ Flags, //
+ },
+ num::casts::IntoSafeCast,
+ pci,
+ prelude::*,
+ sync::Completion,
+ time::Delta, //
+};
+
+const QEMU_EDU_DEVICE_ID: u32 = 0x11e8;
+
+const IRQ_MAGIC_VALUE: u8 = 42;
+
+mod regs {
+ use kernel::io::Region;
+
+ use super::*;
+
+ register! {
+ base: Region<{ END }>;
+
+ pub(super) IDENTIFICATION(u32) @ 0x0 {
+ 31:24 major;
+ 23:16 minor;
+ 7:0 magic;
+ }
+
+ pub(super) LIVENESS_CHECK(u32) @ 0x04 {}
+
+ pub(super) FACTORIAL(u32) @ 0x08 {}
+
+ pub(super) STATUS(u32) @ 0x20 {
+ 0:0 computing;
+ 7:7 raise_interrupt;
+ }
+
+ pub(super) IRQ_STATUS(u32) @ 0x24 {
+ 8:8 dma;
+ 7:0 test;
+ }
+
+ pub(super) IRQ_RAISE(u32) @ 0x60 {
+ 7:0 test;
+ }
+
+ pub(super) IRQ_ACK(u32) @ 0x64 {
+ 8:8 dma;
+ 7:0 test;
+ }
+
+ pub(super) DMA: Dma @ 0x80;
+ }
+
+ impl core::ops::Not for LIVENESS_CHECK {
+ type Output = Self;
+
+ fn not(self) -> Self::Output {
+ Self::from_raw(!self.into_raw())
+ }
+ }
+
+ bitfield! {
+ #[derive(FromBytes, IntoBytes)]
+ pub(super) struct DmaCommand(u64) {
+ 0:0 start_transfer;
+ 1:1 direction;
+ 2:2 raise_irq;
+ }
+ }
+
+ /// DMA register block. Its 64-bit registers need more alignment than `Region` guarantees, so
+ /// project it with `io_project!(bar, try: DMA)`, which checks the alignment at runtime.
+ #[repr(C)]
+ #[derive(FromBytes, IntoBytes)]
+ pub(super) struct Dma {
+ pub(super) src: u64,
+ pub(super) dst: u64,
+ pub(super) count: u64,
+ pub(super) command: DmaCommand,
+ }
+
+ pub(super) const END: usize = 0xA0;
+}
+
+type Bar0<'a> = pci::Bar<'a, { regs::END }>;
+
+struct EduDriver;
+
+#[pin_data(PinnedDrop)]
+struct EduDriverData<'bound> {
+ pdev: &'bound pci::Device<Bound>,
+ #[pin]
+ irq_handler: irq::Registration<'vectors, IrqHandler<'vectors>>,
+ vectors: pci::IrqVectorRegistration<'bound>,
+ // Declared last so the device stays enabled until the IRQ handler is freed.
+ _enable: pci::DeviceEnableGuard<'bound>,
+}
+
+#[pin_data]
+struct IrqHandler<'a> {
+ pdev: &'a pci::Device,
+ bar: Bar0<'a>,
+ #[pin]
+ irq_test_completion: Completion,
+ #[pin]
+ irq_dma_completion: Completion,
+ dma: Coherent<'a, u64>,
+}
+
+impl EduDriverData<'_> {
+ fn selftest(&self) -> Result {
+ self.config_space();
+ self.magic()?;
+ self.liveness_check()?;
+ self.factorial()?;
+ self.test_irq()?;
+ self.test_dma()?;
+ Ok(())
+ }
+
+ fn config_space(&self) {
+ let pdev = self.pdev;
+ let config = pdev.config_space();
+
+ // Some PCI configuration space registers.
+ register! {
+ base: pci::Normal;
+
+ VENDOR_ID(u16) @ 0x0 {
+ 15:0 vendor_id;
+ }
+
+ REVISION_ID(u8) @ 0x8 {
+ 7:0 revision_id;
+ }
+
+ BAR(u32)[6] @ 0x10 {
+ 31:0 value;
+ }
+ }
+
+ dev_info!(
+ pdev,
+ "config space read8 rev ID: {:x}\n",
+ config.read(REVISION_ID).revision_id()
+ );
+
+ dev_info!(
+ pdev,
+ "config space read16 vendor ID: {:x}\n",
+ config.read(VENDOR_ID).vendor_id()
+ );
+
+ dev_info!(
+ pdev,
+ "config space read32 BAR 0: {:x}\n",
+ config.read(BAR::at(0)).value()
+ );
+ }
+
+ fn magic(&self) -> Result {
+ const EXPECTED: regs::IDENTIFICATION =
+ regs::IDENTIFICATION::from_raw(0).with_const_magic::<0xed>();
+
+ let pdev = self.pdev;
+ let bar = &self.irq_handler().handler().bar;
+
+ let identification = bar.read(regs::IDENTIFICATION);
+
+ if identification.magic() != EXPECTED.magic() {
+ dev_err!(
+ pdev,
+ "magic mismatch: expected {:#x} got {:#x}\n",
+ EXPECTED.magic(),
+ identification.magic()
+ );
+ return Err(ENODEV);
+ }
+
+ dev_info!(
+ pdev,
+ "major: {:#x} minor: {:#x}\n",
+ identification.major(),
+ identification.minor()
+ );
+ Ok(())
+ }
+
+ fn liveness_check(&self) -> Result {
+ let pdev = self.pdev;
+ let bar = &self.irq_handler().handler().bar;
+
+ let pattern = regs::LIVENESS_CHECK::from_raw(0xdead_beef);
+ bar.write_reg(pattern);
+
+ let inversed = bar.read(regs::LIVENESS_CHECK);
+ if inversed != !pattern {
+ dev_err!(
+ pdev,
+ "inverse mismatch: expected {:#x} got {:#x}\n",
+ !pattern.into_raw(),
+ inversed.into_raw()
+ );
+ return Err(ENODEV);
+ }
+
+ dev_info!(pdev, "inverse test successful\n");
+ Ok(())
+ }
+
+ fn factorial(&self) -> Result {
+ let pdev = self.pdev;
+ let bar = &self.irq_handler().handler().bar;
+
+ self.wait_until_compute_has_finished()?;
+
+ bar.write(regs::FACTORIAL, 4.into());
+
+ self.wait_until_compute_has_finished()?;
+
+ let result: u32 = bar.read(regs::FACTORIAL).into();
+
+ let expected = 24;
+
+ if result != expected {
+ dev_err!(
+ pdev,
+ "factorial result wrong: expected {} got {}\n",
+ expected,
+ result
+ );
+ return Err(ENODEV);
+ }
+
+ dev_info!(pdev, "factorial test successful\n");
+ Ok(())
+ }
+
+ fn test_irq(&self) -> Result {
+ let pdev = self.pdev;
+ let handler = self.irq_handler().handler();
+
+ dev_dbg!(pdev, "raising irq\n");
+
+ handler.bar.write(
+ regs::IRQ_RAISE,
+ regs::IRQ_RAISE::zeroed().with_test(IRQ_MAGIC_VALUE),
+ );
+
+ handler.irq_test_completion.wait_for_completion();
+
+ dev_info!(pdev, "irq test successful\n");
+ Ok(())
+ }
+
+ fn test_dma(&self) -> Result {
+ let pdev = self.pdev;
+ let handler = self.irq_handler().handler();
+
+ const QEMU_DMA_BASE: u64 = 0x40000;
+
+ dev_dbg!(pdev, "testing dma\n");
+
+ let dma = &handler.dma;
+
+ const DMA_VALUE: u64 = 42;
+
+ dma.write_val(DMA_VALUE);
+
+ let dma_regs = io_project!(handler.bar, try: regs::DMA);
+
+ io_write!(dma_regs, .src, dma.dma_address());
+ io_write!(dma_regs, .dst, QEMU_DMA_BASE);
+ io_write!(dma_regs, .count, dma.size().into_safe_cast());
+ io_write!(
+ dma_regs,
+ .command,
+ regs::DmaCommand::zeroed()
+ .with_start_transfer(true)
+ .with_direction(false)
+ .with_raise_irq(true)
+ );
+
+ handler.irq_dma_completion.wait_for_completion();
+
+ // Destroy previous value to test roundtrip
+ dma.write_val(0);
+
+ io_write!(dma_regs, .src, QEMU_DMA_BASE);
+ io_write!(dma_regs, .dst, dma.dma_address());
+ io_write!(dma_regs, .count, dma.size().into_safe_cast());
+ io_write!(
+ dma_regs,
+ .command,
+ regs::DmaCommand::zeroed()
+ .with_start_transfer(true)
+ .with_direction(true)
+ .with_raise_irq(true)
+ );
+
+ handler.irq_dma_completion.wait_for_completion();
+
+ let result = dma.read_val();
+
+ if result != DMA_VALUE {
+ dev_err!(
+ pdev,
+ "dma result wrong: expected {} got {}\n",
+ DMA_VALUE,
+ result
+ );
+ return Err(ENODEV);
+ }
+
+ dev_info!(pdev, "dma test successful\n");
+ Ok(())
+ }
+
+ fn wait_until_compute_has_finished(&self) -> Result {
+ let pdev = self.pdev;
+ let bar = &self.irq_handler().handler().bar;
+
+ read_poll_timeout(
+ || Ok(bar.read(regs::STATUS)),
+ |status| status.computing() == 0,
+ Delta::from_millis(10),
+ Delta::from_millis(100),
+ )
+ .inspect_err(|_| dev_err!(pdev, "computation bit did not clear before timeout\n"))
+ .map(|_| ())
+ }
+}
+
+impl pci::Driver for EduDriver {
+ type IdInfo = ();
+ type Data<'bound> = EduDriverData<'bound>;
+
+ const ID_TABLE: pci::IdTable<Self::IdInfo> = &PCI_TABLE;
+
+ fn probe<'bound>(
+ pdev: &'bound pci::Device<kernel::device::Core<'_>>,
+ _id_info: Option<&'bound Self::IdInfo>,
+ ) -> impl PinInit<Self::Data<'bound>, Error> + 'bound {
+ try_pin_init!(EduDriverData {
+ _: {
+ dev_dbg!(
+ pdev,
+ "Probe Rust EDU driver sample (PCI ID: {}, 0x{:x}).\n",
+ pdev.vendor_id(),
+ pdev.device_id()
+ );
+ },
+
+ _enable: {
+ let enable = pdev.enable_device()?;
+ pdev.set_master(&enable)?;
+ enable
+ },
+
+ _: {
+ // SAFETY: There are no concurrent calls to DMA allocation and mapping primitives.
+ unsafe { pdev.dma_set_mask_and_coherent(DmaMask::new::<28>())? };
+ },
+
+ vectors: pdev
+ .alloc_irq_vectors(1, 1, pci::IrqType::Msi.into())
+ .inspect_err(|e| dev_err!(pdev, "alloc_irq_vectors failed: {:?}\n", e))?,
+
+ // SAFETY: `irq_handler` is owned by `EduDriverData`, which the driver core drops on
+ // unbind, so the Registration is not leaked.
+ irq_handler <- unsafe {
+ irq::Registration::new(
+ vectors.index(0)?.into(),
+ Flags::TRIGGER_NONE,
+ c"rust_edu_irq",
+ try_pin_init!(IrqHandler {
+ bar: pdev.iomap_region_sized(0, c"rust_driver_edu")?,
+ // Ack interrupts left pending by a previous binding.
+ _: {
+ let status = bar.read(regs::IRQ_STATUS);
+ bar.write(regs::IRQ_ACK, regs::IRQ_ACK::from_raw(status.into_raw()));
+ },
+ irq_test_completion <- Completion::new(),
+ irq_dma_completion <- Completion::new(),
+ dma: Coherent::zeroed(pdev.as_ref(), GFP_KERNEL)?,
+ pdev,
+ }? Error),
+ )
+ },
+
+ pdev,
+ })
+ .pin_chain(|this| {
+ this.selftest()?;
+ dev_info!(this.pdev, "rust_driver_edu successfully initialized\n");
+ Ok(())
+ })
+ }
+}
+
+impl irq::Handler for IrqHandler<'_> {
+ fn handle(&self) -> irq::IrqReturn {
+ dev_dbg!(self.pdev, "irq handler called\n");
+ let status = self.bar.read(regs::IRQ_STATUS);
+ let mut ret = irq::IrqReturn::None;
+
+ if status.dma() != 0 {
+ dev_dbg!(self.pdev, "handling dma completion in irq\n");
+ self.bar
+ .write(regs::IRQ_ACK, regs::IRQ_ACK::zeroed().with_dma(true));
+ self.irq_dma_completion.complete();
+ ret = irq::IrqReturn::Handled;
+ }
+
+ if u8::from(status.test()) == IRQ_MAGIC_VALUE {
+ dev_dbg!(self.pdev, "handling test completion in irq\n");
+ self.bar.write(
+ regs::IRQ_ACK,
+ regs::IRQ_ACK::zeroed().with_test(IRQ_MAGIC_VALUE),
+ );
+ self.irq_test_completion.complete();
+ ret = irq::IrqReturn::Handled;
+ }
+
+ ret
+ }
+}
+
+#[pinned_drop]
+impl PinnedDrop for EduDriverData<'_> {
+ fn drop(self: Pin<&mut Self>) {
+ dev_dbg!(self.pdev, "Remove Rust EDU driver sample.\n");
+ }
+}
+
+kernel::pci_device_table!(
+ PCI_TABLE,
+ <EduDriver as pci::Driver>::IdInfo,
+ [(
+ pci::DeviceId::from_id(pci::Vendor::QEMU, QEMU_EDU_DEVICE_ID),
+ ()
+ )]
+);
+
+kernel::module_pci_driver! {
+ type: EduDriver,
+ name: "rust_driver_edu",
+ authors: ["Maurice Hieronymus"],
+ description: "Rust EDU driver",
+ license: "GPL v2",
+}
--
2.54.0