Programing STM32 like STM8 (register-level GPIO)
Asked Answered
F

2

3

I programmed STM8 GPIO like PD_ODR_ODR4 = 1;, but stm32f10x.h doesn't have this function. Is there any .h file that has definition for bits?

Sorry, but I don't know how to explain this problem better.

I tried multiple GPIO libraries.

Follower answered 17/2, 2019 at 16:31 Comment(3)
and each microcontroller version has it's own. Use stm32 hal drivers.Consulate
GPIOD -> BSRR = (1 << pin); to set GPIOD -> BRR = (1 << pin); to reset fir port DPerrone
@Consulate actually this consistent in all STM32 uCs. He did not ask about HALPerrone
T
11

You mention stm32f10x.h in the question, so I'm assuming it's about the STM32F1 series of controllers. Other series have some differences, but the general procedure is the same.

GPIO pins are arranged in banks of 16 called ports, each having it's own set of control registers, named GPIOA, GPIOB, etc. They are defined as pointers to GPIO_TypeDef structures. There are 3 control registers that affect pin outputs.

Writing ODR sets all 16 pins at once, e.g. GPIOB->ODR = 0xF00F sets pins B0 through B3 and B12 through B15 to 1, and B4 through B11 to 0, regardless of their previous state. One can write GPIOD->ODR |= (1<<4) to set pin GPIOD4 to 1, or GPIOD->ODR &= ~(1<<4) to reset it.

Writing BSRR treats the value written as two bitmasks. The low halfword is the set mask, bits with value 1 set the corresponding bit in ODR to 1. The high halfword is the reset mask, bits with value 1 set the corresponding bit in ODR to 0. GPIOC->BSRR = 0x000701E0 would set pins C5 though C8 to 1, reset C0 through C2 to 0, and leave all other port bits alone. Trying to both set and reset the same bit when writing BSRR, then it will be set to 1.

Writing BRR is the same as writing the reset bitmask in BSRR, i.e. GPIOx->BRR = x is equivalent to GPIOx->BSRR = (x << 16).

Now it's possible to write some macros like

#define GPIOD_OUT(pin, value) GPIOD->BSRR = ((0x100 + value) << pin)
#define GPIOD4_OUT(value) GPIOD_SET(4, value)

to change single pins, but it's not as flexible as it could be, e.g. you cannot take the address of a single pin and pass it around in variables.

Bit Banding

Cortex-M controllers (not all of them, but the STM32F1 series do) have this feature to make individual bits in internal RAM and in hardware registers addressable. Each bit in the 0x40000000-0x400FFFFF range is mapped to a full 32-bit word in the 0x42000000-0x43FFFFFF range. It doesn't work with peripherals outside this address range, like USB or NVIC.

The bit-banding address of a peripheral register can be calculated with this macro

#define BB(reg) ((uint32_t *)(PERIPH_BB_BASE + ((uint32_t)&(reg) - PERIPH_BASE) * 32U))

and you can treat the resulting pointer as the base of an array holding 32 words, each corresponding to a single bit in the peripheral registers. Now it's possible to

#define PD_ODR_ODR4 (BB(GPIOD->ODR)[4])

and use it in assignments. Reading it will give 0 or 1 as its value, values written to it copy the least significant bit of the written value to the peripheral register bit. You can even take its address, and pass it to a function that does something with the pin.

Bit-banding is documented in PM0056 Cortex®-M3 programming manual.

Theosophy answered 18/2, 2019 at 10:58 Comment(0)
K
11

The answer provided by @berendi, and comment by @P__J__ are already quite helpful, but I wanted to provide more insight. For the raw (bare-metal) track-down of an STM32F103CB's GPIO read and write registers, with NO libraries or header files, jump straight to the bottom. The purpose of this answer is to *teach you* how to read datasheets and documentation yourself so you can apply these techniques to *any memory address or register* in *any microcontroller whatsoever*, including STM32.

Note that the "raw, no-header whatsoever" example at the bottom is for educational purposes: I recommend just using the header files provided by CMSIS and STM32, as applicable, rather than writing your own. However, in some circumstances you may need to quickly get access to a register, and this is how.

Quick Reference:

Define ANY address to be readable/writable:

// Be sure to add `UL` after ADDRESS_TO_MY_REGISTER to make it "unsigned long".
#define MY_REGISTER (*(volatile uint32_t *)(ADDRESS_TO_MY_REGISTER)) 

Define ANY address to be readable only:

// Be sure to add `UL` after ADDRESS_TO_MY_REGISTER to make it "unsigned long".
#define MY_REGISTER (*(volatile const uint32_t *)(ADDRESS_TO_MY_REGISTER)) 

Details: how to define any address location or register in memory in C so that it is readable/writable:

The standard (and only way really) way to access any memory address location in C is to use the following #define-based volatile pointer construct:

#define MY_REGISTER (*(volatile uint32_t *)(ADDRESS_TO_MY_REGISTER)) // Be sure to add `UL` after ADDRESS_TO_MY_REGISTER to make it "unsigned long".

How to read this:

(Essentially reading right to left): "Take the ADDRESS_TO_MY_REGISTER and cast it to a pointer to a volatile group of 4 bytes (ie: a group of 4 volatile bytes), then grab the contents of that group of 4 bytes, and make that what MY_REGISTER means." ie: MY_REGISTER now modifies the contents of the memory at this address location.

The cast to a pointer is required to convert the address location to an actual memory address (a pointer), and the dereference (*) at the far left is to make us modify the contents of that register or memory at that address, rather than just modifying the address itself. The keyword volatile is required to prevent compiler optimization which might otherwise try to assume what is in that register and optimize out your code which reads or writes from or to that register. volatile is always required when accessing registers, as one must assume they can be changed from other processes, external events or pin changes, or the hardware and/or peripherals in the mcu itself.

Even though this construct works on all devices in C (not just STM32), note that the size of the type you cast to (uint8_t, uint32_t, etc) is important for your architecture. ie: do NOT try to use uint32_t types on an 8-bit microcontroller because even though it might seem to work, atomic access to a 32-bit chunk of memory on an 8-bit microcontroller is NOT guaranteed. 8-bit accesses on an 8-bit AVR microcontroller, however, are in fact guaranteed to be automatically atomic (related reading: C++ decrementing an element of a single-byte (volatile) array is not atomic! WHY? (Also: how do I force atomicity in Atmel AVR mcus/Arduino)). For an STM32 mcu, however, 32-bit or smaller memory accesses are automatically atomic, as I've researched and described here: https://mcmap.net/q/18855/-which-variable-types-sizes-are-atomic-on-stm32-microcontrollers.

This type of #define-based construct above is used by all microcontrollers everywhere, and you can use it to arbitrarily access any memory location you see fit, literally, on any microcontroller, unless the datasheet and/or Reference Manuals state otherwise (ex: some registers require special unlocking instructions first). If you trace down the registers on AVRLibc, for instance (used by Arduino--download here: https://www.nongnu.org/avr-libc/ --> "Downloads" section), and do all the macro expansions, you'll see that all registers are 8-bits and boil down to this:

#define TCCR2A (*(volatile uint8_t *)(0xB0))

Here, register TCCR2A, or "Timer Counter Control Register A for Timer 2", is set to be 1-byte at address 0xB0.

The same is true of STM32, except that the registers are generally 32-bits instead, so you can use uint32_t instead of uint8_t (although uint8_t also works on STM32), and they frequently use struct-based constructs instead. Ex from "stm32f767xx.h":

#define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)

Where GPIO_TypeDef is a struct:

typedef struct
{
  __IO uint32_t MODER;    /*!< GPIO port mode register,               Address offset: 0x00      */
  __IO uint32_t OTYPER;   /*!< GPIO port output type register,        Address offset: 0x04      */
  __IO uint32_t OSPEEDR;  /*!< GPIO port output speed register,       Address offset: 0x08      */
  __IO uint32_t PUPDR;    /*!< GPIO port pull-up/pull-down register,  Address offset: 0x0C      */
  __IO uint32_t IDR;      /*!< GPIO port input data register,         Address offset: 0x10      */
  __IO uint32_t ODR;      /*!< GPIO port output data register,        Address offset: 0x14      */
  __IO uint32_t BSRR;     /*!< GPIO port bit set/reset register,      Address offset: 0x18      */
  __IO uint32_t LCKR;     /*!< GPIO port configuration lock register, Address offset: 0x1C      */
  __IO uint32_t AFR[2];   /*!< GPIO alternate function registers,     Address offset: 0x20-0x24 */
} GPIO_TypeDef;

And __IO is defined simply as volatile. Since each member of this struct is 4-bytes, and you have 4-byte alignment, the struct is automatically packed, so you end up with each new element of the struct simply pointing to the address location "Address offset" (as shown in the comments to the right) farther from the base address, so everything works out!

An alternative to using the STM32-defined GPIOD->BSRR type construct, for instance, would be to manually do it yourself like this:

#define GPIOD_BSRR (*(volatile uint32_t *)(GPIOD_BASE + 0x18UL)) // Don't forget the `U` or `UL` at the end of 0x18 here!

What if you want to make a register read-only? Simply add const anywhere to the left of the * in the cast-to-a-pointer:

#define MY_REGISTER (*(volatile const uint32_t *)(ADDRESS_TO_MY_REGISTER)) // Be sure to add `UL` after ADDRESS_TO_MY_REGISTER to make it "unsigned long".

Getting, setting, & clearing bits:

Now, you can set or get any bit in the register using bit-shifting and bitmasks and bit manipulation, or using some macros you might define.

Ex:

// get bit30 from the address location you just described above
bool bit30 = (MY_REGISTER >> 30UL) & 0x1UL;
// or (relies on the fact that anything NOT 0 in C is automatically `true`):
bool bit30 = MY_REGISTER & (1UL << 30UL);

// set bit 30 to 1
MY_REGISTER |= (1UL << 30UL);

// clear bit 30 to 0
MY_REGISTER &= ~(1UL << 30UL);

OR: (Ex, as Arduino does here: https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/Arduino.h)

#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitWrite(value, bit, bitvalue) (bitvalue ? bitSet(value, bit) : bitClear(value, bit))

Then:

// get bit 30
bool bit30 = bitRead(MY_REGISTER, 30);

// set bit 30 to 1
bitSet(MY_REGISTER, 30);
// or
bitWrite(MY_REGISTER, 30, 1);

// clear bit 30 to 0
bitClear(MY_REGISTER, 30);
// or
bitWrite(MY_REGISTER, 30, 0);

Raw (bare-metal) track-down of an STM32F103CB's GPIO read and write registers, with NO libraries or header files.

We will need:

  1. Main reference page for this chip: https://www.st.com/content/st_com/en/products/microcontrollers-microprocessors/stm32-32-bit-arm-cortex-mcus/stm32-mainstream-mcus/stm32f1-series/stm32f103/stm32f103cb.html#design-scroll
  2. The STM32 Reference Manual (contains register definitions): RM0008, Reference Manual for STM32F101xx, 101xx, 103xx, etc
  3. The STM32 Datasheet (contains base addresses): DS5319

Read RM0008 p161-162. enter image description here

I won't go into all the details (read above), but to read a pin you need the GPIOx_IDR (GPIO Input Data Register). To write a pin to 0 or 1 you need GPIOx_ODR (GPIO Output Data Register). Apparently (based on the wording in RM0008 as shown above) the writes to GPIOx_ODR are not atomic as a group, so if you want a bunch of pins on a port to be written atomically (all in the same instant) you need to use the GPIOx_BSRR (GPIO Bit Set/Reset Register) or the GPIOx_BRR (GPIO Bit Reset Register--can clear bits to 0 only) instead.

Assuming we are only going to do Port A, that means we need definitions for the following registers:

GPIOA_IDR   // Input Data Register (for reading pins on Port A)
GPIOA_ODR   // Output Data Register (for *nonatomically* writing 0 or 1 to pins on Port A)
GPIOA_BSRR  // Bit Set/Reset Register (for *atomically* setting (writing 1) or resetting (writing 0) pins on Port A)
GPIOA_BRR   // Bit Reset Register (for *atomically* resetting (writing 0) pins on Port A)

Let's go find the addresses to these registers!

See RM0008 p172 to 174.

enter image description here

enter image description here

enter image description here

We can see the offsets and data direction are as follows:

| Register   | "Address offset"| direction
|------------|-----------------|---------------
| GPIOA_IDR  | 0x08            | r (read only)   
| GPIOA_ODR  | 0x0C            | rw (read/write) 
| GPIOA_BSRR | 0x10            | w (write only)
| GPIOA_BRR  | 0x14            | w (write only)

Now we just need the base address for Port A. Go to DS5319 Chapter 4: Memory mapping, Figure 11. Memory Map, and you'll see that the base address for "Port A" is 0x40010800, as shown and highlighted here:

enter image description here

Now, let's manually define the registers:

#define GPIOA_IDR  (*(volatile const uint32_t *)(0x40010800UL + 0x08UL)) // use `const` since this register is read-only
#define GPIOA_ODR  (*(volatile uint32_t *)(0x40010800UL + 0x0CUL))
#define GPIOA_BSRR (*(volatile uint32_t *)(0x40010800UL + 0x10UL))
#define GPIOA_BRR  (*(volatile uint32_t *)(0x40010800UL + 0x14UL))

Now let's read and write a pin:

// Choose a pin number from 0 to 15
uint8_t pin_i = 0; // pin index

// Read it
bool pin_state = (GPIOA_IDR >> (uint32_t)pin_i) & 0x1;

// Write it to 1
GPIOA_ODR |= 1UL << (uint32_t)pin_i; // not to be used for writing to more than 1 pin at a time since apparently its operation is not atomic?
// OR
GPIOA_BSRR |= 1UL << (uint32_t)pin_i; // GOOD! RECOMMENDED approach

// Write it to 0
GPIOA_ODR &= ~(1UL << (uint32_t)pin_i); // not to be used for writing to more than 1 pin at a time since apparently its operation is not atomic?
// OR
GPIOA_BSRR |= (1UL << (uint32_t)pin_i) << 16UL; // GOOD! RECOMMENDED approach, but a bit confusing obviously
// OR
GPIOA_BRR |= 1UL << (uint32_t)pin_i; // GOOD! RECOMMENDED approach

OR: just use the HAL libraries and be done.

Ex: from "STM32Cube_FW_F1_V1.6.0/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_gpio.c":

HAL_GPIO_ReadPin() (notice they use the GPIOx->IDR register for reading):

/**
  * @brief  Reads the specified input port pin.
  * @param  GPIOx: where x can be (A..G depending on device used) to select the GPIO peripheral
  * @param  GPIO_Pin: specifies the port bit to read.
  *         This parameter can be GPIO_PIN_x where x can be (0..15).
  * @retval The input port pin value.
  */
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
  GPIO_PinState bitstatus;

  /* Check the parameters */
  assert_param(IS_GPIO_PIN(GPIO_Pin));

  if ((GPIOx->IDR & GPIO_Pin) != (uint32_t)GPIO_PIN_RESET)
  {
    bitstatus = GPIO_PIN_SET;
  }
  else
  {
    bitstatus = GPIO_PIN_RESET;
  }
  return bitstatus;
}

HAL_GPIO_WritePin() (notice they use the GPIOx->BSRR register for writing a pin to both 0 and 1):

/**
  * @brief  Sets or clears the selected data port bit.
  * 
  * @note   This function uses GPIOx_BSRR register to allow atomic read/modify 
  *         accesses. In this way, there is no risk of an IRQ occurring between
  *         the read and the modify access.
  *               
  * @param  GPIOx: where x can be (A..G depending on device used) to select the GPIO peripheral
  * @param  GPIO_Pin: specifies the port bit to be written.
  *          This parameter can be one of GPIO_PIN_x where x can be (0..15).
  * @param  PinState: specifies the value to be written to the selected bit.
  *          This parameter can be one of the GPIO_PinState enum values:
  *            @arg GPIO_BIT_RESET: to clear the port pin
  *            @arg GPIO_BIT_SET: to set the port pin
  * @retval None
  */
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
  /* Check the parameters */
  assert_param(IS_GPIO_PIN(GPIO_Pin));
  assert_param(IS_GPIO_PIN_ACTION(PinState));

  if(PinState != GPIO_PIN_RESET)
  {
    GPIOx->BSRR = GPIO_Pin;
  }
  else
  {
    GPIOx->BSRR = (uint32_t)GPIO_Pin << 16U;
  }
}

Going further:

  1. atomic variable and register access: If you need atomic access to registers to ensure they are not modified by another thread or context (ex: by an ISR) while you are reading or writing in the main loop, here are all the ways I know of to guarantee atomic access on bare-metal (no operating system) STM32: Multiple ways to enable/disable interrupts in STM32 mcus:
  2. My answer on What is the point of declaring "const volatile int *p"?

TODO

  1. [ ] Add guards (turn off interrupts) to enforce atomic access when doing a read, modify, write (ex: with |= or &=).
Kavita answered 21/2, 2019 at 2:44 Comment(3)
TODO: add guards (turn off interrupts) to enforce atomic access when doing a read, modify, write (ex: with |= or &=).Kavita
Excellent writeup. Upvoted to give credit where it's due.Vig
Excellent answer! Cleared my doubts and acted as a primer to register manipulation on embedded devices.Brandibrandice

© 2022 - 2024 — McMap. All rights reserved.