storage_capsule: initial implementation

Add CMD_GET_NAME for constructing the list of builtins,
CMD_STORAGE_FIND_FILE for finding a file by name, and CMD_STORAGE_READ
for reading a page of data from a previously found file.

CMD_GET_NAME takes a storage offset at which to look for a file
and returns the offset of the next file. If a buffer is allow'd
CMD_GET_NAME copies the file name into the buffer and returns the
length (in bytes).

CMD_STORAGE_FIND_FILE takes a file name and returns a unique "file
identifier" and the file size in bytes. The file identifier is suitable
for issuing CMD_STORAGE_READ requests.

CMD_STORAGE_READ takes a file identifier, an offset into the file, and the
phsical address of an SMC page to write 4KiB (or the remainder at EOF).

More:
- tar searches depend on the per-file magic field being set (true in
  any POSIX-compliant tarball); otherwise falling back to searching
  the entire SPI flash (set at 16MiB)
- the file offset specified for read's must be SPI flash page-aligned
- SMC memory bounds are baked into smc_is_page
- only one app at a time is supported

Bug: 294433731

Change-Id: I0b731707890f0506800938f828a0782b0dbc7264
diff --git a/capsules/src/lib.rs b/capsules/src/lib.rs
index 4051ccb..2e1ce90 100644
--- a/capsules/src/lib.rs
+++ b/capsules/src/lib.rs
@@ -4,5 +4,5 @@
 pub mod dprintf_capsule;
 pub mod elfloader_capsule;
 pub mod mailbox_capsule;
-pub mod storage_capsule;
 pub mod nexus_spiflash;
+pub mod storage_capsule;
diff --git a/capsules/src/storage_capsule.rs b/capsules/src/storage_capsule.rs
index 0dbfed1..db549c7 100644
--- a/capsules/src/storage_capsule.rs
+++ b/capsules/src/storage_capsule.rs
@@ -1,57 +1,163 @@
-//! Stub StorageManager capsule that doesn't do anything yet.
+//! StorageManager capsule
 
+use core::cell::Cell;
+use core::marker::PhantomData;
+use core::mem::size_of;
+use kernel::common::cells::TakeCell;
+use kernel::hil;
+use kernel::hil::flash::Flash;
 use kernel::{AppId, AppSlice, Callback, Driver, Grant, ReturnCode, Shared};
-// TODO(sleffler): remove dprintf noise once this code does something
+use matcha_config::*;
 use matcha_hal::dprintf;
+use matcha_utils::SMC_PAGE_SIZE;
+use matcha_utils::smc_is_page;
+use matcha_utils::smc_ram_memcpy;
+use matcha_utils::tar_loader::TarHeader;
+
+const FLASH_END: usize = 16 * 1024 * 1024; // 16MiB SPI flash
+
+// Returns true if the specified SMC page address fits in SPI flash
+fn is_spi_page(addr: usize) -> bool {
+    addr + SMC_PAGE_SIZE < FLASH_END
+}
+
+#[inline]
+fn howmany(a: u32, b: u32) -> u32 { (a + b - 1) / b }
+#[inline]
+fn roundup(a: u32, b: u32) -> u32 { howmany(a, b) * b }
 
 #[derive(Default)]
 pub struct AppData {
-    pub callback: Option<Callback>,
+    // XXX only need 1 callback since we're single-threaded
+    pub get_name_callback: Option<Callback>,
+    pub find_file_callback: Option<Callback>,
+    pub read_page_callback: Option<Callback>,
     pub buffer: Option<AppSlice<Shared, u8>>,
-    pub minor_num: usize,
-    pub arg2: usize,
-    pub arg3: usize,
 }
 
