Hexadecimal is a base-16 numbering system using digits 0-9 and letters A-F to compactly represent binary states in microcontrollers, memory addresses, and digital communication protocols. While it doesn't change the physical electrons flowing through your circuit, it fundamentally changes how you configure hardware registers, interpret logic analyzer traces, and define I2C addresses by mapping perfectly to 4-bit binary nibbles. The most common confusion among hobbyists is dropping the 0x prefix—typing 10 (decimal ten) when the datasheet demands 0x10 (decimal sixteen)—which silently misconfigures sensors and bricks communication buses.
The Essential Hexadecimal Cheat Sheet Table
Every hex digit represents exactly four binary bits (a nibble). Memorizing this 16-row table eliminates 90% of the mental math required when reading logic analyzer outputs or setting GPIO masks.
| Hex | Decimal | Binary (Nibble) | Common Electronics Use Case |
|---|---|---|---|
| 0x0 | 0 | 0000 | Clearing a register, pulling a pin LOW |
| 0x1 | 1 | 0001 | Setting the least significant bit (LSB) |
| 0x2 | 2 | 0010 | I2C stop condition masking |
| 0x3 | 3 | 0011 | Setting two adjacent low bits |
| 0x4 | 4 | 0100 | Bit 2 high (often an interrupt flag) |
| 0x5 | 5 | 0101 | Alternating bit pattern (testing buses) |
| 0x6 | 6 | 0110 | Mid-byte configuration |
| 0x7 | 7 | 0111 | Masking the lower 3 bits (0x07) |
| 0x8 | 8 | 1000 | Setting the most significant bit (MSB) of a nibble |
| 0x9 | 9 | 1001 | 12-bit ADC resolution config (e.g., INA219) |
| 0xA | 10 | 1010 | Alternating high bits |
| 0xB | 11 | 1011 | Common I2C address suffix (e.g., 0x7B) |
| 0xC | 12 | 1100 | Upper nibble masking |
| 0xD | 13 | 1101 | SPI command bytes |
| 0xE | 14 | 1110 | Clearing only the LSB |
| 0xF | 15 | 1111 | Setting all 4 bits high (full mask) |
Worked Example: Writing Hex to Hardware Registers
Let's look at a real-world scenario: configuring the TI INA219 I2C current sensor. The datasheet tells you to write to the Configuration Register (address 0x00) to set the voltage range, gain, and ADC resolution. The datasheet provides a bit-map, not a single magic number. Here is how you build the hex value from scratch.
We want: 32V bus range, 320mV shunt range (Gain /8), 12-bit resolution for both ADCs, and continuous measurement mode.
- Bit 15 (Reset):
0(Normal operation) - Bit 14 (Reserved):
0 - Bit 13 (Bus Voltage Range):
1(32V range) - Bits 12-11 (PGA Gain):
11(Gain /8, 320mV) - Bits 10-7 (Bus ADC):
1001(12-bit, 1 sample) - Bits 6-3 (Shunt ADC):
1001(12-bit, 1 sample) - Bits 2-0 (Mode):
111(Continuous shunt and bus)
Concatenate the binary: 0011 1100 1100 1111.
Group into nibbles and translate using the cheat sheet:
0011 = 3
1100 = C
1100 = C
1111 = F
0x00 via I2C. If you had used decimal 3 instead of 0x3CCF, the sensor would default to a 16V range and 12-bit mode, potentially clipping your readings on a 24V solar battery bank.
Where You Meet Hex in Physical Circuits and Firmware
You will encounter hexadecimal constantly when bridging the gap between software and physical pins. Here are the four primary domains where hex is mandatory:
- I2C Device Addressing: The NXP I2C specification defines 7-bit addresses. An SSD1306 OLED display is universally known as
0x3C. If you try to scan the bus looking for decimal 3C (which is invalid syntax) or decimal 60 (the decimal equivalent), you will confuse your debugging tools. - WS2812B (NeoPixel) Color Data: These LEDs accept 24-bit GRB (Green-Red-Blue) data streams. Pure red is not
255, 0, 0in memory; it is shifted into a 24-bit hex integer:0x00FF00(Green=00, Red=FF, Blue=00). - MAC Addresses and BLE UUIDs: Network interfaces and Bluetooth Low Energy modules use 48-bit and 128-bit hex strings. An ESP32's base MAC address (e.g.,
A4:CF:12:6B:88:01) is just six hex bytes burned into the eFuse memory. - Memory-Mapped GPIO: On an RP2040 or STM32, toggling a pin often involves writing a hex mask to a specific memory address (e.g.,
0x4001C018) rather than calling a high-level function.
Decision Tree: When to Use Hex, Decimal, or Binary
Choosing the wrong number base in your code makes it unreadable to others and prone to copy-paste errors. Use this decision matrix to pick the right format for your firmware.
| Scenario | If your goal is... | Then use this format | Concrete Example |
|---|---|---|---|
| Setting hardware registers / bitmasks | Mapping directly to datasheet bit-fields | Hexadecimal | REG_WRITE(GPIO_OUT_W1TS_REG, 0x04); |
| Defining I2C / SPI addresses | Matching the manufacturer's silkscreen or datasheet | Hexadecimal | Wire.beginTransmission(0x68); (MPU6050) |
| Setting PWM duty cycles / Timers | Representing a human-readable percentage or time | Decimal | analogWrite(9, 128); (~50% duty cycle) |
| Manipulating single pin states | Checking or flipping one specific bit | Binary (0b) | if (PINB & 0b00000100) { ... } |
| Defining RGB LED colors | Pulling from CSS/web design color pickers | Hexadecimal | strip.setPixelColor(0, 0xFF00FF); (Magenta) |
Common Pitfalls and Debugging Mistakes
Even experienced engineers fall into specific hex traps when moving between high-level code and low-level hardware.
The 7-Bit vs 8-Bit I2C Address Shift
Datasheets usually list I2C addresses as 7-bit hex values (e.g., 0x3C). However, on the physical wire, the protocol shifts this left by one bit to make room for the Read/Write bit. If you hook up a logic analyzer, you won't see 0x3C; you will see 0x78 (which is 0x3C << 1). Never manually shift the address in your Arduino code—the Wire library handles the shift automatically. If you pass 0x78 to Wire.beginTransmission(), the library will shift it again, and your sensor will fail to respond.
The '0x10' vs '10' Typo
If a datasheet says 'Set bit 4 high', the hex mask is 0x10. If you accidentally type 10 in your C++ code, the compiler reads it as decimal ten (binary 0000 1010), which sets bits 1 and 3 instead. Always prefix hex literals with 0x and binary literals with 0b to force the compiler's hand.
Endianness in Multi-Byte Sensors
When reading a 16-bit value from an SPI sensor, the bytes might arrive Most Significant Byte First (Big-Endian) or Least Significant Byte First (Little-Endian). If you read 0x12 then 0x34, combining them blindly might yield 0x3412 instead of 0x1234. Always check the sensor's timing diagram to see which byte hits the MISO line first.
Quick Reference FAQ
Why do we use letters A-F in hex instead of just inventing new symbols?
Because standard ASCII keyboards already have A-F, and early computer systems needed a way to represent values 10-15 using single characters that wouldn't conflict with existing numeric parsing routines. It keeps code parsing simple and unambiguous.
How do I convert a hex color code to PWM values for a standard RGB LED?
Take the hex code (e.g., 0xFF8000 for orange). Break it into three bytes: Red = 0xFF (255), Green = 0x80 (128), Blue = 0x00 (0). Pass those decimal equivalents directly into your analogWrite() or ledcWrite() functions.
What is the default recommendation if a datasheet gives me both hex and decimal for a register value?
Always use the hexadecimal value in your code. Hex maps 1:1 with the binary bitfields shown in the datasheet diagrams, making it infinitely easier to debug with a logic analyzer later. Decimal values in datasheets are only provided for human math convenience, not for firmware implementation.






