Of Pins and Multiplexers (stereo blinky)

with tags kinetis FRDM-K64F slib embedded baremetal api design gpio pinmux -

In the previous article in the series, we ended up with FRDM-K64F’s SDA MCU (K20) executing our code and blinking LED correctly, but the code on the MAIN MCU (K64) did not start.

The reason why MAIN MCU wouldn’t start was because one or more pins on the SDA MCU were keeping the MAIN MCU in a state that wouldn’t allow it to execute code. In order to change this, we need to manipulate the signal levels and configuration of the pins on the SDA MCU. In order to know what we’re doing with pin configuration, we’ll need to take a small detour to pin multiplexing.

At the end we’ll have a small trinket which reacts to user button presses and potentially makes the user blind with different colors. (The LEDs are very bright so please take care when working in the dark.)

Terminology

Some introduction to terms might be in place for readers that are not yet completely at home with system-on-chips (SoCs).

Pin: An interface for a signal (voltage level, current) by which a SoC is glued to the rest of the design (often called a “board”). A single SoC will always have at least two pins: one to supply it with operating voltage (and current) and the other to sink the current (ground). A two pin device however does not have any other ways of interfacing with the world, so it’s common to have at least three pins. Depending on actual package used (see below), pins might look like metal “legs”, “balls”, or just shiny “pads”. We’ll refer to them as pins in all cases for convenience.

Package: The silicon “guts” of the SoC and the metal pathways to the pins are combined together within a plastic or other non-conductive material to make the combination rigid and to withstand environmental changes (heat and mechanical shock mainly). Different package technologies exist to cover differing needs (vendors will package into anything that they think they have a market for). Hobbyists normally work with SOIC, DIP or TQFP packages which are easy to solder manually and perhaps attach probes to. Mass production on the other hand will value price and small physical size (BGA & WLCSP). Devices designed for industrial use and use in space also have their own preferred packaging. The Wikipedia article on package types is a convenient gateway to the wonderful and surprisingly varied world of packaging.

Multiplexer (mux): In this article, means a “selector” which is a device that has multiple devices to connect to, but only one connection leading out of the device. A simple analog is a TV: it can receive multiple channels, but you can only view one of them at a time. A TV however is uni-directional, while a multiplexer in this article can be two-directional. Think of this as watching the news and talking back to the news anchor (and quickly switching the channel again before they can answer back).

Further, faster, cheaper!

Selecting a suitable SoC for a project often goes something like this:

  1. We have a rough idea on what we want to achieve. Sometimes very rough indeed.
  2. We’ll have a list of additional chips that we’ll want to interface with, like sensors, stuff required for Wi-Fi, SD access, display and so on.
  3. If our (not yet selected) SoC will run software that will talk to the chips selected, this talking is normally done over “specialized hardware accelerators”. These specialized hardware accelerators are called “integrated peripherals” of a SoC. If you go back enough years, whole systems were built from discrete processor chips and the peripherals were each also in their separate packages. Then along marched the inevitable progress of miniaturization and gave us the SoC and everything was moved within a single package (and same silicon die as well). Calling the peripherals specialized hardware accelerators now makes more sense. I will mostly call them peripherals although some people use this term to refer to logic chips outside the main SoC.
  4. SoCs with more integrated peripherals (normally) cost more than SoCs that have a limited number of peripherals, so next we try to find a SoC that has the required number of peripherals that we can satisfy our design with.
  5. Assuming we have no problems (har), we clack at the keyboard for a while and presto, we have a product!

Now, number 5 never actually happens this way, so let’s never again mention that.

