When you are configuring DMA controllers, calculating flash memory sector boundaries, or debugging I2C register maps, decimal arithmetic will lead you straight into a hard fault. Microcontrollers think in base-16 and base-2. While a standard hex addition calculator web tool can give you the final sum in a millisecond, relying on black-box tools without understanding the underlying carry mechanics is how you end up with misaligned 32-bit pointers and corrupted memory dumps.

This guide breaks down the mathematical algorithm of hexadecimal addition, tracks the carries across byte boundaries, and walks through a real-world embedded systems failure caused by a single dropped nibble.

The Base-16 Addition Algorithm: Formula and Symbol Definitions

Hexadecimal addition is not just 'counting with letters'. It is a positional arithmetic system with a base ($b$) of 16. When you add two multi-digit hex numbers, the operation is performed digit-by-digit (nibble-by-nibble) from the least significant position ($i=0$) to the most significant, propagating a carry when the sum exceeds 15 (0xF).

The mathematical formula for the sum digit ($S_i$) and the carry-out ($C_{out}$) at any position $i$ is expressed as:

Formula ComponentMathematical Expression
Sum Digit ($S_i$)S_i = (A_i + B_i + C_{in}) mod 16
Carry-Out ($C_{out}$)C_{out} = floor((A_i + B_i + C_{in}) / 16)

Symbol Definition Table

SymbolDefinitionValid Range (Hex)
S_iThe resulting sum digit at position i0x0 to 0xF
A_iThe augend digit (first number) at position i0x0 to 0xF
B_iThe addend digit (second number) at position i0x0 to 0xF
C_{in}The carry-in from the previous (less significant) position0x0 or 0x1
C_{out}The carry-out to the next (more significant) position0x0 or 0x1
iThe positional index (0 = least significant nibble)Integer ≥ 0

For a deeper look at how microcontrollers handle these base-16 memory mappings at the hardware level, refer to the ESP32 Technical Reference Manual, which extensively documents hex address spaces for peripheral registers.

Rearranged Forms for Reverse-Engineering Memory Dumps

When debugging a logic analyzer trace or a raw memory dump, you rarely need to find the sum. Usually, you have the start address, the end address, and you need to find the offset (the addend), or you have a corrupted byte and need to deduce the carry-in. Here are the rearranged forms of the hex addition formula, solving for each variable:

  • Solving for Unknown Addend ($B_i$):
    B_i = (S_i - A_i - C_{in}) mod 16
    Note: If the subtraction yields a negative number, add 16 to the result and set the borrow-out (which acts as a negative carry-in for the next position) to 1.
  • Solving for Carry-In ($C_{in}$):
    C_{in} = S_i - A_i - B_i + (16 * C_{out})
    Use case: Verifying if a previous nibble's addition overflowed when analyzing a fragmented data packet.
  • Solving for Augend ($A_i$):
    A_i = (S_i - B_i - C_{in}) mod 16
    Use case: Finding the base memory address when you only have the final pointer and the struct offset.

Solved Problems: Step-by-Step Hex Addition with Unit Tracking

In base-16 math, 'unit tracking' means explicitly monitoring the nibble (4-bit) and byte (8-bit) boundaries. Dropping a carry across a byte boundary shifts your data by 256 decimal units, which is catastrophic in memory allocation.

Problem 1: 16-bit I2C Register Offset Calculation

Scenario: You are reading a sensor. The base register is 0x4A. You need to read a burst of 0x7F bytes. What is the final register address?

  1. Align the nibbles: Augend = 0x4A (Nibbles: 4, A). Addend = 0x7F (Nibbles: 7, F).
  2. Position $i=0$ (Least Significant Nibble):
    Add A (10) + F (15) + C_{in} (0) = 25.
    S_0 = 25 mod 16 = 9 (0x9).
    C_{out} = floor(25 / 16) = 1.
  3. Position $i=1$ (Most Significant Nibble):
    Add 4 + 7 + C_{in} (1) = 12.
    S_1 = 12 mod 16 = C (0xC).
    C_{out} = floor(12 / 16) = 0.
  4. Final Result: Combine nibbles to get 0xC9.

Problem 2: 32-bit Flash Memory Pointer Addition

