From 9077cb2f4b482b9175b52ff7a5190c8743636e38 Mon Sep 17 00:00:00 2001 From: mlvisaya <38512415+mlvisaya@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:28:04 -0800 Subject: [PATCH] User space interface for DMA and Mailbox (#75) * User space interface for DMA and Mailbox Added user APIs for DMA and mailbox The DMA API allows for AXI-to-AXI transfers or from local buffer to an AXI component. The Mailbox API allows for executing a mailbox command in an atomic fashion, i.e. execute command, wait for response, return response. --- Cargo.lock | 9 ++ Cargo.toml | 2 + runtime/apps/libtockasync/src/future.rs | 92 ++++++++++++++ runtime/apps/syscall/Cargo.toml | 12 ++ runtime/apps/syscall/src/dma.rs | 154 ++++++++++++++++++++++++ runtime/apps/syscall/src/lib.rs | 6 + runtime/apps/syscall/src/mailbox.rs | 104 ++++++++++++++++ 7 files changed, 379 insertions(+) create mode 100644 runtime/apps/syscall/Cargo.toml create mode 100644 runtime/apps/syscall/src/dma.rs create mode 100644 runtime/apps/syscall/src/lib.rs create mode 100644 runtime/apps/syscall/src/mailbox.rs diff --git a/Cargo.lock b/Cargo.lock index 025e7fc..3439147 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1395,6 +1395,15 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +[[package]] +name = "libsyscall-caliptra" +version = "0.1.0" +dependencies = [ + "libtock_console", + "libtock_platform", + "libtockasync", +] + [[package]] name = "libtock" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 0711bdc..0498351 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ members = [ "runtime/apps/libtock/ufmt", "runtime/apps/libtock/unittest", "runtime/apps/libtockasync", + "runtime/apps/syscall", "runtime/capsules", "runtime/i3c", "tests/hello", @@ -131,6 +132,7 @@ libtock_platform = { path = "runtime/apps/libtock/platform" } libtock_runtime = { path = "runtime/apps/libtock/runtime" } libtock_unittest = { path = "runtime/apps/libtock/unittest" } libtock_caliptra = { path = "runtime/apps/apis" } +libsyscall-caliptra = { path = "runtime/apps/syscall" } # caliptra dependencies; keep git revs in sync caliptra-api-types = { git = "https://github.com/chipsalliance/caliptra-sw.git", rev = "2f6de531e321b7bb24b17b1bd02b43d2854aef3a" } diff --git a/runtime/apps/libtockasync/src/future.rs b/runtime/apps/libtockasync/src/future.rs index a2df0bf..77544f9 100644 --- a/runtime/apps/libtockasync/src/future.rs +++ b/runtime/apps/libtockasync/src/future.rs @@ -155,6 +155,98 @@ impl TockSubscribe { f } + pub fn subscribe_allow_ro_rw( + driver_num: u32, + subscribe_num: u32, + buffer_ro_num: u32, + buffer_ro: &[u8], + buffer_rw_num: u32, + buffer_rw: &mut [u8], + ) -> impl Future> { + // Pinning is necessary since we are passing a pointer to the TockSubscribe to the kernel. + let mut f = Pin::new(Box::new(TockSubscribe::new())); + let upcall_fcn = (kernel_upcall:: as *const ()) as usize; + let upcall_data = (&*f as *const TockSubscribe) as usize; + + // Allow RO + // Safety: we are passing in a fixed (safe) function pointer and a pointer to a pinned instance. + // If the instance is dropped before the upcall comes in, then we panic in the Drop impl. + let [r0, r1, r2, _] = unsafe { + S::syscall4::<{ syscall_class::ALLOW_RO }>([ + driver_num.into(), + buffer_ro_num.into(), + buffer_ro.as_ptr().into(), + buffer_ro.len().into(), + ]) + }; + + let return_variant: ReturnVariant = r0.as_u32().into(); + match return_variant { + return_variant::SUCCESS_2_U32 => {} + return_variant::FAILURE_2_U32 => { + f.set_err(r1.as_u32().try_into().unwrap_or(ErrorCode::Fail)); + } + _ => { + f.set_err(ErrorCode::Fail); + } + } + + let returned_buffer: (usize, usize) = (r1.into(), r2.into()); + if returned_buffer != (0, 0) { + C::returned_nonzero_buffer(driver_num, buffer_ro_num); + } + + // Allow RW + // Safety: we are passing in a fixed (safe) function pointer and a pointer to a pinned instance. + // If the instance is dropped before the upcall comes in, then we panic in the Drop impl. + let [r0, r1, r2, _] = unsafe { + S::syscall4::<{ syscall_class::ALLOW_RW }>([ + driver_num.into(), + buffer_rw_num.into(), + buffer_rw.as_mut_ptr().into(), + buffer_rw.len().into(), + ]) + }; + + let return_variant: ReturnVariant = r0.as_u32().into(); + match return_variant { + return_variant::SUCCESS_2_U32 => {} + return_variant::FAILURE_2_U32 => { + f.set_err(r1.as_u32().try_into().unwrap_or(ErrorCode::Fail)); + } + _ => { + f.set_err(ErrorCode::Fail); + } + } + + let returned_buffer: (usize, usize) = (r1.into(), r2.into()); + if returned_buffer != (0, 0) { + C::returned_nonzero_buffer(driver_num, buffer_rw_num); + } + + // Safety: we are passing in a fixed (safe) function pointer and a pointer to a pinned instance. + // If the instance is dropped before the upcall comes in, then we panic in the Drop impl. + let [r0, r1, _, _] = unsafe { + S::syscall4::<{ syscall_class::SUBSCRIBE }>([ + driver_num.into(), + subscribe_num.into(), + upcall_fcn.into(), + upcall_data.into(), + ]) + }; + let return_variant: ReturnVariant = r0.as_u32().into(); + match return_variant { + return_variant::SUCCESS_2_U32 => {} + return_variant::FAILURE_2_U32 => { + f.set_err(r1.as_u32().try_into().unwrap_or(ErrorCode::Fail)); + } + _ => { + f.set_err(ErrorCode::Fail); + } + } + f + } + pub fn subscribe( driver_num: u32, subscribe_num: u32, diff --git a/runtime/apps/syscall/Cargo.toml b/runtime/apps/syscall/Cargo.toml new file mode 100644 index 0000000..b570dd7 --- /dev/null +++ b/runtime/apps/syscall/Cargo.toml @@ -0,0 +1,12 @@ +# Licensed under the Apache-2.0 license + +[package] +name = "libsyscall-caliptra" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[dependencies] +libtock_console.workspace = true +libtock_platform.workspace = true +libtockasync.workspace = true diff --git a/runtime/apps/syscall/src/dma.rs b/runtime/apps/syscall/src/dma.rs new file mode 100644 index 0000000..2f549c8 --- /dev/null +++ b/runtime/apps/syscall/src/dma.rs @@ -0,0 +1,154 @@ +// Licensed under the Apache-2.0 license + +//! # DMA: A DMA Interface for AXI Source to AXI Destination Transfers +//! +//! This library provides an abstraction for performing asynchronous Direct Memory Access (DMA) +//! transfers between AXI source and AXI destination addresses. + +use core::marker::PhantomData; +use libtock_platform::{share, DefaultConfig, ErrorCode, Syscalls}; +use libtockasync::TockSubscribe; +/// DMA interface. +pub struct DMA { + syscall: PhantomData, + driver_num: u32, +} + +/// Define type for AXI address (64-bit wide). +pub type AXIAddr = u64; + +/// Configuration parameters for a DMA transfer. +#[derive(Debug, Clone)] +pub struct DMATransaction<'a> { + /// Number of bytes to transfer. + pub byte_count: usize, + /// Source for the transfer. + pub source: DMASource<'a>, + /// Destination AXI address for the transfer. + pub dest_addr: AXIAddr, +} + +/// Represents the source of data for a DMA transfer. +#[derive(Debug, Clone)] +pub enum DMASource<'a> { + /// AXI memory address as the source. + Address(AXIAddr), + /// A local buffer as the source. + Buffer(&'a [u8]), +} + +impl Default for DMA { + fn default() -> Self { + Self::new() + } +} + +impl DMA { + pub fn new() -> Self { + Self { + syscall: PhantomData, + driver_num: DMA_DRIVER_NUM, + } + } + + /// Do a DMA transfer. + /// + /// This method executes a DMA transfer based on the provided `DMATransaction` configuration. + /// + /// # Arguments + /// * `transaction` - A `DMATransaction` struct containing the transfer details. + /// + /// # Returns + /// * `Ok(())` if the transfer starts successfully. + /// * `Err(ErrorCode)` if the transfer fails. + pub async fn xfer<'a>(&self, transaction: &DMATransaction<'a>) -> Result<(), ErrorCode> { + self.setup(transaction)?; + + match transaction.source { + DMASource::Buffer(buffer) => self.xfer_src_buffer(buffer).await.map(|_| ()), + DMASource::Address(_) => self.xfer_src_address().await.map(|_| ()), + } + } + + async fn xfer_src_address(&self) -> Result<(), ErrorCode> { + let async_start = TockSubscribe::subscribe::(self.driver_num, dma_subscribe::XFER_DONE); + S::command(self.driver_num, dma_cmd::XFER_AXI_TO_AXI, 0, 0).to_result::<(), ErrorCode>()?; + async_start.await.map(|_| ()) + } + + async fn xfer_src_buffer(&self, buffer: &[u8]) -> Result<(), ErrorCode> { + // Use `share::scope` to safely share the buffer with the kernel + share::scope::<(), _, _>(|_| { + let async_start = TockSubscribe::subscribe_allow_ro::( + self.driver_num, + dma_subscribe::XFER_DONE, + dma_ro_buffer::LOCAL_SOURCE, + buffer, + ); + + // Start the DMA transfer + S::command(self.driver_num, dma_cmd::XFER_LOCAL_TO_AXI, 0, 0) + .to_result::<(), ErrorCode>()?; + Ok(async_start) + })? + .await + .map(|_| ()) + } + + fn setup(&self, config: &DMATransaction<'_>) -> Result<(), ErrorCode> { + S::command( + self.driver_num, + dma_cmd::SET_BYTE_XFER_COUNT, + config.byte_count as u32, + 0, + ) + .to_result::<(), ErrorCode>()?; + + if let DMASource::Address(src_addr) = config.source { + S::command( + self.driver_num, + dma_cmd::SET_SRC_ADDR, + (src_addr & 0xFFFF_FFFF) as u32, + (src_addr >> 32) as u32, + ) + .to_result::<(), ErrorCode>()?; + } + + S::command( + self.driver_num, + dma_cmd::SET_DEST_ADDR, + (config.dest_addr & 0xFFFF_FFFF) as u32, + (config.dest_addr >> 32) as u32, + ) + .to_result::<(), ErrorCode>()?; + + Ok(()) + } +} + +// ----------------------------------------------------------------------------- +// Command IDs and DMA-specific constants +// ----------------------------------------------------------------------------- + +// Driver number for the DMA interface +pub const DMA_DRIVER_NUM: u32 = 0x8000_0008; + +/// Command IDs used by the DMA interface. +mod dma_cmd { + pub const SET_BYTE_XFER_COUNT: u32 = 0; + pub const SET_SRC_ADDR: u32 = 1; + pub const SET_DEST_ADDR: u32 = 2; + pub const XFER_AXI_TO_AXI: u32 = 3; + pub const XFER_LOCAL_TO_AXI: u32 = 4; +} + +/// Buffer IDs for DMA (read-only) +mod dma_ro_buffer { + /// Buffer ID for local buffers (read-only) + pub const LOCAL_SOURCE: u32 = 0; +} + +/// Subscription IDs for asynchronous notifications. +mod dma_subscribe { + pub const XFER_DONE: u32 = 0; +} diff --git a/runtime/apps/syscall/src/lib.rs b/runtime/apps/syscall/src/lib.rs new file mode 100644 index 0000000..1a82009 --- /dev/null +++ b/runtime/apps/syscall/src/lib.rs @@ -0,0 +1,6 @@ +// Licensed under the Apache-2.0 license + +#![no_std] + +pub mod dma; +pub mod mailbox; diff --git a/runtime/apps/syscall/src/mailbox.rs b/runtime/apps/syscall/src/mailbox.rs new file mode 100644 index 0000000..9b4c134 --- /dev/null +++ b/runtime/apps/syscall/src/mailbox.rs @@ -0,0 +1,104 @@ +// Licensed under the Apache-2.0 license + +//! # Mailbox Interface + +use core::marker::PhantomData; +use libtock_platform::{share, DefaultConfig, ErrorCode, Syscalls}; +use libtockasync::TockSubscribe; + +/// Mailbox interface user interface. +/// +/// # Generics +/// - `S`: The syscall implementation. +pub struct Mailbox { + syscall: PhantomData, + driver_num: u32, +} + +impl Default for Mailbox { + fn default() -> Self { + Self::new() + } +} + +impl Mailbox { + pub fn new() -> Self { + Self { + syscall: PhantomData, + driver_num: MAILBOX_DRIVER_NUM, + } + } + + /// Executes a mailbox command and returns the response. + /// + /// This method sends a mailbox command to the kernel, then waits + /// asynchronously for the command to complete. The response buffer is filled with + /// the result from the kernel. + /// + /// # Arguments + /// - `command`: The mailbox command ID to execute. + /// - `input_data`: A read-only buffer containing the mailbox command parameters. + /// - `response_buffer`: A writable buffer to store the response data. + /// + /// # Returns + /// - `Ok(usize)` on success, containing the number of bytes written to the response buffer. + /// - `Err(ErrorCode)` if the command fails. + pub async fn execute( + &self, + command: u32, + input_data: &[u8], + response_buffer: &mut [u8], + ) -> Result { + share::scope::<(), _, _>(|_| { + // Subscribe to the asynchronous notification for when the command is processed + let async_command = TockSubscribe::subscribe_allow_ro_rw::( + self.driver_num, + mailbox_subscribe::COMMAND_DONE, + mailbox_ro_buffer::INPUT, + input_data, + mailbox_rw_buffer::RESPONSE, + response_buffer, + ); + + // Issue the command to the kernel + S::command(self.driver_num, mailbox_cmd::EXECUTE_COMMAND, command, 0) + .to_result::<(), ErrorCode>()?; + + // Return the subscription for further processing + Ok(async_command) + })? + .await + .map(|res| res.0 as usize) + } +} + +// ----------------------------------------------------------------------------- +// Command IDs and Mailbox-specific constants +// ----------------------------------------------------------------------------- + +// Driver number for the Mailbox interface +pub const MAILBOX_DRIVER_NUM: u32 = 0x8000_0009; + +/// Command IDs for mailbox operations. +mod mailbox_cmd { + /// Execute a command with input and response buffers. + pub const EXECUTE_COMMAND: u32 = 1; +} + +/// Buffer IDs for mailbox read operations. +mod mailbox_ro_buffer { + /// Buffer ID for the input buffer (read-only). + pub const INPUT: u32 = 0; +} + +/// Buffer IDs for mailbox read-write operations. +mod mailbox_rw_buffer { + /// Buffer ID for the response buffer (read-write). + pub const RESPONSE: u32 = 0; +} + +/// Subscription IDs for asynchronous mailbox events. +mod mailbox_subscribe { + /// Subscription ID for the `COMMAND_DONE` event. + pub const COMMAND_DONE: u32 = 1; +}