If you’re a SoC vendor, then the following realities hold true:

  1. You want to expand your market to include new (hopefully profitable) customers.
  2. Reusing existing designs for peripherals is almost free, while developing new peripheral implementations is quite expensive. Buying peripheral designs is also an option, but carries the risk of an external dependency.
  3. Cost of including more instances of a single type of peripheral is cheap. (Die-space is cheap, and we’ve yet to reach commercial limits here.)
  4. Cost of supporting a single packaged chip is relatively high. Here the cost is realized by each package version being a separate SKU and each version of the SoC requiring separate documentation and support from production line.
  5. Cost of developing testing and supporting software (SDK) is even higher. (I’m guessing since I’m not privy to this data.)
  6. External price (that the customer sees) rarely needs to reflect the actual pricing of development. Total development price might also be impractical to calculate, because a lot of the resources and designers can be shared across multiple separate product developments.

Now, if we take point 4 from the buyer side and the vendor points together, we see that for vendor, it makes sense to pack as many peripherals in a single package as can fit. Remembering the second point however means that keeping the number of separate packages as low as possible also makes sense.

So, the vendor should pack as much as they can fit into a minimum number of packages. This leads to the idea that if we could somehow pack more peripherals than the package’s pins can support, we would gain something for free.

This is exactly the problem that pin multiplexing solves. For each pin, there are up to N different peripherals (and instances) to which the pin could be connected to internally within the package. A pin can normally be connected to only a single peripheral at a time. In most SoCs, it is also possible to change the peripheral to which the pin is connected during runtime. Runtime changing between several peripherals is rarely useful, but switching between being connected to the GPIO functionality and a peripheral is quite often useful.

Now, in the real world, not all SoCs have pinmuxes, not all pins are muxable (some have a single “fixed” function), and the choice between the peripherals to which a single pin could be connected internally might be rather small and/or silly.

In a way, pin multiplexing is an overcommit of package pins by the internal peripherals. This also means that while we might find a SoC that looks like it will satisfy our needs in the number of UARTs, SPIs, I2Cs etc, then like the cake, it might be a lie. Vendors’ web sites often have parametric selector aids which allow one to limit the search to parts with specific number of each type of peripherals, but the tools rarely verify whether your set of peripherals can be selected simultaneously in the package that your SoC will use. Caveat emptor.

In an ideal world, I would send a list of peripheral counts and minimum speeds to the vendor, and tell what kind of package I’d like the whole thing in. The vendor would then make a production run of those chips for me with a custom reference manual and all. However, the world is quickly moving to the opposite direction, making this option all the time more expensive. I could also use FPGAs for my design, but I’m not that much into designing my own specialized hardware accelerators (yet, although Chisel sounds fun).

NOTE: N seems to be quite a low number in the real world. Maximum seems to be 8.

Pin multiplexing on Kinetis K Series

For this series of articles, I’ve been playing around with various K series reference manuals, datasheets and SVD files. This has led to the following list of rules that the vendor seems to obey:

  1. The number of different packaging options seems to be quite limited (the same package types are reused across the series).
  2. Individual signals from peripherals are always mapped on to the same pins across chips, if the peripheral is present. Even the pinmux selector value is the same across the series in this case.
  3. The same package type across different SoCs within the series uses the same pin configuration (and pinmux configuration).
  4. Not all pins are pinmuxable, but most are. For example, pins used for USB differential signaling are “fixed function”.
  5. Each pinmuxable pin has “8 channels” to select from:
    • ALT0: Connection to all internal digital logic severed and the pin is connected to internal analog parts instead. Latter assumes that the pin has an analog peripheral signal to connect to, since not all pins do.
    • ALT1: Connect to a GPIO peripheral (allowing driving high and low voltage levels, as well as sampling voltage as input).
    • ALT2-7: The rest of the channels are connected to the internal signals of the various peripherals in the SoC. The reference manual will have a multiple page table describing this.
  6. There are still some holes in the K series pinmux map, so new peripherals can be included in the future.

All of this means that even if the packages are of different size or pin count, there’s a great deal of compatibility between the products, making switching between different SoCs in the family quite easy. This way the design can start doing a minimum-viable-product on a larger SoC (like the K64 on the FRDM-K64F), and then move to a more properly sized SoC like the K20. Obviously there are additional parametric limitations that might affect “design mobility” (e.g. power management capabilities of older generation chips vs newer generation chips.)

