2’s Complement Examples: Negation, Overflow, Sign Extension and Sensor Data-Free Embedded Course Online

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 · WORKED EXAMPLES

2’s Complement Examples: Negation, Overflow, Sign Extension and Sensor Data

Thirteen worked examples with bit diagrams and verified C programs: negating, decoding, subtraction by addition, signed and unsigned overflow detection, 12-bit sign extension and Cortex-M4 flags.

13
Examples
7 C programs
Compiled & run
Intermediate
Level

2’s Complement Examples: What This Page Covers

This page turns the theory into practice. Every example works with the same idea: 2’s complement examples are easiest when you can see the bits, so each one has a diagram, the arithmetic, and (where relevant) a C program that was compiled and run to produce the output you see.

#ExampleSkill
1Compute −x by invert and add 1Negation
2Decode a negative byte (two ways)Reading signed values
3−1 in 8, 16 and 32 bitsAll ones
4One bit pattern, many interpretations (C program)Type decides meaning
5Subtraction by additionWhy the ALU works
6Signed overflow detection (worked and in C)Spotting wrong results
7Unsigned overflow detectionCarry
8Computing ranges for any bit widthMin and max
9Signed 16-bit sensor data from two bytesReal I2C pattern
10Sign-extending a 12-bit valueOdd-width fields
11Counting loops with signed vs unsignedClassic bug
12APSR flags N, Z, C, V by exampleCortex-M4 internals
13Mini exercisesSelf-test

Example 1: Computing −x by Inverting and Adding 1

Problem: What is the 8-bit 2’s complement representation of −13?

−13: start with +13 = 0000 1101
+13
7
0
6
0
5
0
4
0
3
1
2
1
1
0
0
1
▼ invert all bits
~13
1
1
1
1
0
0
1
0
▼ add 1 (carry ripples from bit 0)
−13 = 0xF3
1
1
1
1
0
0
1
1

Check: 0xF3 as unsigned is 243, and 256 − 13 = 243. As a signed byte the weights give −128 + 64 + 32 + 16 + 2 + 1 = −13.

Table of negations

xx in binary~x~x + 1HexSigned256 − x
10000000111111110111111110xFF−1255
50000010111111010111110110xFB−5251
130000110111110010111100110xF3−13243
400010100011010111110110000xD8−40216
1000110010010011011100111000x9C−100156
1270111111110000000100000010x81−127129

The special case: −(−128)

Negate 1000 0000 (−128): invert gives 0111 1111, add 1 gives 1000 0000 again. The most negative value is its own negation, because +128 does not fit in a signed byte. In C, -INT8_MIN overflows (after promotion to int it is fine, but the cast back to int8_t gives −128 again), and abs(INT_MIN) is undefined.

Example 2: Decoding a Negative Byte (Two Methods)

Problem: What signed value does 0x9C (1001 1100) hold?

Method A — negative MSB weight

1001 1100 as int8_t
bit 7
-128
1
-128
bit 6
64
0
0
bit 5
32
0
0
bit 4
16
1
16
bit 3
8
1
8
bit 2
4
1
4
bit 1
2
0
0
bit 0
1
0
0
(-128) + 16 + 8 + 4 = -100

Method B — negate again

If the MSB is 1, the number is negative. Its magnitude is the 2’s complement of the pattern: invert 1001 1100 → 0110 0011, add 1 → 0110 0100 = 100. So the value is −100. Both methods agree.

Method C — subtract 2N (fastest for a quick check)

0x9C = 156 unsigned; 156 − 256 = −100.

ByteUnsignedSignedMethod C working
0xF3243−13243 − 256
0x9C156−100156 − 256
0x80128−128128 − 256
0xFE254−2254 − 256
0x7F127+127MSB = 0: unchanged

Example 3: −1 in 8, 16 and 32 Bits (All Ones)

−1 is always all ones, whatever the width. Reason: 1 + (−1) must be 0, and only all-ones plus one carries all the way out and leaves zero.

