RFD 291
illumos GPIO Framework
RFD
291
Updated

The purpose of this RFD is to provide an overview and explain our design approach for a GPIO framework for illumos. This document is for Oxide to work through this. Eventually a variant of this after we get through experimentation will also be used as an illumos IPD. Specifically this document proposes

  • A kernel GPIO framework with a corresponding provider API

  • User land components for managing and controlling GPIOs within controllers

  • A way to dedicate GPIOs for better programmatic and semantic consumption within the system.

The rest of this document goes into background on GPIOs and how they fit into the system. It also surveys several different GPIO subsystems found in the wild to help motivate the kernel interfaces. After that, we go into our goals and finally the proposal itself with anticipated deliverables.

GPIOs are used on Gimlet for several different purposes:

  • Detecting whether a sharkfin is present or not ([rfd129])

  • Controlling the T6’s manufacturing mode ([rfd139])

  • Determining the presence of Sidecar ([rfd144], [rfd164])

  • As a possible means for the SP and Host to communicate to interrupt one another ([rfd164])

Background

GPIOs, which generally stand for general purpose I/O, are a common part of most hardware platforms. Most hardware devices consist of a series of pins that are used to interact with the rest of an electronic circuit board. These pins may have a defined purpose such as being used for PCI Express, power, peripherals such as a UART, I2C, SPI, etc.

However, most pins can be put in a mode where they work in a generic fashion. Here, rather than a peripheral or hardware block driving them on an SoC, they are programmable and can be driven by software at runtime. The amount of control here varies based on the device (more on that in a bit).

GPIOs are useful to folks who are designing hardware systems as it allows them to use pins for ancillary services. Here are examples of the ways that GPIOs are used:

  • As a way to detect the presence of something

  • As a way to control the enable or reset of another device

  • As a way to generate an interrupt from one device to another

  • As a way to control a manufacturing mode of a device or toggle a multiplexor

  • As a way to drive a higher speed logical implementation. That is, GPIOs can be used to implement JTAG, SPI, I2C, etc.

Effectively, GPIOs are a rather versatile mechanism for getting at different functions. Of course, because these are exposing pins from the SoC, things are actually quite diverse and different. We’ll go into different aspects of each of these in turn.

Machine, not Platform Focus

In illumos, we often have three levels that we have thought about on x86 side traditionally:

  • Common code (think uts/common) — code that runs on all architectures

  • Architecture specific code (think uts/intel) — code that is specific to a given ISA. In general, this contains things that are always true based on the CPU architecture or other architectural features thereof like performance counters, system call handlers, or other devices that are specific to that (like a memory controller driver).

  • Platform specific code (think uts/oxide and uts/i86pc) — code that is specific to a given platform. The best example of a platform is something like what exists today with x86 and the combination of UEFI/ACPI. It describes how to determine what resources exist, discover devices that aren’t self-discoverable, and more. At Oxide we have our own platform found in uts/oxide because we are quite purposefully not a PC! See [rfd215].

The historical power of x86 has been that the success of the IBM PC eventually lead to the point that by the 1990s, even though the original PC was no longer there, everyone was building systems that were "compatible" with it. This meant that there was a common platform for x86 systems and therefore operating systems trying to operate on a new machine generally didn’t require a lot of porting effort (except for working around all the different firmware issues an errata). In contrast, in similar ARM systems (and really until sometime in the 2010s), most systems had a unique kernel for each ARM board. This eventually consolidated with things such as the atag effort, flattened device tree, and the 64-bit ARMv8-A platform adoption of ACPI + UEFI.

In this model though there’s a lot that electrically is different between these systems. And the promise of ACPI is just around discoverability of what’s there. Tools like that and SMBIOS actually are meant to encourage different pieces of hardware to be radically different. While say a computer built by Dell and one built by HP will contain support for the same CPU socket, DIMMS, and PCI Express devices, the actual way the machine is put together will vary wildly. Even within the same product family, this may be true.

On the SPARC side historically, there was another class called the 'Machine' or 'Implementation' which was meant to refer to a particular instantiation of a platform and go into all of the details and specifics of its implementation. While here we had a different unix kernel image for each machine and drivers that were specific to each machine, the machine did build knowledge about specific devices that were on the board around I2C, GPIOs, etc.

The oxide platform is much closer to SPARC in this regard (see [rfd215] for more specifics). We have the benefit of knowing exactly what our machines will look like and what is inside of it. This will help us out a bunch as we’ll get to later in the specifics of the proposal.

If you look at the embedded space, it is also the case that you may have a common "platform" in the sense of say a development board that brings a lot of pins to headers. However, the specifics of how things are used and what peripherals are mapped to what pins ultimately depends in those case on the end user of it itself. With something like a Raspberry Pi, while some pins are well defined, others are purposefully not so you can do other things.

So why all of this for a moment. The important thing here is that while the SoC defines the total number of GPIOs that it has, the programming interface, and related, the question of which GPIOs are in use, are safe to use, or being muxed with other peripherals requires not only intimate knowledge of the particular machine, but can vary from individual system to individual system.

This in turn provides a challenge for us that we’ll get to in our further explanation of our design, but we really can’t assume that toggling any GPIO arbitrarily or changing its mode is safe. This can actually be used to damage a system! The same GPIO on an AMD system could be harmlessly unused or on another system be used as an input and if you change it to an output, damage the pin and driver!

GPIO Attributes

A different set of challenges is that the way GPIOs are configured and the different settings here actually vary substantially based on the type of hardware in question. To make this a little clearer, let’s discuss several different classes of devices. To help make this clear, we have a classic I2C based GPIO controller, AMD and Intel GPIOs, and then also using two ARM based microcontrollers. While illumos does not support or really try to target MMU-less devices, the microcontrollers help show the diversity of GPIO configurations.

Let’s look at what’s required to configure GPIOs for these different devices in turn:

PCA9506

The PCA9506 ([nxp-pca9506]) is a 40-bit GPIO expander that’s accessible over I2C ans has similar cousins in the form of the PCA9535 and the more venerable PCA9539.

Here, the controls are fairly straightforward. From an actual GPIO perspective we get to control on a per-gpio basis:

  • Whether the GPIO is an input or an output

  • What the level is for an output (high or low)

  • Whether or not there should be polarity inversion on outputs

When we look at the device, there are other properties that exist beyond these. Two notable areas are power and interrupts. In the limit, a device theoretically has an output voltage that it will assign to the different logical levels (e.g. 0 or 1) and then it has input voltage ranges which is what it’ll use to determine if something it reads is a zero or one.

In the case of the PCA9506 there is no software control over this, it is all a function of its input supply VDD. Based on this hardware choice, these values are all derived based on this value (see Table 10 [nxp-pca9506]). In other systems, there may be programmatic control over this. Generally, the input and output voltage are related, but that strictly speaking isn’t required and isn’t an inherent property of the device.

Another similar category that is a little odd, but isn’t strictly speaking a property of how the pin is driven or understood is interrupt generation. To us, the fact that the PCA9506 has an optional alert pin that can be triggered based on register configuration in certain circumstances is a bit of an implementation detail. That is, right now we’re not trying to first class interrupts, but rather they may be used as part of implementing and providing support.

ST H753

The ST H753 processor is an ARM Cortex-M based SoC that comes in several different packages with a varying number of GPIOs. Unlike the PCA9506 and much more like every other device we talk about here, given pins on the package are muxed between anywhere from one to ten different peripherals. While this makes the CPU quite versatile, it also means that which GPIOs are usable and how they are configured varies substantially from implementation to implementation.

Presuming a pin is configured as a GPIO, it has the following different types of options and modes:

  • The voltage of the pin is determined by the voltage of the chip (generally 1.8V or 3.3V). Unlike other items this is not configurable.

  • The pin has a mode, which can be one of an input, output, set into an alternate mode, or be treated as an analog pin

  • What the level is for an output (high or low)

  • The pin has an output type which determines how the pin behaves. In this case this is either configured as a push/pull or as open drain.

  • The pin has a speed which relates to how the GPIO’s rise time. These are generally called 'Low', 'Medium', 'High', and 'Very High'.

  • Finally, there is also control for an integrated pull up, which may be: none, up, or down.

As we can see, compared to the PCA9506 this is a much more complex device and while there is commonality with respect to the output level, there isn’t much else. From here, let’s look at a different microcontroller.

LPC55S69

The LPC55S69 is another family of ARM Cortex-M microcontrollers. While like the STMH753, we are unlikely to end up using this processor, it’s worth understanding how it all fits together. So let’s take a look at how it has slightly different settings for GPIO attributes. While it also has the alternate function behavior, the actual controls are along the following axis:

  • There are controls around whether it is an analog or digital pin

  • There are controls around whether the pin is in an open drain mode or not.

  • There is an explicit slew rate control which is standard or fast

  • There is an inversion option

  • There are controls around how pull ups work, either none, up, down, or a repeater

  • There are controls over whether something is an input or output

  • What the level is for an output (high or low)

BCM2711

The BCM2711 is an ARM A-class CPU which is used in the Raspberry PI 4 B. As such it’s a good example of a standard aarch64 based SoC with GPIO options. Like other peripherals it has a lot of muxed pins that can be used between different controllers. Items that are configurable (§5 [bcm2711-arm-periph])

  • Pin direction as an input or output

  • What the level is for an output (high or low)

  • Pull up control, which may be none, up, or down

From the BCM2711 datasheet there isn’t voltage control nor other features. There is a rich set of features around detecting event changes though. The hardware does have the ability to control clocks, but right now those are beyond the scope of our plans.

Intel C620 Chipset

