diff options
| author | Deborah Brouwer <deborah.brouwer@collabora.com> | 2026-07-28 11:39:29 -0700 |
|---|---|---|
| committer | Alice Ryhl <aliceryhl@google.com> | 2026-07-31 12:36:27 +0000 |
| commit | 44e7e7f7cffb10a93bb88e7cb59b7b8b3e2deb1c (patch) | |
| tree | 54562edb28d491c9752314989f9f7c07eb9ecb70 | |
| parent | aa9ce820136062498e36a3fceeaf172561323fc3 (diff) | |
| download | linux-next-44e7e7f7cffb10a93bb88e7cb59b7b8b3e2deb1c.tar.gz linux-next-44e7e7f7cffb10a93bb88e7cb59b7b8b3e2deb1c.zip | |
drm/tyr: add Microcontroller Unit (MCU) booting
Add a firmware module to load, parse, and map the MCU firmware sections
into shared GEM memory at the required virtual addresses accessible by the
GPU.
Create a firmware instance during probe and store it inside the
TyrDrmRegistrationData to keep it alive after probe. Use the firmware
instance to boot the MCU.
Remove the dead-code annotations from the MMU, VM, slot manager, and
kernel BO code now that these paths are used by the firmware module.
Update Kconfig to add the RUST_FW_LOADER_ABSTRACTIONS dependency
required by this module.
Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Link: https://patch.msgid.link/20260728-fw-boot-b4-v10-7-9187aefa3f2f@collabora.com
Signed-off-by: Alice Ryhl <aliceryhl@google.com>
| -rw-r--r-- | drivers/gpu/drm/tyr/Kconfig | 1 | ||||
| -rw-r--r-- | drivers/gpu/drm/tyr/driver.rs | 23 | ||||
| -rw-r--r-- | drivers/gpu/drm/tyr/fw.rs | 321 | ||||
| -rw-r--r-- | drivers/gpu/drm/tyr/gem.rs | 3 | ||||
| -rw-r--r-- | drivers/gpu/drm/tyr/tyr.rs | 1 | ||||
| -rw-r--r-- | drivers/gpu/drm/tyr/vm.rs | 1 |
6 files changed, 341 insertions, 9 deletions
diff --git a/drivers/gpu/drm/tyr/Kconfig b/drivers/gpu/drm/tyr/Kconfig index 79ea4bb214de..8f13e49f11f9 100644 --- a/drivers/gpu/drm/tyr/Kconfig +++ b/drivers/gpu/drm/tyr/Kconfig @@ -13,6 +13,7 @@ config DRM_TYR select IOMMU_IO_PGTABLE_LPAE select RUST_DRM_GEM_SHMEM_HELPER select RUST_DRM_GPUVM + select RUST_FW_LOADER_ABSTRACTIONS help Rust DRM driver for ARM Mali CSF-based GPUs. diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs index f487b3996293..d78ad9d292ff 100644 --- a/drivers/gpu/drm/tyr/driver.rs +++ b/drivers/gpu/drm/tyr/driver.rs @@ -37,6 +37,7 @@ use kernel::{ use crate::{ file::TyrDrmFileData, + fw::Firmware, gem::Bo, gpu, gpu::GpuInfo, @@ -67,6 +68,9 @@ pub(crate) struct TyrDrmRegistrationData<'drm> { /// Parent platform device. pub(crate) pdev: &'drm platform::Device<Bound>, + /// Firmware sections. + pub(crate) fw: Firmware<'drm>, + #[pin] clks: Mutex<Clocks>, @@ -144,10 +148,21 @@ impl platform::Driver for TyrPlatformDriver { let unreg_dev = drm::UnregisteredDevice::<TyrDrmDriver>::new(pdev, Ok(()))?; - let _mmu = Mmu::new(pdev.as_ref(), iomem.as_arc_borrow(), &gpu_info)?; + let mmu = Mmu::new(pdev.as_ref(), iomem.as_arc_borrow(), &gpu_info)?; + + let firmware = Firmware::new( + pdev.as_ref(), + iomem.clone(), + &unreg_dev, + mmu.as_arc_borrow(), + &gpu_info, + )?; + + firmware.boot()?; - let reg_data = try_pin_init!(TyrDrmRegistrationData { + let reg_data = pin_init!(TyrDrmRegistrationData { pdev, + fw: firmware, clks <- new_mutex!(Clocks { core: core_clk, stacks: stacks_clk, @@ -167,9 +182,7 @@ impl platform::Driver for TyrPlatformDriver { let driver = TyrPlatformDriverData { _reg: reg }; - // We need this to be dev_info!() because dev_dbg!() does not work at - // all in Rust for now, and we need to see whether probe succeeded. - dev_info!(pdev, "Tyr initialized correctly.\n"); + dev_dbg!(pdev, "Tyr initialized correctly."); Ok(driver) } } diff --git a/drivers/gpu/drm/tyr/fw.rs b/drivers/gpu/drm/tyr/fw.rs new file mode 100644 index 000000000000..47d25c901bd0 --- /dev/null +++ b/drivers/gpu/drm/tyr/fw.rs @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: GPL-2.0 or MIT + +//! Firmware loading and management for Mali CSF GPU. +//! +//! This module handles loading the Mali GPU firmware binary, parsing it into sections, +//! and mapping those sections into the MCU's virtual address space. Each firmware section +//! has specific properties (read/write/execute permissions, cache modes) and must be loaded +//! at specific virtual addresses expected by the MCU. +//! +//! See [`Firmware`] for the main firmware management interface and [`Section`] for +//! individual firmware sections. +//! +//! [`Firmware`]: crate::fw::Firmware +//! [`Section`]: crate::fw::Section + +use kernel::{ + device::{ + Bound, + Device, // + }, + drm::{ + gem::BaseObject, // + }, + io::{ + poll, + Io, // + }, + num::Bounded, + prelude::*, + register, + str::CString, + sync::{ + Arc, + ArcBorrow, // + }, + time, // +}; + +use crate::{ + driver::{ + IoMem, + TyrDrmDevice, // + }, + fw::parser::{ + FwParser, + ParsedSection, // + }, + gem, + gem::{ + KernelBo, + KernelBoVaAlloc, // + }, + gpu::GpuInfo, + + mmu::Mmu, + regs::{ + gpu_control::{ + McuControlMode, + McuStatus, + GPU_ID, + MCU_CONTROL, + MCU_STATUS, // + }, // + job_control::{ + JOB_IRQ_CLEAR, + JOB_IRQ_RAWSTAT, // + }, // + }, + vm::Vm, // +}; + +mod parser; + +pub(super) const CSF_MCU_SHARED_REGION_START: u32 = 0x04000000; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u8)] +pub(super) enum CacheMode { + None = 0, + Cached = 1, + UncachedCoherent = 2, + CachedCoherent = 3, +} + +impl From<Bounded<u32, 2>> for CacheMode { + fn from(value: Bounded<u32, 2>) -> Self { + match value.get() { + 0 => Self::None, + 1 => Self::Cached, + 2 => Self::UncachedCoherent, + 3 => Self::CachedCoherent, + _ => unreachable!(), + } + } +} + +impl From<CacheMode> for Bounded<u32, 2> { + fn from(value: CacheMode) -> Self { + Bounded::try_new(value as u32).unwrap() + } +} + +register! { + #[allow(non_upper_case_globals)] + pub(super) SectionFlags(u32) @ 0x0 { + 0:0 read => bool; + 1:1 write => bool; + 2:2 exec => bool; + 4:3 cache_mode => CacheMode; + 5:5 prot => bool; + 30:30 shared => bool; + 31:31 zero => bool; + } +} + +impl SectionFlags { + const VALID_MASK: u32 = Self::READ_MASK + | Self::WRITE_MASK + | Self::EXEC_MASK + | Self::CACHE_MODE_MASK + | Self::PROT_MASK + | Self::SHARED_MASK + | Self::ZERO_MASK; + + fn try_from_fw(value: u32) -> Result<Self> { + if value & !Self::VALID_MASK != 0 { + Err(EINVAL) + } else { + Ok(Self::from_raw(value)) + } + } +} + +/// A parsed section of the firmware binary. +struct Section<'drm> { + // Raw firmware section data for reset purposes + #[expect(dead_code)] + data: KVec<u8>, + + // Keep the BO backing this firmware section so that both the + // GPU mapping and CPU mapping remain valid until the Section is dropped. + #[expect(dead_code)] + mem: gem::KernelBo<'drm>, +} + +/// Loaded firmware with sections mapped into MCU VM. +pub(crate) struct Firmware<'drm> { + /// Iomem need to access registers. + iomem: Arc<IoMem<'drm>>, + + /// MCU VM. + vm: Arc<Vm<'drm>>, + + /// List of firmware sections. + #[expect(dead_code)] + sections: KVec<Section<'drm>>, +} + +impl<'drm> Drop for Firmware<'drm> { + fn drop(&mut self) { + // Stop the MCU before releasing its firmware mappings and memory. + let _ = self.stop(); + + // AS slots retain a VM ref, we need to kill the circular ref manually. + self.vm.kill(); + } +} + +impl<'drm> Firmware<'drm> { + fn init_section_mem(dev: &Device, mem: &mut KernelBo<'drm>, data: &KVec<u8>) -> Result { + if data.is_empty() { + return Ok(()); + } + + let vmap = mem.bo().vmap::<0>()?; + let size = mem.bo().size(); + + if data.len() > size { + dev_err!(dev, "fw section {} bigger than BO {}", data.len(), size); + return Err(EINVAL); + } + + for (i, &byte) in data.iter().enumerate() { + vmap.try_write8(byte, i)?; + } + + Ok(()) + } + + fn request(ddev: &TyrDrmDevice, gpu_info: &GpuInfo) -> Result<kernel::firmware::Firmware> { + let gpu_id = GPU_ID::from_raw(gpu_info.gpu_id); + + let path = CString::try_from_fmt(fmt!( + "arm/mali/arch{}.{}/mali_csffw.bin", + gpu_id.arch_major().get(), + gpu_id.arch_minor().get() + ))?; + + kernel::firmware::Firmware::request(&path, ddev.as_ref().as_ref()) + } + + fn load( + dev: &Device, + ddev: &TyrDrmDevice, + gpu_info: &GpuInfo, + ) -> Result<(kernel::firmware::Firmware, KVec<ParsedSection>)> { + let fw = Self::request(ddev, gpu_info)?; + let mut parser = FwParser::new(dev, fw.data()); + + let parsed_sections = parser.parse()?; + + Ok((fw, parsed_sections)) + } + + /// Load firmware and map sections into MCU VM. + pub(crate) fn new( + dev: &'drm Device<Bound>, + iomem: Arc<IoMem<'drm>>, + ddev: &TyrDrmDevice, + mmu: ArcBorrow<'_, Mmu<'drm>>, + gpu_info: &GpuInfo, + ) -> Result<Firmware<'drm>> { + let vm = Vm::new(dev, ddev, mmu, gpu_info)?; + vm.activate()?; + + let result = (|| { + let (fw, parsed_sections) = Self::load(dev, ddev, gpu_info)?; + let mut sections = KVec::new(); + for parsed in parsed_sections { + let size = u64::from(parsed.va.end.checked_sub(parsed.va.start).ok_or(EINVAL)?); + + let va = u64::from(parsed.va.start); + + let mut mem = KernelBo::new( + ddev, + vm.clone(), + size, + KernelBoVaAlloc::Explicit(va), + parsed.vm_map_flags, + )?; + + let section_start = parsed.data_range.start as usize; + let section_end = parsed.data_range.end as usize; + let mut data = KVec::new(); + + // Ensure that the firmware slice is not out of bounds. + let fw_data = fw.data(); + let bytes = fw_data.get(section_start..section_end).ok_or(EINVAL)?; + data.extend_from_slice(bytes, GFP_KERNEL)?; + + Self::init_section_mem(dev, &mut mem, &data)?; + + sections.push(Section { data, mem }, GFP_KERNEL)?; + } + + Ok(Firmware { + iomem, + vm: vm.clone(), + sections, + }) + })(); + + if result.is_err() { + vm.kill(); + } + + result + } + + pub(crate) fn boot(&self) -> Result { + let io = &self.iomem; + + // Discard any stale global interrupt. + io.write_reg(JOB_IRQ_CLEAR::zeroed().with_glb(true)); + + io.write_reg(MCU_CONTROL::zeroed().with_req(McuControlMode::Auto)); + + if let Err(e) = poll::read_poll_timeout( + || Ok((io.read(MCU_STATUS), io.read(JOB_IRQ_RAWSTAT))), + |(mcu_status, irq_rawstat)| { + mcu_status.value() == McuStatus::Enabled && irq_rawstat.glb() + }, + time::Delta::from_millis(1), + time::Delta::from_millis(100), + ) { + let status = io.read(MCU_STATUS); + dev_err!( + self.vm.dev(), + "MCU failed to boot, status: {:?}", + status.value() + ); + return Err(e); + } + + io.write_reg(JOB_IRQ_CLEAR::zeroed().with_glb(true)); + + Ok(()) + } + + fn stop(&self) -> Result { + let io = &self.iomem; + io.write_reg(MCU_CONTROL::zeroed().with_req(McuControlMode::Disable)); + + if let Err(e) = poll::read_poll_timeout( + || Ok(io.read(MCU_STATUS)), + |status| status.value() == McuStatus::Disabled, + time::Delta::from_micros(10), + time::Delta::from_millis(100), + ) { + let status = io.read(MCU_STATUS); + dev_err!( + self.vm.dev(), + "MCU failed to stop, status: {:?}", + status.value() + ); + return Err(e); + } + + Ok(()) + } +} diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs index 69e1c75e59a5..3bf3787f5c3f 100644 --- a/drivers/gpu/drm/tyr/gem.rs +++ b/drivers/gpu/drm/tyr/gem.rs @@ -72,7 +72,6 @@ pub(crate) fn new_dummy_object(ddev: &TyrDrmDevice) -> Result<ARef<Bo>> { /// An automatic VA allocation strategy will be added in the future. pub(crate) enum KernelBoVaAlloc { /// Explicit VA address specified by the caller. - #[expect(dead_code)] Explicit(u64), } @@ -98,7 +97,6 @@ impl<'drm> KernelBo<'drm> { /// This function allocates a new shmem-backed GEM object and immediately maps /// it into the specified GPU virtual memory space. The mapping is automatically /// cleaned up when the [`KernelBo`] is dropped. - #[expect(dead_code)] pub(crate) fn new( ddev: &TyrDrmDevice, vm: Arc<Vm<'drm>>, @@ -135,7 +133,6 @@ impl<'drm> KernelBo<'drm> { }) } - #[expect(dead_code)] pub(crate) fn bo(&self) -> &Bo { &self.bo } diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs index 92f6885cdaae..e7ec450bdc9c 100644 --- a/drivers/gpu/drm/tyr/tyr.rs +++ b/drivers/gpu/drm/tyr/tyr.rs @@ -9,6 +9,7 @@ use crate::driver::TyrPlatformDriver; mod driver; mod file; +mod fw; mod gem; mod gpu; mod mmu; diff --git a/drivers/gpu/drm/tyr/vm.rs b/drivers/gpu/drm/tyr/vm.rs index aa7c1522b9d7..c5e307b1e241 100644 --- a/drivers/gpu/drm/tyr/vm.rs +++ b/drivers/gpu/drm/tyr/vm.rs @@ -6,7 +6,6 @@ //! the illusion of owning the entire virtual address (VA) range, similar to CPU virtual memory. //! Each virtual memory (VM) area is backed by ARM64 LPAE Stage 1 page tables and can be //! mapped into hardware address space (AS) slots for GPU execution. -#![expect(dead_code)] use core::marker::PhantomData; use core::ops::Range; |
