* [PATCH] samples: rust: add QEMU EDU sample driver
@ 2026-10-12 0:38 Lucas Costa
0 siblings, 0 replies; only message in thread
From: Lucas Costa @ 2026-10-12 0:38 UTC (permalink / raw)
To: Danilo Krummrich, Bjorn Helgaas, Krzysztof Wilczyński,
Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
Daniel Almeida, Tamir Duberstein, Alexandre Courbot,
Onur Özkan
Cc: Lucas Costa, linux-kernel, linux-pci, rust-for-linux
Add a sample PCI driver, written in Rust, for QEMU's educational
("edu") device. The edu device is a simple emulated PCI device meant
for teaching device-driver concepts; this sample serves as a worked
example that exercises the Rust PCI, IRQ and DMA abstractions in a
single, self-contained driver.
On probe(), the driver walks through the edu device's feature set:
- identification and liveness checks over MMIO (reading the ID
register, and verifying the device's bitwise-NOT mirror register);
- the interrupt status/raise/ack registers at the MMIO level;
- an MSI interrupt: arm "raise IRQ on completion", start a factorial
computation, wait on a Completion signalled by the IRQ handler,
then verify the result;
- a full DMA round-trip (RAM -> device buffer -> RAM) using the
coherent DMA API with a 28-bit DMA mask;
- a few PCI configuration-space reads.
To make the driver probe, QEMU must be run with `-device edu`.
Signed-off-by: Lucas Costa <2000.costalucas@gmail.com>
---
rust/kernel/pci/id.rs | 8 +-
samples/rust/Kconfig | 11 +
samples/rust/Makefile | 1 +
samples/rust/rust_driver_edu.rs | 382 ++++++++++++++++++++++++++++++++
4 files changed, 400 insertions(+), 2 deletions(-)
create mode 100644 samples/rust/rust_driver_edu.rs
diff --git a/rust/kernel/pci/id.rs b/rust/kernel/pci/id.rs
index dbaf301666e7..2747d92be91b 100644
--- a/rust/kernel/pci/id.rs
+++ b/rust/kernel/pci/id.rs
@@ -154,9 +154,13 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// Once constructed, a `Vendor` contains a valid PCI Vendor ID.
impl Vendor {
- /// Create a Vendor from a raw 16-bit vendor ID.
+ /// Create a [`Vendor`] from a raw 16-bit vendor ID.
+ ///
+ /// Prefer one of the named [`Vendor`] constants where one exists. This is intended for
+ /// vendor IDs that have no dedicated constant, e.g. test or emulated devices whose IDs
+ /// are not shared between multiple drivers and thus do not belong in `pci_ids.h`.
#[inline]
- pub(super) fn from_raw(vendor_id: u16) -> Self {
+ pub const fn from_raw(vendor_id: u16) -> Self {
Self(vendor_id)
}
diff --git a/samples/rust/Kconfig b/samples/rust/Kconfig
index 31d62533ef25..b654751041fa 100644
--- a/samples/rust/Kconfig
+++ b/samples/rust/Kconfig
@@ -190,4 +190,15 @@ config SAMPLE_RUST_HOSTPROGS
If unsure, say N.
+config SAMPLE_RUST_DRIVER_EDU
+ tristate "QEMU EDU Driver"
+ depends on PCI
+ help
+ This option builds the Rust QEMU EDU PCI driver sample.
+
+ To compile this as a module, choose M here:
+ the module will be called rust_driver_edu.
+
+ If unsure, say N.
+
endif # SAMPLES_RUST
diff --git a/samples/rust/Makefile b/samples/rust/Makefile
index b986b681cde5..57d944baa781 100644
--- a/samples/rust/Makefile
+++ b/samples/rust/Makefile
@@ -17,6 +17,7 @@ obj-$(CONFIG_SAMPLE_RUST_DRIVER_AUXILIARY) += rust_driver_auxiliary.o
obj-$(CONFIG_SAMPLE_RUST_DRIVER_SERDEV) += rust_driver_serdev.o
obj-$(CONFIG_SAMPLE_RUST_CONFIGFS) += rust_configfs.o
obj-$(CONFIG_SAMPLE_RUST_SOC) += rust_soc.o
+obj-$(CONFIG_SAMPLE_RUST_DRIVER_EDU) += rust_driver_edu.o
rust_print-y := rust_print_main.o rust_print_events.o
diff --git a/samples/rust/rust_driver_edu.rs b/samples/rust/rust_driver_edu.rs
new file mode 100644
index 000000000000..e9e5593bb89a
--- /dev/null
+++ b/samples/rust/rust_driver_edu.rs
@@ -0,0 +1,382 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Rust QEMU EDU driver sample (based on QEMU's edu device).
+//!
+//! To make this driver probe, QEMU must be run with `-device edu`.
+
+use kernel::{
+ device::{
+ Bound,
+ Core, //
+ },
+ dma::{
+ Coherent,
+ CoherentBox,
+ Device as _,
+ DmaMask, //
+ },
+ io::{
+ poll::read_poll_timeout,
+ register,
+ register::Array,
+ Io, //
+ },
+ irq::{
+ self,
+ Flags,
+ Registration, //
+ },
+ pci,
+ prelude::*,
+ sync::Completion,
+ time::Delta, //
+};
+
+mod regs {
+ use super::*;
+
+ register! {
+ pub(super) IDENTIFICATION(u32) @ 0x0 {
+ 31:0 version;
+ }
+
+ pub(super) LIVENESS(u32) @ 0x4 {
+ 31:0 value;
+ }
+
+ pub(super) FACTORIAL(u32) @ 0x8 {
+ 31:0 value;
+ }
+
+ pub(super) STATUS(u32) @ 0x20 {
+ 7:7 irq_on_done; // 0x80: raise an IRQ when the factorial is done
+ 0:0 computing; // 0x01: set while a factorial is being computed
+ }
+
+ pub(super) INTERRUPT_STATUS(u32) @ 0x24 {
+ 31:0 status;
+ }
+
+ pub(super) INTERRUPT_RAISE(u32) @ 0x60 {
+ 31:0 raise;
+ }
+
+ pub(super) INTERRUPT_ACK(u32) @ 0x64 {
+ 31:0 ack;
+ }
+
+ // The EDU DMA registers are documented as 64-bit, but QEMU's edu device
+ // accepts 4-byte accesses to these registers and zero-extends the
+ // written value, and this driver only uses addresses/counts that fit in
+ // 32 bits (a 28-bit DMA mask), so a single `u32` access per register is
+ // correct and sufficient here.
+ pub(super) DMA_SRC_ADDR(u32) @ 0x80 {
+ 31:0 src;
+ }
+
+ pub(super) DMA_DST_ADDR(u32) @ 0x88 {
+ 31:0 dst;
+ }
+
+ pub(super) DMA_CNT(u32) @ 0x90 {
+ 31:0 size;
+ }
+
+ pub(super) DMA_CMD(u32) @ 0x98 {
+ 31:0 cmd;
+ }
+ }
+
+ pub(super) const END: usize = 0x9C;
+}
+
+type Bar0<'bound> = pci::Bar<'bound, { regs::END }>;
+
+struct SampleDriverData<'bound> {
+ pdev: &'bound pci::Device,
+ // Held for the lifetime of the bound device so the BAR mapping stays alive;
+ // all register access happens during probe, so the field is never read again.
+ #[expect(dead_code)]
+ bar: Bar0<'bound>,
+}
+
+struct SampleDriver;
+
+// The QEMU EDU device's PCI IDs. These are specific to this emulated device and are not
+// shared with any other driver, so they are kept local here rather than in `pci_ids.h`.
+// `0x1234` is QEMU's conventional vendor ID, which has no dedicated `pci::Vendor` constant.
+const VENDOR_ID_QEMU: u16 = 0x1234;
+const DEVICE_ID_EDU: u32 = 0x11e8;
+
+kernel::pci_device_table!(
+ PCI_TABLE,
+ <SampleDriver as pci::Driver>::IdInfo,
+ [(
+ pci::DeviceId::from_id(pci::Vendor::from_raw(VENDOR_ID_QEMU), DEVICE_ID_EDU),
+ ()
+ )]
+);
+
+impl SampleDriverData<'_> {
+ /// The value the EDU device reports from its `IDENTIFICATION` register.
+ const EDU_ID: u32 = 0x010000ed;
+
+ /// Confirm we are really talking to an EDU device by reading its ID register.
+ fn identification(bar: &Bar0<'_>) -> Result {
+ if bar.read(regs::IDENTIFICATION).version().get() != Self::EDU_ID {
+ return Err(ENODEV);
+ }
+
+ Ok(())
+ }
+
+ /// Liveness check: the EDU device inverts (bitwise NOT) whatever we write to the
+ /// `LIVENESS` register, so writing `x` and reading back `!x` proves MMIO works.
+ fn liveness(bar: &Bar0<'_>) -> Result {
+ let pattern: u32 = 0xdead_beef;
+
+ bar.write_reg(regs::LIVENESS::zeroed().with_value(pattern));
+
+ let readback = bar.read(regs::LIVENESS).value().get();
+
+ if readback != !pattern {
+ return Err(EIO);
+ }
+
+ Ok(())
+ }
+
+ /// Exercise the interrupt registers at the MMIO level.
+ ///
+ /// Writing a bitmask to `INTERRUPT_RAISE` asserts those bits in
+ /// `INTERRUPT_STATUS` (and, if an IRQ is enabled, makes the device raise an
+ /// interrupt); writing the same bitmask to `INTERRUPT_ACK` clears them
+ /// again. Here we raise a test bit, confirm the status register reflects it,
+ /// then acknowledge it and confirm it clears -- all without registering a
+ /// handler. Wiring up MSI and an actual IRQ handler is a later step.
+ fn interrupt_status(bar: &Bar0<'_>) -> Result {
+ let pattern: u32 = 0x0000_0001;
+
+ bar.write_reg(regs::INTERRUPT_RAISE::zeroed().with_raise(pattern));
+
+ if bar.read(regs::INTERRUPT_STATUS).status().get() != pattern {
+ return Err(EIO);
+ }
+
+ bar.write_reg(regs::INTERRUPT_ACK::zeroed().with_ack(pattern));
+
+ if bar.read(regs::INTERRUPT_STATUS).status().get() != 0 {
+ return Err(EIO);
+ }
+
+ Ok(())
+ }
+
+ /// Make the edu device compute a factorial and inform the cpu
+ /// of completion via an IRQ
+ fn factorial_irq(pdev: &pci::Device<Bound>, bar: &Bar0<'_>) -> Result {
+ let operand: u32 = 6;
+
+ let msi = pci::IrqTypes::default().with(pci::IrqType::Msi);
+ let vectors = pdev.alloc_irq_vectors(1, 1, msi)?;
+
+ let registration = KBox::pin_init(
+ unsafe {
+ Registration::new(
+ vectors.index(0)?.into(),
+ Flags::SHARED,
+ c"rust_driver_edu",
+ try_pin_init!(EduIrq {
+ done <- Completion::new(),
+ }? Error),
+ )
+ },
+ GFP_KERNEL,
+ )?;
+
+ // Arm the "raise an IRQ when done" bit, then start the computation.
+ bar.write_reg(regs::STATUS::zeroed().with_irq_on_done(true));
+ bar.write_reg(regs::FACTORIAL::zeroed().with_value(operand));
+
+ // Sleep until the handler calls `complete_all()` from IRQ context.
+ registration.handler().done.wait_for_completion();
+
+ // Acknowledge whatever the device raised so it lowers the interrupt.
+ let pending = bar.read(regs::INTERRUPT_STATUS).status().get();
+ bar.write_reg(regs::INTERRUPT_ACK::zeroed().with_ack(pending));
+
+ let result = bar.read(regs::FACTORIAL).value().get();
+
+ if result != factorial_helper(operand) {
+ return Err(EIO);
+ }
+
+ Ok(())
+ }
+
+ /// Tests a full DMA round-trip, writing from RAM -> device buffer,
+ /// and then from device buffer -> RAM
+ fn dma_test(pdev: &pci::Device<Core<'_>>, bar: &Bar0<'_>) -> Result {
+ const DEV_BUF: u32 = 0x40_000;
+ const LEN: usize = 512;
+ const CMD_START: u32 = 1 << 0;
+ const CMD_TO_RAM: u32 = 1 << 1;
+
+ unsafe { pdev.dma_set_mask_and_coherent(DmaMask::new::<28>())? };
+
+ let mut src: CoherentBox<[u8]> = CoherentBox::zeroed_slice(pdev.as_ref(), LEN, GFP_KERNEL)?;
+ src.fill(0xa5);
+
+ let src: Coherent<[u8]> = src.into();
+
+ let dst: Coherent<[u8]> =
+ CoherentBox::<[u8]>::zeroed_slice(pdev.as_ref(), LEN, GFP_KERNEL)?.into();
+
+ let src_dma = src.dma_address() as u32;
+ let dst_dma = dst.dma_address() as u32;
+
+ let wait_dma = || {
+ read_poll_timeout(
+ || Ok(bar.read(regs::DMA_CMD).cmd().get()),
+ |cmd| *cmd & CMD_START == 0,
+ Delta::from_millis(10),
+ Delta::from_secs(1),
+ )
+ };
+
+ // 1) RAM -> device internal buffer. CMD is written last: it triggers the transfer.
+ bar.write_reg(regs::DMA_SRC_ADDR::zeroed().with_src(src_dma));
+ bar.write_reg(regs::DMA_DST_ADDR::zeroed().with_dst(DEV_BUF));
+ bar.write_reg(regs::DMA_CNT::zeroed().with_size(LEN as u32));
+ bar.write_reg(regs::DMA_CMD::zeroed().with_cmd(CMD_START));
+ wait_dma()?;
+
+ // 2) device internal buffer -> RAM, landing in `dst`.
+ bar.write_reg(regs::DMA_SRC_ADDR::zeroed().with_src(DEV_BUF));
+ bar.write_reg(regs::DMA_DST_ADDR::zeroed().with_dst(dst_dma));
+ bar.write_reg(regs::DMA_CNT::zeroed().with_size(LEN as u32));
+ bar.write_reg(regs::DMA_CMD::zeroed().with_cmd(CMD_START | CMD_TO_RAM));
+ wait_dma()?;
+
+ // Transfer completed, check dst is the expected value
+ let out = unsafe { dst.as_ref() };
+ if out.iter().any(|&b| b != 0xa5) {
+ return Err(EIO);
+ }
+
+ Ok(())
+ }
+
+ fn config_space(pdev: &pci::Device<Bound>) {
+ let config = pdev.config_space();
+
+ // Some PCI configuration space registers.
+ register! {
+ 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,
+ "edu config space read8 rev ID: {:x}\n",
+ config.read(REVISION_ID).revision_id()
+ );
+
+ dev_info!(
+ pdev,
+ "edu config space read16 vendor ID: {:x}\n",
+ config.read(VENDOR_ID).vendor_id()
+ );
+
+ dev_info!(
+ pdev,
+ "edu config space read32 BAR 0: {:x}\n",
+ config.read(BAR::at(0)).value()
+ );
+ }
+}
+
+impl pci::Driver for SampleDriver {
+ type IdInfo = ();
+ type Data<'bound> = SampleDriverData<'bound>;
+
+ const ID_TABLE: pci::IdTable<Self::IdInfo> = &PCI_TABLE;
+
+ fn probe<'bound>(
+ pdev: &'bound pci::Device<Core<'_>>,
+ _info: Option<&'bound Self::IdInfo>,
+ ) -> impl PinInit<Self::Data<'bound>, Error> + 'bound {
+ dev_dbg!(
+ pdev,
+ "Probe Rust QEMU EDU driver sample (PCI ID: {}, 0x{:x}).\n",
+ pdev.vendor_id(),
+ pdev.device_id()
+ );
+
+ pdev.enable_device_mem()?;
+ pdev.set_master();
+
+ let bar = pdev.iomap_region_sized::<{ regs::END }>(0, c"rust_driver_edu")?;
+
+ SampleDriverData::identification(&bar)?;
+ SampleDriverData::liveness(&bar)?;
+ SampleDriverData::interrupt_status(&bar)?;
+ SampleDriverData::factorial_irq(pdev, &bar)?;
+ SampleDriverData::dma_test(pdev, &bar)?;
+ SampleDriverData::config_space(pdev);
+
+ dev_info!(pdev, "EDU device probed: all checks passed.\n");
+
+ Ok(SampleDriverData { pdev, bar })
+ }
+}
+
+impl Drop for SampleDriverData<'_> {
+ fn drop(&mut self) {
+ dev_dbg!(self.pdev, "Remove Rust QEMU EDU driver sample.\n");
+ }
+}
+
+/// Per-interrupt state shared between process context and IRQ context.
+///
+/// The factorial computation signals `done` from the IRQ handler; the process
+/// context that started it waits on the same [`Completion`].
+#[pin_data]
+struct EduIrq {
+ #[pin]
+ done: Completion,
+}
+
+impl irq::Handler for EduIrq {
+ // Runs in IRQ context when the EDU device raises its interrupt.
+ fn handle(&self) -> irq::IrqReturn {
+ self.done.complete_all();
+ irq::IrqReturn::Handled
+ }
+}
+
+// Helper functions
+fn factorial_helper(n: u32) -> u32 {
+ if n <= 1 {
+ return 1;
+ }
+
+ n * factorial_helper(n - 1)
+}
+
+kernel::module_pci_driver! {
+ type: SampleDriver,
+ name: "rust_driver_edu",
+ authors: ["Lucas Costa"],
+ description: "Rust QEMU EDU driver",
+ license: "GPL v2",
+}
--
2.43.0
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2026-10-12 0:39 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-12 0:38 [PATCH] samples: rust: add QEMU EDU sample driver Lucas Costa
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®