The Intel C620 series chipset is how most Intel Xeon platforms expose their GPIOs. Here GPIOs are organized in terms of blocks. Like everything else these GPIOs are muxed together with others and ownership is a little more tangled due to the use by peripherals in the chipset and the ME. GPIOs vary in their capabilities but generally speaking have some of the following controls (§18 [intel-c620]):

  • The voltage can either be 1.8V or 3.3V, though this varies by GPIO and bank.

  • Control over a weak internal pull with both a choice of strength, 5k or 20k, and direction, up or down

  • Ability to control some amount of polarity inversion

  • Glitch filter and input / output buffer control

  • What the level is for an output (high or low)

While there are more controls here around interrupts and routing to different sources, this kind of paints a basic picture of what’s here and the fact that it’s not all the same.

AMD Milan

AMD’s Milan SoC sits in the SP3 socket. The socket itself has a number of pins which have defined purposes, a subset of which are for items which can be muxed with GPIOs. The AMD Milan SoC is the primary target of this work. What’s here generally applies to Rome and could apply to Naples; however, the internal design in Naples is very different so that has not been evaluated.

The SoC inherently has different types of GPIOs that are available and are often muxed with other devices. Whether a given pin is safe to use as a GPIO or is being used by the actual peripheral is something that can only really be known based on a particular motherboard or consumer thereof.

Once we get the question of the pin mux, there are several different types of pins at a higher level here. There are pins that have interrupt capabilities (AGPIOs), ones that do not (EGPIOs), and then there are certain GPIOs that have special capabilities around voltage control. You’ll notice similarities to the above things, but also differences (§14.3.21 [amd-milan-ppr], §14.3.20 [amd-genoa-ppr]).

  • A few pins have control for whether they operate at 1.8V or 3.3V

  • Pull up controls control both the strength, either 4k/8k, and the direction up/down, and of course the ability to turn it off

  • Control for whether the pin is an input or output. A few pins have semantics around being open drain

  • Options to control the drive strength which are generally 40, 80 Ohms for a 3.3V pad and 40, 60, and 80 Ohms for a 1.8V pad.

  • Control over debouncing logic

  • What the level is for an output (high or low)

  • For some pins there are wake up, active high/low selection, and general interrupt control

The set of GPIOs vary slightly between the two sockets as some alternate functions are not available on the second socket and therefore some pins must be used as GPIOs that cannot practically be on the first socket.

It’s worth calling out that we would expect some of these attributes to change between socket types. So while we have documented what’s used for Milan, the same may not be true or its internal organization, for future processors in SP5.

Interrupts

As seen in the discussion of the different GPIO attributes, the design of GPIOs is very different. In addition, the way that interrupts can occur can vary substantially. On x86 these mechanisms often include things such as being able to route to an I/O APIC pin, route to the ACPI related SCI (system control interrupt), and sometimes to an NMI (non-maskable interrupt).

The question of what is usable is something that is ultimately dependent on the machine and more practically, any firmware that may be running on it. The main note from this is that we need to assume and should plan on the idea that interruptibility is going to be a much more complicated thing here.

In particular, when things like the I/O APIC on x86 are used or on other platforms where there are similar smaller numbers of banks available, then it becomes much harder to have well functioning interrupts and the design of such subsystems can become trickier. For example, on most non-Oxide x86 systems, the I/O APIC configuration is owned basically by the ACPI firmware. This leaves us with a few general thoughts:

  • There is unlikely to be a single, generic design for interrupts for GPIOs.

  • The set of interrupt capabilities isn’t even tied to just the hardware, but also the question of what firmware or in the case of Gimlet, software, is running on it.

Goals and Constraints

Before we dive into the design, it’s worth getting into the details of what we’re trying to achieve, but equally what we’re not trying to achieve here. By narrowing the initial focus here, this will hopefully allow us to get something useful initially.

Our primary goal is basically allowing two different modes of using a GPIO:

  1. We want the ability to allow someone to specify / control the attributes of a GPIO. Basically giving someone with sufficient privileges full grained control over every different attribute appropriate for the system. This allows a user/operator that knows the specifics of what they’re doing to change things from the default.

  2. We’d like a second mode that allows someone to basically declare a dedicated function for a GPIO, locking down its attributes. However, by locking down things like pull ups, voltages, direction, etc. it becomes easier to allow broader software (say pieces related to FMA) to fit inside here. These DPIOs want to be consumed by both user land and the kernel.

In particular, we call out the following specific examples as things we’re trying to enable:

  • GPIOs that are dedicated as inputs to determine presence (Sharkfin, Sidecar presence).

  • GPIOs that are used to generate and deliver interrupts to/from another external entity (SP/Host communication).

  • GPIOs that are used simply as outputs to control the enable mode of other components (T6 manufacturing mode).