−1 at three widths
int8_t
7
1
6
1
5
1
4
1
3
1
2
1
1
1
0
1
int16_t
15
1
14
1
13
1
12
1
11
1
10
1
9
1
8
1
7
1
6
1
5
1
4
1
3
1
2
1
1
1
0
1
int32_t
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
TypeHex of −1Hex of minimumHex of maximum
int8_t0xFF0x800x7F
int16_t0xFFFF0x80000x7FFF
int32_t0xFFFFFFFF0x800000000x7FFFFFFF
Practical uses: 0xFFFFFFFF is often used as a “not set” marker (flash erased state is also all ones: 0xFF in every byte). Seeing 0xFFFFFFFF in a debugger can mean −1, an erased flash word, or an invalid handle: check the variable’s type.

Example 4: One Bit Pattern, Many Interpretations (C Program)

The same bits give different numbers depending on type and width. This program prints the byte 0xC8 and two wider patterns in every interpretation.

interpretations.c
#include <stdio.h> #include <stdint.h> int main(void) { uint8_t bits = 0xC8; /* 1100 1000 */ printf(“bit pattern 0x%02X:\n”, bits); printf(” as uint8_t : %u\n”, (unsigned)(uint8_t)bits); printf(” as int8_t : %d\n”, (int)(int8_t)bits); printf(” as uint16_t after zero-extension: %u\n”, (unsigned)(uint16_t)bits); printf(” as int16_t after sign-extension: %d\n”, (int)(int16_t)(int8_t)bits); uint16_t word = 0xFFF6; printf(“16-bit pattern 0x%04X: unsigned %u, signed %d\n”, word, (unsigned)word, (int)(int16_t)word); uint32_t w32 = 0x80000000u; printf(“32-bit pattern 0x%08X: unsigned %u, signed %d\n”, (unsigned)w32, (unsigned)w32, (int)(int32_t)w32); return 0; }
Output (verified)
bit pattern 0xC8: as uint8_t : 200 as int8_t : -56 as uint16_t after zero-extension: 200 as int16_t after sign-extension: -56 16-bit pattern 0xFFF6: unsigned 65526, signed -10 32-bit pattern 0x80000000: unsigned 2147483648, signed -2147483648
0xC8 under four interpretations
uint8_t
200
128 + 64 + 8
int8_t
−56
−128 + 64 + 8
uint16_t
200
zero-extended 0x00C8
int16_t
−56
sign-extended 0xFFC8
The rule behind the numbers: Widening a signed value keeps its meaning (−56 stays −56, so the bits become 0xFFC8). Widening an unsigned value keeps its meaning too (200 stays 200, bits 0x00C8). The bit pattern changes; the number does not.

Example 5: Subtraction by Addition

The ALU has no separate subtractor. It computes a − b as a + (−b), where −b is obtained by inverting b and adding 1 (the +1 is supplied as the initial carry).

9 − 5 = 9 + (−5)

0000 1001 + 1111 1011 (−5): the carry-out is dropped, the byte left is +4
carry
1
1
1
1
1
0
1
1
0
A
0
0
0
0
1
0
0
1
+ B
1
1
1
1
1
0
1
1
sum
1
0
0
0
0
0
1
0
0
9 + (−5): the sum is 1 0000 0100. Drop the carry-out; the byte 0000 0100 = +4

5 − 9 = 5 + (−9) = −4

0000 0101 + 1111 0111 (−9) = 1111 1100 which is −4 (no carry-out)
carry
0
0
0
0
0
1
1
1
0
A
0
0
0
0
0
1
0
1
+ B
1
1
1
1
0
1
1
1
sum
0
1
1
1
1
1
1
0
0
5 + (−9) = 1111 1100, no carry-out; read as signed it is −4
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
1
4
bit 1
2
0
0
bit 0
1
0
0
(-128) + 64 + 32 + 16 + 8 + 4 = -4

Notice the pattern: when the true result is non-negative, a carry out of the MSB appears and is discarded. When it is negative, there is no carry out and the sign bit is 1 automatically. The processor never needs to know which case it is in.

Example 6: Signed Overflow Detection

Problem: add 100 and 50 as signed 8-bit numbers. The true result is 150, which is above the maximum 127.

0110 0100 (100) + 0011 0010 (50) = 1001 0110: two positives give a negative
carry
0
1
1
0
0
0
0
0
0
A
0
1
1
0
0
1
0
0
+ B
0
0
1
1
0
0
1
0
sum
0
1
0
0
1
0
1
1
0
100 + 50 = 1001 0110 (no carry-out, but the sign bit flipped)
bit 7
-128
1
-128
bit 6
64
0
0
bit 5
32
0
0
bit 4
16
1
16
bit 3
8
0
0
bit 2
4
1
4
bit 1
2
1
2
bit 0
1
0
0
(-128) + 16 + 4 + 2 = -106

