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.
Bench Tip: When subtracting hex values on the fly, convert the subtrahend's lowest nibble to its 'distance from 16'. For example, to subtract 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.

Warning: Never use the minus sign (-) 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 was 0xF7 (bit 2 already low) and you subtract 0x04, you get 0xF3, which accidentally clears bit 3 and corrupts your hardware state.
  • The Right Way (Bitwise AND NOT): 0xFF & ~0x04. The bitwise NOT of 0x04 is 0xFB. 0xFF & 0xFB guarantees 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.