Using STM32 GPIO with ChibiOS PAL Driver

General Purpose Input Output

An STM32F103VC having LQFP64 package

The STM32 is equipped with an extremely flexible General Purpose Input Output (or GPIO) peripheral allowing to configure each Input/Output independently. The IO is the simplest interface between the STM32 and the outside world.

As we said in the article “From 0 to STM32“, there are many versions of the same peripherals across the various STM32’s sub-families and this is way each sub-family usually has its own Reference Manual. In this document it is possible to find all the functional information about GPIO and reading many RM we can notice that GPIO peripheral has three hardware version:

  • GPIOv1, the first version of the GPIO used on the first subfamily (the STM32F1);
  • GPIOv2, the second version which has been used on almost every subfamily;
  • GPIOv3, used on the STM32L4 and  STM32L4+.

The GPIOv3 is a GPIOv2 with a little of touch-up indeed the only difference is an additional feature and an additional register for the newer version. The GPIOv1 instead is like the ancient version of GPIOv2. I have to say (my personal opinion here) that the whole STM32F1 appears rudimentary in many ways. In its defense, debuting in 2007, this is the oldest STM32 subfamily (and it still kicks ass).

In this article we will mainly concentrate on GPIOv2 which is the most diffused GPIO but we will point out differences with other versions whereas it is possible.

Despite its complexities, the GPIO can easily be addressed through a software driver from ChibiOS/HAL which offers full support for those functionalities so we do not have to act at register level to use this peripheral. The driver which manages the GPIO peripheral is named Port Abstraction Layer (also shortened PAL).

In this article, we will take a look to the GPIO peripheral explaining how to use it with PAL explaining its API and common use cases.

Functional description

The concept of port

STM32’s I/O pins are organized in groups of 16 elements numbered from 0 to 15. Each group is named Port and identified by a letter: so, we have GPIOA, GPIOB, GPIOC and so on. Pins are hence identified by the combination of the letter P,  the port identifier (A, B, C,…) and the pin identifier (0, 1, 2,..). For example, we have PA1, PB12, PF3, PH0, etc.

From here and out we will refer to the single GPIO pin as pin and to the whole GPIO (the group of 16 Pad) as port.

Each port has many 32-bit registers some of which are dedicated to configurations: acting on these registers it possible to configure independently each pin changing its mode, output speed, output type and pull up/down resistor.

The following figure represents the principle diagram of a single pin

The principle diagram of a pin

Looking at the figure we can easily spot (from the right to the left):

  • the pin itself, represented by the green square on the right;
  • two protection diodes in dark orange;
  • a configurable Pull-up/Pull-down resistors in dark blue;
  • the input driver block in azure;
  • the output driver block in orange;
  • the data and reset register in aquamarine.

Acting on configuration registers it is possible to choose whereas a pin must use the input or output driver, enable/disable pull up/down resistors, bypass the TTL Schmitt trigger, rearrange the output driver.

Note that GPIO registers are synchronous and clocked through the AHB (advanced high-performance bus). The refresh rate of the value of these registers depends on the clock speed of the AHB.

Pull-up/Pull-down resistors

In electronic logic circuits, a pullup resistor is a resistor connected between a signal conductor and a positive power supply voltage to ensure that the signal is in a high logic level state when external devices are disconnected or the circuit itself is in a high-impedance condition.

Similarly, a pull-down resistor is a resistor connected between a signal conductor and a negative power supply voltage (or ground) to ensure that the signal is in a low logic level state when external devices are disconnected or high-impedance is introduced.

GPIO comes with configurable Pull-up/Pull-down resistors. Acting on the Pull-up/Pull-down Register (abbreviated  as GPIOx_PUPDR whereas x is the identifier of the port) it is possible to configure the behavior of the dark blue network of Fig.2 choosing between:

  • Pull-up resistor;
  • Pull-down resistor;
  • Floating.

The output driver

The diagram of a CMOS NOT gate

The output driver could be modelized with a CMOS NOT gate. This gate inverts the input signal and its working principle is shown in Fig.3.

It is composed of two MOSFETs: a PMOS on the high side and the NMOS on the low side. This drivers act as complementary switches: they are not closed simultaneously.

When the input is high the NMOS act as a short-circuit and the PMOS as an open-circuit: the output is pulled down to GND. When the input is low the PMOS act as short-circuit and the NMOS as an open-circuit: the output is pulled up to VCC.

For the previous reason, the PMOS is also known as an active pull-up network and the NMOS is also known as an active pull-down network and these configurations are also called push-pull.

Use case of the open drain configuration: the wired NOR

It is possible to replace the PMOS with a weak pull-up network (i.e. a pull-up resistor) and this configuration is know as open-drain (see Fig.4).

This configuration is very interesting because it is possible to connect the output of two or more ports configured in that way obtaining a wired NOR: as long as one of the gates has the NMOS turned on the output is pulled down. This solution is often used in contended BUSes like I2C.

Note that is not possible to create a wired NOR connecting push-pull gates together because this can cause under certain condition a short circuit (see Fig.5).

The short-circuit condition obtained wiring two CMOS gates

The output buffer can be configured to act as push-pull as well as open-drain. This configuration can be done acting on the Output Type register (also know as GPIOx_OTYPER).

The output buffer could be configured with different slew rate acting on the Output Speed register (also know as GPIOx_OSPEEDR).

Through this register is possible to set the speed of the output port in a different manner, for example, on STM32F4 the available speed are:

  • Low speed;
  • Medium speed;
  • High speed;
  • Highest speed;

whereas the actual value of each speed in term of frequency is reported in Input-Output AC Characteristic chapter of the STM32 Datasheet.

GPIO modes

A GPIO can work in different modes. Acting on the GPIO Mode Register (also known as GPIOx_MODER) it is possible to program a pin in four different modes.

Input mode

This mode is used when we need to sample the logic level of a pin. When a pin is programmed as Input its logic level is sampled each clock cycle and can be retrieved accessing the Input Data Register (also known as GPIOx_IDR).

When a pin is programmed as Input, the output buffer is disabled and the TTL Schmitt trigger is turned on. The pull up\down resistors could be enabled\disabled acting on the GPIOx_PUPDRThe Schmitt trigger is often used in digital circuits because its working principle provides signal robustness in case of noise on the input signal: due to its hysteresis, it is able to avoid multiple transitions during port commutation commonly known as glitches.

Output mode

This mode is used when we need to use a pin as a digital output. When a pin is programmed as General Purpose Output, the output buffer is enabled and configured as push-pull or open-drain according to GPIOx_OTYPER and the TTL Schmitt trigger is turned on. The pull-up\pull-down resistors could be enabled\disabled acting on the GPIOx_PUPDR. The output slew rate can be configured through the GPIOx_OSPEEDR.

The port state can be configured through the Output Data Register (also known as GPIOx_ODR). Note that the GPIOx_IDR still report the status of the current Port status.

Analog mode

This mode is used when we need to generate an analog signal through the DAC peripheral or to sample it using an ADC peripheral. When a pin is programmed as analog, the TTL Schmitt trigger is bypassed and the output buffer is disabled as well as pull-up\down resistors. Read access to the GPIOx_IDR gets a “0”.

We will discuss to use ADC and DAC in a later dedicated article. Anyway, note that to avoid any noise an unused pin should never be programmed as analog but as Input with Pull-Up resistor.

In GPIOv3 there is an additional register named Analog Switch Control Register (also know as GPIOx_ASCR). To use the analog mode it is required to set an additional bit per each pin in this register.

Alternate mode

This mode is used to assign a pin to an STM32’s peripheral. The STM32 is equipped with a big number of internal peripherals which are interconnected to GPIO pins through multiplexers: acting on these multiplexers it is possible to reroute connections.

The Alternate mode requires also a multiplexer selector to choose reroute paths. The selector could be changed acting on a couple of registers named Alternate Function High Register (GPIOx_AFHR) and Alternate Function Low Register (GPIOx_AFLR).

Rerouting are limited and it is not possible to assign any peripheral to any pin, Anyway each peripheral is mapped on more than a pin: this guarantees an higher flexibility during the hardware design phase.

The alternate function map is reported in the STM32 Datasheet under the Chapter Pinouts and pin description in a table named Alternate function mapping (see Fig.6).

A screenshot of the Alternate Function Mapping

When a pin is programmed as alternate mode, the output buffer is enabled and configured as push-pull or open-drain according to GPIOx_OTYPER and the pin is driven by the signal coming from the peripheral.

The pull-up\down resistors could be enabled\disabled acting on the GPIOx_PUPDR. The output slew rate can be configured through the GPIOx_OSPEEDR.

The TTL Schmitt trigger is turned on and accessing the GPIOx_IDR it is possible to check the current port status.

GPIO absolute maximum rating

The GPIO electrical characteristics are reported in the Electrical Characteristics chapter of the STM32 Datasheet and this includes the range of acceptable values of VDD.

The GPIO is powered with VDD which represent the positive voltage which powers the whole STM32. VDD represents also the high logic level of the STM32. Most of the STM32 can operate between 1.7 V and 3.6 V. Anyway, the actual value of VDD depends on board schematics which can be found in the User Manual of the board in use.

Most of the STM32 development boards are equipped with a dual voltage LDO 5 V – 3.3 V. The 5 V is used for certain peripherals like the USB OTG. The STM32 is supplied with the 3.3 V and in this condition, a logic “1” corresponds to 3.3 V while a logic “0” is 0 V.

STM32 GPIOs are 5 V tolerant and they are compatible with the TTL logic levels.

Another important thing is the maximum current a single pin is able to source/sink. Again, this depends on the MCU but most of the STM32 are able to sink/source up to 25 mA per pin with a restriction on the total current sourced/sunk by all the pins which must not exceed 120 mA. These currents are enough to drive 5 mm monochromatic LED which usually sinks not more than 10 mA.

The ChibiOS PAL driver

Preliminary notes

Naming conventions and auto-completion

Each API of the PAL driver starts with the prefix “pal”. Function names are camel-case, pre-processor constants uppercase and variables lowercase.

An example of auto-completion

The prefix is very useful for auto-completion purposes. For example, in Eclipse, starting to write a function and pressing CTRL + SPACE the IDE will show suggestions for auto-completion. In this case, writing “pal” and pressing CTRL + SPACE we will see available API of the PAL driver as suggestions as shown in Fig.7.

This is true also for other ChibiOS/HAL drivers. For example, the functions of the SPI Driver are prefixed by “spi”, the functions of the PWM Driver are prefixed by “pwm”, the functions of the ADC Driver are prefixed by “adc” and so on.

Auto-completion related on Project Index, a list of symbols contained in our code like function names, global variables, macros and so on, which Eclipse creates automatically building the project. To use auto-completion the project must have been built at least once.

In case of problems, it is possible to force the index to rebuild right-clicking on the project and choosing Index -> Rebuild.

Logic levels