The register now holds 1001 0110 = −106 as a signed byte (150 − 256). The result is wrong because the true answer needs 9 bits as a signed number. The two operands were positive and the result is negative: that is the signature of signed overflow.

signed_overflow.c — checking a table of cases
#include <stdio.h> #include <stdint.h> #include <stdbool.h> static void test(int8_t a, int8_t b) { int exact = (int)a + (int)b; /* correct answer, computed in int */ int8_t wrapped = (int8_t)exact; /* what an 8-bit register would hold */ bool ov = (exact > INT8_MAX) || (exact < INT8_MIN); printf(“%4d + %4d = %4d | stored in int8_t: %4d | overflow: %s\n”, a, b, exact, wrapped, ov ? “YES” : “no”); } int main(void) { test(100, 27); test(100, 28); test(100, 50); test(-100, –28); test(-100, –29); test(-100, –50); test(70, –20); test(-128, –1); return 0; }
Output (verified)
100 + 27 = 127 | stored in int8_t: 127 | overflow: no 100 + 28 = 128 | stored in int8_t: -128 | overflow: YES 100 + 50 = 150 | stored in int8_t: -106 | overflow: YES -100 + -28 = -128 | stored in int8_t: -128 | overflow: no -100 + -29 = -129 | stored in int8_t: 127 | overflow: YES -100 + -50 = -150 | stored in int8_t: 106 | overflow: YES 70 + -20 = 50 | stored in int8_t: 50 | overflow: no -128 + -1 = -129 | stored in int8_t: 127 | overflow: YES
CaseOperand signsResult signOverflow?
100 + 50+ and +− (wrong)Yes
−100 + (−50)− and −+ (wrong)Yes
70 + (−20)different signs+Never possible
−128 + (−1)− and −+127 (wrong)Yes (underflow)
Why the program converts to int first: Adding two int8_t values already promotes them to int in C, so the exact sum is available for checking. Only the assignment back into int8_t loses information.

Example 7: Unsigned Overflow Detection

For unsigned numbers the test is simpler: after sum = a + b (modulo 2N), an overflow occurred if and only if sum < a. In hardware this is the C flag, the carry-out of the MSB.

unsigned_overflow.c
#include <stdio.h> #include <stdint.h> #include <stdbool.h> /* For unsigned addition the wrapped result is SMALLER than either operand */ static bool uadd_overflows(uint8_t a, uint8_t b, uint8_t *sum) { *sum = (uint8_t)(a + b); return *sum < a; } int main(void) { uint8_t s; bool o; o = uadd_overflows(200, 55, &s); printf(“200 + 55 = %3u overflow=%d\n”, s, o); o = uadd_overflows(200, 56, &s); printf(“200 + 56 = %3u overflow=%d\n”, s, o); o = uadd_overflows(255, 1, &s); printf(“255 + 1 = %3u overflow=%d\n”, s, o); uint8_t x = 3, y = 5; printf(“3 – 5 as uint8_t = %u (underflow wraps)\n”, (unsigned)(uint8_t)(x – y)); printf(“underflow test x < y : %d\n”, x < y); return 0; }
Output (verified)
200 + 55 = 255 overflow=0 200 + 56 = 0 overflow=1 255 + 1 = 0 overflow=1 3 – 5 as uint8_t = 254 (underflow wraps) underflow test x < y : 1

Subtraction underflows when the subtrahend is larger: check x < y before computing x - y. This is the safe pattern for byte counters, buffer lengths and tick differences.

Example 8: Computing the Range for Any Bit Width

The two formulas to remember: unsigned max = 2N − 1; signed min = −2N−1, signed max = 2N−1 − 1. They apply even to odd widths such as a 10-bit or 12-bit ADC or a 24-bit audio sample.

ranges.c
#include <stdio.h> #include <stdint.h> int main(void) { int widths[] = { 4, 8, 10, 12, 16, 24, 32 }; printf(“bits | unsigned max | signed min | signed max\n”); for (unsigned i = 0; i < sizeof(widths) / sizeof(widths[0]); i++) { int n = widths[i]; long long umax = (1LL << n) – 1; long long smin = -(1LL << (n – 1)); long long smax = (1LL << (n – 1)) – 1; printf(“%4d | %18lld | %17lld | %17lld\n”, n, umax, smin, smax); } return 0; }
Output (verified)
bits | unsigned max | signed min | signed max 4 | 15 | -8 | 7 8 | 255 | -128 | 127 10 | 1023 | -512 | 511 12 | 4095 | -2048 | 2047 16 | 65535 | -32768 | 32767 24 | 16777215 | -8388608 | 8388607 32 | 4294967295 | -2147483648 | 2147483647
Why 1LL? The constant must be wide enough: 1 << 32 on a 32-bit int is undefined. Use 1LL (64-bit) for intermediate calculations and cast down at the end.

Example 9: A Signed 16-bit Sensor Reading Built From Two Bytes

Accelerometers, gyroscopes and many temperature sensors return a 16-bit 2’s complement value in two registers. Suppose the bytes received are 0xFF then 0x9C.

0xFF9C as signed 16-bit
15
1
14
1
13
1
12
1
11
1
10
1
9
1
8
1
7
1
6
0
5
0
4
1
3
1
2
1
1
0
0
0
sign bit
imu_read.c
#include <stdio.h> #include <stdint.h> int main(void) { uint8_t hi = 0xFF, lo = 0x9C; /* two bytes read over I2C (MSB first) */ uint16_t raw_u = (uint16_t)(((uint16_t)hi << 8) | lo); int16_t raw_s = (int16_t)raw_u; /* reinterpret the same 16 bits as signed */ printf(“raw bits : 0x%04X\n”, raw_u); printf(“wrong (unsigned) : %u\n”, (unsigned)raw_u); printf(“right (int16_t) : %d\n”, (int)raw_s); /* scale: sensor full scale +/-2 g over +/-32768 counts */ int32_t milli_g = ((int32_t)raw_s * 2000) / 32768; printf(“acceleration : %d mg\n”, (int)milli_g); return 0; }
Output (verified)
raw bits : 0xFF9C wrong (unsigned) : 65436 right (int16_t) : -100 acceleration : -6 mg

Notes

  • Assemble the bytes in an unsigned 16-bit variable first, then reinterpret as int16_t. This keeps the shift well defined.
  • Reading the same bits as unsigned gives 65436, which would look like a huge acceleration.
  • Scaling uses integer maths: multiply first (in 32 bits so it cannot overflow), divide last.

Example 10: Sign-Extending a 12-bit Value

Some peripherals deliver values narrower than a C type, for example a 12-bit signed field in a 16-bit register. Storing it in an int16_t directly would not extend the sign. We must copy bit 11 into bits 15..12.

12-bit 0xF9C (= −100) extended to 16 bits
12-bit
1
1
1
1
1
0
0
1
1
1
0
0
▼ copy bit 11 into bits 15..12
16-bit
1
1
1
1
1
1
1
1
1
0
0
1
1
1
0
0
sext12.c
#include <stdio.h> #include <stdint.h> /* Sign-extend a 12-bit two’s complement value held in the low bits of a uint16_t */ static int32_t sext12(uint16_t raw) { raw &= 0x0FFFu; /* keep 12 bits */ return (int32_t)((raw ^ 0x800u)) – 0x800; /* portable: flip sign bit, subtract bias */ } int main(void) { uint16_t samples[] = { 0x000, 0x001, 0x7FF, 0x800, 0xF9C, 0xFFF }; for (unsigned i = 0; i < sizeof(samples) / sizeof(samples[0]); i++) { printf(“raw 0x%03X -> %5d\n”, samples[i], (int)sext12(samples[i])); } return 0; }
Output (verified)
raw 0x000 -> 0 raw 0x001 -> 1 raw 0x7FF -> 2047 raw 0x800 -> -2048 raw 0xF9C -> -100 raw 0xFFF -> -1

The trick (raw ^ 0x800) - 0x800 works for any width: XOR the sign bit, then subtract it. If the sign bit was 1, the XOR clears it and the subtraction makes the result negative; if it was 0, the XOR sets it and the subtraction cancels it. There is no shifting of negative numbers and no undefined or implementation-defined behaviour.

Example 11: Counting Down: Signed vs Unsigned Loop Counters

Bug: for (uint8_t i = 3; i >= 0; i--) never ends. After i = 0, i-- wraps to 255, and 255 ≥ 0 is true again. The compiler usually warns: “comparison of unsigned expression ≥ 0 is always true”.
How the unsigned counter wraps: 2, 1, 0, then 255
i = 1
body runs
➜
i = 0
body runs
➜
i– gives 255
1111 1111
➜
255 >= 0
always true
➜
Never exits
watchdog reset?
countdown.c — two correct versions
#include <stdio.h> #include <stdint.h> int main(void) { /* WRONG (infinite): for (uint8_t i = 3; i >= 0; i–) — unsigned i is always >= 0 */ /* Right: signed counter */ for (int i = 3; i >= 0; i–) { printf(“%d “, i); } printf(“\n”); /* Right: unsigned counter with the ‘down to zero’ idiom */ uint8_t n = 4; while (n– > 0) /* test, then decrement; body sees 3,2,1,0 */ { printf(“%u “, (unsigned)n); } printf(“\n”); return 0; }
Output (verified)
3 2 1 0 3 2 1 0

Example 12: APSR Flags N, Z, C, V by Example (Cortex-M4)

The Application Program Status Register holds four condition flags that the ALU updates after every flag-setting instruction (ADDS, SUBS, CMP). Compilers use them to implement <, >=, ==: signed comparisons look at N and V, unsigned comparisons look at C.

Operation (32-bit)ResultNZCV
0x7FFFFFFF + 10x800000001001
0xFFFFFFFF + 10x000000000110
5 − 50x000000000110
3 − 50xFFFFFFFE1000
5 − 30x000000020010
0x80000000 − 10x7FFFFFFF0011
0xFFFFFFFF + 0xFFFFFFFF0xFFFFFFFE1010
  • 0x7FFFFFFF + 1: two positives give a negative: V = 1 (signed overflow), C = 0 (no unsigned carry).
  • 0xFFFFFFFF + 1: unsigned wrap to 0: C = 1 and Z = 1; as signed this is −1 + 1 = 0, no overflow: V = 0.
  • 5 − 5: result zero: Z = 1. On subtraction C = 1 means no borrow.
  • 3 − 5: a borrow was needed so C = 0; the result is negative: N = 1.
  • 0x80000000 − 1: most negative minus one overflows: V = 1.
Values computed programmatically: The table above was generated by a script that models the ARM add/subtract (subtract = add of the inverted operand with carry-in 1), so it matches what the hardware sets.

Example 13: Mini Exercises — Try It Yourself

Exercise 1. Find the 8-bit 2’s complement of +37.
0010 0101 → invert 1101 1010 → +1 → 1101 1011 = 0xDB
Exercise 2. What signed value is 0xA4?
0xA4 = 164; 164 − 256 = −92
Exercise 3. What is the range of a 12-bit signed number?
−2048 to +2047
Exercise 4. Compute 0x40 + 0x40 as int8_t. Is there an overflow?
0x80 = −128. Yes: 64 + 64 = 128 > 127 (signed overflow, V = 1).
Exercise 5. uint8_t a = 10, b = 20; What is (uint8_t)(a – b)?
246 (256 − 10)
Exercise 6. Sign-extend the 8-bit value 0x85 to 16 bits.
0xFF85 (bit 7 is 1, so the upper byte is 0xFF); value −123
Exercise 7. Why is -1 < 1u false?
−1 is converted to unsigned (0xFFFFFFFF = 4294967295), which is greater than 1.
Exercise 8. What is the 16-bit pattern of −1000?
65536 − 1000 = 64536 = 0xFC18
function revealAns(btn){var d=btn.nextElementSibling,o=d.style.display===’block’;d.style.display=o?’none’:’block’;btn.textContent=o?’Show answer’:’Hide answer’;}

Quick Reference Card

TaskRecipe
Negate an N-bit number~x + 1 (or 2N − x)
Decode a signed patternMSB weight is −2N−1, or unsigned − 2N when MSB = 1
−1All ones
Signed range−2N−1 to 2N−1 − 1
Sign-extend a K-bit field(x ^ signbit) - signbit
Detect unsigned add overflowsum < a
Detect signed add overflowSame-sign operands, different-sign result (or check in a wider type)
Subtraction in the ALUa + ~b + 1

Examples complete!

Finish Module 2 with the quiz: 2’s complement computations, signed range questions, overflow identification, code snippet challenges and interview questions.

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 *