Kernel General Purpose I/O (GPIO) HIL
TRD: 103
Working Group: Kernel
Type: Documentary
Status: Draft
Author: Amit Levy, Philip Levis
Draft-Created: Feb 05, 2017
Draft-Modified: April 09, 2021
Draft-Version: 3
Draft-Discuss: devel@lists.tockos.org
Abstract
This document describes the hardware independent layer interface (HIL) for General Purpose Input/Output (GPIO) in the Tock operating system kernel. It describes the Rust traits and other definitions for this service as well as the reasoning behind them. This document is in full compliance with TRD1.
1 Introduction
General Purpose Input/Output (GPIO) controls generic pins. User code can control the output level on the pin (high or low), read the externally drive logic level and often configure pull-up or pull-down resistence. Typically, microcontrollers expose pins in groups called ports however Tock's GPIO HIL exposes pins individually since ports often do not group pins as they are actually used on a board. Software that wishes to control a whole port (e.g. for efficiency) should use the per-chip implementation, which may export this feature.
The GPIO HIL is the kernel crate, in module hil::gpio. It provides the following traits:
kernel::hil::gpio::Outputcontrols an output pin.kernel::hil::gpio::Inputcontrols an input pin.kernel::hil::gpio::Configureconfigures a pin.kernel::hil::gpio::ConfigureInputOutputconfigures a pin that can simultaneously be an input and an output (some hardware supports this. It depends onConfigure).kernel::hil::gpio::Interruptcontrols an interrupt pin. It depends onInput.kernel::hil::gpio::Clienthandles callbacks from pin interrupts.kernel::hil::gpio::InterruptWithValuecontrols an interrupt pin that provides a value in its callbacks. It depends onInput.kernel::hil::gpio::ClientWithValuehandles callbacks from pin interrupts that provide a value (InterruptWithValue).kernel::hil::gpio::Pin depends onInput,Output, andConfigure`.kernel::hil::gpio::InterruptPin depends onPinandInterrupt`.kernel::hil::gpio::InterruptValuePin depends onPinandInterruptWithValue`.
The rest of this document discusses each in turn.
2 Output
The Output trait controls a pin that is an output. It has
four methods:
#![allow(unused)] fn main() { pub trait Output { /// Set the GPIO pin high. If the pin is not an output or /// input/output, this call is ignored. fn set(&self); /// Set the GPIO pin low. If the pin is not an output or /// input/output, this call is ignored. fn clear(&self); /// Toggle the GPIO pin. If the pin was high, set it low. If /// the pin was low, set it high. If the pin is not an output or /// input/output, this call is ignored. Return the new value /// of the pin (false is cleared, true is set). fn toggle(&self) -> bool; /// Activate or deactivate a GPIO pin, for a given activation mode. fn write_activation(&self, state: ActivationState, mode: ActivationMode); } }
The write_activation method has a default implementation. This method
allows software to interact with a GPIO using logical, rather than physical
behavior. For example, consider a button which is "active" when it is
pushed. If the button is connected to ground and a pull-up input pin,
then it is active when the pin is low; if it is connected to Vdd and
a pull-down input pin, it is active when the pin is high. Similarly,
an LED may be connected through an PNP transistor, whose base is
controlled by a GPIO pin, such that setting the pin low turns on the
LED and setting the pin high turns it off. Rather than keeping track
of these polarities, software can use ActivationState to specify
whether the device should be active or inactive, and ActivationMode
specifies the polarity.
#![allow(unused)] fn main() { #[derive(Clone, Copy, PartialEq, Eq)] pub enum ActivationState { Inactive = 0, Active = 1, } /// Whether a GPIO is in the `ActivationState::Active` when the signal is high /// or low. #[derive(Clone, Copy)] pub enum ActivationMode { ActiveHigh, ActiveLow, } }
3 Input
The Input trait controls an input pin. It has two methods:
#![allow(unused)] fn main() { pub trait Input { /// Get the current state of an input GPIO pin. For an output /// pin, return the output; for an input pin, return the input; /// for disabled or function pins the value is undefined. fn read(&self) -> bool; /// Get the current state of a GPIO pin, for a given activation mode. fn read_activation(&self, mode: ActivationMode) -> ActivationState { let value = self.read(); match (mode, value) { (ActivationMode::ActiveHigh, true) | (ActivationMode::ActiveLow, false) => { ActivationState::Active } (ActivationMode::ActiveLow, true) | (ActivationMode::ActiveHigh, false) => { ActivationState::Inactive } } } } }
The read_activation method is similar to the write_activation method in Output,
described below, but operates on input rather than output bits.
4 Configure
The Configure trait allows a caller to configure a GPIO pin. It has 10 methods,
two of which have default implementations.
#![allow(unused)] fn main() { pub enum Configuration { LowPower, // Cannot be read or written or used; effectively inactive. Input, // Calls to the `Input` trait are valid. Output, // Calls to the `Output` trait are valid. InputOutput, // Calls to both the `Input` and `Output` traits are valid. Function, // Chip-specific, requires chip-specific API for more detail, Other, // In a state not covered by other values. } pub enum FloatingState { PullUp, PullDown, PullNone, } pub trait Configure { fn configuration(&self) -> Configuration; fn make_output(&self) -> Configuration; fn disable_output(&self) -> Configuration; fn make_input(&self) -> Configuration; fn disable_input(&self) -> Configuration; fn deactivate_to_low_power(&self); fn set_floating_state(&self, state: FloatingState); fn floating_state(&self) -> FloatingState; // Have default implementations fn is_input(&self) -> bool; fn is_output(&self) -> bool; } }
The Configuration enum describes the current configuration of a pin.
The key property of the enumeration, which prompts its use, is the
fact that some hardware allows a pin to simultaneously be an input
and an output, while in other hardware these states are mutually
exclusive. For example, the Atmel SAM4L GPIO pins are always inputs,
and reading them "indicates the level of the GPIO pins regardless of
the pins being driven by the GPIO or by an external component". In
contrast, on the nRF52 series, a GPIO pin is either an input or
an output.
The Configuration enumeration encapsulates this by reporting
the current configuration after a change. For example, suppose
a pin has Configuration::Input and software calls make_output
on it. A SAM4L will return Configuration::InputOutput while
an nRF52 will return Configuration::Output.
If a client requires a pin be both an input and an output, it can
use the ConfigureInputOutput trait:
#![allow(unused)] fn main() { pub trait ConfigureInputOutput: Configure { /// Make the pin a simultaneously input and output; should always /// return `Configuration::InputOutput`. fn make_input_output(&self) -> Configuration; fn is_input_output(&self) -> bool; } }
Chips that support simultaneous input/output MAY implement this trait, while others that do not support simultaneous input/output MUST NOT implement this trait. Therefore, at compile time, one can distinguish whether the client can operate properly.
The Configure::deactivate_to_low_power method exists because the best
configuration for GPIO pins can depend not only on the chip but
also how they are connected in a system. This method puts the
pin into whatever state is lowest power and causes it to be
both unreadable and unwritable. E.g., even if the lowest power
state is as a pull-down input, when in this state a client cannot
read the pin. Blocking functionality in this way tries to
prevent clients making assumptions about the underlying hardware.
5 Interrupt and Client
The Interrupt and Client traits are how software can control
and handle interrupts generated from a GPIO pin.
#![allow(unused)] fn main() { pub enum InterruptEdge { RisingEdge, FallingEdge, EitherEdge, } pub trait Interrupt<'a>: Input { fn set_client(&self, client: &'a dyn Client); fn enable_interrupts(&self, mode: InterruptEdge); fn disable_interrupts(&self); fn is_pending(&self) -> bool; } pub trait Client { fn fired(&self); } }
These traits assume that hardware can generate interrupts
on rising, falling, or either edges. They do not support
level (high/low) interrupts. Some hardware does not support
level interrupts. The nRF52 GPIOTE peripheral, for example,
doesn't. Chips or capsules that wish to support level interrupts
can define a new trait that depends on the Interrupt trait.
An important aspect of these traits is that they cannot fail.
For example, enable_interrupts does not return anything,
so there is no way to signal failure. Because interrupts
are an extremely low-level aspect of the kernel, these traits
preclude there being complex conditional logic that might cause
them to fail (e.g., some form of dynamic allocation or
mapping). Interrupt implementations that can fail at runtime
should define and use alternative traits.
5 InterruptWithValue and ClientWithValue
The InterruptWithValue and ClientWithValue traits extend
interrupt handling to pass a value with an interrupt. This
is useful when a single method needs to handle callbacks
from multiple pins. Each pin's interrupt can have a different
value, and the callback function can determine which pin
the interrupt is from based on the value passed. This
is used, for example, in the GPIO capsule that allows
userspace to handle interrupts from multiple interrupt pins.
If there weren't a ClientWithValue trait, the capsule would
have to define N different callback methods for N pins. These
would likely each then call a helper function with a parameter
indicating which one was invoked: ClientWithValue provides
this mechanism automatically.
#![allow(unused)] fn main() { pub trait InterruptWithValue<'a>: Input { fn set_client(&self, client: &'a dyn ClientWithValue); fn enable_interrupts(&self, mode: InterruptEdge) -> Result<(), ErrorCode>; fn disable_interrupts(&self); fn is_pending(&self) -> bool; fn set_value(&self, value: u32); fn value(&self) -> u32; } pub trait ClientWithValue { fn fired(&self, value: u32); } }
The InterruptWithValue trait does not depend on the
Interrupt trait because its client has a different type.
Supporting both types of clients would require case logic
within the GPIO implementation, whose cost (increased storage
for the variably-typed reference, increased code for handling
the cases) is not worth the benefit (being able to pass a
Client to an InterruptWithValue.
The GPIO HIL provides a standard implementation of a wrapper
that implements InterruptWithValue. It wraps around an
implementation of Interrupt, defining itself as a Client
and using Client:callback to invoke ClientWithValue::callback.
#![allow(unused)] fn main() { impl<'a, IP: InterruptPin<'a>> InterruptValueWrapper<'a, IP> { pub fn new(pin: &'a IP) -> Self {...} }
InterruptValueWrapper implements InterruptWithValue, Client,
Input, Output, and Configure.
6 Composite Traits: Pin, InterruptPin, InterruptValuePin
The GPIO HIL uses fine-grained traits in order to follow the security principle of least privilege. For example, something that needs to be able to read a GPIO pin should not necessarily be able to reconfigure or write to it. However, because handling multiple small traits at once can be cumbersome, the GPIO HIL defines several standard composite traits:
#![allow(unused)] fn main() { pub trait Pin: Input + Output + Configure {} pub trait InterruptPin<'a>: Pin + Interrupt<'a> {} pub trait InterruptValuePin<'a>: Pin + InterruptWithValue<'a> {} }
6 Example Implementation
As of this writing (April 2021; Tock v1.6 and v2.0), there are example implementations of the GPIO HIL for the Atmel
SAM4L, lowRISC, nrf5x, sifive, stm32f303xc, stm32f4xx, imxrt10xx,
apollo3, and msp432 chips. The lowrisc, sam4l, and sifive chips
support Configuration::InputOutput mode, while the others
support only input or output mode.
7 Authors' Address
Philip Levis
414 Gates Hall
Stanford University
Stanford, CA 94305
email: Philip Levis <pal@cs.stanford.edu>
phone: +1 650 725 9046
Amit Levy
email: Amit Levy <aalevy@cs.princeton.edu>