-pub struct StorageCapsule {
+#[derive(Clone, Copy, Debug, PartialEq)]
+enum StorageState {
+    Idle,
+    FindFile(u32 /* cursor */), // NB: filename passed with allow
+    ReadPage(u32 /* fid */, u32 /* offset */, u32 /* dest */),
+    GetName(u32 /* cursor */),  // NB: filename returned with allow
+}
+impl StorageState {
+    fn is_idle(&self) -> bool {
+        match self {
+            StorageState::Idle => true,
+            _ => false,
+        }
+    }
+}
+
+pub struct StorageCapsule<'a, F: hil::flash::Flash + 'static> {
     app_data_grant: Grant<AppData>,
+    pub current_app: Cell<Option<AppId>>,
+    flash: Option<&'static capsules::virtual_flash::FlashUser<'static, F>>,
+    flash_busy: Cell<bool>,
+    read_page: TakeCell<'static, F::Page>,
+    page_len: u32,
+    state: Cell<StorageState>,
+    phantom: PhantomData<&'a ()>,
 }
 
-impl StorageCapsule {
+impl<'a, F: hil::flash::Flash> StorageCapsule<'a, F> {
     pub fn new(app_data_grant: Grant<AppData>) -> Self {
-        return StorageCapsule {
+        Self {
             app_data_grant: app_data_grant,
-        };
+            current_app: Cell::new(None),
+            flash: None,
+            flash_busy: Cell::new(false),
+            read_page: TakeCell::empty(),
+            page_len: 0,
+            state: Cell::new(StorageState::Idle),
+            phantom: PhantomData,
+        }
+    }
+
+    pub fn set_flash(
+        &mut self,
+        flash: &'static capsules::virtual_flash::FlashUser<'static, F>,
+        read_page: &'static mut F::Page,
+    ) {
+        self.flash = Some(flash);
+        self.read_page.replace(read_page);
+        let mut page_len = 0;
+        self.read_page.map(|page| {
+            let mut_page = page.as_mut();
+            page_len = mut_page.len() as u32;
+        });
+        self.page_len = page_len;
     }
 
     pub fn handle_command(
         &self,
+        app_id: AppId,
         app_data: &mut AppData,
         minor_num: usize,
         arg2: usize,
         arg3: usize,
     ) -> ReturnCode {
-        dprintf!(
-            "StorageCapsule::handle_command({}, {}, {})",
-            minor_num,
-            arg2,
-            arg3
-        );
-        app_data.minor_num = minor_num;
-        app_data.arg2 = arg2;
-        app_data.arg3 = arg3;
         match minor_num {
-            0 => ReturnCode::SUCCESS,
-            1 => {
-                if let Some(mut callback) = app_data.callback {
-                    dprintf!("StorageCapsule::handle_command : Calling callback!");
-                    callback.schedule(1, 2, 3);
-                    app_data.callback = Some(callback);
-                    ReturnCode::SUCCESS
-                } else {
-                    dprintf!("StorageCapsule::handle_command : No callback!");
-                    ReturnCode::EINVAL
+            CMD_STORAGE_INIT => {
+                self.current_app.set(Some(app_id));
+                ReturnCode::SUCCESS
+            }
+            CMD_STORAGE_GET_NAME => {
+                let state = self.state.get();
+                if !state.is_idle() {
+                    dprintf!("storage: GET_NAME: state {:?}\r\n", state);
+                    return ReturnCode::EBUSY;
                 }
+                // NB: buffer is optional
+                let fid = arg2 as u32;
+                self.state.set(StorageState::GetName(fid));
+                self.read_page(fid); // First read, kicks off state machine
+                return ReturnCode::SUCCESS
+            }
+            CMD_STORAGE_FIND_FILE => {
+                let state = self.state.get();
+                if !state.is_idle() {
+                    dprintf!("storage: FIND_FILE: state {:?}\r\n", state);
+                    return ReturnCode::EBUSY;
+                }
+                // Verify the filename has been passed to allow and check
+                // the string is valid so work inside the state machine
+                // can just unwrap the result of str::from_utf8.
+                if let Some(app_slice) = app_data.buffer.as_ref() {
+                    if core::str::from_utf8(app_slice.as_ref()).is_ok() {
+                        self.state.set(StorageState::FindFile(0));
+                        self.read_page(0); // First read, kicks off state machine
+                        return ReturnCode::SUCCESS
+                    }
+                }
+                ReturnCode::EINVAL
+            }
+            CMD_STORAGE_READ_PAGE => {
+                let state = self.state.get();
+                if !state.is_idle() {
+                    dprintf!("storage: READ: state {:?}\r\n", state);
+                    return ReturnCode::EBUSY;
+                }
+                // NB: this checks the entire 4K page can be read from SPI;
+                // we don't really handle reading less than a full page.
+                let fid = arg2 as u32;
+                if !is_spi_page(fid as usize) {
+                    dprintf!("storage: READ: extends past the end-of-flash (fid  {})\r\n", fid);
+                    return ReturnCode::EINVAL;
+                }
+                // The best we can do is check |dest| looks like an SMC
+                // address; otherwise we just spray data where ever the
+                // SecurityCoordinator tells us.
+                let dest = arg3 as u32;
+                if !smc_is_page(dest as usize) {
+                    dprintf!("storage: READ: destination not in SMC (dest {:#x})\r\n", dest);
+                    return ReturnCode::EINVAL;
+                }
+                self.state.set(StorageState::ReadPage(fid, 0, dest));
+                self.read_page(fid); // First read, kicks off state machine
+                ReturnCode::SUCCESS
             }
             _ => ReturnCode::EINVAL,
         }
@@ -59,43 +165,217 @@
 
     pub fn handle_subscribe(
         &self,
+        _app_id: AppId,
         app_data: &mut AppData,
         minor_num: usize,
         callback: Option<Callback>,
     ) -> ReturnCode {
-        dprintf!("StorageCapsule::handle_subscribe({})", minor_num);
-        if callback.is_some() {
-            dprintf!("StorageCapsule::handle_subscribe got Some callback");
-        } else {
-            dprintf!("StorageCapsule::handle_subscribe got None callback");
+        match minor_num {
+            CMD_STORAGE_GET_NAME => {
+                app_data.get_name_callback = callback;
+                ReturnCode::SUCCESS
+            }
+            CMD_STORAGE_FIND_FILE => {
+                app_data.find_file_callback = callback;
+                ReturnCode::SUCCESS
+            }
+            CMD_STORAGE_READ_PAGE => {
+                app_data.read_page_callback = callback;
+                ReturnCode::SUCCESS
+            }
+            _ => ReturnCode::EINVAL,
         }
-        app_data.callback = callback;
-        return ReturnCode::SUCCESS;
     }
 
     pub fn handle_allow(
         &self,
+        _app_id: AppId,
         app_data: &mut AppData,
-        _minor_num: usize,
+        minor_num: usize,
         slice: Option<AppSlice<Shared, u8>>,
     ) -> ReturnCode {
-        if let Some(slice) = slice {
-            dprintf!("StorageCapsule::handle_allow({})", slice.len());
-            app_data.buffer = Some(slice);
-        } else {
-            dprintf!("StorageCapsule::handle_allow(None)");
+        match minor_num {
+            CMD_STORAGE_GET_NAME
+            | CMD_STORAGE_FIND_FILE => {
+                app_data.buffer = slice;
+                ReturnCode::SUCCESS
+            }
+            _ => ReturnCode::EINVAL,
         }
-        return ReturnCode::SUCCESS;
+    }
+
+    // GetName state machine callback on flash read complete.
+    fn get_name_callback(&self) {
+        self.current_app.get().map(|app_id| {
+            let _ = self.app_data_grant.enter(app_id, |app_data, _| {
+                self.read_page.map(|page| {
+                    let mut_page = page.as_mut(); // XXX mut?
+                    match self.state.get() {
+                        StorageState::GetName(cursor) => {
+                            let tar_header = TarHeader::from_bytes(mut_page);
+                            if tar_header.has_magic() {
+                                // Copy back filename if a buffer is setup.
+                                let name_len = if let Some(app_slice) = app_data.buffer.as_mut() {
+                                    let slice = app_slice.as_mut();
+                                    let tar_bytes = tar_header.name().as_bytes();
+                                    if slice.len() > tar_bytes.len() {
+                                        &mut slice[..tar_bytes.len()].copy_from_slice(tar_bytes);
+                                        tar_bytes.len()
+                                    } else {
+                                        slice.copy_from_slice(&tar_bytes[..slice.len()]);
+                                        slice.len()
+                                    }
+                                } else { 0 };
+                                let tar_size = tar_header.size();
+                                let next_cursor = cursor
+                                    + self.page_len
+                                    + roundup(tar_size, size_of::<TarHeader>() as u32);
+                                self.state.set(StorageState::Idle);
+                                app_data.get_name_callback.map(|mut callback| {
+                                    callback.schedule(1, name_len, next_cursor as usize);
+                                });
+                            } else {
+                                // Tar header looks invalid, this is typically how a failed lookup ends.
+                                self.state.set(StorageState::Idle);
+                                app_data.get_name_callback.map(|mut callback| {
+                                    callback.schedule(0, 0, 0);
+                                });
+                            }
+                        }
+                        _ => panic!("get_name: bad state"),
+                    }
+                });
+            });
+        });
+    }
+
+    // FindFile state machine callback on flash read complete.
+    fn find_file_callback(&self) {
+        self.current_app.get().map(|app_id| {
+            let _ = self.app_data_grant.enter(app_id, |app_data, _| {
+                self.read_page.map(|page| {
+                    let mut_page = page.as_mut(); // XXX mut?
+                    match self.state.get() {
+                        StorageState::FindFile(cursor) => {
+                            let tar_header = TarHeader::from_bytes(mut_page);
+                            if tar_header.has_magic() {
+                                // NB: app_slice is checked to be valid in command; we assume
+                                //   it does not change while we're running
+                                let app_slice = app_data.buffer.as_ref().unwrap();
+                                let name = core::str::from_utf8(app_slice.as_ref()).unwrap();
+                                let tar_name = tar_header.name();
+                                let tar_size = tar_header.size();
+                                if tar_name == name {
+                                    // Found file, return fid = the offset to the first byte of data
+                                    self.state.set(StorageState::Idle);
+                                    app_data.find_file_callback.map(|mut callback| {
+                                        callback.schedule(
+                                            1,                                          // success
+                                            (cursor as usize) + size_of::<TarHeader>(), // fid
+                                            tar_size as usize,                          // file size (bytes)
+                                        );
+                                    });
+                                } else {
+                                    // Advance to the next file if possible. We depend on
+                                    // each header having a magic marker; otherwise we use
+                                    // a fixed bound on the flash size to (try to) avoid
+                                    // reading past the end of memory.
+                                    let next_cursor = cursor
+                                        + self.page_len
+                                        + roundup(tar_size, size_of::<TarHeader>() as u32);
+                                    if ((next_cursor as usize) + size_of::<TarHeader>()) >= FLASH_END {
+                                        dprintf!("storage: {} not found, end of flash\r\n", name);
+                                        self.state.set(StorageState::Idle);
+                                        app_data.find_file_callback.map(|mut callback| {
+                                            callback.schedule(0, 0, 0);
+                                        });
+                                    } else {
+                                        self.state.set(StorageState::FindFile(next_cursor));
+                                        // NB: defer read to below so we release self.read_page
+                                    }
+                                }
+                            } else {
+                                // Tar header looks invalid, this is typically how a failed lookup ends.
+                                self.state.set(StorageState::Idle);
+                                app_data.find_file_callback.map(|mut callback| {
+                                    callback.schedule(0, 0, 0);
+                                });
+                            }
+                        }
+                        _ => panic!("find_file: bad state"),
+                    }
+                });
+                match self.state.get() {
+                    StorageState::FindFile(cursor) => self.read_page(cursor),
+                    _ => {}
+                }
+            });
+        });
+    }
+
+    // ReadPage state machine callback on flash read complete.
+    fn read_page_callback(&self) {
+        self.current_app.get().map(|app_id| {
+            let _ = self.app_data_grant.enter(app_id, |app_data, _| {
+                self.read_page.map(|page| {
+                    let mut_page = page.as_mut();
+                    match self.state.get() {
+                        StorageState::ReadPage(fid, offset, dest) => {
+//                            dprintf!("copy page {} to {:x}\r\n", fid + offset, dest);
+                            smc_ram_memcpy(
+                                mut_page,
+                                dest,
+                                self.page_len as usize, // XXX short read?
+                            );
+                            let next_offset = offset + self.page_len;
+                            if next_offset >= (SMC_PAGE_SIZE as u32) {
+                                self.state.set(StorageState::Idle);
+                                app_data.read_page_callback.map(|mut callback| {
+                                    callback.schedule(1, fid as usize, SMC_PAGE_SIZE);
+                                });
+                            } else {
+                                self.state.set(
+                                    StorageState::ReadPage(
+                                        fid,
+                                        next_offset,
+                                        dest + self.page_len,
+                                    )
+                                );
+                                // NB: defer read to below so we release self.read_page
+                            }
+                        }
+                        _ => panic!("read_page: bad state"),
+                    }
+                });
+                match self.state.get() {
+                    StorageState::ReadPage(fid, offset, _) => self.read_page(fid + offset),
+                    _ => {}
+                }
+            });
+        });
+    }
+
+    // Kicks off a SPI flash read at the byte offset |page|.
+    fn read_page(&self, page: u32) {
+        debug_assert!((page % self.page_len) == 0);
+        self.flash.map(|flash| {
+            self.read_page.take().map(|read_page| {
+                self.flash_busy.set(true);
+                if let Err((_, buf)) = flash.read_page((page / self.page_len) as usize, read_page) {
+                    self.read_page.replace(buf);
+                }
+            });
+        });
     }
 }
 
 /// Driver impl just enters the app_data grant and delegates to StorageCapsule.
 
-impl Driver for StorageCapsule {
+impl<'a, F: hil::flash::Flash> Driver for StorageCapsule<'a, F> {
     fn subscribe(&self, minor_num: usize, callback: Option<Callback>, app_id: AppId) -> ReturnCode {
         self.app_data_grant
             .enter(app_id, |app_data, _| {
-                self.handle_subscribe(app_data, minor_num, callback)
+                self.handle_subscribe(app_id, app_data, minor_num, callback)
             })
             .unwrap_or_else(|err| err.into())
     }
@@ -103,7 +383,7 @@
     fn command(&self, minor_num: usize, r2: usize, r3: usize, app_id: AppId) -> ReturnCode {
         self.app_data_grant
             .enter(app_id, |app_data, _| {
-                self.handle_command(app_data, minor_num, r2, r3)
+                self.handle_command(app_id, app_data, minor_num, r2, r3)
             })
             .unwrap_or_else(|err| err.into())
     }
@@ -116,8 +396,33 @@
     ) -> ReturnCode {
         self.app_data_grant
             .enter(app_id, |app_data, _| {
-                self.handle_allow(app_data, minor_num, slice)
+                self.handle_allow(app_id, app_data, minor_num, slice)
             })
             .unwrap_or_else(|err| err.into())
     }
 }
+
+impl<'a, F: hil::flash::Flash> hil::flash::Client<capsules::virtual_flash::FlashUser<'a, F>>
+    for StorageCapsule<'a, F>
+{
+    fn read_complete(&self, read_page: &'static mut F::Page, _err: kernel::hil::flash::Error) {
+        self.read_page.replace(read_page);
+        self.flash_busy.set(false);
+        match self.state.get() {
+            StorageState::GetName(..) => self.get_name_callback(),
+            StorageState::FindFile(..) => self.find_file_callback(),
+            StorageState::ReadPage(..) => self.read_page_callback(),
+            _ => panic!("bad state"),
+        }
+    }
+    fn write_complete(
+        &self,
+        _: &'static mut <F as kernel::hil::flash::Flash>::Page,
+        _: kernel::hil::flash::Error,
+    ) {
+        todo!()
+    }
+    fn erase_complete(&self, _: kernel::hil::flash::Error) {
+        todo!()
+    }
+}
diff --git a/config/src/lib.rs b/config/src/lib.rs
index aa777b3..14d4d19 100644
--- a/config/src/lib.rs
+++ b/config/src/lib.rs
@@ -46,5 +46,10 @@
 pub const SPI_HOST0_BASE_ADDRESS: u32 = 0x4030_0000; // TOP_MATCHA_SPI_HOST0_BASE_ADDR
 pub const SPI_HOST0_SPI_EVENT_IRQ: u32 = 132; // kTopMatchaPlicIrqIdSpiHost0SpiEvent
 
+pub const CMD_STORAGE_INIT: usize = 1;
+pub const CMD_STORAGE_GET_NAME: usize = 2;
+pub const CMD_STORAGE_FIND_FILE: usize = 3;
+pub const CMD_STORAGE_READ_PAGE: usize = 4;
+
 pub const UART0_BASE_ADDRESS: u32 = 0x40000000; // TOP_MATCHA_UART0_BASE_ADDR
 pub const UART0_BAUDRATE: u32 = 115200;
diff --git a/platform/src/main.rs b/platform/src/main.rs
index 0d96a7e..2ce5d09 100644
--- a/platform/src/main.rs
+++ b/platform/src/main.rs
@@ -119,7 +119,13 @@
         VirtualMuxAlarm<'static, timer_hal::RvTimer<'static>>,
     >,
     dprintf_capsule: &'static DprintfCapsule,
-    storage_capsule: &'static StorageCapsule,
+    storage_capsule: &'static StorageCapsule<
+        'static,
+        matcha_capsules::nexus_spiflash::NexusSpiflash<
+            'static,
+            capsules::virtual_spi::VirtualSpiMasterDevice<'static, spi_host_hal::SpiHw>,
+        >,
+    >,
     elfloader_capsule: &'static ElfLoaderCapsule<
         'static,
         matcha_capsules::nexus_spiflash::NexusSpiflash<
@@ -246,9 +252,14 @@
     );
 
     let storage_capsule = static_init!(
-        matcha_capsules::storage_capsule::StorageCapsule,
+        matcha_capsules::storage_capsule::StorageCapsule<
+            'static,
+        matcha_capsules::nexus_spiflash::NexusSpiflash<
+            'static,
+            capsules::virtual_spi::VirtualSpiMasterDevice<'static, spi_host_hal::SpiHw>,
+        >>,
         matcha_capsules::storage_capsule::StorageCapsule::new(
-            board_kernel.create_grant(&memory_allocation_cap)
+             board_kernel.create_grant(&memory_allocation_cap)
         )
     );
 
@@ -325,22 +336,38 @@
         capsules::virtual_flash::MuxFlash::new(nexus_spiflash)
     );
     hil::flash::HasClient::set_client(nexus_spiflash, mux_flash);
-    let virtual_flash = static_init!(
+
+    // Storage access reads (only) from flash.
+    let storage_virtual_flash = static_init!(
         capsules::virtual_flash::FlashUser<'static, matcha_capsules::nexus_spiflash::NexusSpiflash<
             'static,
             capsules::virtual_spi::VirtualSpiMasterDevice<'static, spi_host_hal::SpiHw>,
         >>,
         capsules::virtual_flash::FlashUser::new(mux_flash)
     );
-    let flash_read_page: &'static mut matcha_capsules::nexus_spiflash::NexusSpiflashPage = static_init!(
+    let storage_read_page: &'static mut matcha_capsules::nexus_spiflash::NexusSpiflashPage = static_init!(
         matcha_capsules::nexus_spiflash::NexusSpiflashPage,
         matcha_capsules::nexus_spiflash::NexusSpiflashPage::default()
     );
+    storage_capsule.set_flash(storage_virtual_flash, storage_read_page);
+    storage_virtual_flash.set_client(storage_capsule);
 
-    elfloader_capsule.set_flash(virtual_flash, flash_read_page);
+    // Elfloader reads artifacts from flash & boots the SMC.
     elfloader_capsule.set_smc_ctrl(&smc_ctrl_hal::SMC_CTRL);
-    virtual_flash.set_client(elfloader_capsule);
-    // elfloader_capsule.load_sel4();
+    let elfloader_virtual_flash = static_init!(
+        capsules::virtual_flash::FlashUser<'static, matcha_capsules::nexus_spiflash::NexusSpiflash<
+            'static,
+            capsules::virtual_spi::VirtualSpiMasterDevice<'static, spi_host_hal::SpiHw>,
+        >>,
+        capsules::virtual_flash::FlashUser::new(mux_flash)
+    );
+    let elfloader_read_page: &'static mut matcha_capsules::nexus_spiflash::NexusSpiflashPage = static_init!(
+        matcha_capsules::nexus_spiflash::NexusSpiflashPage,
+        matcha_capsules::nexus_spiflash::NexusSpiflashPage::default()
+    );
+    elfloader_capsule.set_flash(elfloader_virtual_flash, elfloader_read_page);
+    elfloader_virtual_flash.set_client(elfloader_capsule);
+    // NB: main app triggers the Elfloader bootstrap of seL4
 
     let platform = MatchaPlatform {
         console_capsule: console_capsule,
diff --git a/utils/src/lib.rs b/utils/src/lib.rs
index d21d6e9..25f0f1f 100644
--- a/utils/src/lib.rs
+++ b/utils/src/lib.rs
@@ -13,6 +13,15 @@
 
 pub const SMC_CONTROL_BLOCK: *mut u32 = 0x54020000 as *mut u32;
 
+pub const SMC_PAGE_SIZE: usize = 4096;         // SMC page size
+pub const SMC_BEGIN: usize = 0x50000000;       // Start of SMC memory in SEC map
+pub const SMC_END: usize = SMC_BEGIN + (4*1024*1024); // NB: 4MiB of SMC memory
+
+// Returns true if the specified page address fits in SMC memory
+pub fn smc_is_page(addr: usize) -> bool {
+    SMC_BEGIN <= addr && (addr + SMC_PAGE_SIZE) < SMC_END
+}
+
 pub fn round_up_to_page(addr: u32) -> u32 {
     return (addr + 4095) & !4095;
 }