Here are some things that aren’t goals and things we more so don’t want to support:

  • Any kind of high-speed 'bit banging'. This is a common technique used to control a GPIO to drive a protocol interface. While this can be a useful technique, there are a lot of constraints around timing that likely would suggest a different API and thus something we’d rather not embark upon.

  • We’re not trying to create uniformity between all of the different possible GPIO controllers out there. To use GPIOs you need to understand the specifics of the platform (unless someone creates a dedicated GPIO). Instead we will discuss how this is something we want to make first class.

  • There are a lot of issues here around pin muxing. This is something that we will need to solve and have better controls around as it ties into how different GPIOs are available and what are options are. At the moment, our preference is to ask drivers to be conservative here and use knowledge from the platforms to figure out what is generally safe to use or available.

  • There are several different ways to slice and dice GPIOs in ACPI; however, they are complicated by the fact that hardware-reduced platforms use GPIOs for signaling and therefore some amount of control over them needs to be used by AML (§4.1 [acpi-62]). There is no intent to automate the creation of DPIOs from ACPI objects. However, GPIO controllers do exist as objects (§5.6.5.1 [acpi-62]) that are allowed to be used by the Operating System and one could add support for such a controller at that level.

Finally, as part of prototyping, here are some things we hope to more generally learn about:

  • What is a useful way to actually design and phrase the different attributes of GPIOs. Does our subsequent proposal make sense?

  • How general or specific does a given GPIO controller driver need to be? That is can we design something that at least works across Rome, Milan, and Genoa? We are unlikely to have time to look at AM4/AM5.

  • Begin to understand what the right way to model interrupts in this specific implementation is.

Proposal

With all the above, it’s time to turn to what we’d like to implement. As we dig in, it’s worth calling out that right now our focus is on the high-level look and feel. As we dig into the implementation itself this will sharpen.

GPIOs and DPIOs

As discussed in the goals section, there are two main concepts that we have:

  1. GPIOs — General Purpose I/O

  2. DPIOs — Dedicated Purpose I/O

A GPIO is the fundamental unit that hardware exposes that has a set of attributes associated with it. As seen in the background section, the actual set of attributes there are large and vary from device to device. Our focus here is on having a uniform way of describing attributes, but acknowledging that attributes are GPIO-specific.

We also want to introduce the idea of DPIO, which is a GPIO that has had its attributes constrained to specific values. At that point, if the GPIO is an output someone could control a single property: whether it is low or high. Similarly if the GPIO is an input, one could only read the current value. It’s important to note that a DPIO cannot exist on its own. It always is created by constraining a GPIO.

These two ideas — GPIOs and DPIOs — are tied together into the various abstractions that we want to create. It’s also worth noting that some of this inspiration comes from [humility] and [hubris]. There, a GPIO is enabled and configured with all of the relevant attributes and then it is easy to use in a way that doesn’t need to think about those attributes.

Kernel Abstractions and GPIO Attributes

This effort will create a new kernel framework for managing and using GPIOs and create a private GPIO provider interface for device drivers. Our intent is that the kernel framework takes care of things like minor node management, character ops, interfacing with other illumos subsystems, and related.

Between a provider and the framework a GPIO is identified by a controller instance-specific ID, generally just an unsigned integer. From there, we would take the idea from MAC link properties, basically describing and defining attributes on a per-GPIO basis. The useful bits of the MAC property abstraction come from the idea of the three property entry points that basically allow one to say separately:

  • Describe a property, its default values, and allowed values.

  • Get the current value of a property.

  • Set the value of a property.

Here, we’d adopt something similar for attributes of GPIOs. While there may be an attribute that can be shared across all GPIOs by such as voltage or the human-name name of the GPIO, the vast majority of these are somewhat different and trying to create a global list will lead to a lot of duplication or trying to fit things that aren’t really the same into shapes. Instead, we suggest that most attributes are scoped to the driver. So if say we had say an AMD SP3 specific driver named for the sake of example sp3gpio, things like how pull up’s are configured, drive strengths, and related would be phrased as a property specific to that. If we look at some of the earlier examples this might result in properties like:

  • sp3:drivestrength

  • sp3:pull

The driver would be responsible for describe what the valid values for these are. The form that these take is private to the driver and framework right now. They could be strings, integers that correspond to enumeration values, or a nested structure. We don’t expect users to interact directly with these right now. Instead users would specify strings that would be translated into the corresponding structure by code in userland that corresponds to each driver.

With this in mind, it’d probably make sense to use nvlist_t structures that are passed around internally with each key in the nvlist_t corresponding to an attribute. This also would provide a relatively easy way for us to pass all of the valid values. Simply fill out all possible values as an array of items in the nvlist_t.

The reason for this is we’d love for when someone lists attributes of a GPIO it becomes obvious what they all are ala dladm’s link properties. For example we could see something like:

GPIO	PROPERTY		PERM	VALUE	POSSIBLE
agpio21	sp3gpio:drivestrength	rw	40	40,80
agpio21	sp3gpio:pull		rw	none	none,up,down
...

The above is just an example of what this could be and this is all still up in the air and will be tbd based on what we actually figure out in the internals, but this suggests the main way that these attributes would come up.

Controller Device Nodes

When we have a GPIO controller, there would be a single character device that corresponds to it. The GPIO controller character devices would have their own minor node type that would be used for discovery. However, the use of these nodes and the ioctls on them would be private to the broader framework and users would not interact with them directly. These would not be expected to provide a stable API.

