Reading a Button on STM32 | STM32 on Registers #4

A button is one of the simplest communication interfaces between a microcontroller and a human. It’s an input interface, so it takes information from us. How to read a button on STM32? That’s exactly what I’ll show you today. Of course using only registers 🥸

Recently we built ourselves a delay function, so now we can move on to an activity where it will come in handy. We will read the state on a pin, i.e., we will react to a button press.

For now, in a very simple and blocking way, using exactly that blocking delay.

👉 Do you want to learn STM32 register programming fully? I invite you to my full course on this topic (the content is conducted in Polish): https://stm32narejestrach.pl

STM32 on Registers series on YouTube

These posts are created in parallel with the series on my YouTube on the same topic. If you prefer the video version, I invite you there. These articles are a summary of what I show on YouTube.

Link to the entire YouTube Playlist

Connecting the button to GPIO

The first thing we have to do is configure the appropriate GPIO pin to input mode. When blinking an LED we used an output, i.e., Output. Now we need to read the input state, i.e., Input.

On the NUCLEO-C031 board we have one button available to the user. We need to find out which pin it’s on and how it is connected. We will, of course, learn this from the Nucleo schematic.

In NUCLEO-C031, a released button causes a high state on pin PC13 via a pull-up resistor. Pressing the button will short it to ground.

Next to it there is also capacitor C16 and resistor R26, which form a hardware contact-bounce elimination circuit. Better check how it is for you if you have a different Nucleo.

It can be the other way around, like for example in NUCLEO-G474RE. It’s better to check and confirm such things than to believe every word I say.

Register configuration

What do we know about the button? First of all, that it is on pin PC13. We must configure PORTC and its pin 13 to operate as GPIO Input. This will be very similar to what we did for the LED. We’ll immediately make an appropriate configuration function.

We don’t need to invent this path. We will model it on what we did for GPIO Output. Remember that we must have the Reference Manual open. Without it we won’t manage!

  1. The first and most important thing is enabling the clock for the port we need. Without it, it won’t work. The button is on port C. We enable it analogously to port A like we did for the LED.
  2. Next we set the pin operating mode in the MODER register. Now it should be Input. Don’t forget we’re working on port C, not A. Don’t mindlessly copy from the LED 🙂
  3. Output Type doesn’t interest us, because we’re not an output. Same for Output Speed. These settings have no effect on GPIO Input. You can safely skip them.

    How do I know this? There are additional documents such as an Application Note. They describe the use of specific peripherals. For GPIO there is such a document (>right here<). We can find in it a note that the GPIO output buffer is completely disconnected when GPIO is set as input.


  4. What we do care about is setting pull-up and pull-down, in short PU/PD. If we look at the GPIO block diagram, we’ll notice these resistors are right at the output pin.

    They apply to both input and output. Do we have to set them? Not necessarily. We already have a hardware pull-up placed on the board. We don’t need to enable another pull-up in the microcontroller. We leave the default value, i.e., zero. We can write it in, but it’s not necessary.


And that’s it when it comes to configuring the pin for a button. Here is what our configuration looks like:

// PC13 - Button
// 0 - Pushed
// 1 - Released / Idle
void ConfigureButton(void)
{
	// Enable Clock for PORTC
	RCC->IOPENR |= RCC_IOPENR_GPIOCEN;

	// Configure GPIO Mode - Input
	GPIOC->MODER &= ~(GPIO_MODER_MODE13);
	
	// Configure Pull-up/Pull-down - no PU/PD
	GPIOC->PUPDR &= ~(GPIO_PUPDR_PUPD13);
}

Reading GPIO Input

Now let’s think about how to read it and react to what’s happening on it.

For GPIO output we had two paths (actually 3, but the third one is a bit more advanced, so I skipped it). When reading a regular GPIO input we no longer have such abundant options and we only have one way.

We have to use the IDR (Input Data Register), which stores information about the state of all inputs of the entire port. Notice that its fields are marked as r, which means read-only.

We need to extract information about one specific bit from it. How?

  1. Take the entire register value
  2. Apply a bit mask with the bit we’re interested in

This way we’ll obtain a logical value that corresponds to the state on the bit we masked.

  1. Value 0 = low state
  2. Anything other than 0 = logical TRUE, i.e., high state

First, we will turn the LED on according to the button state. Pressed = on, released = off.

if( GPIOC->IDR & (1<<13) )

 this checks the state on pin 13 of the IDR register. Of course you can also do it differently:

if( GPIOC->IDR & GPIO_IDR_ID13 )

and you can also write your own definition for pin number 13.

#define PC13 (1<<13)
if( GPIOC->IDR & PC13 )

You must remember which state on pin PC13 corresponds to which button position. Here it is “the other way around” than we’d intuitively want. Pressing (the action) causes a low state. Releasing causes a high state.

We could write a macro that tells us whether the button is pressed. Then we won’t wonder which state corresponds to a press; instead we already consider at a higher level whether the button was pressed.

#define BUTTON_PRESSED (!(GPIOC->IDR & PC13))

By inverting the check logic we get what we wanted. Let’s analyze this macro.

