Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
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.
What You Will Learn
Key Terms
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.
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).
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:
| Bits | Unsigned range | Max (hex) | Signed range (2’s complement) | Min to max (hex) | C types |
|---|---|---|---|---|---|
| 8 | 0 to 255 | 0xFF | -128 to 127 | 0x80 to 0x7F | uint8_t / int8_t |
| 16 | 0 to 65,535 | 0xFFFF | -32,768 to 32,767 | 0x8000 to 0x7FFF | uint16_t / int16_t |
| 32 | 0 to 4,294,967,295 | 0xFFFFFFFF | -2,147,483,648 to 2,147,483,647 | 0x80000000 to 0x7FFFFFFF | uint32_t / int32_t |
| 64 | 0 to 18,446,744,073,709,551,615 | 0xFFFFFFFFFFFFFFFF | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | 0x8000000000000000 to 0x7FFFFFFFFFFFFFFF | uint64_t / int64_t |
abs(INT8_MIN) cannot be represented in 8 bits.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 pattern | Unsigned | Sign-magnitude | 1’s complement | 2’s complement |
|---|---|---|---|---|
| 0000 | 0 | 0 | 0 | 0 |
| 0001 | 1 | 1 | 1 | 1 |
| 0010 | 2 | 2 | 2 | 2 |
| 0011 | 3 | 3 | 3 | 3 |
| 0100 | 4 | 4 | 4 | 4 |
| 0101 | 5 | 5 | 5 | 5 |
| 0110 | 6 | 6 | 6 | 6 |
| 0111 | 7 | 7 | 7 | 7 |
| 1000 | 8 | −0 | −7 | −8 |
| 1001 | 9 | −1 | −6 | −7 |
| 1010 | 10 | −2 | −5 | −6 |
| 1011 | 11 | −3 | −4 | −5 |
| 1100 | 12 | −4 | −3 | −4 |
| 1101 | 13 | −5 | −2 | −3 |
| 1110 | 14 | −6 | −1 | −2 |
| 1111 | 15 | −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.
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.
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.
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
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.
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:
| Bit pattern | Hex | Signed value | Reasoning |
|---|---|---|---|
| 0111 1111 | 0x7F | +127 | Largest positive: all bits except the sign bit |
| 0000 0001 | 0x01 | +1 | |
| 0000 0000 | 0x00 | 0 | The one and only zero |
| 1111 1111 | 0xFF | −1 | −128 + 127 |
| 1111 1110 | 0xFE | −2 | −128 + 126 |
| 1000 0001 | 0x81 | −127 | −128 + 1 |
| 1000 0000 | 0x80 | −128 | Most negative value |
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.
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.
| Property | Sign-magnitude | 1’s complement | 2’s complement |
|---|---|---|---|
| Number of zeros | 2 | 2 | 1 |
| Addition circuit | Needs sign comparison and subtract path | Needs end-around carry | Ordinary adder |
| Subtraction a − b | Special logic | a + ~b + carry correction | a + ~b + 1 |
| Range of 8 bits | −127 to +127 | −127 to +127 | −128 to +127 |
| Used today | Floating-point sign bit | Internet checksum | All 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
| Hex | Binary | As uint8_t | As int8_t |
|---|---|---|---|
| 0x00 | 00000000 | 0 | 0 |
| 0x01 | 00000001 | 1 | 1 |
| 0x7F | 01111111 | 127 | 127 |
| 0x80 | 10000000 | 128 | −128 |
| 0x81 | 10000001 | 129 | −127 |
| 0xC8 | 11001000 | 200 | −56 |
| 0xF0 | 11110000 | 240 | −16 |
| 0xFE | 11111110 | 254 | −2 |
| 0xFF | 11111111 | 255 | −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.
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.
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:
| Unsigned | Signed (2’s complement) | |
|---|---|---|
| Overflow | 255 + 1 = 0 (wraps) | 127 + 1 = −128 in hardware (wraps) |
| Underflow | 0 − 1 = 255 (wraps) | −128 − 1 = +127 in hardware |
| Defined by the C standard? | Yes: modulo 2N | No: undefined behaviour; the compiler may assume it never happens |
| Hardware flag | C (carry): carry out of the MSB | V (overflow): carry into the MSB differs from carry out |
| Typical cause | Counter reaches maximum, 0 − 1 in loops | Accumulating sensor values, multiplying |
Detecting signed overflow by hand: 127 + 1
- 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
| Flag | Name | Set when | Used for |
|---|---|---|---|
| N | Negative | Result bit 31 = 1 | Signed comparisons, sign test |
| Z | Zero | Result is 0 | ==, != |
| C | Carry | Unsigned carry out (or NOT borrow on subtract) | Unsigned comparisons, multi-word arithmetic |
| V | Overflow | Signed overflow | Signed comparisons (with N) |
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).
| Line | Why |
|---|---|
-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_t | Promotion to int happens first; truncation happens on assignment |
(t1 - t2) < 0 with uint32_t is never true | Unsigned values cannot be negative; the subtraction wraps instead |
-8 >> 1 is −4 | Right shift of a negative signed value is implementation-defined; GCC does an arithmetic shift (keeps the sign) |
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
| Situation | Correct type | Why |
|---|---|---|
| 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 − measured | int32_t | Error is positive or negative; unsigned would wrap to a huge number |
| ADC raw value (12-bit) | uint16_t | Always 0..4095; differential ADCs use signed |
| Millisecond tick counter | uint32_t | Wrap-around subtraction works only for unsigned |
| Register values and bit masks | uint32_t | No sign bit, shifts are logical and well defined |
| Motor speed / audio sample | int16_t | Signed waveform around zero |
| Array index or size | size_t or uint16_t | Never negative; avoid signed/unsigned comparison warnings |
12. Common Mistakes With Signed and Unsigned Integers
uint8_t temp = -5; stores 251. Comparisons such as temp < 0 are never true.uint16_t it becomes 65526 and your control loop goes wild.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.for (uint8_t i = 9; i >= 0; i--) never terminates because an unsigned i is always ≥ 0.abs(INT_MIN) works: The positive counterpart of the most negative value does not exist; the result stays negative.printf("%u", -1) prints 4294967295; printf("%d", 0xFFFFFFFFu) prints −1.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
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