A hexadecimal address is a base-16 numerical label (using 0-9 and A-F) that uniquely identifies a specific device, memory location, or register on a digital bus, acting as the exact routing coordinate for your microcontroller's data. In a real circuit, changing this hex address changes the physical destination of your I2C, SPI, or Modbus packets; get it wrong, and your microcontroller talks to the wrong sensor, writes to the wrong memory sector, or causes a catastrophic bus lockup. On the bench, the most common mistake makers and junior engineers make is confusing the device address (the chip's ID on the bus) with the register address (the specific internal variable inside that chip), or tripping over the 7-bit versus 8-bit address shifting inherent to the I2C protocol.

The Anatomy of a Hexadecimal Address in Embedded Systems

To understand why hex addresses cause so many headaches, we have to look at how microcontrollers actually transmit them over the wire. We use base-16 (hexadecimal) because it maps perfectly to binary nibbles (4 bits), making it vastly easier to read than raw binary and more hardware-aligned than decimal.

Let's look at a worked numeric example using the ubiquitous PCF8574 I2C port expander or a standard I2C LCD backpack. The default address is often printed on the silkscreen as 0x27 (Hex) = 39 (Decimal) = 00100111 (Binary).

Here is where the 7-bit trap catches people. The official I2C specification reserves 7 bits for the device address and 1 bit for the Read/Write (R/W) command. The address 0x27 in 7-bit binary is 010 0111. When your ESP32 or Arduino wants to write data to this device, it appends a 0 to the end, making the transmitted byte 0100 1110 (which is 0x4E in hex). When it wants to read, it appends a 1, making the byte 0100 1111 (0x4F).

The Datasheet Discrepancy: This 1-bit shift is exactly why a chip's datasheet (like the TI PCF8574 datasheet) might list the write address as 0x4E, while your Arduino LiquidCrystal_I2C library asks you to pass 0x27 in the setup code. The library handles the bit-shift for you behind the scenes. Always check whether a library expects the 7-bit base address or the 8-bit shifted address.

Where You Meet Hexadecimal Addresses in Practice

You will encounter hex addressing anytime a microcontroller needs to talk to multiple peripherals over a shared bus without using a dedicated wire for each one. Think of the device address as the street address of an apartment building, and the register address as the specific unit number inside.

  • I2C Sensors and Displays: Environmental sensors (BME280), OLEDs (SSD1306), and real-time clocks (DS3231) all rely on 7-bit hex addresses to differentiate themselves on the SDA/SCL lines.
  • Modbus RTU over RS-485: In industrial PLC setups, inverters and power meters use 8-bit hex addresses (often referred to as Slave IDs, ranging from 0x01 to 0xF7) to route serial commands over long differential pairs.
  • SPI Memory and DMA: While SPI uses chip-select (CS) lines for device routing, the internal flash memory chips (like the W25Q128) use 24-bit hex addresses to point to exact 256-byte pages for reading and writing firmware.
  • DMX512 Lighting: Stage lighting fixtures are assigned a starting hex/decimal address so the controller knows which 512-byte universe slot corresponds to which pan/tilt/color channel.

Scenario Walkthrough: The Twin OLED I2C Bus Collision

Abstract definitions are fine, but let's look at what happens when hex addressing goes wrong on the workbench.

The Setup: You are building a dual-telemetry dashboard for a solar charge controller using an ESP32 DevKit v1. You wire two identical 0.96-inch SSD1306 I2C OLED displays to the same I2C bus (SDA on GPIO 21, SCL on GPIO 22). Both displays share the same VCC and GND rails.

The Numbers: Out of the factory, both SSD1306 displays are hardcoded to respond to the 7-bit hex address 0x3C. Your code initializes them using display.begin(SSD1306_SWITCHCAPVCC, 0x3C);.

The Outcome: You upload the firmware. One screen lights up with perfect telemetry, but the second screen remains black, or worse, both screens display garbled, overlapping, flickering pixels. The ESP32 serial monitor occasionally throws an I2C timeout error, and eventually, the hardware watchdog timer resets the board.