NOTE: My list included some of the chips in K20, K26, K64, K65 and K66 series, which by any means is not exhaustive. However, doing comparative analysis of the pinmux tables takes time, so I drew the line at these chips.

Kitchen knives anyone?

But wait, there is more! The pin-level mux is not the only mux in the Kinetis series. There are also multiplexers that select which ADC channel is used (each channel terminating to a specific pin externally, but a more complex setup of pairing channels is also possible). There’s also a possibility to run the FlexBus logic so that the meaning of the FlexBus pins will differ (via internal mux in the FlexBus) and there are several other muxes too!

However, the rest of the muxes are all peripheral type specific, and are not orthogonal between each other (the peripherals in general are quite dissimilar in Kinetis due to a long product development history and design recycling). Also, we don’t need to switch any of those other muxes in order to get the MAIN MCU running.

General-purpose Input/Output (GPIO)

If the digital output voltage level of a pin can be set directly via software, it is already general in the way that we can use the pin for whatever our fancy in our design. When we set the voltage, we’re using the GPIO pin in “output” mode. Normally such pins also support sampling the input voltage level (called often “input” or “in”). Depending on the SoC, it might be possible to do both simultaneously, or the “direction” of the pin needs to be set first, which will alter how the electrical buffer attached to the pin behaves.

GPIO on Kinetis

All of the pins on Kinetis K series that support pinmux configuration have the option to attach the pins to GPIO logic. There are as many instances of GPIO peripherals as there are PORTs (which are the pinmux peripherals and input change detection engines). So, in theory the maximum number of pins that we could twiddle is 160 (5 instances with 32 pins per instance). Most Kinetis K packages don’t expose this many pins, so sadly we’ll be restricted to smaller sets.

Kinetis requires switching the direction of the pin if we don’t want to drive a level out (either low or high, zero or one).

Pin Multiplexing with General-purpose Input/Output

In order to get the MAIN MCU up and running, we’ll need to drive high level on pin PTD6 of the SDA MCU. This pin connects to the external MIC2005-0.8YM6 current limiting switch, behind which is the rather complicated logic of FRDM-K64F that selects which power source should be used to power the MAIN MCU.

There are many ways of solving this rather easy problem, each of which will be described below.

KSDK v1 (previous SDK for Kinetis series)

KSDKv1 provides functions at two levels: either very trivial inline functions that will wrap your simple operation to a single register field update (almost always using a read-modify-write -sequence), or much higher level utilities that hide everything under them.

The following sequence can be used and is similar to the sequence within the high level utility GPIO_DRV_OutputPinInit(..):

// D is the 4th letter in alphabet, and since we start counting from zero, it is
// the 3rd. This also neatly exposes a problem with many vendor SDKs: it's
// difficult to design orthogonal APIs and API parameter types, and here we see
// a function that uses an peripheral instance number as the parameter, and the
// rest will use pointers to the memory mapped peripheral address. On Kinetis
// there seems to be a 1-to-1 mapping between the two, but this is not utilized
// by KSDK.
CLOCK_SYS_EnablePortClock(3);
// Modify (only) the MUX field of the PCR register for pin 6 of PORT D so that
// we select ALT1 function for the pin (enumerated in KSDK as seen).
PORT_HAL_SetMuxMode(PORTD, 6, kPortMuxAsGpio);
// Switch (only) the DIR bit to indicate that we want to activate the buffer in
// order to "set" a voltage. The function call will cause a voltage to be set
// as well, even if this is not evident from the function name. Try to guess
// what the voltage level driven is after this function returns.
GPIO_HAL_SetPinDir(GPIOD, 6, kGpioDigitalOutput);
// 1 here represents the signal level (high) that we want to drive out (as
// opposed to 0 for driving low out)
GPIO_HAL_WritePinOutput(GPIOD, 6, 1); 

Now, you could also use these lower level functions directly, and while the performance impact is still quite low, this becomes quite tedious, especially if you always need to carry with you the concept of “PORTx, y” and “GPIOx, y”- pairs.

