Hexadecimal subtraction is the process of deducting base-16 numbers using digits 0-9 and letters A-F, borrowing in increments of 16 rather than 10. While the physical copper traces on your ESP32 or Arduino do not care what base you use to count, you do when configuring hardware registers, calculating memory offsets, or debugging I2C bus collisions. In a real embedded installation, miscalculating a hex offset does not just throw a compiler math error; it writes a configuration byte to the wrong memory address, potentially bricking a peripheral, corrupting an NVS partition, or causing a silent brownout. The most common point of failure for hobbyists is confusing base-16 arithmetic subtraction with bitwise masking operations, leading to corrupted register states.
The Base-16 Borrowing and Subtraction Reference Table
Before writing custom firmware or partitioning flash memory, you need an intuitive grasp of how borrowing cascades across hex boundaries. Unlike decimal, where a borrow yields 10, a hex borrow yields 16 (0x10). The table below maps common subtraction scenarios you will encounter when calculating embedded memory limits and peripheral addresses.
| Minuend (Hex) | Subtrahend (Hex) | Borrow Required? | Decimal Equivalent | Result (Hex) | Common Embedded Use Case |
|---|---|---|---|---|---|
0x10 |
0x01 |
Yes (borrow 16) | 16 - 1 = 15 | 0x0F |
Decrementing an 8-bit loop counter to zero. |
0x2A |
0x0F |
Yes (borrow 16) | 42 - 15 = 27 | 0x1B |
Calculating I2C address offsets for sensor arrays. |
0x100 |
0x01 |
Yes (double borrow) | 256 - 1 = 255 | 0xFF |
Finding the maximum value of an 8-bit hardware register. |
0x4000 |
0x0800 |
No | 16384 - 2048 = 14336 | 0x3800 |
Sizing an ESP32 SPIFFS or LittleFS partition block. |
0x7FFF |
0x0001 |
No | 32767 - 1 = 32766 | 0x7FFE |
Stepping down from the max positive signed 16-bit integer limit. |
0x0F (15), you are subtracting (16 - 1). Subtracting 16 drops the next highest nibble by 1, and adding 1 back gives you the final nibble.
Step-by-Step Worked Example: Calculating an ESP32 Memory Offset
Let us apply this to a real-world scenario. You are writing a custom bootloader or configuring the partition table for an ESP32-WROOM-32 module with a 4MB flash chip. You need to calculate the exact remaining hex space after allocating specific partitions.
The Scenario:
- Total Flash Size: 4MB =
0x00400000 - App Partition: 1.5MB =
0x00180000 - NVS (Non-Volatile Storage) Partition: 16KB =
0x00004000
First, we add the used space. 0x00180000 + 0x00004000 = 0x00184000.
Now, we must subtract the used space from the total flash size to find the remaining unallocated space:
0x00400000 - 0x00184000
Step 1: Align and subtract the lowest nibbles.
The last three nibbles are all zeros in both numbers, so they subtract cleanly to 000.
Step 2: Handle the fourth nibble (the 4096s place).
We have 0 - 4. We cannot do this, so we must borrow from the next column. However, the next column is also 0. We must cascade the borrow all the way to the 4 in the millions place.
The 4 becomes a 3. The first borrowed 0 becomes an F (15), and it lends 1 to the next 0, making it an F, which finally lends 1 to our target 0, making it 10 (16 in decimal).
Step 3: Execute the subtraction on the modified top number.
- Millions place:
3 - 0 = 3 - Hundred-thousands place:
F (15) - 1 = E (14) - Ten-thousands place:
F (15) - 8 = 7 - Thousands place:
10 (16) - 4 = C (12)
Result: 0x0027C000 (which translates to 2,605,056 bytes, or exactly 2.48MB of remaining space). If you had mistakenly borrowed in base-10 during the thousands place, you would have calculated 10 - 4 = 6, resulting in a phantom partition size that overlaps your NVS data, causing immediate filesystem corruption on boot.
Where You Meet Hex Math in Practice
Hexadecimal subtraction is not just an academic exercise; it is a daily requirement for several core electronics and embedded tasks.
1. I2C Address Collision Resolution
Many I2C sensors, like the Bosch BME280, have selectable addresses (e.g., 0x76 and 0x77). When integrating multiple identical sensors on the same bus via multiplexers or address pins, you often need to calculate register offsets or verify bus scanning ranges. If your logic analyzer shows a device ACKing at 0x77 but your library is polling 0x76, understanding the delta (a difference of 0x01) helps you quickly trace whether the SDO pin is pulled high or floating.
2. Logic Analyzer Packet Decoding
When debugging SPI or UART traffic using a Saleae Logic Pro 16 or the open-source Sigrok suite, the payload is displayed in hex. If you are reverse-engineering a proprietary protocol and need to determine the payload length byte, you will frequently subtract the start-of-frame memory pointer from the end-of-frame pointer. The software gives you the hex addresses; you must do the hex math to extract the byte count.
3. Direct Memory Access (DMA) Buffer Sizing
When configuring DMA controllers on ARM Cortex-M or ESP32 chips, buffers must often be aligned to specific hex boundaries (like 0x1000). Calculating the padding required to align a buffer end-address involves subtracting the current pointer address from the next aligned boundary.
Common Pitfalls: Arithmetic vs. Bitwise Confusion
The most frequent error makers encounter when working with hex registers is confusing arithmetic subtraction with bitwise clearing.
-) to clear a specific bit in a hardware register.
Suppose you have an 8-bit control register currently set to 0xFF (all bits high), and you want to clear bit 2 (which has a hex weight of 0x04).
- The Wrong Way (Arithmetic Subtraction):
0xFF - 0x04 = 0xFB. This works only if you are absolutely certain bit 2 was already a 1. If the register was0xF7(bit 2 already low) and you subtract0x04, you get0xF3, which accidentally clears bit 3 and corrupts your hardware state. - The Right Way (Bitwise AND NOT):
0xFF & ~0x04. The bitwise NOT of0x04is0xFB.0xFF & 0xFBguarantees bit 2 is cleared to 0, regardless of the previous state of the register, leaving all other bits untouched.
Another common pitfall is unsigned underflow wrapping. If an 8-bit microcontroller timer register is at 0x00 and you subtract 0x01, the result is not -1. Because the register is unsigned, it wraps around to 0xFF (255). Failing to account for this hex wrap-around is the root cause of countless infinite-loop bugs in DIY motor controllers and PWM timing routines.
Frequently Asked Questions
Q: Can I just use a decimal calculator and convert back to hex?
A: You can, but it introduces a translation layer where human error thrives. When reading a datasheet memory map, the boundaries are in hex. Converting 0x00184000 to 1,589,248, doing your math, and converting back takes longer and risks transcription errors. Native hex math keeps your working memory aligned with the silicon.
Q: How do I handle negative results in hex?
A: Hardware registers do not natively display negative signs. They use Two's Complement representation. If you subtract a larger hex number from a smaller one in an 8-bit space (e.g., 0x05 - 0x08), the result is 0xFD. To read this as a negative decimal, invert the bits (0x02) and add 1 (0x03), yielding -3.






