[PATCH v5 36/38] gpu: nova-core: Hopper/Blackwell: integrate FSP boot path into boot()

From: John Hubbard

Date: Fri Feb 20 2026 - 21:21:55 EST


Add the FSP boot path for Hopper and Blackwell GPUs. These architectures
use FSP with FMC firmware for Chain of Trust boot, rather than SEC2.

boot() now dispatches to boot_via_sec2() or boot_via_fsp() based on
architecture. The SEC2 path keeps its original command ordering. The
FSP path sends SetSystemInfo/SetRegistry after GSP becomes active.
The GSP sequencer only runs for SEC2-based architectures.

Signed-off-by: John Hubbard <jhubbard@xxxxxxxxxx>
---
drivers/gpu/nova-core/firmware/fsp.rs | 2 -
drivers/gpu/nova-core/fsp.rs | 5 -
drivers/gpu/nova-core/gsp/boot.rs | 190 +++++++++++++++++++-------
3 files changed, 144 insertions(+), 53 deletions(-)

diff --git a/drivers/gpu/nova-core/firmware/fsp.rs b/drivers/gpu/nova-core/firmware/fsp.rs
index bb35f363b998..0e72f1378ef0 100644
--- a/drivers/gpu/nova-core/firmware/fsp.rs
+++ b/drivers/gpu/nova-core/firmware/fsp.rs
@@ -13,7 +13,6 @@
gpu::Chipset, //
};