This minor node would be associated with the kernel framework itself which would have its own general instance ala the ksensor framework. This allows us to not need to interpose on the actual minor nodes and instances that the underlying device driver has and allows them to do their own thing if they want to. This has proven rather advantageous for the ksensor framework and was pretty important when we were extending it to other things like MAC.

An important piece here is that there is not a character device that exists for every GPIO that exists by default. Instead, all information about a GPIO, including its current status is accessed through the controller’s node. While this does preclude locking access to a given GPIO, our intent is that this is done via the DPIO abstraction.

Userland Control Abstractions

Our intent here is introduce a gpioadm(8) command that will be driven by a corresponding library. While we intend to make the command more stable, at this time the library will be private. The gpioadm command will follow in the vein of other commands and have several sub-commands that relate to different aspects of this in a hierarchical fashion. The following describes the tree of commands. Intermediate items would simply list the other things that they can execute:

gpioadm
        controller
                    list
        gpio
              list
              attr list
              attr set
        dpio
              define
              undefine
              list (optional)
              read (optional)
              set (optional)

The intent here is that gpioadm controller list will list all the current controllers and some basic information about the controller itself and other attributes here.

The gpioadm gpio list would list all GPIOs in the system with the ability filter that list based on controller or other filters. The gpioadm gpio list is not intended to also list attributes. This is mostly just to make everything usable. For example, there are maybe on the order of 3-10 attributes per GPIO. And while for a device like the PCA9535 there’d only be 16 GPIOs worth of things to show (times attributes), when we get to say the SP3 use case, there are over 75 GPIOs. So suddenly this makes the list output a lot harder to use if there are a lot of attributes there.

The gpioadm gpio attr list would list the attributes of a single GPIO in a dladm linkprop style. The gpioadm gpio attr set would be able to set a number of attributes for a single GPIO. It’s important to call out that these all work on a single GPIO.

The gpioadm gpio define basically is used to transform a GPIO into a DPIO. This would have the side effect of creating a DPIO character device and allow the DPIO to be named. Once a GPIO has been defined as a DPIO, most of the attributes and related will be frozen until the DPIO is undefined. The gpioadm gpio undefine would be use to remove a dpio.

An idea that is less clear if it is useful right now is to also have a set of dpio subcommands that allow for listing items that are specifically DPIOs (gpioadm dpio list), reading the current value for DPIOs that are inputs (gpioadm dpio read), and changing the output value for DPIOs that are ouptuts (gpioadm dpio set). As we work through this, that’ll tell us what makes more sense here in terms of abstractions and what’ll be useful. Most of the features of reading and setting will be something that can be obtained through gpio attributes normally.

Dedicated Purpose I/Os

We’ve talked a bit about DPIOs, so it’s worth going into more detail about what these are. Specifically a DPIO is a character device that is managed by the kernel gpio framework. This would show up as /dev/gpio/:name. The name must be globally unique on the system. It has the following attributes:

  • open(2) with support for FEXCL allowing exclusive access. In addition, the DPIO can be constrained such that only the kernel can open it via an ldi_open. The latter bit is important to make sure that something that ties into another hardware device or interface isn’t consumable by userland.

  • read(2) which will return a uint8_t a value of either 0 or 1 indicating the current state. This will only work when the DPIO is an input. Read will always return the current value and will not block waiting for any kind of internal event (though mutexes may still need to be entered).

  • write(2) which will allow you to write a uint8_t with either the value of 0 or 1 to set the new state. This will only function when the DPIO is an output.

  • Longer term, we’d like a way to cause a hardware interrupt that fires on change to cause the device to be pollable. The exact form of this in terms of whether we use basic poll bits or want to explore device-specific event ports is an open question and part of the exploration. However, a simple way to describe this is that the device will indicate POLLIN when the hardware notifies us of a change until the next read acknowledges it. The semantics of this will need to be firmed up.

  • We would probably add an ioctl that would allow one to get basic information about the DPIO, such as what GPIO and controller it is associated with. This ioctl is how you would get the information about what the output being driven is (which may be different from what’s read back in, particularly in an open-drain system).

An alternative approach to using read/write is to use ioctls to get and set this. The advantage here is that they give us a bit more control over interface evolution, but they can be more annoying to use.

While the existing controls are useful for ad-hoc configuration, we expect that DPIOs are how folks will generally interact with the GPIO subsystem for more persistent and programmatic consumption, at least initially. The fact that the creation of the DPIO constrains the GPIO is what makes it much easier to tie into other software and why we only really want to expose the more standard character device this way.

Persistence

Persistence and platform configuration is an important thing that we need to consider. We want to give folks the ability to control and persist DPIOs and the corresponding set of attributes that we got there. However, a challenge is that this requires a degree of stability around the set of attributes and how we anticipate naming them that may or may not hold initially.

In theory the platform or machine code if it exists could establish DPIOs. While this can’t be done on i86pc automatically by the kernel, this can where the hardware is more constrained and known. In particular, for Gimlet, we would have the platform apply knowledge about what the set of DPIOs should be and have them come into effect early on. Whether this is something that calls into a platform-hook, or is a file that’s delivered to source this from, or something that the driver just knows initially, isn’t quite clear right now. Longer term, what we believe we should do is have the ability to save a set of DPIOs with their attributes and provide a means of replaying that.

One way that this could be done is that we could save all of the resulting nvlist_t attributes for a given GPIO and the corresponding things reqiured to turn it into a DPIO (e.g. the name of the DPIO, the underlying GPIO controller name and instance, etc.). This is something that the DPIO framework could read at some point ala /etc/path_to_inst or the persistent unit addresses.

Currently our intent is to defer this as someone who wants this could write a service with a series of gpioadm commands. This isn’t ideal, but does at least seem a reasonable starting point. We may opt to implement the kernel-based knowledge based on the machine (which only applies to the oxide platform).

Initial Deliverables

As part of implementing this, our intent is to implement the following:

  • The kernel GPIO framework

  • GPIO providers for:

    • AMD Zen 3 Milan CPUs

    • A gpio simulation driver that can be used to exercise the framework and test things programmatically

    • Userland commands

  • Manual pages for the kernel provider, dpio interface, and userland command

I/O Muxing Complications and Design Thoughts

There is a bit of an elephant in the room that we have tried to pretend doesn’t exist, just as much as we have tried to pretend that the emperor isn’t naked: the actual set of output pins on an SoC package can be switched between multiple, disjoint peripherals. This is true on pretty much every SoC that we looked at above. As examples:

  • The Milan SoC has the SMBus 0 controller, I2C 2 controller, and EGPIO113/114 all share the same set of pins: DA42 and DB42 ([amd-sp3-fds]).

  • The STM32H753 is even more featureful here. Each pin has up to 16 alternate functions that be selected. And here a given controller can show up on multiple different pins. For example, I2C controller 2’s data line can use PB11, PF0, and PH5 ([stm32h7-ds]).

  • The BCM27111 has six alternate functions on pins. So for example GPIO0 in addition to being a GPIO can be used for two different I2C controllers, an external memory interface, SPI, UARTs, and a display interface (§5.3 [bcm2711-arm-periph]).

As you might begin to imagine that this process is a bit complicated. Before proceeding further, it’s worth familiarizing oneself with the AMD SP3 GPIO Guide and in particular, it’s useful overview on multiplexing and nomenclature which establishes a set of terminology for talking and thinking about this, which we use:

  • Pins/Lands: Which refer to the actual electrical contact of a chip.

  • Pads: A chunk of logic in a device that configures and manipulates a specific pin.

  • I/O mux: A device which allows different peripherals (defined below) to share a specific pin. Generally, only one such peripheral is allowed to use a pin at a time.

  • Peripherals: These are different functional units that exist inside a chip. For example these may be I2C, SPI, and UART controllers or other discrete logic blocks.

The fact that all this exists and that there is an I/O multiplexor in the system means that just because one sets up a bunch of GPIOs or tries to send commands to the SPI controller doesn’t mean that anything’ll actually happen. One can often manipulate all the registers for the different peripherals all up front, but unless the SoC points the pins at the peripheral, nothing it going to happen. This in turn requires us to answer the question of how should we think about controlling the I/O mux.

Dynamicism

The first major question that we need to answer for ourselves is how dynamic does this need to be? A lot of this depends on the environment itself and how much information you know in advance.

At one extreme here is [hubris]. In particular, there’s an important aspect of hubris that answers this question in a rather nice way: the total set of applications that may exist and run on a device is known at build time. This means that one can, generally speaking, definitively know the total set of peripherals that will be used and the set of pins that they are on.

More or less, unless someone is using [humility] to manipulate things, this means that we can set up the I/O mux in a relatively static fashion and use that. This doesn’t mean that there isn’t any dynamicism in there. For example, in the case of Sidecar and Gimlet we actually have a single I2C controller adjust between multiple distinct sets of pins. However, from an external perspective, what is needed is all a 'known known' at build time. This is one of the key aspects of Hubris and allows for a lot of power simplifications in its design and implementation.

On the other hand, when we consider a traditional multi-process unixy system, this is the opposite of the case. We generally use a single set of packages and binaries to run on an entire class of systems and instead dynamically discover the set of hardware that is there either through self-describing buses ala PCI or through other means such as ACPI, UEFI, device tree, etc.

Here, the set of applications that can run on the system at run-time is pretty much independent (ignoring things like ISA, etc.) from the actual kernel and broader operating system. While there are lots of ways to try and reason about I/O pin muxing or to use knowledge about the specific application at hand and try and create a specific set of rules, there’s also no reason that you can’t have this truly be dynamic as well.