The logic levels of a pin are defined in PAL as:

 * @name    Logic level constants
 * @{
 * @brief   Logical low state.
#define PAL_LOW                         0U

 * @brief   Logical high state.
#define PAL_HIGH                        1U
/** @} */
Port, pad, and line

When we use the PAL driver we need to identify which I/O pins we are going to program. Basically, there are four sets of function in this driver:

  • Pad related which act on a single I/O. They need at least two parameters: port and pad. The first one is a macro (GPIOA, GPIOB, GPIOC, GPIOD and so on), the pad is an unsigned integer (from 0 to 15).
  • Group related which act on a group of I/O. They need at least three parameters: port, mask and offset. Mask is a bit mask to identify which pins of the port must be programmed, offset is the offset of the mask (left shifting).
  • Port related which act on the whole port. They need at least one parameter: port.
  • Line related which are an alternative to Pad related: the difference is that in these functions the pin identifier is represented by a single parameter instead of two: line.

The line is a smart way to encode the information of both port and pad in a single identifier. User can generate a line identifier using the macro

 * @brief   Forms a line identifier.
 * @details A port/pad pair are encoded into an @p ioline_t type. The encoding
 *          of this type is platform-dependent.
#define PAL_LINE(port, pad)                                                 \
  ((ioline_t)((uint32_t)(port)) | ((uint32_t)(pad)))

So, for example, with the code


We are defining a line identifier for the pin PA5.

To identify a single I/O we need a port and pad identifier or otherwise a line identifier. Each identification method has its own set of functions.

From here and out we will often rely on bit-wise operators to do bit-masking. If you are not used to these kinds of operators I suggest you read Registers and bit masks.

Board level initializations

Each pad must be properly configured before being used. The configuration for each Pad generally depends on our application but in certain cases, it is bonded by the board schematics. For example, let us consider the schematic in Fig.7: this figure represents a schematic from the STM32F3 Discovery User Manual.

A schematic from the STM32F3 Discovery User Manual

Here it is possible to see that colored LEDs are connected to pads from PE8 to PE15 and to use them these pads must be configured as Output Push-Pull with no Pull-up/Pull-down resistor.

A screen of projects and its board files.

Similarly, the pad PA0 is connected to the User Button (already provided or Pull down resistor and de-bouncing circuitry) and to use it we need to configure this pin as input floating. In this schematic are also visible a couple of MEMS which requires other pin configurations.

Well, we don’t have to worry too much about these configurations because when the PAL driver is initialized it programs all the GPIO according to a series of configuration which is stored in a header file named board.h. Each board has is own board header file. For ease of use, the board file is linked into the project into the linked folder named board.

The board files are a quick reference to understand how pins are pre-configured. Recapping:

All the board-dependent configuration related to GPIO are already performed by ChibiOS PAL driver. These configuration are stored in the board files which could be explored as quick reference.

The initialization is performed by palInit() and this API is executed by halInit() whenever the PAL switch in halconf.h is enabled

 * @brief   Enables the PAL subsystem.
#if !defined(HAL_USE_PAL) || defined(__DOXYGEN__)
#define HAL_USE_PAL                 TRUE

To improve code readability in the board files pin identifiers are redefined as pre-processor constants. For example, the next code is from the STM32 Nucleo F401RE board.h file

 * IO pins assignments.
#define GPIOA_ARD_A0                0U
#define GPIOA_ADC1_IN0              0U
#define GPIOA_ARD_A1                1U
#define GPIOA_ADC1_IN1              1U
#define GPIOA_ARD_D1                2U
#define GPIOA_USART2_TX             2U
#define GPIOA_ARD_D0                3U
#define GPIOA_USART2_RX             3U
#define GPIOA_ARD_A2                4U
#define GPIOA_ADC1_IN4              4U
#define GPIOA_LED_GREEN             5U
#define GPIOA_ARD_D13               5U
#define GPIOA_ARD_D12               6U
#define GPIOA_ARD_D11               7U
#define GPIOA_ARD_D7                8U
#define GPIOA_ARD_D8                9U
#define GPIOA_ARD_D2                10U
#define GPIOA_OTG_FS_DM             11U
#define GPIOA_OTG_FS_DP             12U
#define GPIOA_SWDIO                 13U
#define GPIOA_SWCLK                 14U
#define GPIOA_PIN15                 15U

Well, this means that the following two statements are equivalent

palClearPad(GPIOA, 5);


palClearPad(GPIOA, GPIOA_ARD_D13);

but the second is much clearer: we are acting on the LED_GREEN. The board file contains also line redefinition

 * IO lines assignments.
#define LINE_ARD_A0                 PAL_LINE(GPIOA, 0U)
#define LINE_ADC1_IN0               PAL_LINE(GPIOA, 0U)
#define LINE_ARD_A1                 PAL_LINE(GPIOA, 1U)
#define LINE_ADC1_IN1               PAL_LINE(GPIOA, 1U)
#define LINE_ARD_D1                 PAL_LINE(GPIOA, 2U)
#define LINE_USART2_TX              PAL_LINE(GPIOA, 2U)
#define LINE_ARD_D0                 PAL_LINE(GPIOA, 3U)
#define LINE_USART2_RX              PAL_LINE(GPIOA, 3U)
#define LINE_ARD_A2                 PAL_LINE(GPIOA, 4U)
#define LINE_ADC1_IN4               PAL_LINE(GPIOA, 4U)
#define LINE_LED_GREEN              PAL_LINE(GPIOA, 5U)
#define LINE_ARD_D13                PAL_LINE(GPIOA, 5U)
#define LINE_ARD_D12                PAL_LINE(GPIOA, 6U)
#define LINE_ARD_D11                PAL_LINE(GPIOA, 7U)
#define LINE_ARD_D7                 PAL_LINE(GPIOA, 8U)
#define LINE_ARD_D8                 PAL_LINE(GPIOA, 9U)
#define LINE_ARD_D2                 PAL_LINE(GPIOA, 10U)
#define LINE_OTG_FS_DM              PAL_LINE(GPIOA, 11U)
#define LINE_OTG_FS_DP              PAL_LINE(GPIOA, 12U)
#define LINE_SWDIO                  PAL_LINE(GPIOA, 13U)
#define LINE_SWCLK                  PAL_LINE(GPIOA, 14U)

and this means that the following two statements are equivalent as well



Changing the logic level of output pins

The simplest use case of the PAL Driver is the LED toggling we have already seen in previous articles. When an I/O is configured as an output is possible to switch on (and off) a LED just changing the logic level of the pin.

There are many functions we can use to act on logic levels. To deal with a single pin we can use “Pad” related function or “Line” related functions.

Common API are the Set/Clear/Toggle Pad/Line functions

palSetPad(port, pad)

palClearPad(port, pad) 

palTogglePad(port, pad) 

The first couple set an output pin as PAL_HIGH the second set an output pin as PAL_LOW and the latest couple toggle the status of the pin which means that if pin logic level is PAL_HIGH after a toggle function call it will change its level to PAL_LOW (and vice-versa).

This last couple of functions is a little bit different because they receive an extra parameter

palWritePad(port, pad, bit)
palWriteLine(line, bit)

the parameter bit, in this case, represent the logic level we want to write on the targeted pin (PAL_HIGH or PAL_LOW). Just to make things clearer the next statements are equivalent

palWriteLine(LINE_ARD_D1, PAL_HIGH);

and also

palWriteLine(LINE_ARD_D1, PAL_LOW);

Similarly, it is possible to act on the whole port through the functions

palWritePort(port, bits)

Just note that here bits is a 16-bit unsigned representing the status of each pin. So as example

palWritePort(GPIOA, 3);

sets the PA0 and PA1 as PAL_HIGH while pads from PA2 to PA15 are set as PAL_LOW.

It is also possible to act on a group of pads or on the whole port using the functions

palWriteGroup(port, mask, offset, bits)

whereas port is the port identifier, mask is the pad selection mask, offset the group bit offset, bits represent bits to be written. This function acts on the GPIOx_ODR replacing the bits masked by

mask << offset


bit << offset

It should be almost immediate to understand what we have just said for those who know how bit-masking works. In any case, you can take a look to Registers and bit masks.

Getting the logic level of input pins

The PAL driver offers some functions to handle pins set as input similar to those used to manage output. In this case, these functions get the logic level of pads. As example

palReadPad(port, pad)

return PAL_HIGH or PAL_LOW according to the status of the pin. It is also possible to read group and ports as well through the functions

palReadGroup(port, mask, offset)

where the returned value is an unsigned integer whereas each bit represents the status of the port/group.

Configuring pins on-the-fly

Some application requires to change the configuration of the pins during the code execution. To do that PAL offers a specific set of functions to change pins configuration reprogramming their behavior. Even in this case, there are functions to configure a single pin, a group of pins or the whole port

palSetPadMode(port, pad, mode)
palSetLineMode(line, mode)
palSetGroupMode(port, mask, offset, mode)
palSetPortMode(port, mode)

Here we have to spend some words about the modePAL is designed to work with many microcontrollers and comes with a set of generic modes which are suitable for every microcontroller. These modes are:

  • PAL_MODE_RESET, which represent the after reset state. Note that this status is hardware dependent;
  • PAL_MODE_UNCONNECTED, which represent a safe state for unconnected pads and is hardware dependent;
  • PAL_MODE_INPUT, which stands for input with no pull-up\down resistors;
  • PAL_MODE_INPUT_PULLUP, which stands for input with pull-up resistor;
  • PAL_MODE_INPUT_PULLDOWN, which stands for input with pull-down resistor;
  • PAL_MODE_INPUT_ANALOG, which stands for input suitable for analog sampling;
  • PAL_MODE_OUTPUT_PUSHPULL, which stands for the output with a push-pull network;
  • PAL_MODE_OUTPUT_OPENDRAIN, which for the output with an open drain network;

Of course, these modes are generic and cannot map all the modes of an STM32 microcontroller. So there is some additional specific mode for STM32 which can be mixed to previous to obtain all needed modes.

For example, we have an additional mode to deal with alternate functions:


For port speed:


For pull-up\down configuration:


Further readings and Hands-on

The PAL driver is very simple and it is used often joint with another driver. The best way to do some experiment with it is to modify the main demo for your development board. There are also some articles I suggest you read:

Previous and next

This article is part of a series of articles which are meant to be tutorials. I have composed them to be read in sequence. Here the previous and next article of this series:


Replies to Using STM32 GPIO with ChibiOS PAL Driver

  • Thank You for this nice tutorial. I found a small mistake in chapter 3.4. – there should be palReadPad and palReadLine instead of palWritePad and palWriteLine.

  • At first I have to thank you so much for all of articles in this blog.
    I followed the defines in Chibistudio and finally got to this:

    #define pal_lld_setport(port, bits) \
    do { \
    (void)port; \
    (void)bits; \
    } while (false)

    but How can it work? I have never seen thing like this!

    and it is more interesting that I see this:

    #define pal_lld_clearport(port, bits) \
    do { \
    (void)port; \
    (void)bits; \
    } while (false)

    which means the implementations are the same? I’m not a beginner in codding in c, but this is so strange for me, would you please explain it or even refer me to more resources?

    • Hello there,
      most likely you have the Eclipse index which is not built correctly thus you are exploring a file under the folder templates. These file are dummy templates used as starting point to create drivers on a new platform. Most likely you are interested to this code
      * @brief Sets a bits mask on a I/O port.
      * @details This function is implemented by writing the GPIO BSRR register, the
      * implementation has no side effects.
      * @param[in] port port identifier
      * @param[in] bits bits to be ORed on the specified port
      * @notapi
      #define pal_lld_setport(port, bits) ((port)->BSRR.H.set = (uint16_t)(bits))

      * @brief Clears a bits mask on a I/O port.
      * @details This function is implemented by writing the GPIO BSRR register, the
      * implementation has no side effects.
      * @param[in] port port identifier
      * @param[in] bits bits to be cleared on the specified port
      * @notapi
      #define pal_lld_clearport(port, bits) ((port)->BSRR.H.clear = (uint16_t)(bits))

      which is under the folder \os\hal\ports\STM32\LLD\GPIOv2.

      Note that the version of LLD for STM32 depends on hw in use.

  • may board is STM32F103XXX and I do not see any AFx (x is number) at Reference manual . so how do I set PAL_MODE_ALTERNATE() thankyou !!

    • Hi Skyyu,
      I totally missed this comment, sorry for this late reply.

      The STM32F103 is the oldest STM32 and is equipped with the GPIOv1. This chip is quite obsolete and I suggest you move to something new.

      Anyway, in this GPIO there is only one alternate function (thus no number). To understand how to configure a certain pin to route out internal peripherals you should cross-read both datasheet (DocID13587, table5 Medium Density STM32F103xx pin definitions) and reference manual (DocID13902 aka RM0008, chapter 9.1.11 GPIO configurations for device peripherals).

  • Hi,
    Thanks for your article. It was very informative. I had a question. How do you find version of gpio used based on the family? I could not locate that information on ST website.

    • Hi,
      this goes particularly well with GPIOv2 (F2, F3, F4, F7, L0, L1, H7)
      GPIOv1 is quite obsolete (F1 only)
      if I recall right GPIOv3 is very close to v2 with some extra registers (L4, L5)

      Nevertheless the api at high level is identical no matter the version of the GPIO

Leave a Reply