Wednesday, 23 April 2025

Implementing Bare-Metal GPIO Read/Write/Toggle APIs

 Modern embedded projects often outgrow vendor-supplied HALs and require lean, deterministic drivers written from scratch. This article explains how to finish the core data-path of a custom GPIO driver for STM32-class MCUs: reading a pin or an entire port, writing a pin/port, and toggling a pin. We’ll walk through the bit-manipulation patterns, show why each register is chosen, and finish with a checklist you can reuse on any Cortex-M design.



1 | Key Registers & Bit-Math Recap

  • IDR (Input Data Register) – delivers the sampled logic level for every pin on a port. Each bit maps 1-to-1 to a pin. 

  • ODR (Output Data Register) – drives (or reflects) the logic level for output-configured pins. 

  • Atomic bit access – bit-wise OR |= sets a bit; bit-wise AND with an inverted mask &= ~() clears a bit; XOR ^= toggles.


2 | Reading a Single Pin


uint8_t GPIO_ReadFromInputPin(GPIO_RegDef_t *pGPIOx, uint8_t pin) { return (uint8_t)((pGPIOx->IDR >> pin) & 0x01U); }

Shift returns the pin’s bit to position 0, mask removes all other bits, producing 0 or 1. This mirrors the approach in ST’s reference manual examples and common bare-metal tutorials.

3 | Reading the Whole Port


uint16_t GPIO_ReadFromInputPort(GPIO_RegDef_t *pGPIOx) { return (uint16_t)(pGPIOx->IDR); // raw 16-bit snapshot }

Useful when multiple pins change simultaneously (e.g., 8-bit parallel databus).

4 | Writing a Single Pin


void GPIO_WriteToOutputPin(GPIO_RegDef_t *pGPIOx, uint8_t pin, uint8_t value) { if (value) pGPIOx->ODR |= (1U << pin); // set else pGPIOx->ODR &= ~(1U << pin); // clear }

The pattern preserves every unrelated bit in ODR, a critical safety rule noted by ST and ARM app notes.

5 | Writing an Entire Port


void GPIO_WriteToOutputPort(GPIO_RegDef_t *pGPIOx, uint16_t value) { pGPIOx->ODR = value; // atomic 16-bit update }

Ideal for LED matrices or address buses where all lines must switch at the same instant.

6 | Toggling a Pin


void GPIO_ToggleOutputPin(GPIO_RegDef_t *pGPIOx, uint8_t pin) { pGPIOx->ODR ^= (1U << pin); // XOR flip }

The single-cycle XOR technique is the standard recommendation in ARM’s CMSIS examples and ST training material.

7 | Integration Checklist

  1. Clock gating first – ensure the port’s AHB1ENR bit is set before any read/write (macro GPIOA_PCLK_EN() etc.) .

  2. Configure mode, speed, pull-ups before data access – writing ODR while a pin is still in analog mode has no effect.

  3. Verify with the debugger – watch IDR/ODR live to confirm shifts and masks. STM32CubeIDE or open-ocd both expose these registers directly.

Conclusion

By pairing clean bit-field math with disciplined register access, these five APIs deliver deterministic GPIO control without vendor overhead. The same idioms apply across Cortex-M families—only the base addresses change. Combine them with the earlier clock-control and init functions to complete a lightweight, reusable driver layer that you can drop into any bare-metal project.

Written By: Musaab Taha

This article was improved with the assistance of AI.

No comments:

Post a Comment