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 <[email protected]> --- 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 <[email protected]> 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
