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 Component | Mathematical 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
| Symbol | Definition | Valid Range (Hex) |
|---|---|---|
S_i | The resulting sum digit at position i | 0x0 to 0xF |
A_i | The augend digit (first number) at position i | 0x0 to 0xF |
B_i | The addend digit (second number) at position i | 0x0 to 0xF |
C_{in} | The carry-in from the previous (less significant) position | 0x0 or 0x1 |
C_{out} | The carry-out to the next (more significant) position | 0x0 or 0x1 |
i | The 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?
- Align the nibbles: Augend =
0x4A(Nibbles: 4, A). Addend =0x7F(Nibbles: 7, F). - Position $i=0$ (Least Significant Nibble):
AddA(10) +F(15) +C_{in}(0) = 25.
S_0= 25 mod 16 = 9 (0x9).
C_{out}= floor(25 / 16) = 1. - Position $i=1$ (Most Significant Nibble):
Add4+7+C_{in}(1) = 12.
S_1= 12 mod 16 = C (0xC).
C_{out}= floor(12 / 16) = 0. - 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.
- Position $i=0$ (0 + 0): Sum = 0, Carry = 0.
- Position $i=1$ (A + 6): 10 + 6 = 16. Sum = 16 mod 16 = 0, Carry = 1. (Byte boundary crossed here).
- Position $i=2$ (4 + B + Carry 1): 4 + 11 + 1 = 16. Sum = 16 mod 16 = 0, Carry = 1.
- Position $i=3$ (F + 0 + Carry 1): 15 + 0 + 1 = 16. Sum = 16 mod 16 = 0, Carry = 1. (Critical 16-bit boundary crossed).
- Position $i=4$ (0 + 0 + Carry 1): 0 + 0 + 1 = 1, Carry = 0.
- Remaining positions: 8, 0, 0 remain unchanged.
- 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 adding0x10(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
0x01to 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.