The higher level GPIO functions use a combined type “Pin” which is an unsigned 32-bit integer where the lowest 8-bits encode the pin index (pin number within one PORT or GPIO peripheral instance), and the next higher 8-bits encode the instance number of the peripheral. So, in fact the tedious parameter pairs are converted into a single flat “integer” space, which allows easy renaming of pins when required or using pins in variables and #defines..

KSDK v2 (current SDK for Kinetis series, also included via MCUxpresso)

Seems that something really drastic happened after KSDKv1 was pushed out, and in many ways, KSDKv2 looks like a clean rewrite of everything. There’s hardly any of the KSDKv1 orthogonality issues left and they managed to clean up the USB stack considerably, so yay for that.

However, most of the APIs changed in quite incompatible ways, and for example for GPIOs, there’s no longer a high level API. So if you were previously using GPIO_DRV_OutputPinInit or GPIO_DRV_Init(..) (even higher convenience function), then tough. “Ah, but I was already using the lower level functions, they won’t change much, right?” Well, all the enumerations changed, and some functions started using structures as parameters too. So, fuster cluck for anyone trying to write code that would run both on KSDKv1 and KSDKv2.

// (no high level utilities exist, so, we use the low level helpers only)
CLOCK_EnableClock(kCLOCK_PortD);
// the alternative is using PORT_SetPinConfig, which will override the mux, but
// also all other settings except for the event configuration bits (highest
// 16-bits of PCR)
PORT_SetPinMux(PORTD, 6, kPORT_MuxAsGpio);
const gpio_pin_config_t pinConf = {
	.pinDirection = kGPIO_DigitalOutput,
	.outputLogic = 1,
};
GPIO_PinInit(GPIOD, 6, &pinConf);

So, pretty close and similar to what we had before, but now instead of passing the direction and new value as parameters to GPIO_PinInit, a pointer to a structure is passed instead. So, still slightly ewwy.

CMSIS-like register manipulation

Suppose you’d have to support both KSDKs simultaneously with your codebase. You could of course write higher level wrapper functions to encapsulate both APIs, but with that wrapping you’d always get some undesirable side-effects (debugging being deeper by one call level at least).

By throwing out the utilities completely, we will arrive at code that will work irrespective of which KSDK version is used:

// Macro to shift the MUX field of PCR into the proper position
#define PORT_PCR_MUX(n) \
  ((uint32_t)(((n) << PORT_PCR_MUX_offset) & PORT_PCR_MUX_mask))

// Enable clock for PORT D (otherwise bus fault)
SIM->SCGC5 |= SIM_SCGC5_PORTD_mask;
// Configure direction for pin 6 of GPIO D instance (PTD4)
GPIOD->PDDR |= (1 << 6);
// Drive it high (PS = Pin Set (>1), as opposed to clear (>0))
GPIOD->PSOR = (1 << 6);
// Select MUX (and set all bits to zero in PCR, clearing all other features)
// The value going into MUX field is the ALT choice, ALT1 = GPIO
PORTD->PCR[6] = PORT_PCR_MUX(1);

Now, hardly readable anymore, especially once you remove the comments. And especially if you never have read the Kinetis reference manual and are scared of doing so. But this is technically the minimum number of actions that needs to be done. You might also notice how the order of actions has changed slightly. This order is correct also in the case that the pin has been used for something else that left the level low, or there is an external pull-down on the signal before we switch to driving to high level.

For extra crypticity, you could resolve the addresses of individual registers in the memory mapped peripherals and “poke” values directly to internal bus addresses as well. This would result in the same code being executed by the CPU after compilation, but would be even less maintainable (sounds like a challenge, right?). Those kind of sequences are sometimes used by JTAG/SWD programming tools since they might only support “writing” and “reading” absolute values to specific bus addresses.

NOTE: The code above uses slib/regmap generated structures, but the same code with minor modifications will also work with the vendor provided structures and macros.

Vendor lock-in

