Pins & General-Purpose Input/Outputs (GPIOs)
GPIOs are the most basic peripheral microcontrollers can have: they allow reading the logic level of an input or setting the logic level of an output. Most pins of microcontrollers can be used as GPIOs, and GPIOs are sometimes grouped into ports, which each contains 16 or 32 GPIOs depending on the microcontroller family.
Pin Structure and Alternate Functions
Microcontroller pins, also referred to as pads, include the necessary circuitry to connect between the outside world and the internal peripherals.
Tip
In this context, “pad” refers to a die’s pad, not specifically to a package’s pad. Additionally, “pin” and “pad” may refer only to the outward-facing electrical contact itself or may also include the internal circuitry between it and the internal registers.
Pins can be configured in the following states:
- The GPIO logic can be enabled.
- The GPIOs can be instead connected to be controlled by other digital peripherals (often called “alternate functions”), e.g., I2C, SPI, UART,
- The analog inputs (i.e., upstream of the Schmitt trigger) can be directed to an analog-to-digital converter (ADC), and
- The analog outputs can be connected to a digital-to-analog converter (DAC), if present on the microcontroller.
The alternate functions available on a given pin—i.e., the peripherals that can be connected to it—is generally fixed and limited. Some microcontroller families (e.g., ESP32, nRF) do however provide greater flexibility and allow connecting most peripherals to almost any pin. These restrictions are enforced by Ariel OS (and by Embassy-style HALs) at compile time, so it should not be possible to connect the wrong pin to a peripheral. Refer to your microcontroller’s datasheet to learn about the structure of its pins and about the peripherals that can be used with each of them.
GPIOs as Inputs
Ariel OS provides a portable driver allowing to read the logic level of a GPIO:
#![allow(unused)]
fn main() {
let btn0 = Input::new(peripherals.buttons.button0, Pull::Up);
let level = btn0.is_low();
}
The driver implements embedded_hal::digital::InputPin, so it can be passed to higher-level drivers that require an input.
Tip
The required peripheral ZST can be obtained using the usual Ariel OS mechanisms.
Digital inputs are generally classified as “active low” or “active high”: this is not a property of the input itself but of what each logic level represents, which depends on the board hardware. For instance, an input that is low when a push-button connected to it is pressed is considered active low.
In addition, depending on the purpose of the input and on the board hardware, it may be necessary to enable the pin’s internal pull-up or pull-down resistor so that the input is not left floating.
Each digital input comprises a Schmitt trigger that implements a hysteresis on the (analog) input signal to prevent the digital input from “flickering” when the input switches levels.
External Interrupts
To avoid polling the input as above, interrupts can be used instead, after enabling the external-interrupts Cargo feature:
#![allow(unused)]
fn main() {
let mut btn0 = Input::builder(peripherals.buttons.button0, Pull::Up)
.build_with_interrupt()
.unwrap();
loop {
btn0.wait_for_low().await;
led0.toggle();
Timer::after_millis(100).await;
}
}
Interrupts have many benefits: in particular, they
- allow to respond with a lower latency than polling,
- may allow the processor and (some of the) peripherals to enter some low-power mode until the interrupt is triggered, greatly reducing power consumption, and
- allow to structure the application in an event-driven manner.
GPIOs as Outputs
Digital outputs are tri-state: they use a pair of transistors to either drive the output low, high, or leave it floating, i.e., in a high impedance state (Hi-Z). When the pair of transistors is used in a complementary way—i.e., driving the output either low or high—the output is used as a push–pull (PP). If only the transistor able to drive the output low is used—i.e., the output is either driven low or is left in high impedance—the output is used as open-drain (OD). Open-drain outputs are useful to “release” the output and allow another device, or a pull-up resistor, to take control of it. They are for instance used by certain bus protocols, such as I2C.
Ariel OS provides a portable driver allowing to set the level of a GPIO:
#![allow(unused)]
fn main() {
let mut led0 = Output::new(peripherals.leds.led0, Level::Low);
led0.set_high();
}
The driver implements embedded_hal::digital::OutputPin and embedded_hal::digital::StatefulOutputPin, so it can be passed to higher-level drivers that require an output, like SPI device drivers.
Note
Ariel OS does not currently provide a dedicated portable driver for open-drain outputs.
Similarly to inputs, outputs can generally be classified as “active low” or “active high” depending on how they are to be used. For instance, an output that must be set low to turn an LED on is considered active low.
The current that the microcontroller can source (when the GPIO is high) or sink (when low) is limited for each pin. In addition, the total current, across all pins at the same time, is also limited, to a value typically lower than the sum of the limit of each individual pin. How much current is drawn depends on what is connected to the microcontroller pin.
Warning
Exceeding these limits may damage the microcontroller. Refer to your microcontroller’s datasheet.
Output Configuration
Depending on the microcontroller, outputs may support configuring their drive strength and/or their speed/slew rate.
In Ariel OS, this is done through Output::builder().
The drive strength of a pin determines how much current it can source/sink without the voltage being affected. Increasing this setting is necessary when the load attached to the pin requires more current than the default, or when it is necessary to reduce the rise/fall time (i.e., to increase the slew rate).
Important
The drive strength does not cap the current through the pin.
Depending on the MCU family, the speed/slew rate of pins may also be configurable: faster transitions between levels may be required by certain protocols or components, but may increase electromagnetic interference as lower rise times make the unwanted electromagnetic emissions richer in higher frequencies.
Tip
The speed of GPIOs should be left at their default value unless actually required by the board hardware.
Pull-up/Pull-down Resistors
Pull-up and pull-down resistors have two main use cases: forcing a known level on an otherwise-floating input, or in combination with an open-drain output to force the output high when it is not driven low.
Because the logic level of a floating input is unpredictable, inputs should not be left floating and should either be driven by an external component or by a pull-up or pull-down resistor.
Such resistors can be added on the board, but all supported microcontrollers also feature internal pull-up and pull-down resistors which can be enabled in software.
In Ariel OS, Pull is used in the input constructor to configure its resistors.
Tip
Enabling internal pull-up resistors even if external ones are also present is electrically safe, and only reduces the resistance as the resistors are in parallel, which only (slightly) increases power consumption.
Pull-up and pull-down resistors can be classified as strong or weak depending on their value: a low resistance allows more current through the resistor and makes it pull stronger, while a higher resistance makes the pull-up/pull-down resistor weaker. Internal pull-up and pull-down resistors typically are quite weak, with a resistance of a few tens of kilohms1. Weaker pull-up/pull-down resistors result in a lower power consumption (as they let less current flow), but stronger ones are required in certain cases, e.g., to achieve higher frequencies with open-drain buses, like I2C.
Internal resistors, manufactured in integrated circuits, are less accurate than external ones. They are well-suited as generic pull-up or pull-down resistors, but must not typically be used when specific values are required.
-
With the exception of nRF MCUs, which have stronger pull-up and pull-down resistors. ↩