GPIO tutorial

From Miosix Wiki
Jump to navigation Jump to search

Include statements and namespace required to use this library

#include <miosix.h>
using namespace miosix;

This is a C++ library, it cannot be used from a C source file.

A GPIO or general purpose input-output is a software-controllable pin of a microcontroller. To be able to control multiple GPIOs at the same time, most microcontrollers group GPIOs in ports, so GPIOs are specified by a port name, and a pin number within the port. The STM32 use letters to identify ports, so the GPIO 0 of port A is called PA0. The GPIO port and number are often specified at the pin headers on development boards such as the STM32F4 Discovery, in order to make it easy to connect wires to the correct GPIO. On the contrary, NXP uses numbers also for the ports, so the GPIO 0 of port 1 is called P1.0.

Declaring GPIOs

A GPIO in Miosix is accessed using a C++ template class, the rationale behind this choice can be found in this article, but in short it provides both optimal performance and a high-level API.

As an example, assume there is a blue LED connected to GPIO PD15 and a button connected to PA0, to control them in software you first have to declare the GPIOs using the following syntax

typedef Gpio<GPIOD_BASE,15> blueLed;
typedef Gpio<GPIOA_BASE,0> button;

A declaration like this can appear both inside a function, if you are going to use the GPIO only inside that function, or be put at the top of a source file, to access the GPIO from all the functions of that file. Notice how GPIOD_BASE means that this is a GPIO of port D, and 15 is the GPIO number within the port. This declaration introduces a template named blueLed that can later be used to access the GPIO.

A common mistake is to declare the GPIO like this

/* DON'T WRITE CODE LIKE THIS */
typedef Gpio<GPIOD_BASE,15> pd15;
typedef Gpio<GPIOA_BASE,0> pa0;

While the following code will work, it is discouraged to choose the GPIO id as the name of the template. It is way better to try using a name related to the function that the GPIO will be used for, as it leads to code that is easier to read and understand. For example, the first GPIO is connected to a blue LED, so blueLed is an appropriate name.

Configuring GPIOs

Once you have declared a GPIO, you need to to configure it before using it. The most basic level of configuration is to select the GPIO direction, i.e, whether it is going to be used as an output, or an input.

If a GPIO is configured as an output software can control the voltage at that pin, by selecting between a logic level 0, which corresponds to 0V, or a logic 1, whose voltage is VDD, which is the voltage used to power the microcontroller, in most cases 3.3V for the typical microcontrollers supported by Miosix.

If a GPIO is configured as an input, software can sense the logic level at the pin, reading a logic level 0 if the voltage is close to 0V, or a logic 1 if it is close to VDD. Note that if the voltage is closed to hald of VDD, or if the GPIO is not connected to anything, the reading could be either 0 or 1, see the discussion about pull-up resistors on Wikipedia for that.

To configure the GPIO you can use the mode() member function of the GPIO class. For example, in your main() function you can do

int main()
{
    blueLed::mode(Mode::OUTPUT);
    button::mode(Mode::INPUT);
}

Note that all boards supported by Miosix have the OUTPUT and INPUT modes, but may have more options.

Some for example, may have software controllable on-chip pull-up and/or pull-down resistors, or an analog input mode connected to an ADC to read analog voltages. The list of supported modes can be found in the gpio_impl.h file of your microcontroller. For example, for the microcontroller in the STM32F4 Discovery board, the file is in miosix/arch/cortexM4_stm32f4/common/interfaces-impl/gpio_impl.h.

A noteworty example is the ALTERNATE function mode, that when selected connects the GPIO to some device-dependent hardware peripheral, such as an SPI, I2C or UART peripheral. In such a case, the pin stops being a GPIO in the sense that it is no longer software controllable, but is instead driven by the hardware peripheral. This is often used to implement communication protocols like SPI or I2C.

However, these device-specific extensions are not discussed further here.

GPIO Writing

To write to an OUTPUT configured GPIO, you can used the high() and low() member functions, as shown in this example that blinks a LED

#include <miosix.h>
using namespace miosix;

typedef Gpio<GPIOD_BASE,15> blueLed;

int main()
{
    blueLed::mode(Mode::OUTPUT);
    for(;;)
    {
        blueLed::high();    //Turn the LED on
        Thread::sleep(500); //Wait 500ms
        blueLed::low();     //Turn the LED off
        Thread::sleep(500); //Wait 500ms
    }
}

GPIO Reading