We extract information about bit 13 from the GPIOC IDR register. If there is zero there, it means a press. We would like this macro to return TRUE on a press. Since we got zero, and on release we’ll get something other than zero, that’s wrong. We need to invert the logic with the logical negation operator. So we invert the logic:

  1. If bit 13 is zero, it’s a press. Negation of zero = TRUE
  2. If bit 13 is one, it’s a release. Negation of non-zero = FALSE

We have what we wanted.

Changing state on a press

Let’s go a bit further. We now want to toggle the LED state on each button press.

We need to write a macro that toggles the state of the output pin. The standard way to do this is an XOR operation on that bit. We won’t go into details. What matters is that it works. I invite you to my C language course (the content is conducted in Polish), where we go into exactly such details.

But what will we toggle the bit on? We can’t use the atomic register, because it doesn’t allow this. We need to use the “normal” GPIO ODR register and in it change the state of the appropriate bit to the opposite. This is more than one operation on the CPU, but it gets the job done.

#define LD4_TOGGLE GPIOA->ODR ^= GPIO_ODR_OD5

Button debounce

It works as we want, but not quite. The LED state sometimes seems not to change, or kind of randomly. There are two reasons, one of which is particularly relevant here:

  1. The mythical contact bounce. A mechanical button has something like contact bounce. The idea is that a released button can still jitter and change the state on the GPIO pin. On Nucleo we have hardware elimination of this, so it’s not our main problem.

    However, we’ll write code that also eliminates it and I’ll tell you later why.


  2. Our main problem is the program behavior itself. We have a program that basically does nothing. It checks the button and if it’s pressed, it toggles the LED. The problem is that the main loop does tens of thousands, sometimes even millions of iterations per second.

    And tens of thousands of times per second we toggle the LED if the button is pressed. Sometimes the release will happen on an even count, sometimes it won’t. That’s the whole secret of why it behaves so strangely.


There are two methods to eliminate such behavior. The first is a double-check after waiting a short period of time. The second is a state machine combined with waiting. That’s already more advanced and we won’t deal with it in this series. I discussed it in detail in my STM32 programming course using HAL (the content is conducted in Polish).

How to wait? We can use the SysTick Delay that we wrote in the previous episodes.

  1. Check whether there was a press
  2. Wait 20-50 ms
  3. Check a second time whether the button is still pressed
  4. If yes, perform the action.

Let’s write it.

// Check if button was pressed
if( BUTTON_PRESSED )
{
	Delay(50); // Dummy debounce

	if( BUTTON_PRESSED ) // Check button again
	{
		// Toggle LED
		LD4_TOGGLE;
	}
}

It has one big drawback. We block the CPU for 50 milliseconds. Unfortunately, we used a blocking wait, so we get what we asked for.

The second drawback is that while still holding the button, the loop will come back around and execute this code again. Just with a 50 ms delay each time. That’s why a state-machine solution is much better.

However, it’s not that hopeless. For quick presses it will work very effectively, and that’s what I would use it for.

Ok, but why do we eliminate contact bounce by waiting in the program if we have a hardware filter? Against an unintentional press. This protects us against accidentally brushing the button.

A human compared to a microcontroller is slow. All their actions take a long time. So to match the interface to a human, you simply slow down the reaction to their actions. Then we are sure these were deliberate actions, and not e.g. something falling on the button.

Until I release it

There is one more thing we can add to our code to be sure the action executes only once. Unfortunately, it’s another blocking element, so I’m warning you right away against overusing it.

We can put an empty loop that will be active until we release the button. We can do that with a while loop.

// Check if button was pressed
if( BUTTON_PRESSED )
{
	Delay(50); // Dummy debounce

	if( BUTTON_PRESSED ) // Check button again
	{
		// Toggle LED
		LD4_TOGGLE;
		
		while(BUTTON_PRESSED){} // Wait for button release
	}
}

The advantage is that TOGGLE will execute only once. The disadvantage is that by holding the button we block the entire program. The danger here is that the CPU will be blocked until you release the button.

The remedy for this is also building button handling on a state machine, which I discuss in my full STM32 for Beginners course (the content is conducted in Polish). These are already more related to programming techniques than to STM32 programming itself. We won’t deal with it in this series. Here we are only learning to work with STM32 registers.

Try playing with this code and see how the reaction changes depending on the length of the waiting time. In the next episode I’ll show you a way to wait without blocking the microcontroller’s work. Good luck!

Summary

Reading a button using registers comes down to reading only one register. Of course, the pin itself must first be configured as a GPIO input. The subsequent steps—what we do with this information—cause the most difficulty. How we handle a press is independent of the type of microcontroller and is done similarly everywhere. In my opinion, best with a simple state machine.

In the next post we’ll deal with a Software Timer. It’s a universal way to do multitasking on microcontrollers. It allows multiple tasks to cooperate “simultaneously”.

Let me know in the comments if you liked this post! Maybe you have a suggestion for what to show as part of the STM32 on Registers series? Share this article with your friends.

I also invite you to my store, where you can buy interesting electronics for programming, like e.g. NUCLEO-C031C6, which we use in this series: https://sklep.msalamon.pl

You can find the project from this article at: https://github.com/lamik/stm32narejestrach_4

Podobne artykuły

.
Categories: STM32

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *