2’s Complement in Embedded C: Signed vs Unsigned Integers Explained-Free Embedded C Tutorial

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

MODULE 2 · LESSON 2.3 · THEORY

2’s Complement in Embedded C: Signed vs Unsigned Integers Explained

How negative numbers are stored, how to compute 1’s and 2’s complement, the ranges of 8/16/32-bit types, sign extension, overflow and the signed/unsigned traps that cause real firmware bugs.

50 min
Read Time
Beginner → Intermediate
Level
M2 · L3
Module / Lesson

What You Will Learn

✓ Unsigned and signed interpretation of the same bits
✓ Sign-magnitude, 1’s complement and 2’s complement compared
✓ How to compute 1’s and 2’s complement (three methods)
✓ Why every CPU, including the Cortex-M4, uses 2’s complement
✓ Ranges of 8, 16, 32 and 64-bit signed and unsigned integers
✓ Sign extension and truncation between types
✓ Overflow, underflow and the APSR flags N, Z, C, V
✓ Signed/unsigned mixing bugs and safe patterns in C
✓ Real embedded uses: temperatures, PID errors, ADC offsets
✓ Common mistakes and best practices

Key Terms

UnsignedSignedSign bit (MSB)1’s complement2’s complementSign extensionOverflowUnderflowCarry flag (C)Overflow flag (V)INT8_MIN / INT8_MAXInteger promotion

1. Same Bits, Two Meanings: Why Signed and Unsigned Matter

Understanding 2’s complement in embedded C starts with one uncomfortable fact: a register or variable does not store “a number”, it stores a bit pattern. Whether 1111 1111 means 255 or −1 depends only on the type your C code uses to interpret it. The hardware does not know and does not care.

One byte, two interpretations
7
1
6
1
5
1
4
1
3
1
2
1
1
1
0
1
as uint8_t
255
as int8_t
−1

Consequently, choosing between uint8_t and int8_t (or uint32_t and int32_t) is not a style question. It decides how comparisons work, what happens when you divide or shift, how a value is printed and what range is representable. Bugs here are among the most common and most expensive in firmware, because they usually appear only for certain input values (a temperature below zero, a counter that wraps).

Mental model: Bits are the storage; the type is the lens. The CPU executes the same ADD instruction for both; only the way the result is interpreted (and which flags you test) differs.

2. Unsigned Representation and the Ranges of Integer Types

An unsigned integer uses all N bits for magnitude. Bit k is worth 2k, so the range is 0 to 2N − 1. A signed integer reserves the most significant bit for sign information and covers a range that is roughly half positive, half negative:

BitsUnsigned rangeMax (hex)Signed range (2’s complement)Min to max (hex)C types
80 to 2550xFF-128 to 1270x80 to 0x7Fuint8_t / int8_t
160 to 65,5350xFFFF-32,768 to 32,7670x8000 to 0x7FFFuint16_t / int16_t
320 to 4,294,967,2950xFFFFFFFF-2,147,483,648 to 2,147,483,6470x80000000 to 0x7FFFFFFFuint32_t / int32_t
640 to 18,446,744,073,709,551,6150xFFFFFFFFFFFFFFFF-9,223,372,036,854,775,808 to 9,223,372,036,854,775,8070x8000000000000000 to 0x7FFFFFFFFFFFFFFFuint64_t / int64_t
The asymmetry: The signed range has one more negative value than positive: int8_t covers −128 to +127. Zero uses one of the non-negative patterns, so there is no room for +128. This has a nasty consequence: abs(INT8_MIN) cannot be represented in 8 bits.
limits.c — the exact limits from <stdint.h>
#include <stdio.h> #include <stdint.h> int main(void) { printf(“int8_t : %d to %d\n”, INT8_MIN, INT8_MAX); printf(“uint8_t : 0 to %u\n”, UINT8_MAX); printf(“int16_t : %d to %d\n”, INT16_MIN, INT16_MAX); printf(“uint16_t : 0 to %u\n”, UINT16_MAX); printf(“int32_t : %ld to %ld\n”, (long)INT32_MIN, (long)INT32_MAX); printf(“uint32_t : 0 to %lu\n”, (unsigned long)UINT32_MAX); return 0; }
Output (verified)
int8_t : -128 to 127 uint8_t : 0 to 255 int16_t : -32768 to 32767 uint16_t : 0 to 65535 int32_t : -2147483648 to 2147483647 uint32_t : 0 to 4294967295

3. How Can a Computer Store Negative Numbers? Three Schemes

Three schemes were invented to encode negative numbers in a fixed number of bits. All of them use the MSB as a sign indicator (0 = positive or zero, 1 = negative), but they differ in how the remaining bits are interpreted. The table shows every 4-bit pattern under each scheme:

Bit patternUnsignedSign-magnitude1’s complement2’s complement
00000000
00011111
00102222
00113333
01004444
01015555
01106666
01117777
10008−0−7−8
10019−1−6−7
101010−2−5−6
101111−3−4−5
110012−4−3−4
110113−5−2−3
111014−6−1−2
111115−7−0−1
  • Sign-magnitude: MSB is the sign, the rest is the absolute value. Easy for humans, but there are two zeros (0000 and 1000) and addition needs special sign logic.
  • 1’s complement: a negative number is the bitwise inversion of the positive one. Still two zeros (0000 and 1111), and an “end-around carry” correction is needed after addition.
  • 2’s complement: a negative number is the inversion plus one. One zero, and the ordinary binary adder works for positive and negative numbers alike. This is what every modern processor uses.
All 16 four-bit patterns: unsigned value (u) versus 2’s complement value (s)
0000
u: 0
s: 0
0001
u: 1
s: 1
0010
u: 2
s: 2
0011
u: 3
s: 3
0100
u: 4
s: 4
0101
u: 5
s: 5
0110
u: 6
s: 6
0111
u: 7
s: 7
1000
u: 8
s: −8
1001
u: 9
s: −7
1010
u: 10
s: −6
1011
u: 11
s: −5
1100
u: 12
s: −4
1101
u: 13
s: −3
1110
u: 14
s: −2
1111
u: 15
s: −1
MSB = 0: non-negativeMSB = 1: negative in signed interpretation

4. 1’s Complement: Invert Every Bit

The 1’s complement of a number is obtained by flipping every bit: each 0 becomes 1 and each 1 becomes 0. In C this is the bitwise NOT operator ~. For an 8-bit value x, the 1’s complement is 255 − x (all ones minus x), because inverting every bit of x subtracts it from 1111 1111.

1’s complement of 5 (0000 0101)
+5
7
0
6
0
5
0
4
0
3
0
2
1
1
0
0
1
▼ invert every bit (~)
~5
1
1
1
1
1
0
1
0

Under the 1’s complement scheme 1111 1010 would represent −5. The scheme has a flaw you can see immediately: 0000 0000 (+0) inverts to 1111 1111 (−0). Two representations of zero waste a pattern and complicate comparisons (is −0 equal to +0?). This is the reason 1’s complement is not used for integer arithmetic today.

It is still used in two places in embedded work: the Internet checksum (IP, TCP and UDP headers use 1’s complement addition), and as the first step of computing the 2’s complement.

ones_complement.c
#include <stdio.h> #include <stdint.h> int main(void) { uint8_t x = 5; uint8_t inv = (uint8_t)~x; /* cast: ~ promotes to int */ printf(“x = 0x%02X (%u)\n”, x, x); printf(“~x = 0x%02X (%u)\n”, inv, inv); printf(“x + ~x = 0x%02X (always all ones)\n”, (uint8_t)(x + inv)); printf(“255 – x = %u (same as ~x)\n”, 255u – x); return 0; }
Output (verified)
x = 0x05 (5) ~x = 0xFA (250) x + ~x = 0xFF (always all ones) 255 – x = 250 (same as ~x)

5. 2’s Complement: How Negative Numbers Are Stored

The 2’s complement of an N-bit number x is defined as 2N − x. Computing 2N directly is awkward, but there is a trivially easy shortcut: invert all bits, then add 1. Because x + ~x = 1111…1 = 2N − 1, adding one more gives ~x + 1 = 2N − x.

Method 1: invert and add one — find the representation of −5 in 8 bits

−5 = ~5 + 1
+5
7
0
6
0
5
0
4
0
3
0
2
1
1
0
0
1
▼ step 1: invert
~5
1
1
1
1
1
0
1
0
▼ step 2: add 1
−5 = 0xFB
1
1
1
1
1
0
1
1

Method 2: the right-to-left shortcut

Starting at the LSB, copy bits unchanged up to and including the first 1, then invert all remaining bits. For 0010 1000 (40): the lowest 1 is at bit 3, so keep 1000 and invert the rest: 1101 1000 = −40.

Right-to-left shortcut for 40 → −40
+40
7
0
6
0
5
1
4
0
3
1
2
0
1
0
0
0
−40
1
1
0
1
1
0
0
0
first 1 from the rightcopied unchanged

Method 3: subtract from 2N

−5 in 8 bits = 256 − 5 = 251 = 0xFB. This view explains why it works: negative numbers are stored as large unsigned numbers that behave like negatives under arithmetic modulo 2N.

Decoding a negative pattern: the MSB has a negative weight

To read a 2’s complement number, treat the MSB as worth −2N−1 instead of +2N−1. Then add the weights as usual:

1111 1011 decoded as a signed byte
bit 7
-128
1
-128
bit 6
64
1
64
bit 5
32
1
32
bit 4
16
1
16
bit 3
8
1
8
bit 2
4
0
0
bit 1
2
1
2
bit 0
1
1
1
(-128) + 64 + 32 + 16 + 8 + 2 + 1 = -5
1000 0000 = −128 (only the sign bit set: the most negative value)
bit 7
-128
1
-128
bit 6
64
0
0
bit 5
32
0
0
bit 4
16
0
0
bit 3
8
0
0
bit 2
4
0
0
bit 1
2
0
0
bit 0
1
0
0
(-128) = -128
Bit patternHexSigned valueReasoning
0111 11110x7F+127Largest positive: all bits except the sign bit
0000 00010x01+1
0000 00000x000The one and only zero
1111 11110xFF−1−128 + 127
1111 11100xFE−2−128 + 126
1000 00010x81−127−128 + 1
1000 00000x80−128Most negative value
Handy facts: −1 is always all ones (0xFF, 0xFFFF, 0xFFFFFFFF). The most negative value is a 1 followed by zeros. The sign of a number is simply its MSB.

6. Why Hardware Uses 2’s Complement: One Adder for Everything

Processors use 2’s complement because the same binary adder works for signed and unsigned numbers, and because subtraction becomes addition. The circuit that adds 5 + 3 also adds 5 + (−3); the ALU needs no sign logic.

5 + (−3) using plain binary addition (the carry out of the MSB is discarded)
carry
1
1
1
1
1
1
0
1
0
A
0
0
0
0
0
1
0
1
+ B
1
1
1
1
1
1
0
1
sum
1
0
0
0
0
0
0
1
0
5 + (−3): the sum is 1 0000 0010. Drop the carry-out; the byte 0000 0010 = +2

Here 1111 1101 is −3 (2’s complement). The addition produces 1 0000 0010; the 9th bit (carry out) is dropped and the remaining byte 0000 0010 is +2, the correct answer for 5 − 3. The carry-out is not an error here; it is part of the arithmetic modulo 256.

PropertySign-magnitude1’s complement2’s complement
Number of zeros221
Addition circuitNeeds sign comparison and subtract pathNeeds end-around carryOrdinary adder
Subtraction a − bSpecial logica + ~b + carry correctiona + ~b + 1
Range of 8 bits−127 to +127−127 to +127−128 to +127
Used todayFloating-point sign bitInternet checksumAll integer ALUs

On the Cortex-M4, SUB r0, r1, r2 is executed internally as r1 + ~r2 + 1. The C (carry) flag afterwards means “no borrow occurred”, which is the inverse of what many programmers expect. You will see this when you read the disassembly.

7. Same Bit Pattern, Signed versus Unsigned: The Complete Table

HexBinaryAs uint8_tAs int8_t
0x000000000000
0x010000000111
0x7F01111111127127
0x8010000000128−128
0x8110000001129−127
0xC811001000200−56
0xF011110000240−16
0xFE11111110254−2
0xFF11111111255−1

Notice the pattern: for MSB = 0 both interpretations agree. For MSB = 1, the signed value is the unsigned value minus 256 (minus 2N in general). That is the formula to remember: signed = unsigned − 2N when the sign bit is set.

Click the bits: watch unsigned and signed change together
7
0
128
6
0
64
5
0
32
4
0
16
3
0
8
2
0
4
1
0
2
0
0
1
Unsigned decimal
0
Signed (2’s complement)
0
Hexadecimal
0
Binary
0
(function(){var N=8,v=0; function r(){var u=v>>>0,s=(N>(N-1))&1)?v-Math.pow(2,N):v,b=”,h; for(var i=N-1;i>=0;i–){var on=(v>>>i)&1,e=document.getElementById(‘t8s_b’+i);b+=on;e.textContent=on; e.style.background=on?’#10b981′:’#e2e8f0′;e.style.color=on?’white’:’#475569′;e.style.borderColor=on?’#10b981′:’#cbd5e1′;if(i%4==0&&i>0)b+=’ ‘;} h=u.toString(16).toUpperCase();while(h.length<N/4)h=’0’+h; document.getElementById(‘t8s_dec’).textContent=u;document.getElementById(‘t8s_sgn’).textContent=s; document.getElementById(‘t8s_hex’).textContent=’0x’+h;document.getElementById(‘t8s_bin’).textContent=b;} window.tgl_t8s=function(i){v=(v^(1<>>0;if(N<32)v=v&((1<<N)-1);r();}; window.rst_t8s=function(){v=0;r();}; window.ful_t8s=function(){v=(N<32)?((1<<N)-1):0xFFFFFFFF;r();}; r();})();

8. Sign Extension and Truncation When Changing Type Width

When a narrower signed value is stored in a wider variable, the C compiler performs sign extension: the sign bit is copied into all the new upper bits so that the numeric value is preserved. Unsigned values are zero-extended: new upper bits are zeros.

int8_t −5 extended to int16_t (sign bit copied into the 8 new bits)
int8_t
1
1
1
1
1
0
1
1
▼ widen to 16 bits: copy bit 7 upwards
int16_t
1
1
1
1
1
1
1
1
1
1
1
1
1
0
1
1
uint8_t 0xFB extended to uint16_t (zero-extension: new bits are 0)
uint8_t
1
1
1
1
1
0
1
1
▼ widen to 16 bits: new bits are zero
uint16_t
0
0
0
0
0
0
0
0
1
1
1
1
1
0
1
1
extend.c — widening, narrowing and the printf trap
#include <stdio.h> #include <stdint.h> int main(void) { int8_t s8 = –5; /* bit pattern 0xFB */ uint8_t u8 = 0xFB; /* same bits, value 251 */ int16_t s16 = s8; /* sign extension */ uint16_t u16 = u8; /* zero extension */ int16_t big = –300; /* 0xFED4 */ int8_t cut = (int8_t)big; /* narrowing: keeps low byte 0xD4 */ printf(“int8_t -5 -> int16_t : %d (0x%04X)\n”, s16, (unsigned)(uint16_t)s16); printf(“uint8_t FB -> uint16_t : %u (0x%04X)\n”, u16, u16); printf(“(int8_t)(-300) : %d (0x%02X)\n”, cut, (unsigned)(uint8_t)cut); printf(“same byte, signed vs unsigned: %d and %u\n”, s8, u8); return 0; }
Output (verified)
int8_t -5 -> int16_t : -5 (0xFFFB) uint8_t FB -> uint16_t : 251 (0x00FB) (int8_t)(-300) : -44 (0xD4) same byte, signed vs unsigned: -5 and 251
Narrowing is silent: Assigning a wider value to a narrower variable keeps only the low bits. For unsigned types this is well-defined modular arithmetic. For signed targets the result of an out-of-range conversion is implementation-defined (GCC wraps as shown), so do not rely on it for portable code.

9. Overflow and Underflow With Signed and Unsigned Integers

Overflow means the mathematically correct result does not fit in the available bits. Underflow is the same event in the negative direction (below the minimum). The two number types behave differently:

UnsignedSigned (2’s complement)
Overflow255 + 1 = 0 (wraps)127 + 1 = −128 in hardware (wraps)
Underflow0 − 1 = 255 (wraps)−128 − 1 = +127 in hardware
Defined by the C standard?Yes: modulo 2NNo: undefined behaviour; the compiler may assume it never happens
Hardware flagC (carry): carry out of the MSBV (overflow): carry into the MSB differs from carry out
Typical causeCounter reaches maximum, 0 − 1 in loopsAccumulating sensor values, multiplying

Detecting signed overflow by hand: 127 + 1

0111 1111 + 0000 0001 = 1000 0000: two positives gave a negative result
carry
0
1
1
1
1
1
1
1
0
A
0
1
1
1
1
1
1
1
+ B
0
0
0
0
0
0
0
1
sum
0
1
0
0
0
0
0
0
0
127 + 1 = 1000 0000 (no carry-out, but the sign bit flipped)
Positive + positive = negative → signed overflow. The unsigned view is fine (127 + 1 = 128).
  • Rule 1: adding two numbers of the same sign and getting the opposite sign means overflow.
  • Rule 2: adding numbers of different signs can never overflow.
  • Rule 3 (hardware): V flag = carry into MSB XOR carry out of MSB.

The APSR flags on the Cortex-M4

FlagNameSet whenUsed for
NNegativeResult bit 31 = 1Signed comparisons, sign test
ZZeroResult is 0==, !=
CCarryUnsigned carry out (or NOT borrow on subtract)Unsigned comparisons, multi-word arithmetic
VOverflowSigned overflowSigned comparisons (with N)
overflow_check.c — safe detection in C
#include <stdio.h> #include <stdint.h> #include <stdbool.h> /* Signed overflow check done safely: never compute the overflowing sum in int8_t */ bool add_overflows_i8(int8_t a, int8_t b) { int sum = (int)a + (int)b; /* computed in int, cannot overflow here */ return (sum > INT8_MAX) || (sum < INT8_MIN); } int main(void) { uint8_t u = 200; u = (uint8_t)(u + 100); /* unsigned: wraps, well defined */ printf(“uint8_t 200 + 100 = %u (wrapped)\n”, u); printf(“int8_t 100 + 27 overflow? %d\n”, add_overflows_i8(100, 27)); printf(“int8_t 100 + 28 overflow? %d\n”, add_overflows_i8(100, 28)); printf(“int8_t -100 + -28 overflow? %d\n”, add_overflows_i8(-100, –28)); printf(“int8_t -100 + -29 overflow? %d\n”, add_overflows_i8(-100, –29)); int8_t r; bool ov = __builtin_add_overflow((int8_t)127, (int8_t)1, &r); /* GCC/Clang built-in */ printf(“builtin: 127 + 1 -> result %d, overflow flag %d\n”, r, ov); return 0; }
Output (verified)
uint8_t 200 + 100 = 44 (wrapped) int8_t 100 + 27 overflow? 0 int8_t 100 + 28 overflow? 1 int8_t -100 + -28 overflow? 0 int8_t -100 + -29 overflow? 1 builtin: 127 + 1 -> result -128, overflow flag 1
Do not test for overflow after it happened: int8_t r = a + b; if (r < a) ... relies on undefined behaviour for signed types and may be optimised away. Check before adding (as above), widen the type, or use __builtin_add_overflow.

10. Mixing Signed and Unsigned in C: Classic Bugs

The C rules for combining signed and unsigned operands are a rich source of defects. When a signed and an unsigned operand of the same width meet in an expression, the signed operand is converted to unsigned. A negative number becomes a very large positive number. Small types (char, short, uint8_t) are first promoted to int (integer promotion).

mixing.c — surprising results
#include <stdio.h> #include <stdint.h> int main(void) { int s = –1; unsigned u = 1; printf(“-1 < 1u : %d (surprise!)\n”, s < u); /* s converted to unsigned */ printf(“(int)-1 < (int)1u : %d\n”, s < (int)u); uint8_t a = 5, b = 10; printf(“a – b as uint8_t : %u\n”, (uint8_t)(a – b)); /* wraps to 251 */ printf(“a – b as int : %d\n”, a – b); /* promoted to int: -5 */ printf(“(a – b) < 0 ? : %d\n”, (a – b) < 0); /* int comparison: true */ uint32_t t1 = 5, t2 = 10; printf(“t1 – t2 < 0 (uint32): %d (never true)\n”, (t1 – t2) < 0); printf(“-8 >> 1 : %d (arithmetic shift on GCC)\n”, –8 >> 1); printf(“0xF8 >> 1 (uint8_t): %u\n”, (unsigned)((uint8_t)0xF8 >> 1)); return 0; }
Output (verified)
-1 < 1u : 0 (surprise!) (int)-1 < (int)1u : 1 a – b as uint8_t : 251 a – b as int : -5 (a – b) < 0 ? : 1 t1 – t2 < 0 (uint32): 0 (never true) -8 >> 1 : -4 (arithmetic shift on GCC) 0xF8 >> 1 (uint8_t): 124
LineWhy
-1 < 1u is 0-1 is converted to unsigned: 4294967295, which is not less than 1
a - b with uint8_t gives −5 as int but 251 when stored back in uint8_tPromotion to int happens first; truncation happens on assignment
(t1 - t2) < 0 with uint32_t is never trueUnsigned values cannot be negative; the subtraction wraps instead
-8 >> 1 is −4Right shift of a negative signed value is implementation-defined; GCC does an arithmetic shift (keeps the sign)
Safe pattern for loops and counters: Use int (signed) for loop counters that count down to zero and test >= 0; use unsigned only for bit patterns, registers and values that are never negative. Never mix them in a comparison without an explicit cast and a comment.

11. Real Uses of 2’s Complement in Embedded C: Signed and Unsigned Integers

SituationCorrect typeWhy
Temperature sensor (−40 to +125 °C)int16_t (or int8_t in whole degrees)Negative values exist; 0xFF must be read as −1, not 255
16-bit signed sensor value split over two I2C bytes (accelerometer, gyro)int16_t v = (int16_t)((hi << 8) | lo);The combined bits are 2’s complement; cast to int16_t after combining
PID controller error = setpoint − measuredint32_tError is positive or negative; unsigned would wrap to a huge number
ADC raw value (12-bit)uint16_tAlways 0..4095; differential ADCs use signed
Millisecond tick counteruint32_tWrap-around subtraction works only for unsigned
Register values and bit masksuint32_tNo sign bit, shifts are logical and well defined
Motor speed / audio sampleint16_tSigned waveform around zero
Array index or sizesize_t or uint16_tNever negative; avoid signed/unsigned comparison warnings

12. Common Mistakes With Signed and Unsigned Integers

Mistake 1 — using unsigned for values that can be negative: uint8_t temp = -5; stores 251. Comparisons such as temp < 0 are never true.
Mistake 2 — reading a signed sensor into an unsigned variable: A 16-bit gyroscope value 0xFFF6 is −10. Stored as uint16_t it becomes 65526 and your control loop goes wild.
Mistake 3 — checking overflow after the fact on signed integers: Signed overflow is undefined behaviour; the compiler may delete the check.
Mistake 4 — comparing signed with unsigned: if (len > -1) with size_t len is always true; if (x < sizeof(buf)) with negative int x converts x to a huge unsigned value.
Mistake 5 — unsigned loop counter that counts down: for (uint8_t i = 9; i >= 0; i--) never terminates because an unsigned i is always ≥ 0.
Mistake 6 — assuming abs(INT_MIN) works: The positive counterpart of the most negative value does not exist; the result stays negative.
Mistake 7 — printing with the wrong specifier: printf("%u", -1) prints 4294967295; printf("%d", 0xFFFFFFFFu) prints −1.
Mistake 8 — right-shifting negative values and expecting a logical shift: On most compilers the shift is arithmetic (keeps the sign). Cast to unsigned first if you want zeros shifted in.

13. Best Practices for 2’s Complement in Embedded C

Good habits around 2’s complement in embedded C prevent most signed/unsigned defects before they reach the test bench:

  • Use fixed-width types (int16_t, uint32_t) so you know the exact range.
  • Choose unsigned for bit patterns, registers, masks and counters; signed for physical quantities that can be negative.
  • Never mix signed and unsigned in comparisons; cast explicitly to the intended type and add a comment.
  • Cast to the signed type after assembling bytes: (int16_t)((uint16_t)hi << 8 | lo).
  • Check for overflow before performing signed arithmetic, or widen to a larger type first.
  • Enable compiler warnings: -Wall -Wextra -Wconversion -Wsign-compare.
  • Document the unit and range next to every variable: int16_t temp_c_x10; /* −400..+1250 */.

Key Takeaways — Lesson 2.3

  • Bits have no sign; the type decides whether 0xFF means 255 or −1.
  • 1’s complement = invert all bits (~x). It has two zeros and is not used for integer arithmetic.
  • 2’s complement = invert and add 1 (equivalently 2N − x). It has one zero and one adder for signed and unsigned numbers.
  • The MSB of a signed number has weight −2N−1; −1 is all ones; the most negative value is 1 followed by zeros.
  • Ranges: unsigned 0 to 2N − 1; signed −2N−1 to 2N−1 − 1.
  • Sign extension copies the sign bit; zero extension adds zeros.
  • Unsigned overflow wraps (defined). Signed overflow is undefined behaviour in C; on the Cortex-M4 it sets the V flag.
  • Mixing signed and unsigned converts the signed operand to unsigned: -1 < 1u is false.
  • Knowing 2’s complement in embedded C lets you decode registers, sensor data and debugger values correctly.

Frequently Asked Questions

What is 2’s complement in embedded C?
It is the way signed integers are stored by virtually all processors: a negative number is the bitwise inversion of its positive value plus one. In C, int8_t, int16_t and int32_t are 2’s complement on every mainstream compiler (and mandatory in C23).
How do I find the 2’s complement of a number?
Invert all bits and add 1. For +5 (0000 0101) the inversion is 1111 1010; adding 1 gives 1111 1011 = −5 = 0xFB. Equivalent: 2N − x, or copy bits up to the first 1 from the right and invert the rest.
Why is the range of int8_t −128 to +127?
Eight bits give 256 patterns. Half have MSB = 1 (negative, −128 to −1) and half have MSB = 0 (0 to +127). Zero occupies one non-negative pattern, so there is one more negative value than positive.
Is 0xFF equal to 255 or −1?
Both, depending on the type: as uint8_t it is 255, as int8_t it is −1. The bits are identical; only the interpretation differs.
What is the difference between 1’s complement and 2’s complement?
1’s complement is just the inverted bits and has two zeros (+0 and −0). 2’s complement adds one to the inverted bits, has a single zero and lets the same adder handle signed and unsigned arithmetic.
What happens when a signed integer overflows in C?
It is undefined behaviour. The hardware wraps (127 + 1 gives −128 and the V flag is set), but the compiler is allowed to assume it never happens and optimise accordingly. Check before the operation or use wider types or __builtin_add_overflow.
What is sign extension?
When a signed value is stored in a wider type, the sign bit is copied into the new upper bits so the value is preserved: int8_t −5 (0xFB) becomes int16_t 0xFFFB. Unsigned values are zero-extended instead.
Why does -1 < 1u evaluate to false in C?
The signed operand is converted to unsigned, so −1 becomes 4294967295 (0xFFFFFFFF), which is larger than 1. Always compare values of the same signedness.
Should I use signed or unsigned for loop counters?
Use signed int for counters that count down to zero and test >= 0; use unsigned for bit patterns, registers and sizes. Avoid mixing them without explicit casts.

Ready to practise?

Next: worked 2’s complement examples, overflow detection and signed-versus-unsigned interpretation of the same bit patterns.

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *