Dealing with push-buttons using an STM32

About

This article contains some simple examples to understand how to deal with push-buttons when you are approaching STM32 and ChibiOS. The button can be considered the simplest input peripheral that can be connected to a microcontroller. Because of that, usually, every embedded development board is equipped with a button marked as “User Button” and this means it is actually connected to a GPIO pin you can read via software.

Requirements

To understand this article with proficiency, you should match few requirements:

Theory inside

The push-button

The push button schematic

A push-button is a switch equipped with a spring which can stand in two states: pushed or released. Thanks to the spring, normally the button is released. From a circuital point of view, a push-button is a two terminal device which acts as a short circuit when pushed and as an open circuit when released.

As push-button are cheap and easy to use, they are largely used in embedded systems as simple input devices. To work properly, a push-button requires a week pull-up or pull-down resistor. However, STM32 GPIO peripheral offers embedded week networks and it is up to us to decide if we want to use the internal or external one.

 

Possible circuit to connect a Push-Button to an STM32

Anyway, as resistors are very cheap components, it is convenient to place an external network near to the button. This is highly recommended in those cases in which the connection button-microcontroller is achieved using a cheap Dupont wire. Such kind of connections is very common when we are dealing with break-out boards and they can get us nasty surprises: a long line made of low-quality conductive material can exhibit large parasitic effects which can cause signal degradation and thus component misbehaviour.

Figure 2 shows the two different way to connect a button to our microcontroller. The left side schematic is called active low configuration while the right one active high. In the active low configuration, when the button is pressed, we would read a logical ‘0’, when the button is released a logical ‘1’. In the active high configuration, things work in the reverse order (‘1’ when pushed, ‘0’ when released).

How to size pull-up/down resistor

So, how this works? To answer this question we have to use a Digital Circuit approach: according to this method, we can modelling an IO PIN as a capacitor and its logical state depends on the voltage across it. So, if the external circuit will pump or drain current into or from the capacitor. The capacitor would then be charged or discharged by the external circuitry. When the transient is over, if the capacitor is charged to Vcc, then we would read a logical ‘1’; otherwise, if it is completely discharged we would read a logical ‘0’.

The equivalent circuit of an active low push button

As example, let us consider the active low configuration: to perform our analysis let us replace the GPIO with a capacitor and the push button with its electrical model. The resulting circuit will be something that looks like figure 3.

Now, physics and circuit theory shows that current always takes the path with least resistance. This means that when the button is released the capacitor will be charged through the pull-up resistor and when the button is pressed it would discharge through the button. This model confirms what we have said before (i.e. when the button is pressed we get a logical ‘0’ otherwise we get a logical ‘1’).

To make the circuit work properly, we have to size the resistor following some constraints. The lower limit to the resistor value is fixed by the current. Indeed, in both the configuration, when the button is pressed and the transient is over, the current flows from Vcc to ground roughly through the resistor.

If the resistor is too small the current would be too high and this could cause damage to components (e.g the onboard LDO, the button, the resistor itself). Moreover, the current flow introduces a power loss according to the formula

P_{loss}^{R} = R_{pu/pd} \cdot I^2

The greater would be the resistance, the smaller the current, the smaller the power consumption will be. A current should be considered affordable if its magnitude is in the order of hundreds of micro-amperes or at least a few milliamperes. Therefore, considering that typical Vcc values are in range 3 to 5 V, the resistance should range from a few up to dozens of kilo-ohm.

The higher limit to the resistor value is fixed by the logical transient time of the GPIO (i.e. the time that the GPIO requires to commute its logical state). Back to figure 3, let us consider the low to high transition which happens when the button has been released. The equivalent capacitor will then drain current through the pull-up resistor and the rise time would be

t_{rise}= 2.19 \cdot R_{pu}C_{eq}

Considering an STM32, the typical equivalent IO capacitance is about 5 pF and with a pull-up resistor of 10 MΩ, the rise time will be almost 109.5 us which is not a reasonable rise time. With a 10 kΩ resistor instead, the rise time would be 109.5 ns and, therefore, more reasonable.

