| // SPDX-License-Identifier: GPL-2.0 |
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| |
| //! FSP (Foundation Security Processor) falcon engine for Hopper/Blackwell GPUs. |
| //! |
| //! The FSP falcon handles secure boot and Chain of Trust operations |
| //! on Hopper and Blackwell architectures, replacing SEC2's role. |
| |
| use kernel::{ |
| io::{ |
| poll::read_poll_timeout, |
| register::{ |
| Array, |
| RegisterBase, |
| WithBase, // |
| }, |
| Io, // |
| }, |
| prelude::*, |
| sizes::SZ_1K, |
| time::Delta, |
| }; |
| |
| use crate::{ |
| falcon::{ |
| Falcon, |
| FalconEngine, |
| PFalcon2Base, |
| PFalconBase, // |
| }, |
| num, |
| regs, // |
| }; |
| |
| /// FSP message timeout in milliseconds. |
| const FSP_MSG_TIMEOUT_MS: i64 = 2000; |
| |
| /// Size of the FSP EMEM channel 0 that we can use. |
| const FSP_EMEM_CHANNEL_0_SIZE: usize = SZ_1K; |
| |
| /// Type specifying the `Fsp` falcon engine. Cannot be instantiated. |
| pub(crate) struct Fsp(()); |
| |
| impl RegisterBase<PFalconBase> for Fsp { |
| const BASE: usize = 0x8f2000; |
| } |
| |
| impl RegisterBase<PFalcon2Base> for Fsp { |
| const BASE: usize = 0x8f3000; |
| } |
| |
| impl FalconEngine for Fsp {} |
| |
| impl<'a> Falcon<'a, Fsp> { |
| /// Writes `data` to FSP external memory at offset `0`. |
| /// |
| /// `data` is interpreted as little-endian 32-bit words. Returns `EINVAL` |
| /// if the `data` length is not 4-byte aligned. |
| fn write_emem(&mut self, data: &[u8]) -> Result { |
| if data.len() % 4 != 0 { |
| return Err(EINVAL); |
| } |
| |
| // Begin a write burst at offset `0`, auto-incrementing on each write. |
| self.bar.write( |
| WithBase::of::<Fsp>(), |
| regs::NV_PFALCON_FALCON_EMEMC::zeroed().with_aincw(true), |
| ); |
| |
| for chunk in data.chunks_exact(4) { |
| let value = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); |
| |
| // Write the next 32-bit `value`; hardware advances the offset. |
| self.bar.write( |
| WithBase::of::<Fsp>(), |
| regs::NV_PFALCON_FALCON_EMEMD::zeroed().with_data(value), |
| ); |
| } |
| |
| Ok(()) |
| } |
| |
| /// Reads FSP external memory from offset `0` into `data`. |
| /// |
| /// `data` is stored as little-endian 32-bit words. Returns `EINVAL` if |
| /// the `data` length is not 4-byte aligned. |
| fn read_emem(&mut self, data: &mut [u8]) -> Result { |
| if data.len() % 4 != 0 { |
| return Err(EINVAL); |
| } |
| |
| // Begin a read burst at offset `0`, auto-incrementing on each read. |
| self.bar.write( |
| WithBase::of::<Fsp>(), |
| regs::NV_PFALCON_FALCON_EMEMC::zeroed().with_aincr(true), |
| ); |
| |
| for chunk in data.chunks_exact_mut(4) { |
| // Read the next 32-bit word; hardware advances the offset. |
| let value = self |
| .bar |
| .read(regs::NV_PFALCON_FALCON_EMEMD::of::<Fsp>()) |
| .data(); |
| chunk.copy_from_slice(&value.to_le_bytes()); |
| } |
| |
| Ok(()) |
| } |
| |
| /// Poll FSP for incoming data. |
| /// |
| /// Returns the size of available data in bytes, or 0 if no data is available. |
| /// |
| /// Returns [`EIO`] if the queue pointers are bogus (`tail < head`). |
| /// |
| /// The FSP message queue is not circular. Pointers are reset to 0 after each |
| /// message exchange, so `tail >= head` is always true when data is present. |
| fn poll_msgq(&self) -> Result<u32> { |
| let head = self.bar.read(regs::NV_PFSP_MSGQ_HEAD::at(0)).val(); |
| let tail = self.bar.read(regs::NV_PFSP_MSGQ_TAIL::at(0)).val(); |
| |
| if head == tail { |
| Ok(0) |
| } else { |
| // TAIL points at the last DWORD written, so the size is `tail - head + 4`. |
| tail.checked_sub(head) |
| .and_then(|delta| delta.checked_add(4)) |
| .ok_or(EIO) |
| } |
| } |
| |
| /// Writes `packet` to FSP EMEM and updates the queue pointers to notify FSP. |
| /// |
| /// Returns `EINVAL` if `packet` is empty or its length is not 4-byte aligned. |
| pub(crate) fn send_msg(&mut self, packet: &[u8]) -> Result { |
| if packet.is_empty() { |
| return Err(EINVAL); |
| } |
| |
| self.write_emem(packet)?; |
| |
| // Update queue pointers. TAIL points at the last DWORD written. |
| let tail_offset = u32::try_from(packet.len() - 4).map_err(|_| EINVAL)?; |
| self.bar.write( |
| Array::at(0), |
| regs::NV_PFSP_QUEUE_TAIL::zeroed().with_address(tail_offset), |
| ); |
| self.bar.write( |
| Array::at(0), |
| regs::NV_PFSP_QUEUE_HEAD::zeroed().with_address(0), |
| ); |
| |
| Ok(()) |
| } |
| |
| /// Reads the next message from FSP EMEM into a newly-allocated buffer and resets the queue |
| /// pointers. |
| /// |
| /// Returns `ETIMEDOUT` if no message was available until timeout, or a regular error code if a |
| /// memory allocation error occurred. |
| pub(crate) fn recv_msg(&mut self) -> Result<KVec<u8>> { |
| let msg_size = read_poll_timeout( |
| || self.poll_msgq(), |
| |&size| size > 0, |
| Delta::from_millis(10), |
| Delta::from_millis(FSP_MSG_TIMEOUT_MS), |
| ) |
| .map(num::u32_as_usize)?; |
| |
| // Don't blindly allocate more than the maximum we expect from FSP. |
| if msg_size > FSP_EMEM_CHANNEL_0_SIZE { |
| return Err(EMSGSIZE); |
| } |
| |
| let mut buffer = KVec::<u8>::new(); |
| buffer.resize(msg_size, 0, GFP_KERNEL)?; |
| |
| self.read_emem(&mut buffer)?; |
| |
| // Reset message queue pointers after reading. |
| self.bar |
| .write(Array::at(0), regs::NV_PFSP_MSGQ_TAIL::zeroed().with_val(0)); |
| self.bar |
| .write(Array::at(0), regs::NV_PFSP_MSGQ_HEAD::zeroed().with_val(0)); |
| |
| Ok(buffer) |
| } |
| } |