Introducing the sgpiom peripheral in ast10x0
diff --git a/hal/blocking/src/gpio_port.rs b/hal/blocking/src/gpio_port.rs index 606fa56..2ed422e 100644 --- a/hal/blocking/src/gpio_port.rs +++ b/hal/blocking/src/gpio_port.rs
@@ -28,6 +28,8 @@ /// The pin is not configured for the requested operation /// (e.g., reading output value from input pin) InvalidMode, + /// The pin is reserved and cannot be configured or driven + ReservedPin, } /// Trait for GPIO errors @@ -46,6 +48,64 @@ } } +/// Active polarity for a GPIO pin +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum ActivePolarity { + /// Pin is active when driven high + ActiveHigh, + /// Pin is active when driven low + ActiveLow, +} + +/// Direction for a GPIO pin +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum PinDirection { + /// Pin is configured as an input + Input, + /// Pin is configured as an output + Output, +} + +/// Standard pin configuration combining direction, polarity, and optional initial output level +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct PinConfig { + /// Pin direction (input or output) + pub direction: PinDirection, + /// Active polarity declaration + pub polarity: ActivePolarity, + /// Initial driven value when direction is Output; ignored for inputs + pub initial_output: Option<bool>, +} + +impl PinConfig { + /// Convenience constructor for a standard active-high output with a defined initial level + pub const fn output_active_high(initial: bool) -> Self { + Self { + direction: PinDirection::Output, + polarity: ActivePolarity::ActiveHigh, + initial_output: Some(initial), + } + } + + /// Convenience constructor for a standard active-low output with a defined initial level + pub const fn output_active_low(initial: bool) -> Self { + Self { + direction: PinDirection::Output, + polarity: ActivePolarity::ActiveLow, + initial_output: Some(initial), + } + } + + /// Convenience constructor for an input pin + pub const fn input(polarity: ActivePolarity) -> Self { + Self { + direction: PinDirection::Input, + polarity, + initial_output: None, + } + } +} + /// Edge sensitivity for interrupt configuration #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum EdgeSensitivity { @@ -172,3 +232,15 @@ /// Automatically implement GpioController for any type implementing both required traits impl<T: GpioPort + GpioInterrupt> GpioController for T {} + +/// Trait for SGPIO passthrough: mirrors sampled inputs to output latch under a mask (FR-05) +pub trait GpioBankPassthrough: GpioPort { + /// Set the mask of pins subject to passthrough. + /// When active, sampled input state for masked pins is forwarded to the output latch. + fn set_passthrough_mask(&mut self, mask: Self::Mask) -> Result<(), Self::Error>; + + /// Disable passthrough for all pins. + fn clear_passthrough(&mut self) -> Result<(), Self::Error>; +} + +
diff --git a/target/ast10x0/peripherals/BUILD.bazel b/target/ast10x0/peripherals/BUILD.bazel index 450531b..554c537 100644 --- a/target/ast10x0/peripherals/BUILD.bazel +++ b/target/ast10x0/peripherals/BUILD.bazel
@@ -33,6 +33,11 @@ "scu/routing.rs", "scu/status.rs", "scu/types.rs", + "sgpiom/controller.rs", + "sgpiom/hal_impl.rs", + "sgpiom/mod.rs", + "sgpiom/register_block.rs", + "sgpiom/types.rs", "smc/controller.rs", "smc/device/block_device.rs", "smc/device/flash.rs",
diff --git a/target/ast10x0/peripherals/lib.rs b/target/ast10x0/peripherals/lib.rs index be02f13..e34dea0 100644 --- a/target/ast10x0/peripherals/lib.rs +++ b/target/ast10x0/peripherals/lib.rs
@@ -5,6 +5,7 @@ pub mod i2c; pub mod scu; +pub mod sgpiom; pub mod smc; pub mod spimonitor; pub mod uart;
diff --git a/target/ast10x0/peripherals/sgpiom/controller.rs b/target/ast10x0/peripherals/sgpiom/controller.rs new file mode 100644 index 0000000..5dc39f7 --- /dev/null +++ b/target/ast10x0/peripherals/sgpiom/controller.rs
@@ -0,0 +1,44 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use openprot_hal_blocking::gpio_port::GpioErrorKind; + +use super::hal_impl::SgpiomBankPort; + +/// Concrete SGPIOM controller owning up to `N` child bank ports (US-01, US-05). +/// +/// Each slot is `Option<SgpiomBankPort>` so that absent banks (disabled in +/// Devicetree) are represented safely without crashing callers (US-07). +/// Operations on one bank do not affect any other bank. +pub struct SgpiomController<const N: usize> { + banks: [Option<SgpiomBankPort>; N], +} + +impl<const N: usize> SgpiomController<N> { + /// Construct from an array of optional bank instances. + pub fn new(banks: [Option<SgpiomBankPort>; N]) -> Self { + Self { banks } + } + + /// Return the total number of bank slots (present or absent). + pub fn num_banks(&self) -> usize { + N + } + + /// Obtain a mutable reference to the bank at `index`. + /// + /// Returns `Err(GpioErrorKind::InvalidPort)` when the index is out of range + /// or the bank slot is absent. + pub fn bank(&mut self, index: usize) -> Result<&mut SgpiomBankPort, GpioErrorKind> { + self.banks + .get_mut(index) + .and_then(Option::as_mut) + .ok_or(GpioErrorKind::InvalidPort) + } + + /// Return `None` when the bank is absent; never errors. + /// Prefer this for optional-bank patterns (US-07). + pub fn bank_opt(&mut self, index: usize) -> Option<&mut SgpiomBankPort> { + self.banks.get_mut(index)?.as_mut() + } +}
diff --git a/target/ast10x0/peripherals/sgpiom/hal_impl.rs b/target/ast10x0/peripherals/sgpiom/hal_impl.rs new file mode 100644 index 0000000..9bba5e0 --- /dev/null +++ b/target/ast10x0/peripherals/sgpiom/hal_impl.rs
@@ -0,0 +1,148 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use openprot_hal_blocking::gpio_port::{ + ActivePolarity, GpioBankPassthrough, GpioError, GpioErrorKind, GpioErrorType, GpioPort, + PinConfig, PinDirection, PinMask, +}; + +use super::register_block::Sgpiom; +use super::types::{BankDevice, Error}; + +/// 32-bit pin mask for a single SGPIOM bank. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct SgpiomMask(pub u32); + +impl PinMask for SgpiomMask { + fn empty() -> Self { + Self(0) + } + + fn all() -> Self { + Self(0xFFFF_FFFF) + } + + fn is_empty(&self) -> bool { + self.0 == 0 + } + + fn contains(&self, other: Self) -> bool { + (self.0 & other.0) == other.0 + } + + fn union(&self, other: Self) -> Self { + Self(self.0 | other.0) + } + + fn intersection(&self, other: Self) -> Self { + Self(self.0 & other.0) + } + + fn toggle(&self) -> Self { + Self(!self.0) + } +} + +impl GpioError for Error { + fn kind(&self) -> GpioErrorKind { + match self { + Error::InvalidPin => GpioErrorKind::InvalidPin, + Error::InvalidNgpios => GpioErrorKind::UnsupportedConfiguration, + Error::UnsupportedFlags => GpioErrorKind::UnsupportedConfiguration, + } + } +} + +/// A single SGPIOM bank exposed as a HAL [`GpioPort`]. +/// +/// Combines the shared [`Sgpiom`] register block with a [`BankDevice`] descriptor. +/// Use [`SgpiomBankPort::new`] to construct; the same safety contract as [`Sgpiom::new`] +/// applies. +pub struct SgpiomBankPort { + pub(super) sgpiom: Sgpiom, + pub(super) dev: BankDevice, +} + +impl SgpiomBankPort { + /// Create a bank port from an existing `Sgpiom` instance and a `BankDevice` descriptor. + /// + /// # Safety + /// + /// Same contract as [`Sgpiom::new`]: the register block pointer must be valid, non-null, + /// and access must be externally coordinated for the lifetime of this value. + pub const unsafe fn new(sgpiom: Sgpiom, dev: BankDevice) -> Self { + Self { sgpiom, dev } + } +} + +impl GpioErrorType for SgpiomBankPort { + type Error = Error; +} + +impl GpioPort for SgpiomBankPort { + type Config = PinConfig; + type Mask = SgpiomMask; + + fn configure(&mut self, pins: Self::Mask, config: Self::Config) -> Result<(), Self::Error> { + // Reject pins outside this bank's ngpios window. + let valid_mask: u32 = if self.dev.ngpios >= 32 { + u32::MAX + } else { + (1u32 << self.dev.ngpios) - 1 + }; + if (pins.0 & !valid_mask) != 0 { + return Err(Error::InvalidPin); + } + + if config.direction == PinDirection::Output { + if let Some(logical_high) = config.initial_output { + // Map logical level through active polarity to physical drive level. + let drive_high = match config.polarity { + ActivePolarity::ActiveHigh => logical_high, + ActivePolarity::ActiveLow => !logical_high, + }; + if drive_high { + self.sgpiom.port_set_bits_raw(self.dev.bank, pins.0); + } else { + self.sgpiom.port_clear_bits_raw(self.dev.bank, pins.0); + } + } + } + + // SGPIOM direction is hardware-managed; no direction register write is required. + Ok(()) + } + + fn set_reset( + &mut self, + set_mask: Self::Mask, + reset_mask: Self::Mask, + ) -> Result<(), Self::Error> { + // Atomically apply both masks: set wins over reset for overlapping bits. + self.sgpiom + .port_set_masked_raw(self.dev.bank, set_mask.0 | reset_mask.0, set_mask.0); + Ok(()) + } + + fn read_input(&self) -> Result<Self::Mask, Self::Error> { + Ok(SgpiomMask(self.sgpiom.port_get_raw(self.dev.bank))) + } + + fn toggle(&mut self, pins: Self::Mask) -> Result<(), Self::Error> { + self.sgpiom.port_toggle_bits(self.dev.bank, pins.0); + Ok(()) + } +} + +impl GpioBankPassthrough for SgpiomBankPort { + /// Sample current inputs for `mask` pins and write them to the output latch (one-shot). + fn set_passthrough_mask(&mut self, mask: Self::Mask) -> Result<(), Self::Error> { + self.sgpiom.passthrough_masked(self.dev.bank, mask.0); + Ok(()) + } + + /// No persistent passthrough hardware state exists; this is a no-op. + fn clear_passthrough(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +}
diff --git a/target/ast10x0/peripherals/sgpiom/mod.rs b/target/ast10x0/peripherals/sgpiom/mod.rs new file mode 100644 index 0000000..e2e3ab4 --- /dev/null +++ b/target/ast10x0/peripherals/sgpiom/mod.rs
@@ -0,0 +1,18 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! AST10x0 Serial GPIO Matrix (SGPIOM) peripheral driver. + +mod controller; +mod hal_impl; +mod register_block; +mod types; + +pub use controller::SgpiomController; +pub use hal_impl::{SgpiomBankPort, SgpiomMask}; +pub use register_block::Sgpiom; +pub use types::{ + Bank, BankDevice, Direction, Error, InitialLevel, InterruptMode, InterruptTrigger, + SgpiomPinConfig, +}; +
diff --git a/target/ast10x0/peripherals/sgpiom/register_block.rs b/target/ast10x0/peripherals/sgpiom/register_block.rs new file mode 100644 index 0000000..4d45e61 --- /dev/null +++ b/target/ast10x0/peripherals/sgpiom/register_block.rs
@@ -0,0 +1,272 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use ast1060_pac as device; + +use super::types::{ + Bank, BankDevice, Direction, Error, InitialLevel, InterruptMode, InterruptTrigger, + SgpiomPinConfig, +}; + +pub struct Sgpiom { + sgpiom: *const device::sgpiom::RegisterBlock, +} + +impl Sgpiom { + /// Create an SGPIOM instance from a raw register-block pointer. + /// + /// # Safety + /// + /// - `sgpiom` must be a valid, non-null pointer to the AST1060 SGPIOM register block. + /// - The pointed register block must remain valid for the lifetime of this `Sgpiom`. + /// - Caller must enforce global ownership so concurrent mutable access does not occur. + pub const unsafe fn new(sgpiom: *const device::sgpiom::RegisterBlock) -> Self { + Self { sgpiom } + } + + /// Create an instance pointing to the global AST1060 SGPIOM register block. + /// + /// # Safety + /// + /// Caller must ensure access to the singleton SGPIOM is coordinated. + pub unsafe fn new_global() -> Self { + // SAFETY: Caller upholds the singleton access contract. + unsafe { Self::new(device::Sgpiom::ptr()) } + } + + #[inline] + fn regs(&self) -> &device::sgpiom::RegisterBlock { + // SAFETY: `Sgpiom` construction is `unsafe`, so caller upholds pointer validity, + // non-nullness, and aliasing/ownership requirements. + unsafe { &*self.sgpiom } + } + + /// Configures SGPIOM global settings. + /// + /// `ngpios` is total SGPIO count across banks. + pub fn configure_global(&self, ngpios: u16, clock_div: u16) -> Result<(), Error> { + if ngpios == 0 { + return Err(Error::InvalidNgpios); + } + + let numbers = ((ngpios as u32 + 7) / 8) & 0x1f; + let mut value = self.regs().gpio554().read().bits(); + + value |= 1; // enable + value &= !(0x1f << 6); + value |= numbers << 6; + value &= !(0xffff << 16); + value |= (clock_div as u32) << 16; + + self.regs().gpio554().write(|w| unsafe { w.bits(value) }); + Ok(()) + } + + /// Read the raw 32-bit output register for a bank. + #[must_use] + pub fn port_get_raw(&self, bank: Bank) -> u32 { + match bank { + Bank::Ad => self.regs().gpio500().read().bits(), + Bank::Eh => self.regs().gpio51c().read().bits(), + Bank::Il => self.regs().gpio538().read().bits(), + Bank::Mp => self.regs().gpio590().read().bits(), + } + } + + pub fn port_set_masked_raw(&self, bank: Bank, mask: u32, value: u32) { + let current = self.port_get_raw(bank); + let next = (current & !mask) | (value & mask); + self.port_write_raw(bank, next); + } + + pub fn port_set_bits_raw(&self, bank: Bank, mask: u32) { + self.port_set_masked_raw(bank, mask, mask); + } + + pub fn port_clear_bits_raw(&self, bank: Bank, mask: u32) { + self.port_set_masked_raw(bank, mask, 0); + } + + pub fn port_toggle_bits(&self, bank: Bank, mask: u32) { + let current = self.port_get_raw(bank); + self.port_write_raw(bank, current ^ mask); + } + + pub fn pin_set_raw(&self, dev: &BankDevice, pin: u8, high: bool) -> Result<(), Error> { + dev.validate_pin(pin)?; + let bit = 1u32 << pin; + if high { + self.port_set_bits_raw(dev.bank, bit); + } else { + self.port_clear_bits_raw(dev.bank, bit); + } + Ok(()) + } + + pub fn configure_pin( + &self, + dev: &BankDevice, + pin: u8, + cfg: SgpiomPinConfig, + ) -> Result<(), Error> { + dev.validate_pin(pin)?; + + if cfg.pull_up || cfg.pull_down { + return Err(Error::UnsupportedFlags); + } + + if cfg.direction == Direction::Output { + if let Some(initial) = cfg.initial { + self.pin_set_raw(dev, pin, initial == InitialLevel::High)?; + } + } + + // SGPIOM direction is hardware managed in this design; no extra register write needed. + Ok(()) + } + + pub fn configure_interrupt( + &self, + dev: &BankDevice, + pin: u8, + mode: InterruptMode, + trig: InterruptTrigger, + ) -> Result<(), Error> { + dev.validate_pin(pin)?; + + let bit = 1u32 << pin; + let int_type = match mode { + InterruptMode::Disabled => 0u8, + InterruptMode::Level => match trig { + InterruptTrigger::Low => 2, + InterruptTrigger::High => 3, + InterruptTrigger::Both => return Err(Error::UnsupportedFlags), + }, + InterruptMode::Edge => match trig { + InterruptTrigger::Low => 0, + InterruptTrigger::High => 1, + InterruptTrigger::Both => 4, + }, + }; + + match mode { + InterruptMode::Disabled => { + let en = self.int_en_read(dev.bank) & !bit; + self.int_en_write(dev.bank, en); + } + _ => { + let en = self.int_en_read(dev.bank) | bit; + self.int_en_write(dev.bank, en); + + let mut s0 = self.int_sens_read(dev.bank, 0) & !bit; + let mut s1 = self.int_sens_read(dev.bank, 1) & !bit; + let mut s2 = self.int_sens_read(dev.bank, 2) & !bit; + + if (int_type & 0x1) != 0 { + s0 |= bit; + } + if (int_type & 0x2) != 0 { + s1 |= bit; + } + if (int_type & 0x4) != 0 { + s2 |= bit; + } + + self.int_sens_write(dev.bank, 0, s0); + self.int_sens_write(dev.bank, 1, s1); + self.int_sens_write(dev.bank, 2, s2); + } + } + + Ok(()) + } + + /// Read the latched interrupt status register for a bank. + #[must_use] + pub fn interrupt_status(&self, bank: Bank) -> u32 { + match bank { + Bank::Ad => self.regs().gpio514().read().bits(), + Bank::Eh => self.regs().gpio530().read().bits(), + Bank::Il => self.regs().gpio54c().read().bits(), + Bank::Mp => self.regs().gpio5a4().read().bits(), + } + } + + /// Acknowledge (clear) interrupt status bits for a bank. + pub fn clear_interrupt_status(&self, bank: Bank, mask: u32) { + match bank { + Bank::Ad => self.regs().gpio514().write(|w| unsafe { w.bits(mask) }), + Bank::Eh => self.regs().gpio530().write(|w| unsafe { w.bits(mask) }), + Bank::Il => self.regs().gpio54c().write(|w| unsafe { w.bits(mask) }), + Bank::Mp => self.regs().gpio5a4().write(|w| unsafe { w.bits(mask) }), + }; + } + + pub fn passthrough_masked(&self, bank: Bank, mask: u32) { + let sampled = self.port_get_raw(bank); + self.port_set_masked_raw(bank, mask, sampled); + } + + fn port_write_raw(&self, bank: Bank, value: u32) { + match bank { + Bank::Ad => self.regs().gpio500().write(|w| unsafe { w.bits(value) }), + Bank::Eh => self.regs().gpio51c().write(|w| unsafe { w.bits(value) }), + Bank::Il => self.regs().gpio538().write(|w| unsafe { w.bits(value) }), + Bank::Mp => self.regs().gpio590().write(|w| unsafe { w.bits(value) }), + }; + } + + fn int_en_read(&self, bank: Bank) -> u32 { + match bank { + Bank::Ad => self.regs().gpio504().read().bits(), + Bank::Eh => self.regs().gpio520().read().bits(), + Bank::Il => self.regs().gpio53c().read().bits(), + Bank::Mp => self.regs().gpio594().read().bits(), + } + } + + fn int_en_write(&self, bank: Bank, value: u32) { + match bank { + Bank::Ad => self.regs().gpio504().write(|w| unsafe { w.bits(value) }), + Bank::Eh => self.regs().gpio520().write(|w| unsafe { w.bits(value) }), + Bank::Il => self.regs().gpio53c().write(|w| unsafe { w.bits(value) }), + Bank::Mp => self.regs().gpio594().write(|w| unsafe { w.bits(value) }), + }; + } + + fn int_sens_read(&self, bank: Bank, index: u8) -> u32 { + match (bank, index) { + (Bank::Ad, 0) => self.regs().gpio508().read().bits(), + (Bank::Ad, 1) => self.regs().gpio50c().read().bits(), + (Bank::Ad, 2) => self.regs().gpio510().read().bits(), + (Bank::Eh, 0) => self.regs().gpio524().read().bits(), + (Bank::Eh, 1) => self.regs().gpio528().read().bits(), + (Bank::Eh, 2) => self.regs().gpio52c().read().bits(), + (Bank::Il, 0) => self.regs().gpio540().read().bits(), + (Bank::Il, 1) => self.regs().gpio544().read().bits(), + (Bank::Il, 2) => self.regs().gpio548().read().bits(), + (Bank::Mp, 0) => self.regs().gpio598().read().bits(), + (Bank::Mp, 1) => self.regs().gpio59c().read().bits(), + (Bank::Mp, 2) => self.regs().gpio5a0().read().bits(), + _ => 0, + } + } + + fn int_sens_write(&self, bank: Bank, index: u8, value: u32) { + match (bank, index) { + (Bank::Ad, 0) => self.regs().gpio508().write(|w| unsafe { w.bits(value) }), + (Bank::Ad, 1) => self.regs().gpio50c().write(|w| unsafe { w.bits(value) }), + (Bank::Ad, 2) => self.regs().gpio510().write(|w| unsafe { w.bits(value) }), + (Bank::Eh, 0) => self.regs().gpio524().write(|w| unsafe { w.bits(value) }), + (Bank::Eh, 1) => self.regs().gpio528().write(|w| unsafe { w.bits(value) }), + (Bank::Eh, 2) => self.regs().gpio52c().write(|w| unsafe { w.bits(value) }), + (Bank::Il, 0) => self.regs().gpio540().write(|w| unsafe { w.bits(value) }), + (Bank::Il, 1) => self.regs().gpio544().write(|w| unsafe { w.bits(value) }), + (Bank::Il, 2) => self.regs().gpio548().write(|w| unsafe { w.bits(value) }), + (Bank::Mp, 0) => self.regs().gpio598().write(|w| unsafe { w.bits(value) }), + (Bank::Mp, 1) => self.regs().gpio59c().write(|w| unsafe { w.bits(value) }), + (Bank::Mp, 2) => self.regs().gpio5a0().write(|w| unsafe { w.bits(value) }), + _ => {} + }; + } +}
diff --git a/target/ast10x0/peripherals/sgpiom/types.rs b/target/ast10x0/peripherals/sgpiom/types.rs new file mode 100644 index 0000000..6e4a45a --- /dev/null +++ b/target/ast10x0/peripherals/sgpiom/types.rs
@@ -0,0 +1,93 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + InvalidPin, + InvalidNgpios, + UnsupportedFlags, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + Input, + Output, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InitialLevel { + Low, + High, +} + +/// Low-level per-pin configuration used by [`crate::sgpiom::register_block::Sgpiom::configure_pin`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SgpiomPinConfig { + pub direction: Direction, + pub initial: Option<InitialLevel>, + pub pull_up: bool, + pub pull_down: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InterruptMode { + Disabled, + Level, + Edge, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InterruptTrigger { + Low, + High, + Both, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Bank { + Ad = 0, + Eh = 1, + Il = 2, + Mp = 3, +} + +impl Bank { + #[inline] + pub const fn from_pin_offset(pin_offset: u8) -> Option<Self> { + match pin_offset >> 5 { + 0 => Some(Self::Ad), + 1 => Some(Self::Eh), + 2 => Some(Self::Il), + 3 => Some(Self::Mp), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BankDevice { + pub bank: Bank, + pub pin_offset: u8, + pub ngpios: u8, +} + +impl BankDevice { + #[inline] + pub const fn new(bank: Bank, pin_offset: u8, ngpios: u8) -> Self { + Self { + bank, + pin_offset, + ngpios, + } + } + + #[inline] + pub fn validate_pin(&self, pin: u8) -> Result<(), Error> { + if pin < self.ngpios && pin < 32 { + Ok(()) + } else { + Err(Error::InvalidPin) + } + } +}