When you are writing firmware for an ESP32, configuring AVR port registers, or debugging a logic analyzer trace, you need instant translation between base-2, base-10, and base-16. A microcontroller processes everything in binary, but humans read decimal, and memory addresses are written in hexadecimal. This reference provides a complete table of binary, decimal, and hexadecimal conversions standardized for 8-bit and 16-bit embedded systems, eliminating the need to mentally calculate bit-weights on the fly.
The Master Table of Binary, Hex, and Decimal Conversions
The following table maps the 16 bit positions of a standard 16-bit integer. The notation and prefix conventions (such as 0x for hex and 0b for binary) align with the ISO/IEC 80000-13 standard for information science and technology quantities, which formalizes how binary and hexadecimal prefixes are represented in technical documentation and modern C/C++ compilers.
How to read this table: The 'Bit Index' (n) represents the positional weight ($2^n$). The '8-bit Binary' column shows the byte boundary (bits 0-7), while the '16-bit Hex' column shows the full register width. Use the 'Embedded Use Case' column to see how that specific bit-weight is typically applied in hardware abstraction layers (HAL) or direct register manipulation.
| Bit Index (n) | Weight ($2^n$) | Decimal | Hex (16-bit) | Binary (8-bit) | Embedded Use Case |
|---|---|---|---|---|---|
| 0 (LSB) | $2^0$ | 1 | 0x0001 | 00000001 | GPIO Pin 0 mask / Odd parity check |
| 1 | $2^1$ | 2 | 0x0002 | 00000010 | I2C address bit 0 / UART TX enable |
| 2 | $2^2$ | 4 | 0x0004 | 00000100 | SPI Chip Select (CS) line toggle |
| 3 | $2^3$ | 8 | 0x0008 | 00001000 | Timer prescaler bit 0 configuration |
| 4 | $2^4$ | 16 | 0x0010 | 00010000 | ADC channel 4 selection mask |
| 5 | $2^5$ | 32 | 0x0020 | 00100000 | Interrupt flag clear (e.g., INTF5) |
| 6 | $2^6$ | 64 | 0x0040 | 01000000 | PWM output enable / Motor direction |
| 7 (MSB 8-bit) | $2^7$ | 128 | 0x0080 | 10000000 | Signed 8-bit integer sign bit / Parity |
| 8 | $2^8$ | 256 | 0x0100 | N/A (9-bit) | 9-bit UART mode address bit |
| 9 | $2^9$ | 512 | 0x0200 | N/A | DMA transfer size threshold |
| 10 | $2^{10}$ | 1024 | 0x0400 | N/A | 10-bit ADC maximum resolution (1023) |
| 11 | $2^{11}$ | 2048 | 0x0800 | N/A | Flash memory page boundary offset |
| 12 | $2^{12}$ | 4096 | 0x1000 | N/A | 12-bit DAC maximum resolution (4095) |
| 13 | $2^{13}$ | 8192 | 0x2000 | N/A | External RAM bank switching mask |
| 14 | $2^{14}$ | 16384 | 0x4000 | N/A | Watchdog timer maximum timeout scalar |
| 15 (MSB 16-bit) | $2^{15}$ | 32768 | 0x8000 | N/A | Signed 16-bit integer sign bit |
Selecting the Right Column for Your Code or Circuit
Knowing the numbers is only half the battle; knowing which column to use in your specific installation or codebase prevents subtle bugs. Here is the decision framework for selecting your base format:
When to Use Hexadecimal (The 0x Column)
Use hex when configuring memory addresses, 32-bit registers, and multi-byte masks. Hexadecimal maps perfectly to binary nibbles (4 bits). If you are writing to the ESP32 GPIO_OUT_REG to set pins 8 through 11 high, writing 0x0F00 is instantly readable to another engineer. Writing the decimal equivalent (3840) or the binary equivalent (0b0000111100000000) obscures the nibble boundaries and invites transcription errors. According to the All About Circuits digital textbook, hex is the universal shorthand for machine-level architecture because it compresses long binary strings without losing the visual bit-to-position mapping.
When to Use Decimal (The Base-10 Column)
Use decimal for human-readable math, PWM duty cycles, and analog thresholds. If you are setting a PWM duty cycle to 50% on an 8-bit timer, you write 127 (or 128), not 0x7F. Similarly, when calculating voltage dividers or setting an analogRead() threshold for a 5V system on a 10-bit ADC, you use the decimal value 512 to represent the 2.5V midpoint. Decimal is for the physical world; binary and hex are for the silicon.
When to Use Binary (The 0b Column)
Use explicit binary notation strictly for bitwise masking, visual GPIO pin mapping, and truth tables. When you need to clear a specific bit in a register using the bitwise AND operator, writing PORTB &= ~0b00000100; visually proves to the reader that exactly the 3rd pin (Bit 2) is being cleared, while leaving all other pins untouched. Modern C++ compilers (and the GNU C Library manual) fully support the 0b prefix, making it safe for production firmware.
Signed Modifiers and Bit-Shifting: Altering the Base Value
The table above assumes unsigned integers (where all bits contribute positive weight). However, in embedded C/C++, variables are often signed, and modifying the base value requires understanding Two's Complement and bit-shifting.
How Two's Complement Modifies the Base Value
If your variable is a signed 8-bit integer (int8_t), the Most Significant Bit (Bit 7) ceases to be a positive weight of +128 and instead becomes a negative weight of -128.
- Unsigned
10000000: Bit 7 is 1. Value = $2^7 = 128$. - Signed
10000000: Bit 7 is 1. Value = $-2^7 = -128$. - Signed
11111111: Value = $-128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = -1$.
Rule of thumb: If the MSB is 1 in a signed variable, the decimal value is negative. This is why an 8-bit signed integer maxes out at +127 (01111111), not +255.
How Bit-Shifting Modifies the Base Value
Instead of looking up the table every time you need a specific bit mask, embedded engineers use the left-shift operator (<<). Shifting a base value of 1 to the left by n positions mathematically multiplies it by $2^n$.
Writing (1 << 5) tells the compiler to take the decimal value 1 (00000001) and shift it 5 places left, resulting in 00100000 (Decimal 32, Hex 0x20). This is functionally identical to looking up Bit 5 in the table, but it allows you to write dynamic masks like (1 << pin_number) where the pin number is a variable determined at runtime.
Limitations: What the Table Cannot Tell You
While this table of binary conversions defines the mathematical weight of every bit, it lacks the physical and architectural context required to safely interface with real hardware. Keep these three blind spots in mind:
1. Endianness (Byte Ordering in Memory)
The table shows that a 16-bit decimal value of 4660 is 0x1234 in hex. However, it does not tell you how those bytes are stored in physical RAM. On a Little-Endian architecture (like ARM Cortex-M chips in STM32 or the ESP32), the least significant byte is stored first. The memory address will hold 0x34 followed by 0x12. On a Big-Endian system, it stores 0x12 then 0x34. If you are casting a 16-bit integer pointer to an 8-bit array to send over I2C, ignoring endianness will result in the receiver reading the bytes backward.
2. Physical Logic Voltage Levels
A binary '1' is a mathematical abstraction. In the physical world, a '1' is represented by a voltage threshold. On an Arduino Uno (ATmega328P), a logic HIGH is nominally 5V (acceptable range 3.0V to 5V). On an ESP32 or Raspberry Pi Pico, a logic HIGH is 3.3V. If you use this table to calculate a bitmask and output a '1' on an ESP32 GPIO pin directly into a 5V CMOS input, the receiving chip may not recognize it as a HIGH, or worse, back-feed current. Always verify the logic family (TTL vs. CMOS) and VCC levels before connecting outputs.
3. Binary Prefixes vs. Decimal Prefixes in Storage
When scaling beyond 16 bits into memory sizes (kilobytes, megabytes), the NIST guidelines on binary prefixes draw a hard line between base-10 and base-2. A 'Kilobyte' (KB) in strict SI terms is 1,000 bytes (decimal). A 'Kibibyte' (KiB) is 1,024 bytes ($2^{10}$, binary). When sizing EEPROM buffers or SD card partitions in your firmware, confusing the decimal 1000 multiplier with the binary 1024 multiplier will cause buffer overflows and off-by-one memory faults.