Nothing would make the SoC vendor happier than us using the vendor SDK functions, and they’d probably be also happy for us littering our code with cryptic register update sequences (last example above). As long as we cannot take our code and run it with minor or no modifications on another target, the vendor has us in their pocket. I’m not claiming that the pocket is a bad place to be necessarily, but it is an external dependency. If your project is large enough and you plan your software to outlive your currently shipping product, these things start to matter.

So, can we salvage something from the above? Luckily for us, we know that all vendors behave similarly, replicating business models from each other after some delay. This also means that utilization of pinmuxes is common, most of the GPIO peripherals work in a similar manner, and all of this stuff can probably be modeled and used in a portable way.

Simple API for PinMux and GPIO

We start with the realization that the number of pins available in a SoC package does not change during the runtime of our system. This means that we should have all of the information that we need during build time. We decide to represent all software accessible pins with an unsigned number that is split to instance and pin index parts. Instead of leaving holes (like high level KSDKv1 interface), we use flat consecutive allocation of codes.

So, something like this:

#include <stdint.h>

// A simple linearly identifiable pin. enough for 8 ports, if each has 32 pins.
// Kinetis only has max 5 (so over half populated).
typedef uint8_t Pin;

// Create a pin from instance and pin index
#define MAKE_PIN(instance, pinIndex) \
  (Pin)( ((instance) << 5) | pinIndex )

// Get the instance from pin
#define PIN_GET_INSTANCE(p) ((p) >> 5)
// Get the pin index from pin
#define PIN_GET_INDEX(p) ((p) & 31)

We then proceed to name all of the pins that are present on a package, similar to this:

// Declare configurable pins available on K20_50
#define PIN_PTA0  MAKE_PIN(0, 0)
#define PIN_PTA1  MAKE_PIN(0, 1)
...
#define PIN_PTA4  MAKE_PIN(0, 4)
#define PIN_PTA18 MAKE_PIN(0, 18)
#define PIN_PTA19 MAKE_PIN(0, 19)
#define PIN_PTB0  MAKE_PIN(1, 0)
...
#define PIN_PTD6  MAKE_PIN(3, 6)
#define PIN_PTD7  MAKE_PIN(3, 7)

Having a list of actual usable pins per package makes it more difficult to use the wrong pins by mistake (since they don’t exist at build time). There are other ways of achieving this as well, and perhaps we’ll return to those in the future.

At this point, we move just “outside” of the SoC and name the signals attached to the pins in our design. This allows our code and us to refer to the pins by their schema-intended function. The names in this case are from the FRDM-K64F schema, with a minor notational change:

#include "configuration/pins_k20_50.h"

// These pins are used to program K20_50
#define PIN_SDA_JTAG_TCLK     PIN_PTA0
#define PIN_SDA_JTAG_TDI      PIN_PTA1
#define PIN_SDA_JTAG_TDO      PIN_PTA2
#define PIN_SDA_JTAG_TMS      PIN_PTA3

// Turns into SDA_SWD_EN via U9 buffer
#define PIN_nSDA_SWD_EN       PIN_PTA4
...
// Sinks current going through D2 (Green LED)
#define PIN_SDA_LED           PIN_PTD4
// Can be used to measure the voltage via a divider (ADC possible)
#define PIN_SDA_USB_P5V_SENSE PIN_PTD5
// Can be used to enable the MC2005 switch to supply additional power
#define PIN_POWER_EN          PIN_PTD6
// Feedback from MC2005 to indicate fault (overcurrent etc)
#define PIN_nVTRG_FAULT       PIN_PTD7

Using schema signal names in code means that when there is a problem and one needs to locate the relevant schema part based on code in software, one can use the “Find” feature in the schema viewer. You can always make aliases for internal software use if required, especially if the schema names are weird or don’t make sense to software people. The two UART signals in FRDM-K64F are a good example of where aliases for software might make sense.

We’ve now dealt with the pin and parameter dilemma in a satisfactory way. If we have a much larger design that exposes more than 256 pins, we can easily just scale to a larger type for the Pin type, but odds are that most SoCs will get away with uint8_t for the type. Instead of defines, we could also use enumerations. While on the surface using them does provide additional debugging super powers, there are also some drawbacks. Perhaps later a comparison will be done with real pins to see which way is superior to using plain uint8_t.

Next we think about the pinmux problem. We probably have to have some hardware specificity in the number of alternative selections and flags related to electric operation, so we’ll define them per SoC type. This also allows us to declare the defines in a way that results in no run-time processing. Perhaps best demonstrated with an example for Kinetis. If you also view the PCR register from the reference manual at the same time, you will see how to the mux values map into hardware:

// Disable connection to digital circuits (analog connection might exist)
#define PINMUX_DISABLED (0)
#define PINMUX_ANALOG   PINMUX_DISABLED

// Alt settings 2..7 select non-GPIO peripherals (if any)
#define PINMUX_ALT2     (2)
#define PINMUX_ALT3     (3)
#define PINMUX_ALT4     (4)
#define PINMUX_ALT5     (5)
#define PINMUX_ALT6     (6)
#define PINMUX_ALT7     (7)

You might notice that ALT1 (GPIO) is missing from the list. It is handled in a slightly sneaky way as follows. The MUX field is exactly 3 bits wide (big enough to hold 0-7). Placing value 1 in that field will encode ALT1 as one might have expected. However, since it’s quite common to select whether we want to use a pin for input or an output when we’re already setting up the mux, we implement the following trick:

// Pseudo pinmux modes that all select the GPIO ALT1 but additionally cause
// configuring of the respective GPIO peripheral. These all alias to ALT1
// setting if all but the lowest 3 bits are masked out.
#define PINMUX_INPUT       ((0 << 3)+1)
#define PINMUX_OUTPUT_LOW  ((1 << 3)+1)
#define PINMUX_OUTPUT_HIGH ((2 << 3)+1)

By isolating the three lowest bits of the values, we’ll always get 1, which is the code to put into PCR’s MUX field. But we also provide an easy way to specify what we want to do with the GPIO peripheral if the lowest bits represent ALT1.

Finally, we need a method to toggle various on/off features which are mostly related to low level electrical operation of the pin or its buffer. The list below is just a starting point, but most SoCs nowadays support both internal pull-ups and pull-downs, so let’s start with those:

// Pin flags, modifying the operation somehow
#define PINMUX_FLAG_NONE     (0)

// Flag to request pull-up
// NOTE: Other flags will be added later, or re-designed perhaps
#define PINMUX_FLAG_PULLUP   (1 << 0)
#define PINMUX_FLAG_PULLDOWN (1 << 1)

Here we deviate slightly from Kinetis internals, as it actually has a separate bit to enable pullup/down and another one to select whether the enabled pull is up or down. To avoid confusion, we define only the combinations that make electrical sense.

Which brings us to the final boarding call:

void Pin_configure(Pin p, uint8_t pinmux, uint8_t flags)

Which in our case would look like this:

// Enable clock for PORT D (otherwise bus fault). Yes, this is somewhat ugly but
// a better solution will be developed later. However, explicit clock control
// is still better than hiding this into the depths of some SDK function.
SIM->SCGC5 |= SIM_SCGC5_PORTD_mask;
// Drive the level high and switch the pin to GPIO so that the high level
// appears on pin. This switches power on the MIC2005, so that K64 can get power
// as well
Pin_configure(PIN_POWER_EN, PINMUX_OUTPUT_HIGH, PINMUX_FLAG_NONE);

And this finally will enable us to have blinky running on both of the SoCs on the board (great excitement!):

Blinky running on SDA and MAIN MCUs

Actual implementation for the Pin_configure(..) function, as well as for useful utilities like Pin_getInput(), Pin_setOutput() and friends are available at the slib link at the end of this article. You will hopefully notice that there’s nothing really special in implementing these functions on Kinetis, and in-fact, it really is this easy on other SoC families as well. The GPIO utilities are not very efficient, but more efficient ones will be presented in a later article.