-#[expect(dead_code)]
pub(crate) struct FspFirmware {
/// FMC firmware image data (only the "image" ELF section).
pub(crate) fmc_image: DmaObject,
@@ -22,7 +21,6 @@ pub(crate) struct FspFirmware {
}

impl FspFirmware {
- #[expect(dead_code)]
pub(crate) fn new(
dev: &device::Device<device::Bound>,
chipset: Chipset,
diff --git a/drivers/gpu/nova-core/fsp.rs b/drivers/gpu/nova-core/fsp.rs
index c66ad0a102a6..3749b5e3a677 100644
--- a/drivers/gpu/nova-core/fsp.rs
+++ b/drivers/gpu/nova-core/fsp.rs
@@ -238,7 +238,6 @@ pub(crate) struct FmcBootArgs<'a> {
impl<'a> FmcBootArgs<'a> {
/// Build FMC boot arguments, allocating the DMA-coherent boot parameter
/// structure that FSP will read.
- #[expect(dead_code)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
dev: &device::Device<device::Bound>,
@@ -287,7 +286,6 @@ pub(crate) fn new(

/// DMA address of the FMC boot parameters, needed after boot for lockdown
/// release polling.
- #[expect(dead_code)]
pub(crate) fn boot_params_dma_handle(&self) -> u64 {
self.fmc_boot_params.dma_handle()
}
@@ -301,7 +299,6 @@ impl Fsp {
///
/// Polls the thermal scratch register until FSP signals boot completion
/// or timeout occurs.
- #[expect(dead_code)]
pub(crate) fn wait_secure_boot(
dev: &device::Device<device::Bound>,
bar: &crate::driver::Bar0,
@@ -331,7 +328,6 @@ pub(crate) fn wait_secure_boot(
///
/// Extracts real cryptographic signatures from FMC ELF32 firmware sections.
/// Returns signatures in a heap-allocated structure to prevent stack overflow.
- #[expect(dead_code)]
pub(crate) fn extract_fmc_signatures(
dev: &device::Device<device::Bound>,
fmc_fw_data: &[u8],
@@ -391,7 +387,6 @@ pub(crate) fn extract_fmc_signatures(
///
/// Builds the COT message from the pre-configured [`FmcBootArgs`], sends it
/// to FSP, and waits for the response.
- #[expect(dead_code)]
pub(crate) fn boot_fmc(
dev: &device::Device<device::Bound>,
bar: &crate::driver::Bar0,
diff --git a/drivers/gpu/nova-core/gsp/boot.rs b/drivers/gpu/nova-core/gsp/boot.rs
index 0db2c58e0765..1fdcb72ce163 100644
--- a/drivers/gpu/nova-core/gsp/boot.rs
+++ b/drivers/gpu/nova-core/gsp/boot.rs
@@ -13,6 +13,7 @@
use crate::{
driver::Bar0,
falcon::{
+ fsp::Fsp as FspEngine,
gsp::Gsp,
sec2::Sec2,
Falcon,
@@ -24,6 +25,7 @@
BooterFirmware,
BooterKind, //
},
+ fsp::FspFirmware,
fwsec::{
FwsecCommand,
FwsecFirmware, //
@@ -31,9 +33,17 @@
gsp::GspFirmware,
FIRMWARE_VERSION, //
},
- gpu::Chipset,
+ fsp::{
+ FmcBootArgs,
+ Fsp, //
+ },
+ gpu::{
+ Architecture,
+ Chipset, //
+ },
gsp::{
commands,
+ fw::LibosMemoryRegionInitArgument,
sequencer::{
GspSequencer,
GspSequencerParams, //
@@ -188,8 +198,83 @@ fn run_booter(
booter.run(dev, bar, sec2_falcon, wpr_meta)
}

+ /// Boot GSP via SEC2 booter firmware (Turing/Ampere/Ada path).
+ ///
+ /// This path uses FWSEC-FRTS to set up WPR2, then boots GSP directly,
+ /// then uses SEC2 to run the booter firmware.
+ #[allow(clippy::too_many_arguments)]
+ fn boot_via_sec2(
+ dev: &device::Device<device::Bound>,
+ bar: &Bar0,
+ chipset: Chipset,
+ gsp_falcon: &Falcon<Gsp>,
+ sec2_falcon: &Falcon<Sec2>,
+ fb_layout: &FbLayout,
+ libos: &CoherentAllocation<LibosMemoryRegionInitArgument>,
+ wpr_meta: &CoherentAllocation<GspFwWprMeta>,
+ ) -> Result {
+ // Run FWSEC-FRTS to set up the WPR2 region
+ let bios = Vbios::new(dev, bar)?;
+ Self::run_fwsec_frts(dev, gsp_falcon, bar, &bios, fb_layout)?;
+
+ // Reset and boot GSP before SEC2
+ gsp_falcon.reset(bar)?;
+ let libos_handle = libos.dma_handle();
+ let (mbox0, mbox1) = gsp_falcon.boot(
+ bar,
+ Some(libos_handle as u32),
+ Some((libos_handle >> 32) as u32),
+ )?;
+ dev_dbg!(dev, "GSP MBOX0: {:#x}, MBOX1: {:#x}\n", mbox0, mbox1);
+ dev_dbg!(
+ dev,
+ "Using SEC2 to load and run the booter_load firmware...\n"
+ );
+
+ // Run booter via SEC2
+ Self::run_booter(dev, bar, chipset, sec2_falcon, wpr_meta)
+ }
+
+ /// Boot GSP via FSP Chain of Trust (Hopper/Blackwell+ path).
+ ///
+ /// This path uses FSP to establish a chain of trust and boot GSP-FMC. FSP handles
+ /// the GSP boot internally - no manual GSP reset/boot is needed.
+ fn boot_via_fsp(
+ dev: &device::Device<device::Bound>,
+ bar: &Bar0,
+ chipset: Chipset,
+ gsp_falcon: &Falcon<Gsp>,
+ wpr_meta: &CoherentAllocation<GspFwWprMeta>,
+ libos: &CoherentAllocation<LibosMemoryRegionInitArgument>,
+ ) -> Result {
+ let fsp_falcon = Falcon::<FspEngine>::new(dev, chipset)?;
+
+ Fsp::wait_secure_boot(dev, bar, chipset.arch())?;
+
+ let fsp_fw = FspFirmware::new(dev, chipset, FIRMWARE_VERSION)?;
+
+ let signatures = Fsp::extract_fmc_signatures(dev, &fsp_fw.fmc_full)?;
+
+ let args = FmcBootArgs::new(
+ dev,
+ chipset,
+ &fsp_fw.fmc_image,
+ wpr_meta.dma_handle(),
+ core::mem::size_of::<GspFwWprMeta>() as u32,
+ libos.dma_handle(),
+ false,
+ &signatures,
+ )?;
+
+ Fsp::boot_fmc(dev, bar, &fsp_falcon, &args)?;
+
+ let fmc_boot_params_addr = args.boot_params_dma_handle();
+ Self::wait_for_gsp_lockdown_release(dev, bar, gsp_falcon, fmc_boot_params_addr)?;
+
+ Ok(())
+ }
+
/// Wait for GSP lockdown to be released after FSP Chain of Trust.
- #[expect(dead_code)]
fn wait_for_gsp_lockdown_release(
dev: &device::Device<device::Bound>,
bar: &Bar0,
@@ -233,45 +318,49 @@ pub(crate) fn boot(
sec2_falcon: &Falcon<Sec2>,
) -> Result {
let dev = pdev.as_ref();
-
- let bios = Vbios::new(dev, bar)?;
+ let uses_sec2 = matches!(
+ chipset.arch(),
+ Architecture::Turing | Architecture::Ampere | Architecture::Ada
+ );

let gsp_fw = KBox::pin_init(GspFirmware::new(dev, chipset, FIRMWARE_VERSION), GFP_KERNEL)?;

let fb_layout = FbLayout::new(chipset, bar, &gsp_fw)?;
dev_dbg!(dev, "{:#x?}\n", fb_layout);

- Self::run_fwsec_frts(dev, gsp_falcon, bar, &bios, &fb_layout)?;
-
let wpr_meta =
CoherentAllocation::<GspFwWprMeta>::alloc_coherent(dev, 1, GFP_KERNEL | __GFP_ZERO)?;
dma_write!(wpr_meta[0] = GspFwWprMeta::new(&gsp_fw, &fb_layout))?;

- self.cmdq
- .send_command(bar, commands::SetSystemInfo::new(pdev, chipset))?;
- self.cmdq.send_command(bar, commands::SetRegistry::new())?;
-
- gsp_falcon.reset(bar)?;
- let libos_handle = self.libos.dma_handle();
- let (mbox0, mbox1) = gsp_falcon.boot(
- bar,
- Some(libos_handle as u32),
- Some((libos_handle >> 32) as u32),
- )?;
- dev_dbg!(
- pdev,
- "GSP MBOX0: {:#x}, MBOX1: {:#x}\n",
- mbox0,
- mbox1
- );
-
- dev_dbg!(
- pdev,
- "Using SEC2 to load and run the booter_load firmware...\n"
- );
+ // Architecture-specific boot path
+ if uses_sec2 {
+ // SEC2 path: send commands before GSP reset/boot (original order).
+ self.cmdq
+ .send_command(bar, commands::SetSystemInfo::new(pdev, chipset))?;
+ self.cmdq.send_command(bar, commands::SetRegistry::new())?;

- Self::run_booter(dev, bar, chipset, sec2_falcon, &wpr_meta)?;
+ Self::boot_via_sec2(
+ dev,
+ bar,
+ chipset,
+ gsp_falcon,
+ sec2_falcon,
+ &fb_layout,
+ &self.libos,
+ &wpr_meta,
+ )?;
+ } else {
+ Self::boot_via_fsp(
+ dev,
+ bar,
+ chipset,
+ gsp_falcon,
+ &wpr_meta,
+ &self.libos,
+ )?;
+ }

+ // Common post-boot initialization
gsp_falcon.write_os_version(bar, gsp_fw.bootloader.app_version);

// Poll for RISC-V to become active before running sequencer
@@ -282,22 +371,31 @@ pub(crate) fn boot(
Delta::from_secs(5),
)?;

- dev_dbg!(
- pdev,
- "RISC-V active? {}\n",
- gsp_falcon.is_riscv_active(bar),
- );
+ dev_dbg!(dev, "RISC-V active? {}\n", gsp_falcon.is_riscv_active(bar));

- // Create and run the GSP sequencer.
- let seq_params = GspSequencerParams {
- bootloader_app_version: gsp_fw.bootloader.app_version,
- libos_dma_handle: libos_handle,
- gsp_falcon,
- sec2_falcon,
- dev: pdev.as_ref().into(),
- bar,
- };
- GspSequencer::run(&mut self.cmdq, seq_params)?;
+ // For FSP path, send commands after GSP becomes active.
+ if matches!(
+ chipset.arch(),
+ Architecture::Hopper | Architecture::Blackwell
+ ) {
+ self.cmdq
+ .send_command(bar, commands::SetSystemInfo::new(pdev, chipset))?;
+ self.cmdq.send_command(bar, commands::SetRegistry::new())?;
+ }
+
+ // SEC2-based architectures need to run the GSP sequencer
+ if uses_sec2 {
+ let libos_handle = self.libos.dma_handle();
+ let seq_params = GspSequencerParams {
+ bootloader_app_version: gsp_fw.bootloader.app_version,
+ libos_dma_handle: libos_handle,
+ gsp_falcon,
+ sec2_falcon,
+ dev: dev.into(),
+ bar,
+ };
+ GspSequencer::run(&mut self.cmdq, seq_params)?;
+ }

// Wait until GSP is fully initialized.
commands::wait_gsp_init_done(&mut self.cmdq)?;
@@ -305,8 +403,8 @@ pub(crate) fn boot(
// Obtain and display basic GPU information.
let info = commands::get_gsp_info(&mut self.cmdq, bar)?;
match info.gpu_name() {
- Ok(name) => dev_info!(pdev, "GPU name: {}\n", name),
- Err(e) => dev_warn!(pdev, "GPU name unavailable: {:?}\n", e),
+ Ok(name) => dev_info!(dev, "GPU name: {}\n", name),
+ Err(e) => dev_warn!(dev, "GPU name unavailable: {:?}\n", e),
}

Ok(())
--
2.53.0