For example, consider a world where a pair of pins can be shifted between distinct GPIO, I2C, and SMBus controllers. In this case, you could take the dynamicism to the opposite extreme and basically say that the act of opening the controller or using it to drive a transaction on the bus is what switches around and activates the I/O mux itself. That is, there’s no reason that we have to set this in advance and can instead let the system actually drive and dictate that.

Because we are designing and thinking about this right now in terms of the broader more unixy systems where dynamicism exists, this is where a lot of our initial thoughts are turned towards.

Policy Questions

Intertwined with the dynaicism is the matter of how policy can and should be expressed. Let’s discuss some of the different aspects of where policy comes from:

  • A lot of policy comes from the physical nature of hardware. One probably shouldn’t try to run a SPI controller on something that actually is only being used for I2C.

  • Some policy comes from the fact that firmware may assume it is in total control of a given peripheral or that no one will manipulate it out from under them. This makes certain things off limit for the sake of correctness.

  • The end-user of a system may know that they want to perform a certain application and prefer that over another. That is, the owner of the computer may say I want to ensure that these pins can only ever be used for I2C so that way there isn’t fighting for control or the ephemeral EBUSY (though its worth noting this may also occur at even a process level fighting over a single peripheral).

  • Some hardware has the ability to lock the registers (§18 [intel-c620]), which leads to the fact that policy may become more restricted while the system is running.

How exactly this policy should be structured and controller and how it should be implemented from an architectural perspective are a little unclear, but it does suggest that policy may be a discrete, but intertwined piece of the puzzle.

Put differently, while there are drivers and hardware that describes the general relationship between things, it won’t and shouldn’t be able to describe policy itself as that ties into the actual machine implementation and broader hardware platform design, particularly in worlds where you have to deal with platform firmware that may use these.

Structuring Information

If you start to manually try to write out all the conflicts for even a simple SoC with very few alternate functions like an AMD Milan CPU, the rules start getting quite complicated to manually write out. From surveying other system design and implementations, an important detail comes out as implemented by the Linux pinmux subsystem (and possibly others): individual drivers should not try to separately ask the question of what overlaps or implement custom selection logic, instead there should be a single implementation of this.

Put differently, we should instead structure this information such that shared logic in the kernel (whether a nexus driver, library-like misc module, something like ksensor, etc.) should deal with figuring out what is overlapping and presenting things to user land. For example, imagine if a tool could spit out the following:

CTRLR           PERIPHERAL      ACTIVE  NPINS   CONFLICTS
zeniomux        EMMC            Y       8       ESPI, LPC, etc.
zeniomux        UART0/fc        N       4       UART2, GPIO

To help talk about this we can actually structure this information as a large series of data tables. Here are the following layers:

  1. At the top, there are different sets of peripherals that exist. Each peripheral refers to a hardware block.

  2. Next, you have the notion of configurations for a peripheral. In some cases there may only be one, but for others there may be multiple different configurations that have different constraints. As an example, the Milan UART0 may either have access to hardware flow control or it may not. If it does, it requires two additional pins.

  3. For each different configuration, there is a number of different pin sets. Each set represents a different way that the given peripheral configuration may be active.

  4. Finally, each pin set has the actual pins that make this up.

Let’s make this a little more concrete. Here’s a JSONish example (used for making this clear as opposed to saying this should be JSON) of structuring this data we’d do for the AMD SP3 UARTs:

{
    "uart0": [ {
        "config": "basic",
        "sets": [ {
            "pins": [ "CV39", CW39" ]
        } ]
    }, {
        "config": "hardware flow control",
        "sets": [ {
            "pins": [ "CV41", "CV38", "CV39", CW39" ]
        } ]
    } ],
    "uart1": [ {
        "config": "basic",
        "sets": [ {
            "pins": [ "DB36", DA37" ]
        } ]
    }, {
        "config": "hardware flow control",
        "sets": [ {
            "pins": [ "CY38", "DB38", "DB36", DA37" ]
        } ]
    } ],
    "uart2": [ {
        "config": "basic",
        "sets": [ {
            "pins": [ "CV41", "CV38" ]
        } ]
    } ],
    "uart3": [ {
        "config": "basic",
        "sets": [ {
            "pins": [ "CY38", "DB38" ]
        } ]
    } ],
    ...
    "egpio140": [ {
        "sets": [ {
            "pins": [ "CY38" ]
        } ]
    } ]
}

With the above declarative declaration we would know that if we CY38 as EGPIO140 then we can’t use uart3 in its basic configuration or uart1 with hardware flow control, though we’d still be able to use a basic uart1. Similarly if someone asked to use uart0 with hardware flow control the corresponding uart2 and GPIOs would all be blocked out.

While the above with AMD only includes a single set of pins that can be used for each peripheral configuration, this is not the case on the H753 and other smaller devices.

Driver Design Implications

While the overall I/O mux part of this RFD is still exploratory, it does suggest that the nexus and more generally the ndi has a role to play here (though this should not be treated as a commitment to that fact). Ideally individual drivers should not need to know about the specifics of the actual pins or other pieces here. Instead, they should just know that a certain set of configurations is available to them and be able to request and hold the system in a given configuration, say while open or while waiting for some other event to complete.

Carrying on our UART example, the parent (e.g. huashan or a similar FCH driver) is responsible for knowing that up to 4 UARTs exist on this system and the different set of configurations that may or may not exist. It would need to program what exists specific to the hardware and know that if we’re on an SP3 system it is these set of pins; however, on SP5 it is something else and if we’re on a client system the number and count of such devices changes again.

This has a complication with pad configuration as the way that pads should be set may vary between different peripherals and it’s not quite clear right now.

However, it’s worth noting that whatever is setting policy is somewhat disjoint from this part of the stack. That is, if you imagine the parent of the UART driver exists on many different AMD systems, whether an i86pc-based or oxide system. However, on i86pc there are different constraints that come from ACPI / UEFI that constrain whether that is even possible to use or not due to the fact that AML may itself tweak and change something into a GPIO as part of its implementation under the hood. That is, what is safe, is a harder question to answer there. Conversely, on an Oxide system we may have a very different policy because we actually know everything that is going on.

While none of this in itself a plan, it does point out several things we need to consider when we dig deeper.

In the short term, we are likely to stay focused on the specifics of Gimlet and then come back and fill this out as we have more experience and figure out what actually makes sense for muxing.

Determinations

We will prototype and experiment with the described set of interfaces and incorporate them into stlouis as part of the MVP.

Security Considerations

The biggest challenge with GPIOs is that because one can manipulate the core electrical state of pins on the system, someone can easily create permanent damage. For example, if a pin is truly an input with some other external device driving it, using a GPIO to turn it into an output driver can wreak havoc and more likely than not permanently damage the pin. While folks often talk about a root user being able to do damage to the system such as rebooting it, erasing all the data on say hard drive or solid state drives, accessing other users files, etc. It is not often the case that these actions lead to permanent physical damage.

As such, we need to ensure that we lock down and use the appropriate privileges in the system to limit what one can do with GPIOs. The idea of DPIOs and their use is intended to make it so it is less likely that a consumer can accidentally change an attribute that they shouldn’t (e.g. drive an input). However, we should not assume that this is a panacea.

External References

  • [acpi-62] UEFI Forum. Advanced Configuration and Power Interface Specification. Version 6.2. May 2017.

  • [amd-genoa-ppr] Advanced Micro Devices. Preliminary Processor Programming Reference (PPR) for AMD Family 19h Model 10h, Revision A0 (Stones) Processors. Publication number 55901. Revision 1.32. January 2022. Distributed only under NDA.

  • [amd-milan-ppr] Advanced Micro Devices. Preliminary Processor Programming Reference (PPR) for AMD Family 19h Model 01h, Revision B2 (Genesis) Processors. Publication number 55898. Revision 0.57. January 2022. Distributed only under NDA.

  • [amd-sp3-fds] Advanced Micro Devices. Socket SP3 Processor Functional Data Sheet for AMD Family 17h Models 00h-0Fh, Family 17h Models 30h-3Fh, and Family 19h Models 00h-0Fh. Publication number 55426, revision 1.04. October 2020. Distributed only under NDA.

  • [bcm2711-arm-periph] Raspberry Pi. BCM2711 ARM Peripherals. Release 4. January 2022.

  • [hubris] Oxide Computer Company. Hubris: A lightweight, memory-protected, message-passing kernel for deeply embedded systems. https://github.com/oxidecomputer/hubris/.

  • [humility] Oxide Computer Company. Humility: Debugger for Hubris. https://github.com/oxidecomputer/humility/.

  • [intel-c620] Intel. C620 Series Chipset Platform Controller Hub Datasheet. Document Number 336067-007US. Revision 007. May 2019.

  • [nxp-pca9506] NXP. PCA9505/06 Product data sheet. Revision 4. March 2010.

  • [rfd129]] Oxide Computer Company. RFD 129 Gimlet: Storage Midplane. https://rfd.shared.oxide.computer/rfd/0129

  • [rfd139]] Oxide Computer Company. RFD 139 Gimlet: Chelsio T6 Implementation. https://rfd.shared.oxide.computer/rfd/0139

  • [rfd144]] Oxide Computer Company. RFD 144 Sidecar: Detailed Design. https://rfd.shared.oxide.computer/rfd/0144

  • [rfd164]] Oxide Computer Company. RFD 164 Gimlet SP3 Pin Assignments. https://rfd.shared.oxide.computer/rfd/0164

  • [rfd215]] Oxide Computer Company. RFD 215 The oxide Machine Architecture. https://rfd.shared.oxide.computer/rfd/0215

  • [stm32h7-ds] ST. STM32H753xI Datasheet. DS12117 Rev 7. April 2019.

  • [zen-gpio] Oxide Computer Company. AMD SP3 GPIO Guide. https://github.com/oxidecomputer/shared-engineering/blob/master/zen-gpio.adoc