As to how I arrived at knowing that just the POWER_EN signal needed to be switched high? From previous article, you might remember that one task in the long list of things tried was about going through the schema and trying to figure out what the dang was going on. I had three more or less obvious signals to experiment with: the SWD_RST, the SWD_OE and finally the POWER_EN driving the switch. Since switching the pins to specific configuration was now simple, I just made a program that configured all three to the configuration that I was guessing might work, and once the MAIN MCU started, I commented out the pins that weren’t needed. So, while I’d like to claim that “I just saw the solution in the schema”, it was really brute force that solved this. Helped by a nice API, but brute force nevertheless.

Simple games for simpler times

The “trinket” code running on the MAIN MCU is something like this:

// start with blue, but we can switch between three different internal LEDs to
// toggle (not always while previous would be toggled off). So, in effect we
// have three LEDs controllable by the MAIN MCU: red, green and blue.
Pin toggleTarget = PIN_LED_BLUE;

while (1) { 

  // stay for a while, in the most generic way that GCC cannot optimize away on
  // Cortexes. This sadly brings some assembler into our project. We could've
  // also just done: (void)GPIOA->PDIR , which will delay the core (even more
  // than a nop), and while this would be without assembler, our code would now
  // rely on that specific peripheral being powered on and available. We could
  // perhaps use CMSIS intrinsic inline helpers as well (__NOP()). The current
  // code demonstrates the only proper way of doing a nop with GCC, so it's
  // somewhat useful.
  for (uint32_t u = 0; u < 1000000; u++) 
    asm volatile ("nop");
  }

  Pin_toggleOutput(toggleTarget);

  if (Pin_getInput(PIN_nSW2) == 0) {

    // button is being pressed, advance to next LED. This switch could probably
    // be replaced with a constant array of three Pin values and we'd just push
    // iterate through the array instead, but whether this leads to better code
    // or better clarity is really in the eye of the beholder

    switch (toggleTarget) {
      case PIN_LED_RED:
        toggleTarget = PIN_LED_GREEN;
        break;
      case PIN_LED_GREEN:
        toggleTarget = PIN_LED_BLUE;
        break;
      case PIN_LED_BLUE:
        toggleTarget = PIN_LED_RED;
        break;
    }
  }

} // while(1)

The fun part in the code is that we just advance to the next color channel of the tri-color LED when we detect the button is being pressed down. We don’t actually know whether the LED is on or off at that point (since we’re just toggling it mindlessly). Keeping the button pressed for multiple iterations of the loop for example will result in a sequence of different colors being blinked through, and one can also try to get a specific color by holding the button down “just so”. Needless to say, it takes a specific kind of person to enjoy games made from a single LED and a single switch.

Other solutions

The problem of modeling pinmux and GPIO pins has been solved before, for example in Linux. The Linux model represents a GPIO on a very high level so that drivers within the kernel do not need to care whether the pin that they’re manipulating is connected directly to the SoC, or located behind an I2C port extender or two. The model also supports dynamically adding and removing ranges of pins all during regular kernel execution. The high level modeling approach does enable great flexibility (when utilized correctly), but requires many things that might be prohibitive in a resource constrained environment. The Linux approach also incurs forced performance penalty even if the pin to be manipulated is directly accessible in a SoC. For blinking LEDs this might not be a big issue, but for high speed software protocol implementations it might. It is also quite rare to require such dynamic capabilities in resource constrained devices, so all of the features would end up as code and memory bloat that would be difficult to get rid of.

Source code

Please find this delightful code dump for your enjoyment and joke material: https://github.com/majava3000/slib/tree/2017-03-08

(You might notice that compared to the previous article, the repository now has even less proper structure. I do apologize, but I haven’t really found a good solution yet to overall structure of the repo. I do have some ideas floating around the old cuckoo box, but it will take some custom tooling and that requires a bit of time to implement. Ideas are welcome though!)

There might be a pause before the next article in this series, but more are coming!

Written by Aleksandr Koltsoff
comments powered by Disqus