STM32 Programming. Part 5: GPIO I/O Ports
Posted: 16 Oct 2023, 02:14
In this part we will understand the GPIO I/O pores of the STM32F103C8 microcontroller and write "Hello, World!" with a blinking LED, as well as learn how to read the state of the microcontroller pins and use the built-in pull-up resistor.
Introduction
General-purpose input/output (GPIO) is an important component of any microcontroller through which it interacts with the world around it. In STM32 microcontrollers the ports are named with letters A, B, C and so on: GPIOA, GPIOB, GPIOC.... Each GPIO has 16 I/O lines, and each line can be configured independently of the others. Here are the configuration options:
Analog mode. Inside the microcontroller there are analog-to-digital converters, which, as you know, must have analog inputs. So, in Analog mode, the microcontroller leg is connected to the analog input of the ADC inside the microcontroller. In addition, all digital pipelining of this leg is disabled to reduce digital noise and power consumption.
Alternate function. In this mode, the microcontroller leg is controlled by internal digital peripherals such as the USART module.
GPIO registers
Let's take a look at the GPIO port registers.
Port configuration register low (GPIOx_CRL)
This is a configuration register for port pins numbered 0 through 7. Each pin is provided with 4 configuration bits: 2 MODEy bits and 2 CNFy bits.
MODEy[1:0]: Mode of the port pin, input or output. In the output mode you should select the maximum switching frequency of this leg, as far as I understand it is to optimize the power consumption of the port.
In input mode (MODEy[1:0]=00):
This is a configuration register for port pins numbered 8 through 15. This is the same as the GPIOx_CRL register.
Port input data register (GPIOx_IDR)
IDRy: these bits contain the input value of the corresponding I/O port.
Port output data register (GPIOx_ODR)
ODRy: port output data.
Port bit set/reset register (GPIOx_BSRR)
This register can be used to reset or set any bit of the ODR register without read-modify-write operations.
BRy: Reset a bit of the I/O port ODR register (y= 0 ... 15)
This register can be used to reset any bit of the ODR register without read-modify-write operations.
BRy: Reset a bit in the ODR register of the I/O port (y= 0 ... 15)
This register is used to lock the port configuration bits after writing a valid sequence to 16 bits (LCKK) of the register. The bit values [15:0] are used to lock the GPIO configuration. During the locking sequence in LCKK, the LCKR values [15: 0] must not be changed. When the locking sequence has been written, the configuration of the selected I/O ports can only be changed after resetting the microcontroller. Each LCKy bit blocks the ability to change the four port configuration bits (CRL, CRH).
LCKK[16]: Lock key.
So, the registers have been dealt with, now it's time to practice. All examples in this article are for STM32F103C8 microcontroller. I have at my disposal such a debug board:
It has an 8 MHz quartz resonator and an LED on the PB12 port. With the help of this LED we will organize Hello, World!
The task is clear: we set PB12 to output in push-pull mode and use the ODR register to pull pin 12 of the GPIOB port back and forth! But we forgot about one small detail: RCC. The point is that by default after resetting the microcontroller all peripheral modules are disconnected from the clock signal source, including GPIO. And clocking can be supplied using RCC registers. I talked about it in Part 3. First of all, we need to determine which bus GPIOB is connected to. Open the datasheet of the microcontroller and look for this table:
GPIOB is connected to the APB2 bus. Go to Reference manual, open the section about RCC, go to paragraph 7.3.7 APB2 peripheral clock enable register (RCC_APB2ENR). Using this register you can send a clock signal to the APB2 bus devices:
The RCC_APB2ENR register contains many flags for different peripherals, including our GPIOB, the flag is called IOPBEN. Before we start initializing PB12 we need to set this bit to one.
Let's go programming! Let's take the project from Part 2 as a basis: Let's create a function to initialize the port:
The first thing to do is to enable the GPIOB port clocking:
Next, configure the port. We need pin PB12. Its configuration bits are in the CRH register:
All Setup is complete! Here is the full code of the function:
Now the control. For this we need the ODR register (Figure 4). Here is the function to set the high level on PB12:
Here's a low one:
Nothing complicated, almost like on AVRs! However, we have a more serious microcontroller and it has some tricks. Expressions like GPIOB->ODR |= (1<<12) are read-modify-write operations. This is long and has side effects when the same register is accessed simultaneously from different parts of the program (for example, from an interrupt and the main program thread). This is called violation of the atomicity of the operation. But in STM32 you can do it like this:
The constructs (1<<12) are turned into a 0x1000 bitmask at compile time, and we get only one write operation to the BSRR or BRRR register (see Figures 5, 6).
Ni and a simple main() to check:
That's it, the LED connected to BP12 is blinking! "Hello, World!" is ready!
Let's now configure some port pin, like PB15, to be a pull-up input to the power supply. When we connect PB15 to minus, we will have an LED light up. The task is clear, let's proceed to implementation. Let's add a couple of lines to PortInit():
Everything here is almost the same as when setting PB12, only a different pin and MODE/CNF parameters. But the last line requires some explanations. The point is that the ODR register has a dual purpose. If a pin is configured for output, the corresponding bit in ODR is responsible for the value of the logic level on this pin. But if the pin is set to input, and CNF=10 (Input with pull-up / pull-down), then the corresponding bit in ODR controls the pull-up resistors of this pin. By the way, there is such a picture in the Reference manual:
The PortInit() function takes the following form:
void PortInit(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; //Turn on GPIOB port clocking
/// Set PB12 to output ///
GPIOB->CRH &= ~(GPIO_CRH_MODE12 | GPIO_CRH_CNF12); //reset everything to zero for starters
//MODE: output with a maximum frequency of 2 MHz
//CNF: push-pull mode
GPIOB->CRH |= (0x02 << GPIO_CRH_MODE12_Pos) | (0x00 << GPIO_CRH_CNF12_Pos);
/// Set PB15 to a pull-up input ///
GPIOB->CRH &= ~(GPIO_CRH_MODE15 | GPIO_CRH_CNF15);
//MODE: input, leave at zero
//CNF: input with pull-up / pull-down
GPIOB->CRH |= (0x00 << GPIO_CRH_MODE15_Pos) | (0x02 << GPIO_CRH_CNF15_Pos);
GPIOB->ODR |= (1<<15); //Enable pull-up
}
Let's move on to reading the state of PB15. There is an IDR register for this purpose (Fig. 3):
I think everything is clear here: if 15 bits in the IDR register are equal to 1, we return one, otherwise we return zero.
In this case main() will look like this:
On the debug board with the stm32f103c8 microcontroller, the LED is lit low on pin PB12. This means that when PB15 is shorted to ground, the zero value on this pin will be read and set to zero on PP12, and the LED will light up. If PB15 is disconnected from ground, the internal pull-up will push the pin to the plus side of the microcontroller power supply, the ReadPort() function will read a logical one on PB15, and a log. 1, which will cause the LED to turn off.
Introduction
General-purpose input/output (GPIO) is an important component of any microcontroller through which it interacts with the world around it. In STM32 microcontrollers the ports are named with letters A, B, C and so on: GPIOA, GPIOB, GPIOC.... Each GPIO has 16 I/O lines, and each line can be configured independently of the others. Here are the configuration options:
- Input floating - input with pull-up resistors disabled
- Input pull-up - input with pull-up to logic one
- Input-pull-down - input with pull-up to logic zero
- Analog - analog input (e.g. for ADC)
- Output open-drain - open-collector output (write 1 - output in high impedance state, write 0 - output is pressed to ground by internal transistor).
- Output push-pull - output push-pull (write 1 - output is in log. 1, write 0 - output is in log. 0).
- Alternate function push-pull - alternate function in push-pull mode.
- Alternate function open-drain - alternate function in the open-collector mode
Analog mode. Inside the microcontroller there are analog-to-digital converters, which, as you know, must have analog inputs. So, in Analog mode, the microcontroller leg is connected to the analog input of the ADC inside the microcontroller. In addition, all digital pipelining of this leg is disabled to reduce digital noise and power consumption.
Alternate function. In this mode, the microcontroller leg is controlled by internal digital peripherals such as the USART module.
GPIO registers
Let's take a look at the GPIO port registers.
Port configuration register low (GPIOx_CRL)
This is a configuration register for port pins numbered 0 through 7. Each pin is provided with 4 configuration bits: 2 MODEy bits and 2 CNFy bits.
MODEy[1:0]: Mode of the port pin, input or output. In the output mode you should select the maximum switching frequency of this leg, as far as I understand it is to optimize the power consumption of the port.
- 00: Input (value after reset)
- 01: Output, maximum frequency 10 MHz.
- 10: Output, maximum frequency 2 MHz.
- 11: Output, maximum frequency 50 MHz.
In input mode (MODEy[1:0]=00):
- 00: Analog mode - analog mode (connected to ADC or DAC)
- 01: Floating input - input with pull-up resistors disabled (value after reset)
- 10: Input with pull-up / pull-down - input with pull-up or pull-down.
- 11: Reserved - not used
- 00: General purpose output push-pull - output in pull/push mode
- 01: General purpose output Open-drain - open-collector output
- 10: Alternate function output Push-pull - Alternate function output push/pull mode
- 11: Alternate function output Open-drain - Alternate function output with open collector
This is a configuration register for port pins numbered 8 through 15. This is the same as the GPIOx_CRL register.
Port input data register (GPIOx_IDR)
IDRy: these bits contain the input value of the corresponding I/O port.
Port output data register (GPIOx_ODR)
ODRy: port output data.
Port bit set/reset register (GPIOx_BSRR)
This register can be used to reset or set any bit of the ODR register without read-modify-write operations.
BRy: Reset a bit of the I/O port ODR register (y= 0 ... 15)
- 0: Does not affect the corresponding bit of ODRx
- 1: Resets the corresponding ODRx bit to zero.
- 0: Does not affect the corresponding bit of ODRx
- 1: Sets the corresponding ODRx bit to one
This register can be used to reset any bit of the ODR register without read-modify-write operations.
BRy: Reset a bit in the ODR register of the I/O port (y= 0 ... 15)
- 0: No effect on the corresponding ODRx bit
- 1: Resets the corresponding ODRx bit to zero
This register is used to lock the port configuration bits after writing a valid sequence to 16 bits (LCKK) of the register. The bit values [15:0] are used to lock the GPIO configuration. During the locking sequence in LCKK, the LCKR values [15: 0] must not be changed. When the locking sequence has been written, the configuration of the selected I/O ports can only be changed after resetting the microcontroller. Each LCKy bit blocks the ability to change the four port configuration bits (CRL, CRH).
LCKK[16]: Lock key.
- 0: Port configuration lock is not active.
- 1: Port configuration lock is active. GPIOx_LCKR is locked until the next microcontroller reset.
- Write 1
- Write 0
- Write 1
- Read 0
- Read 1 (this read operation is not mandatory, but merely confirms that the lock has been successfully set).
LCKy: These bits can be read and written, but can only be written if the LCKK bit is zero.
- 0: The configuration of pin number y is not inhibited.
- 1: The configuration of pin number y is inhibited.
So, the registers have been dealt with, now it's time to practice. All examples in this article are for STM32F103C8 microcontroller. I have at my disposal such a debug board:
It has an 8 MHz quartz resonator and an LED on the PB12 port. With the help of this LED we will organize Hello, World!
The task is clear: we set PB12 to output in push-pull mode and use the ODR register to pull pin 12 of the GPIOB port back and forth! But we forgot about one small detail: RCC. The point is that by default after resetting the microcontroller all peripheral modules are disconnected from the clock signal source, including GPIO. And clocking can be supplied using RCC registers. I talked about it in Part 3. First of all, we need to determine which bus GPIOB is connected to. Open the datasheet of the microcontroller and look for this table:
GPIOB is connected to the APB2 bus. Go to Reference manual, open the section about RCC, go to paragraph 7.3.7 APB2 peripheral clock enable register (RCC_APB2ENR). Using this register you can send a clock signal to the APB2 bus devices:
The RCC_APB2ENR register contains many flags for different peripherals, including our GPIOB, the flag is called IOPBEN. Before we start initializing PB12 we need to set this bit to one.
Let's go programming! Let's take the project from Part 2 as a basis: Let's create a function to initialize the port:
Code: Select all
void PortInit(void)
{
}Code: Select all
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; //Turn on GPIOB port clocking.Code: Select all
GPIOB->CRH &= ~(GPIO_CRH_MODE12 | GPIO_CRH_CNF12); //reset everything to zero at first
//MODE: output with a maximum frequency of 2 MHz
//CNF: push-pull mode
GPIOB->CRH |= (0x02 << GPIO_CRH_MODE12_Pos) | (0x00 << GPIO_CRH_CNF12_Pos);Code: Select all
void PortInit(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; //Turn on GPIOB port clocking
GPIOB->CRH &= ~(GPIO_CRH_MODE12 | GPIO_CRH_CNF12); //reset everything to zero for starters
//MODE: output with a maximum frequency of 2 MHz
//CNF: push-pull mode
GPIOB->CRH |= (0x02 << GPIO_CRH_MODE12_Pos) | (0x00 << GPIO_CRH_CNF12_Pos);
}
Now the control. For this we need the ODR register (Figure 4). Here is the function to set a high level on PB12:
void PortSetHi(void)
{
GPIOB->ODR |= (1<<12);
}Code: Select all
void PortSetHi(void)
{
GPIOB->ODR |= (1<<12);
}Code: Select all
void PortSetLow(void)
{
GPIOB->ODR &= ~(1<<12);
}The constructs (1<<12) are turned into a 0x1000 bitmask at compile time, and we get only one write operation to the BSRR or BRRR register (see Figures 5, 6).
Ni and a simple main() to check:
Code: Select all
void main()
{
int i;
PortInit();
for(;;)
{
PortSetHi();
for(i=0; i<0x40000; i++)
;
PortSetLow();
for(i=0; i<0x40000; i++)
;
}
}Let's now configure some port pin, like PB15, to be a pull-up input to the power supply. When we connect PB15 to minus, we will have an LED light up. The task is clear, let's proceed to implementation. Let's add a couple of lines to PortInit():
Code: Select all
/// Configure PB15 to be a pull-up input ///
GPIOB->CRH &= ~(GPIO_CRH_MODE15 | GPIO_CRH_CNF15);
//MODE: input, leave at zero
//CNF: input with pull-up / pull-down
GPIOB->CRH |= (0x00 << GPIO_CRH_MODE15_Pos) | (0x02 << GPIO_CRH_CNF15_Pos);
GPIOB->ODR |= (1<<15); //Enable pull-up.The PortInit() function takes the following form:
void PortInit(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; //Turn on GPIOB port clocking
/// Set PB12 to output ///
GPIOB->CRH &= ~(GPIO_CRH_MODE12 | GPIO_CRH_CNF12); //reset everything to zero for starters
//MODE: output with a maximum frequency of 2 MHz
//CNF: push-pull mode
GPIOB->CRH |= (0x02 << GPIO_CRH_MODE12_Pos) | (0x00 << GPIO_CRH_CNF12_Pos);
/// Set PB15 to a pull-up input ///
GPIOB->CRH &= ~(GPIO_CRH_MODE15 | GPIO_CRH_CNF15);
//MODE: input, leave at zero
//CNF: input with pull-up / pull-down
GPIOB->CRH |= (0x00 << GPIO_CRH_MODE15_Pos) | (0x02 << GPIO_CRH_CNF15_Pos);
GPIOB->ODR |= (1<<15); //Enable pull-up
}
Let's move on to reading the state of PB15. There is an IDR register for this purpose (Fig. 3):
Code: Select all
int ReadPort(void)
{
if(GPIOB->IDR & (1<<15))
return 1;
return 0;
}In this case main() will look like this:
Code: Select all
void main()
{
PortInit();
for(;;)
{
if(ReadPort())
PortSetHi();
else
PortSetLow();
}
}