Note that, a similar argument can be introduced for the pull-down resistance and the fall time in the active high configuration.

The bounce effect

The inside of a push button
The push button bounce effect

A push-button is composed of two contacts, a plate and a spring. When the button is pressed, the plate short-circuits the two contacts and when the button is released the spring pushes away the plate from the contacts.

When the button gets old the spring tends to become too lazy. If this happens on button release it could be that the plate bounces on the contacts different times: this is known as the bounce effect. This effect can lead to spurious effects as shown in figure 5.

The debouncing is a tentative to mitigate this effect and remove any eventual spurious detection. There are two ways to debounce a push-button:

  • in hardware, by filtering the input signal;
  • in software, by adding a dead time after the first detection.

The hardware debouncing is a preferable solution as it requires basically two discrete components avoiding software workaround. Basically, this can be implemented by adding a low pass filter between the button network and the GPIO.

The push button hardware debouncing circuit

The simplest way to implement an LP filter is an RC network as shown in figure 6. The question now is how to properly size the capacitor and the resistor.

This depends on the bouncing dynamics. To do that we should measure the time interval between bounces in order to compute the frequency of the phenomena and choose the LP frequency at least ten times smaller than the bouncing frequency. This lead us to the formula

f_{LP} = \frac{1}{2 \pi RC} = 0.1 \cdot f_{bounce}

Note that if the low pass frequency is too small the filter would reject even status changes caused by the normal user activity. To be clear, a user would not be able to press the button with a cadence higher than 1 kHz while slower bouncing effects have a frequency of douzen\hundreds of kHz. Therefore, is reasonable to choose the low pass filter frequency as 10 kHz and thus an RC network composed with a 160 Ω resistor and a 100 nF capacitor.

Examples

Read a Button

Create a single thread project that switches a LED according to the button status (e.g. the LED is on when the button is pressed and it is off when the button is released)

For this example, we are going to use one of the LED and the Button already available on our development kit. Up to now, every official STM32 development kit (except Nucleo-32) is equipped with a button. We have just to pay attention to the schematic in order to figure out where the button is connected.

For example, on Nucleo-64 and Nucleo-144 the User Button is always connected to PC13, on many STM32 Discovery kit, it is connected to PA0. A quick way to figure out where the button is connected is to take a look to ChibiOS’ board files. This information is available on the User Manual of the development kit along with the board schematics that we should read to understand if the button is connected in active high or active low configuration.

A similar approach can be used to check how many User LED are available on the development kit we are using, where and how they are connected to our MCU.

For example, let us assume we are going to use the STM32F3 Discovery board. In the following figure, there is an excerpt of its schematic where we can see both User button connected to PA0 as active high and the 8 user LEDs connected as active high on the second half of the GPIOE (from PE8 to PE15).

An excerpt of the STM32F3 Discovery schematic
Preliminary considerations

Considering the previous schematic a palReadPad() on PA0 will return PAL_HIGH if the button is pressed and PAL_LOW otherwise. A palSetPad() on PE8 would turn on the blue LED marked as LD4 and while a palClearPad() would turn it off.

The first step is to duplicate a default demo for our development kit and make it work. So let’s create a new project giving it a proper name, for instance, RT-STM32F303-Discovery-Simple_Btn_Led. After editing properly the makefile and debug configuration we can start to edit the main.c:

  1. removing the inclusion of ch_test.h, because actually, we will not use the test suite;
  2. removing the sdStart, because we are not going to use the serial driver which previously was used by the test suite;
  3. removing all the threads except main because we are going to create a single threaded application;
  4. cleaning the main() loop;
  5. editing the comment within the code (not necessary but is a good habit).

At this point, the code should look like this

This application runs doing nothing. Now we have to properly complete the main() thread loop. To complete the task we should note that

  • to get the button status we have to read a GPIO on every cycle;
  • according to the button status, we have to change the status of the LED;
  • when our thread is in sleep it is “blind” and would not be able to detect eventual changes of the button status;
  • for STM32F3 Discovery, PA0 is pre-configured as PAL_MODE_INPUT at board level initialization as well as all the GPIO from PE8 to PE15 are configured as PAL_MODE_OUTPUT_PUSHPULL.
Polling-based implementation

The following code is one of the possible implementations of this example.

In this implementation, the Core checks the PA0 status every 10 milliseconds. Such kind of process is usually called polling or polled operation and it is basically a synchronous activity. From the efficiency point of view, this is not the best way to proceed because the CPU has to execute some operations with a constant cadence regardless of the actual button activity.

To clarify, let us consider a use case in which user is not pushing the button for a long time (e.g. 5 seconds); regardless of that, the CPU will check the button status accessing peripheral registers even if nothing has changed. Looking at figure 8 we can draw more conclusions.

The timing diagram of polling based implementation

The azure waveform represents the button status, the green waveform the LED status and the orange circles are the moments in which polled operation happens. We can thus notice that:

  • The button can be pressed or released at any moment and most likely these moments will not be aligned to polled operations;
  • This solution is able to react only synchronously, this introduces some delays between event triggering and the related reaction. Moreover, the maximum delay amount will be strictly less of polling period;
  • Ideally it is possible to mitigate the delay speeding up the polling loop, however, this would even increase the CPU load.
Event-based implementation

A more efficient solution would be an event based solution. This solution aims to configure the GPIO (to which the button is connected) as interrupt source for our core. In this way, we do not have to poll the button status but our thread would be woken up asynchronously when an event occurs.

This implementation requires that our microcontroller is able to use GPIO as an external interrupt source and this is true on STM32 which has is equipped with the EXTended Interrupt and events controller (EXTI). This controller manages external and internal asynchronous events/interrupts and is able to generate requests to the CPU’s interrupt controller.

Involving interrupts, we should be prepared to deal with Interrupt Service Routines and EXTI low-level registers. Fortunately, the ChibiOS PAL driver offers a simple interface to manage external interrupts and thus we would easily able to exploit these features.

As we can there are basically two differences:

  • Just before the loop, we have configured the PAL driver to listen to status change on PA0.
  • We have replaced the timed with a wait and moved it at the beginning of the loop.
Final remarks

The palEnablePadEvent configures a GPIOA as interrupt source stating which edge has to be detected. In this case, we would detect both rising and falling edge. Anyway, it is possible to listen only one of the edges using PAL_EVENT_MODE_RISING_EDGE or PAL_EVENT_MODE_FALLING_EDGE. The palWaitPadTimeout makes the calling thread in the sleep state until an event on the specified pad occurs. Thus, in this implementation, the thread would perform a cycle only once for each time the button status changes. This means more efficient use of the CPU, potentially zero delays between event triggering and software reaction and it is definitely much more elegant.

To conclude only some additional notes:

  • The event/wait API has been introduced starting from ChibiOS 18.2.x and it is not available on previous versions;
  • To use PAL event/wait mechanism it is required to switch on PAL_USE_WAIT in halconf.h;
  • It is not possible to use more pin on different ports but with the same identifier number at the same time (e.g PA0 and PC0, PB13 and PC13, PD1 and PA1). This is due to hardware limitations. The EXTI controller allocates 16 lines to GPIO (EXTI0, EXTI1, EXTI2, …, EXTI15). Each line can be assigned to one pin at a time (e.g. EXTI2 can be assigned to PA2 or PB2 or PC2 or Px2, EXTI13 can be assigned to PA13 or PB13 or Px13).

Edge detection

Create a single thread project that toggles a LED when button change its state from pressed to released (e.g. pressing and releasing the button the LED toggles)

The preliminary considerations and the schematic analysis performed in Example 3.1 are still valid. We can start the task using again the previous code snippet with an empty loop

This time we have to detect an edge and not simply the button state. As before we have two possible implementations: the polling-based and the event-based.

Polling-based implementation

The edge detection can be done nesting two palReadPad. The previous statement would sound logical if we consider that basically an edge is nothing more than the moment in which the button changes its state from one to another. Basically, this means that to detect an edge we need to somehow make a comparison between the current status of the button and the previous one.

The following code would clarify how this works.

This implementation is clearly polling-based because, regardless of which loop is running, the core would check the button status periodically even if two different cadences (every 10 milliseconds to check if the button has been pressed, every 30 milliseconds to check if the button has been released). Next figure would dispel any doubt.

The timing diagram of edge-detection polling based implementation

Note here that the polling cadency changes depending on which loop is currently running (inner or outer) and that there is always a potential delay between event triggering and software reaction.

Event-based implementations: event/wait paradigm

In this case, the event-based solution makes things much easier and offers two different implementations. The first one uses the already seen event/wait paradigm and related API which becomes available when PAL_USE_WAIT switch is enabled in halconf.h.

Event-based implementations: event/callback paradigm

We can notice that the thread here is doing a very simple operation: the palTogglePad. Well, the PAL driver offers even another paradigm: the event/callback and to use it we need to switch on PAL_USE_CALLBACKS in halconf.h. At this point, we can associate a callback which would be triggered by the event.

Final remarks

The event/callback solution makes the main thread free to be used for other purposes as it is not needed anymore. Note that main cannot be terminated thus even if unused main should stay alive.

In comparison to the event/wait implementation, the event/callback solution has only one limitation: the callback is executed from interrupt context and this means that it is out of the scheduling flow. The code executed in here should not be blocking, thus, no synchronous of blocking function are allowed (e.g chThdSleep(), spiExchange(), adcConvert(), and any other synchronous API).

Long press and single press

Create a single thread project that detect if button has been pressed or kept pressed (e.g. pressing and releasing the button the LED turns on, keeping pressed and releasing the button the LED turns off)

We can use the empty loop code snippet again. This time, we need to detect the falling edge plus we need to know whereas the button has been kept pressed for a long time (e.g. 500 milliseconds). We have again multiple solutions for the same problem.

Polling-based implementation

To solve this exercise we can use some concepts seen in the previous exercise. Indeed, we have to detect a falling edge with the difference that we have to measure the time in which button stays pressed. This can be done using a simple variable. The following code would clarify how this works.

Event-based implementation

In an event-based implementation, we need to detect both rising and falling edge and measure time between them. On the rising edge we could start a counter and on the falling edge we could check the counter and take a decision.

To do that we have used two new functions: the chVTGetSystemTimeX() which return the current system time expressed as system ticks and chVTIsSystemTimeWithinX() which checks if the current system time is within the specified time window.

The function chVTGetSystemTimeX() function is called on the rising edge to get the systime at that moment and store it inside the variable named start_time. The function chVTIsSystemTimeWithinX() is called on falling edge using a time window the interval between start_time and start_time + 500ms. If the falling edge happens inside this time window then we have detected a short press otherwise we have detected a long press.

Exercises

To complete this article let me propose you some assignment you should do on your own.

Connecting an external button

Connect an external button to your board as active low and try to read it

Hints
  • Read the user manual check the schematic, be sure that the GPIO you are going use is connected to anything else than your button.
  • Configure the GPIO as INPUT FLOATING if you use an external pull up resistor or as INPUT PULL UP in case you use the button only.

Double button

Connect two external button in OR configuration

Connect two external button in AND configuration

Hints
  • To make the OR connection buttons should be arranged as a series connection.
  • To make the AND connection buttons should be arranged as a parallel connection.

Help and share

Do you need help with exercises? Something is unclear? Comment below or subscribe to our forum. Help us to spread the knowledge sharing this article: share is caring!

Be the first to reply at Dealing with push-buttons using an STM32

Leave a Reply