Scenario: You are writing a bootloader. The current flash write pointer is 0x0800_F4A0. You write a payload of 0x0000_0B60 bytes. Calculate the new pointer.

  1. Position $i=0$ (0 + 0): Sum = 0, Carry = 0.
  2. Position $i=1$ (A + 6): 10 + 6 = 16. Sum = 16 mod 16 = 0, Carry = 1. (Byte boundary crossed here).
  3. Position $i=2$ (4 + B + Carry 1): 4 + 11 + 1 = 16. Sum = 16 mod 16 = 0, Carry = 1.
  4. Position $i=3$ (F + 0 + Carry 1): 15 + 0 + 1 = 16. Sum = 16 mod 16 = 0, Carry = 1. (Critical 16-bit boundary crossed).
  5. Position $i=4$ (0 + 0 + Carry 1): 0 + 0 + 1 = 1, Carry = 0.
  6. Remaining positions: 8, 0, 0 remain unchanged.
  7. Final Result: 0x0801_0000. Notice how the cascading carries perfectly aligned the pointer to the next 64KB flash sector boundary.

Real-World Scenario: The DMA Descriptor Chain Crash

Understanding the math is useless if you don't recognize what happens when it fails on the bench. Here is a narrative of a DMA (Direct Memory Access) configuration failure on an STM32F407 that cost me two days of debugging.

The Setup

I was configuring a circular DMA buffer for an audio I2S interface. The DMA controller requires a linked list of memory descriptors. Each descriptor struct is exactly 0x1C (28 decimal) bytes long. The first descriptor was placed at the base of SRAM1: 0x2000_0000. I needed to manually calculate the memory address of the 4th descriptor in the chain to verify the linker script.

The Numbers

To find the 4th descriptor, I needed to add the struct size three times (Offset = 0x1C * 3 = 0x54 bytes) to the base address.
Base: 0x2000_0000
Offset: 0x0000_0054
Target Address: 0x2000_0054.

The Outcome

The code compiled, but the moment the audio stream started, the microcontroller threw a HardFault. The logic analyzer showed the DMA controller attempting to read from 0x2000_0540 instead of 0x2000_0054. This address was outside the allocated SRAM1 block, triggering the Memory Protection Unit (MPU).

What Went Wrong

I had used a quick online hex addition calculator to verify my linker math, but I misread the output and transposed the nibbles in my C header file, writing 0x540 instead of 0x054. In decimal, this is the difference between 1344 bytes and 84 bytes. Because 32-bit ARM Cortex-M4 architectures require strict memory alignment for DMA pointers, the shifted address wasn't just 'wrong'—it was fundamentally illegal for the hardware bus. For official alignment constraints, refer to the ARM Cortex-M3/M4 Devices Generic User Guide. Always double-check your nibble placement when copying from a calculator output into a 32-bit register definition.

Assumptions, Unit Mistakes, and Magnitude Sanity Checks

Before you plug numbers into a hex addition calculator or write a script to automate offset generation, you must understand the boundaries of the formula.

When the Formula Applies (and Its Assumptions)

The modulo-16 addition formula assumes unsigned integer arithmetic. It applies perfectly to memory addresses, raw register configurations, and color codes. It does not apply to IEEE 754 floating-point hex representations (like 0x42C80000 for 100.0f), where the bits represent sign, exponent, and mantissa rather than a flat positional value. Adding two hex floats using standard base-16 addition will yield garbage data.

Unit Mistakes That Break the Math

  • Decimal vs. Hex Confusion: Adding 10 (decimal ten) is not the same as adding 0x10 (hex sixteen). If your calculator lacks a clear base indicator, you will introduce a 1.6x scaling error into your offsets.
  • Byte vs. Word Addressing: In some DSPs and older architectures, memory is word-addressed (16-bit or 32-bit words) rather than byte-addressed. Adding 0x01 to a pointer on a 32-bit word-addressed machine advances the pointer by 4 bytes, not 1. The math holds, but the physical memory translation changes.
  • Ignoring the Carry-Out: In 8-bit or 16-bit registers, the carry-out is often routed to a specific 'Carry Flag' (C-flag) in the CPU status register. If you are simulating ALU behavior and drop the $C_{out}$, your conditional branching logic will fail.

What a Realistic Answer Magnitude Looks Like

Use magnitude sanity checks to catch gross errors. If you are calculating an offset for an internal SRAM block on a standard 32-bit microcontroller (like an STM32 or ESP32), your final address should never exceed 0xFFFFFFFF. More specifically, if you know your SRAM is only 64KB (0x10000 bytes), and your base address is 0x2000_0000, any calculated sum exceeding 0x2000_FFFF is an immediate red flag that you have either added a decimal number by mistake, misaligned a byte boundary, or suffered a buffer overflow in your calculation logic.

Mastering base-16 arithmetic isn't about doing it all in your head—it is about knowing exactly what the calculator is doing under the hood so you can catch the errors before they brick your hardware.