Reading from an INPUT configured GPIO is performed by using the value() member function, as shown in this example that polls a button every 10ms and turns on a LED for 10 seconds once it detects it is pressed.

#include <miosix.h>
using namespace miosix;

typedef Gpio<GPIOD_BASE,15> blueLed;
typedef Gpio<GPIOA_BASE,0> button;

int main()
{
    blueLed::mode(Mode::OUTPUT);
    button::mode(Mode::INPUT);
    for(;;)
    {
        //Poll the button every 10ms until it is pressed
        while(button::value()==0) Thread::sleep(10);

        blueLed::high();      //Turn the LED on
        Thread::sleep(10000); //Wait 10s
        blueLed::low();       //Turn the LED off
    }
}

A common mistake is to forget the sleep when polling for the button, resulting in a code like this

/* DON'T WRITE CODE LIKE THIS */
for(;;)
{
    //Poll the button until it is pressed
    while(button::value()==0) ;

    blueLed::high();      //Turn the LED on
    Thread::sleep(10000); //Wait 10s
    blueLed::low();       //Turn the LED off
}

Although this code will work, it is bad. To see why, consider that Miosix is a multithreaded OS, so in the general case there will be other threads running other than your code, so it is not polite to spin in a while loop that may take an undefined time to complete chewing up 100% of the CPU. This will for sure prevent the kernel from putting the CPU in sleep resulting in a higher power consumption, and may slow down other threads that can have more important tasks to do than spinning on a GPIO.

But you may be thinking that your application needs to be notified of the button being pressed as soon as possible and even a few milliseconds of delay are too much. In this case, this code is even more wrong, because Miosix is a timesharing system, so your code can and will be preempted by the operating system to prevent other threads from starving. If the event that you need to be notified about happens while your code is preempted, there will be a delay of up to a few milliseconds between the actual event and when you notice it anyway.

To deal with situations where you need to be notified of pin change events events as soon as possible you need something more complicated, such as an Interrupt on pin change that will call an interrupt routine in response to an event, usually offering a time determinism of a few tens of microseconds, or a Timer input capture channel that provides a timestamp of an event with an accuracy of a few tens of nanoseconds.

Thread safety considerations

All microcontroller, for self-evident reasons, provide hardware support to allow writing to GPIOs configured as OUTPUT in a thread-safe way, so you are free to call the high() and low() (and value() as well) from multiple threads.

Unfortunately, configuring GPIOs is on most microcontrollers not thread-safe. That is, if you call mode() concurrently from multiple threads on the same GPIO port, you may inadvertently misconfigure some of the pins in that port.

There are two ways to work around that, configuring all GPIOs in the main() before spawning any thread, and disabling interrupts while configuring GPIOs.

Configuring GPIOs in the main()

If you configure all the GPIOs in the main(), before spawning any thread, and later you no longer call mode() there are no way two threads could call mode() concurrently, and so there is no problem. However, this often goes against code modularity, and in some circumstances you may need to change a GPIO configuration at runtime.

Disabling interrupts while configuring GPIOs

To allow mode() to be freely called, you have to make sure no two concurrent calls to mode() can happen. Considering that configuring a GPIO is a very fast operation, it requires just a few clock cycles, this can be achieved by disabling interrupts while configuring the GPIOs. Keep in mind that disabling interrupts must be done with care, and striving as much as possible to minimize the amount of time spent with interrupts disables, so as not to interfere with the real-time capabilities of the Miosix kernel.

void someFunction()
{
    [...] //Some code

    {
        FastInterruptDisableLock dLock;
        blueLed::mode(Mode::OUTPUT);
        button::mode(Mode::INPUT);
    }
   
    [...] //Some other code
}

Note the two curly braces surrounding the dLock object, that create a scope delimiting the lines of code where interrupts are disabled.

As a last note, you could also use a global mutex instead of disabling interrupts, but as previously mentioned configuring a GPIO is a very fast operation, it requires just a few clock cycles, so using a mutex would introduce a significant overhead.

Accessing GPIOs from multiple source files

When dealing with large projects, it is common to split them in multiple source files. To access the GPIO definitions from multiple source files it is common practice in Miosix to declare them in a C++ header file, and #include it where needed. If that file contains all the GPIOs of a specific board, is is often called hwmapping.h. In such a case, namespaces can be used to group GPIOs by function. For an example, see the hwmapping.h file for the Sony smartwatch board [1].

Related pages