Embedded Port I/O Read/Write: uint8_t/uint16_t/uint32_t and More
Embedded Port I/O Read/Write: uint8_t/uint16_t/uint32_t and More
This guide shows safe patterns to read and write hardware registers (MMIO) for byte/halfword/word widths. It also covers endianness, alignment, memory barriers, and bit operations.
Core MMIO helpers (C/C++)
#include <cstdint>
static inline void write8(uintptr_t addr, uint8_t v) {
*reinterpret_cast<volatile uint8_t*>(addr) = v;
}
static inline uint8_t read8(uintptr_t addr) {
return *reinterpret_cast<volatile const uint8_t*>(addr);
}
static inline void write16(uintptr_t addr, uint16_t v) {
*reinterpret_cast<volatile uint16_t*>(addr) = v;
}
static inline uint16_t read16(uintptr_t addr) {
return *reinterpret_cast<volatile const uint16_t*>(addr);
}
static inline void write32(uintptr_t addr, uint32_t v) {
*reinterpret_cast<volatile uint32_t*>(addr) = v;
}
static inline uint32_t read32(uintptr_t addr) {
return *reinterpret_cast<volatile const uint32_t*>(addr);
}
Notes
volatileprevents the compiler from reordering or eliding register accesses.- Use the exact width that the IP specifies; some peripherals require aligned, native-width accesses.
Memory barriers (ordering)
On ARM (CMSIS), use data/instruction barriers when required by your SoC/peripheral:
#if defined(__ARM_ARCH)
#include <cmsis_gcc.h> // or relevant CMSIS header
static inline void mb() { __DMB(); }
static inline void wmb() { __DSB(); }
static inline void rmb() { __DMB(); }
#else
static inline void mb() { asm volatile(""); }
static inline void wmb() { asm volatile(""); }
static inline void rmb() { asm volatile(""); }
#endif
Typical usage: write registers then wmb() before starting the peripheral; or mb() after polling a status bit before reading a data register.
Endianness helpers
If the peripheral/descriptor is little‑endian on a big‑endian CPU (or vice versa), swap:
static inline uint16_t bswap16(uint16_t x) { return (x>>8) | (x<<8); }
static inline uint32_t bswap32(uint32_t x) {
return ((x & 0x000000FFu) << 24) |
((x & 0x0000FF00u) << 8) |
((x & 0x00FF0000u) >> 8) |
((x & 0xFF000000u) >> 24);
}
// Example: read little-endian 32-bit value regardless of host endianness
static inline uint32_t read32_le(uintptr_t addr) {
uint32_t v = read32(addr);
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
return v;
#else
return bswap32(v);
#endif
}
Bit operations (set/clear/toggle/mask)
static inline void set_bits32(uintptr_t addr, uint32_t mask) {
write32(addr, read32(addr) | mask);
}
static inline void clr_bits32(uintptr_t addr, uint32_t mask) {
write32(addr, read32(addr) & ~mask);
}
static inline void upd_bits32(uintptr_t addr, uint32_t mask, uint32_t value) {
uint32_t v = read32(addr);
v = (v & ~mask) | (value & mask);
write32(addr, v);
}
For 8/16‑bit registers, use read8/write8 or read16/write16 with matching masks.
Packed structs (register blocks)
Define a header that matches the IP register layout. Use volatile and correct widths; avoid unaligned fields.
struct GpioRegs {
volatile uint32_t MODER; // 0x00 mode
volatile uint32_t OTYPER; // 0x04 output type
volatile uint32_t OSPEEDR; // 0x08 speed
volatile uint32_t PUPDR; // 0x0C pull-up/down
volatile uint32_t IDR; // 0x10 input data
volatile uint32_t ODR; // 0x14 output data
volatile uint32_t BSRR; // 0x18 bit set/reset
// ...
};
static inline GpioRegs* gpio(uintptr_t base) {
return reinterpret_cast<GpioRegs*>(base);
}
// Example: set pin 5 high using BSRR
static inline void gpio_set_pin(uintptr_t base, uint32_t pin) {
gpio(base)->BSRR = (1u << pin);
}
Alignment and atomicity
- Many MCUs require aligned 16/32‑bit accesses; unaligned writes may bus‑fault.
- Some status registers are write‑1‑to‑clear (W1C); never read‑modify‑write blindly. Use documented write semantics.
- For multi‑producer/consumer between ISR and main, guard shared registers/buffers or use atomic/disable IRQ sections as needed.
Example: UART write with polling
namespace UART {
static constexpr uintptr_t BASE = 0x40011000u; // example
static constexpr uintptr_t DR = BASE + 0x00; // data
static constexpr uintptr_t SR = BASE + 0x04; // status
static constexpr uint32_t TXE = (1u << 7); // transmit empty
static inline void putc(uint8_t ch) {
while ((read32(SR) & TXE) == 0) { /* spin */ }
write8(DR, ch);
}
}
Example: GPIO direction and value (8/16/32 widths)
// Assume 8-bit direction register and 8-bit data register
static constexpr uintptr_t GPIO_DIR8 = 0x50000000u;
static constexpr uintptr_t GPIO_OUT8 = 0x50000004u;
void gpio_make_output8(unsigned pin) { set_bits32(GPIO_DIR8, (1u << pin)); }
void gpio_write8(unsigned pin, bool high) {
uint8_t v = read8(GPIO_OUT8);
v = high ? (uint8_t)(v | (1u << pin))
: (uint8_t)(v & ~(1u << pin));
write8(GPIO_OUT8, v);
}
// 16-bit variant
static constexpr uintptr_t GPIO_DIR16 = 0x50000100u;
static constexpr uintptr_t GPIO_OUT16 = 0x50000104u;
void gpio_make_output16(unsigned pin) { write16(GPIO_DIR16, read16(GPIO_DIR16) | (uint16_t)(1u << pin)); }
void gpio_write16(unsigned pin, bool high) {
uint16_t v = read16(GPIO_OUT16);
v = high ? (uint16_t)(v | (1u << pin))
: (uint16_t)(v & ~(1u << pin));
write16(GPIO_OUT16, v);
}
// 32-bit variant
static constexpr uintptr_t GPIO_DIR32 = 0x50000200u;
static constexpr uintptr_t GPIO_OUT32 = 0x50000204u;
void gpio_make_output32(unsigned pin) { write32(GPIO_DIR32, read32(GPIO_DIR32) | (1u << pin)); }
void gpio_write32(unsigned pin, bool high) {
uint32_t v = read32(GPIO_OUT32);
v = high ? (v | (1u << pin))
: (v & ~(1u << pin));
write32(GPIO_OUT32, v);
}
Checklist for interviews
- Use
volatileon MMIO and respect documented access widths. - Consider ordering (barriers) and W1C semantics; avoid unsafe read‑modify‑write.
- Handle endianness and alignment explicitly.
- Prefer register block structs for clarity; keep addresses/types central.
- Isolate ISR vs. main access; use atomics/critical sections if shared.