What Went Wrong: You created an address collision. When the ESP32 broadcast a packet to 0x3C, both displays recognized their address and attempted to send an ACKnowledge (ACK) bit by pulling the SDA line low at the exact same microsecond. This violates I2C arbitration, corrupts the data frame, and causes the bus to lock up. The ESP32's I2C peripheral hangs waiting for the bus to clear, triggering the watchdog reset.

The Fix: You must change the hex address of the second display. Flip the second OLED over and locate the SA0 (sometimes labeled SDO) resistor pads. Desolder the 0-ohm bridge from the default pad and move it to the alternate pad. This physically changes the second display's hex address to 0x3D. Update your code to initialize the second display at the new coordinate: display2.begin(SSD1306_SWITCHCAPVCC, 0x3D);. The bus collision is resolved.

How to Read and Change Hardware Addresses on the Bench

When working with port expanders like the MCP23017 or the PCF8591 ADC, you don't need a soldering iron to change the hex address. These chips feature hardware pins (usually A0, A1, A2) that let you configure the address via pull-up or pull-down resistors.

  1. Run an I2C Scanner: Before wiring anything complex, flash a standard 'I2C Scanner' sketch to your microcontroller. Open the serial monitor at 115200 baud. The scanner will sweep addresses 0x01 through 0x7F and print the exact hex address of any device that pulls SDA low to acknowledge.
  2. Identify the Address Pins: Check the datasheet for the A0, A1, and A2 pins. On the MCP23017, these pins default to internal pull-downs, meaning if left floating, they read as 0.
  3. Calculate the Base Offset: The base 7-bit hex address for the MCP23017 is 0x20 (binary 0100000). The hardware pins act as binary adders. A0 adds 1, A1 adds 2, and A2 adds 4.
  4. Wire the Jumpers: Connect the pins to VCC (logic HIGH / 1) or GND (logic LOW / 0) to set your desired address. Refer to the table below to map your jumper configuration to the resulting hex address.
A2 (Pin 17)A1 (Pin 16)A0 (Pin 15)Binary OffsetFinal 7-Bit Hex Address
GND (0)GND (0)GND (0)+00x20
GND (0)GND (0)VCC (1)+10x21
GND (0)VCC (1)GND (0)+20x22
GND (0)VCC (1)VCC (1)+30x23
VCC (1)GND (0)GND (0)+40x24
VCC (1)VCC (1)VCC (1)+70x27
Bench Tip: Never leave address pins floating on a noisy bench. Even if the datasheet claims internal pull-downs exist, a long jumper wire can act as an antenna, picking up EMI from a nearby switching power supply and flipping the pin state mid-transaction. Always use a 10kΩ physical pull-down or pull-up resistor, or tie the pin directly to GND/VCC.

Frequently Asked Questions About Hex Addressing

Why do we use hex instead of decimal for bus addresses?

Microcontrollers process data in 8-bit bytes. Hexadecimal maps perfectly to this architecture: one hex digit represents exactly 4 bits (a nibble), and two hex digits represent exactly 8 bits (a byte). Looking at 0x3C, an engineer instantly knows the binary state is 0011 1100. Looking at the decimal equivalent (60), the binary bit-masking is not visually obvious, making debugging with logic analyzers much harder.

Can two devices have the same hex address if they are on different I2C buses?

Yes. The hex address is only relevant to the local bus segment. If your ESP32 has a hardware I2C bus (Wire) and a software I2C bus (Wire1), you can have a BME280 sensor at 0x76 on the first bus, and another BME280 at 0x76 on the second bus. They will not collide because they are physically isolated on separate SDA/SCL wire pairs. For larger systems, you can also use an I2C multiplexer like the TCA9548A, which uses its own hex address (0x70) to route traffic to up to 8 isolated sub-buses, allowing you to use the same sensor address 8 times.

What is the difference between a hex address and a baud rate?

They serve entirely different layers of communication. The baud rate (e.g., 9600 or 115200) is the speed at which the physical bits are toggled on the wire (timing). The hex address is the destination label inside the data packet (routing). If your baud rate is wrong, you get garbage characters. If your hex address is wrong, the bus ignores you or collides.

Understanding hexadecimal addresses moves you from blindly copying Arduino setup code to actually engineering robust embedded systems. Always verify your 7-bit vs 8-bit shifts, hard-tie your configuration pins, and run an I2C scanner before writing a single line of application logic.