Volume 03 Beginner 5 sub-modules ~15 min read

Operators Without Surprises

Most beginner bugs in embedded C are not in the logic. They are in the arithmetic: a division that threw away the answer, a comparison between a signed and an unsigned value, or an expression that C grouped differently from the way it reads. This volume walks through each one, with a program that proves it.

You will learn
  • Why 1 / 2 is 0, and how to round without using a float
  • How && and || stop early, and why & is not &&
  • How C widens small types before arithmetic, and what that fixes and breaks
  • Why comparing a signed value with an unsigned one can give the opposite answer
  • The precedence traps that appear in real register code, and the brackets that stop them
  • Why 0.1 + 0.2 is not 0.3, and how firmware holds fractions in integers instead
You need
  • Volume 02: bits, bytes and the integer types

3.1 Arithmetic and integer division

Divide two whole numbers in C and you get a whole number. The fraction is not rounded - it is thrown away. That one rule causes more wrong readings in firmware than any other.


7 / 2   = 3   (the fraction is thrown away)
7 % 2   = 1   (the remainder)
-7 / 2  = -3   (C rounds towards zero)
-7 % 2  = -1
1 / 2   = 0   (not 0.5)
1.0 / 2 = 0.5 (one side is a float, so both are)
7 / 2 rounded down = 3, rounded to nearest = 4
adc 2048 of 4095 is 1650 mV
dividing first gives 0 mV, which is useless

/ gives the whole part and % gives what is left over. Together they are exactly the division you did at primary school, before fractions.

In plain words

7 ÷ 2 is "3 remainder 1". C hands you the 3 with / and the 1 with %. It never gives you 3.5 unless you ask for a number with a fractional part.

Rounding without a float

Adding half the divisor before dividing rounds to the nearest:


int down    = total / count;                /* 7 / 2 = 3 */
int nearest = (total + count / 2) / count;  /* (7 + 1) / 2 = 4 */

Multiply before you divide

This is the rule that saves ADC readings. To turn a 12-bit reading into millivolts at 3.3 V:


uint32_t mv    = (adc * 3300u) / 4095u;   /* 1650 mV - right */
uint32_t wrong = (adc / 4095u) * 3300u;   /* 0 mV    - the division went first */

Dividing first turns 2048/4095 into 0, and 0 times anything is 0. Multiplying first keeps the information. Watch the size of the top: adc * 3300 reaches about 13 million, which needs a uint32_t, not a uint16_t.

Common mistake

Dividing by a variable that can be zero. In C, dividing by zero is undefined behaviour: on a small chip it may hang, restart, or give a meaningless answer. Check first, always:


if (count != 0u) {
    average = total / count;
}
Going deeper: division is expensive

Many small cores have no divide instruction at all, so / becomes a library routine taking tens or hundreds of cycles. Dividing by a power of two is free, because the compiler turns it into a shift: x / 8 becomes x >> 3 for unsigned values. If a divisor is a constant power of two, you have already paid nothing. If it is not, and the code is in a hot loop, that is worth knowing.

Quick check

An ADC reading of 1000 (out of 4095) must become millivolts at a 3.3 V reference. Which line is right?

Show the answer

Answer: C. Only the third keeps the information: it multiplies first, then divides. The others divide first, and integer division turns the fraction into 0 before it can be used.

3.2 Comparison and logical operators

In C, 0 is false and anything else is true. && and || work on that truth, and they stop as soon as the answer is known. They are not the same as the bitwise & and |.

Operator Meaning Example
== != equal, not equal if (mode == 3)
< <= > >= smaller, larger if (temp >= limit)
&& both true if (ready && !busy)
\|\| either true if (error \|\| timeout)
! not if (!enabled)

They stop early


after &&, the sensor was read 0 time(s)
ready, so the sensor was not needed
after ||, the sensor was read 0 time(s)
flags & mask  = 0  (the bits they share: none)
flags && mask = 1  (both are non-zero, so true)

The sensor function was never called. With enabled && read_sensor(), C saw that enabled was 0, knew the answer had to be false, and stopped. This is called short-circuit evaluation, and firmware leans on it:


if (buffer != NULL && buffer[0] == 0x55u) {   /* safe: the test on the right */
    ...                                       /* only runs if the pointer is good */
}
Common mistake

Writing & where you meant &&. Look at the last two lines of the output: flags & mask is 0 because those two bytes share no bits, while flags && mask is 1 because neither is zero. Both compile. In an if, only one of them is usually right.

The plain rule: & and | work on bits; && and || work on yes and no.

Quick check

What does if (count != 0 && total / count > 5) protect against?

Show the answer

Answer: A. && stops as soon as the left side is false. When count is 0 the division is never reached, which is the standard way to guard it.

3.3 Type conversion and promotion

C converts types for you before it does arithmetic. Most of the time that helps. When a signed value meets an unsigned one, it can invert your logic completely.


a + b as int      = 300   (promotion made room)
(uint8_t)(a + b)  = 44   (stored back in 8 bits, it wraps)
-1 > 1 is TRUE once -1 becomes unsigned: it is 4294967295
(int)3.99f = 3, not 4
to round: (int)(3.99f + 0.5f) = 4
16777217 stored in a float reads back as 16777216
'A' is 65, and 'A' + 1 is 'B'

Small types grow before they are used

Anything smaller than int is widened to int first. This is integer promotion, and it is why 200 + 100 in two uint8_t variables gives 300, not 44. The wrap only happens when you store the result back into eight bits.

Remember

Promotion is usually your friend: it stops intermediate sums from overflowing. The danger is forgetting it in the other direction, where a cast silently throws the top bits away. Write the cast yourself when you mean it:


uint8_t sum = (uint8_t)(a + b);    /* yes, I want only the low 8 bits */

Signed meets unsigned

This is the one to fear. When a signed value meets an unsigned value of the same width, the signed one is converted to unsigned. -1 becomes 4,294,967,295 and every comparison flips.


int      i = -1;
unsigned u = 1u;
if (i < u) { ... }        /* false: i becomes 4294967295 */
Common mistake

The loop that runs backwards for ever:


for (unsigned i = 10u; i >= 0u; i--) { ... }   /* i >= 0 is always true */

An unsigned value is never negative, so the test can never fail. After 0 it wraps to the largest value and keeps going. Count down with a signed type, or test i > 0 and handle the last turn outside the loop.

Floats lose the small change

(int)3.99f is 3, not 4: converting to an integer truncates. And a float keeps only about seven digits, which is why 16,777,217 came back as 16,777,216. For counts and addresses, stay in integers.

Quick check

uint8_t a = 200, b = 100; What does printf("%d", a + b) print, and why?

Show the answer

Answer: B. Both operands are promoted to int first, so the addition happens in int and gives 300. You would only see 44 if the result were stored back into a uint8_t.

3.4 Precedence traps

C groups operators by precedence, and its order is not always the one your eye expects. In register code, that turns into silent bugs.

C operator precedence, from the tightest binding to the loosest tightest loosest ! ~ ++ -- (type) sizeof * / % + - << >> < <= > >= == != & then ^ then | && then || then ?: then = not, invert, cast multiply, divide, remainder add, subtract shifts - looser than + and - comparisons - tighter than the bit operators the bit operators logic, then the ternary, then assignment The gold rows are the traps. When in doubt, add brackets.
Figure 3.1 - The order C applies operators in, tightest first. The four rows in the middle are where firmware gets caught: shifts bind looser than arithmetic, the bitwise operators bind looser than the comparisons, and assignment binds loosest of all.

The four that catch people


status & (mask == required) = 0  (the trap: == runs first)
(status & mask) == required = 1  (what was meant)
1 << (n + 1) = 8  (the trap: + runs first)
(1 << n) + 1 = 5  (what was meant)
(!flags) & 0x10 = 0  (the trap: ! runs first)
!(flags & 0x10) = 1  (what was meant)
10 + (mode ? 1 : 2) = 11
You write C reads it as You probably meant
status & mask == required status & (mask == required) (status & mask) == required
1 << n + 1 1 << (n + 1) (1 << n) + 1
!flags & 0x10 (!flags) & 0x10 !(flags & 0x10)
a & b \| c (a & b) \| c usually right, but say it anyway

The first row matters most. Testing whether a group of bits is set is something you write constantly in driver code, and without brackets it quietly tests something else.

Remember

Turn the warnings on and the compiler catches several of these for you. With -Wall, gcc says:


warning: suggest parentheses around operand of '!' or change '&' to '&&' or '!' to '~'

The habit that never fails: when bitwise operators meet comparisons, bracket the bitwise part.

Quick check

How does C group if (status & 0x0C == 0x0C)?

Show the answer

Answer: C. == binds tighter than &, so the comparison runs first and gives 1. The line then tests only the bottom bit of status. Brackets around the bitwise part fix it.

3.5 Floating point on small chips

A float is an approximation, and on most small chips it is also slow and large. Firmware usually keeps fractions in integers instead, on an agreed scale.


0.1 + 0.2 = 0.30000000000000004
is it equal to 0.3? no
is it within 0.000001 of 0.3? yes
float: 23.45 degrees
fixed: 23.45 degrees, held in a 16-bit integer
average of ten readings = 23.45 degrees

Why 0.1 + 0.2 is not 0.3

Floating point stores numbers in binary places: a half, a quarter, an eighth, and so on. One tenth cannot be written exactly that way, any more than a third can be written exactly in decimal. So 0.1 is stored as something very close, and the tiny errors add up.

Common mistake

Comparing floats with ==. The test if (sum == 0.3) is false even though the sum looks right. Compare with a tolerance instead:


if (fabs(sum - 0.3) < 0.000001) { ... }

What floats cost on a chip

Chip with a hardware FPU Chip without one
A float add a few cycles tens to hundreds of cycles, in a library
Code size small a few kilobytes pulled in
printf with %f large often larger than the rest of the program
Guaranteed exact? no no

Many popular cores, including the plain Cortex-M0 and M3, have no floating-point hardware at all. Every float sum becomes a call into a software library.

Fixed point: keep the integers

Choose a scale and stay on it. Hundredths of a degree, millivolts, milliseconds, thousandths of a millimetre - whatever suits the job:


float   t_float = 23.45f;    /* 4 bytes, approximate, slow without an FPU */
int16_t t_fixed = 2345;      /* 2 bytes, exact, fast everywhere           */

printf("%d.%02d degrees\n", t_fixed / 100, t_fixed % 100);

Fixed point is exact, small and fast. There are two rules. Write the scale in a comment, and watch that intermediate sums still fit. Ten readings of about 2345 add up to 23,450, which needs more than 16 bits, so the total is kept in an int32_t.

Going deeper: when a float is the right answer

Floats earn their place when the range is huge or unknown - GPS coordinates, long filter chains, signal processing on a chip that has an FPU. The test is simple: if the values are readings from hardware with a known range, fixed point is almost always better. If you are doing mathematics with very large and very small numbers together, use the float and enjoy the FPU you paid for.

Quick check

Why does firmware often store 23.45 degrees as the integer 2345?

Show the answer

Answer: B. It is fixed point: an integer with an agreed scale. It avoids the rounding of binary fractions, it is smaller, and on a chip without an FPU it is far faster than the software float library.

What you learned

Key words from this volume

Every word below has a plain-English entry in the glossary.

Practice

Practice 1

Fix the average

A program adds ten sensor readings and divides. Readings are about 2345 each, and the code is:


uint16_t total = 0;
for (unsigned i = 0; i < 10u; i++) {
    total += readings[i];
}
uint16_t average = total / 10u;
Show the solution

The total overflows. Ten readings of about 2345 add up to roughly 23,450 - which does fit in a uint16_t (up to 65,535). But make the readings 8000 each and the total is 80,000, which does not. The variable wraps, and the average comes out badly wrong with no warning at all.

Keep the running total in a wider type, and round while you are there:


uint32_t total = 0;
for (unsigned i = 0; i < 10u; i++) {
    total += readings[i];
}
uint16_t average = (uint16_t)((total + 5u) / 10u);

The rule to take away: the accumulator must be wide enough for the worst case, not the typical case.

Practice 2

Group it yourself

How does C group each of these? Add the brackets it would add.


a = b & 0x0F == 0x0F;
x = 1 << 4 + 2;
y = !ready & 0x01;
Show the solution
  • a = b & (0x0F == 0x0F); → b & 1. The comparison ran first.
  • x = 1 << (4 + 2); → 64, not 18. The addition ran first.
  • y = (!ready) & 0x01; → the not ran first, so this is 1 only when ready is 0.

The versions almost everyone means are (b & 0x0F) == 0x0F, (1 << 4) + 2 and !(ready & 0x01).

Practice 3

Scale it

A pressure sensor gives a 10-bit reading, 0 to 1023, covering 0 to 500 kPa. Write one line of integer C that turns a reading into pressure in tenths of a kPa, and say what reading 512 gives.

Show the solution

uint32_t tenths = ((uint32_t)reading * 5000u) / 1023u;

500 kPa in tenths is 5000, so full scale maps to 5000. For a reading of 512: 512 × 5000 = 2,560,000, divided by 1023 gives 2502, which is 250.2 kPa - just over half scale, as expected.

Note the cast to uint32_t before the multiply. Without it, on a chip where int is 16 bits, 512 × 5000 would overflow long before the division could help.

Practice 4

Spot the flip

Why does this never print, even when margin is small?


unsigned level = 3u;
int margin = level - 5;          /* meant to be -2 */
if (margin < 0) {
    printf("below the floor\n");
}
Show the solution

level - 5 is unsigned arithmetic, because level is unsigned. 3 − 5 wraps to 4,294,967,294, and *then* that huge value is converted into the signed margin, where it does not fit. The result is not −2.

Cast before subtracting, so the arithmetic itself is signed:


int margin = (int)level - 5;     /* -2, as intended */

Building with -Wsign-conversion makes the compiler point at the original line.

Interview corner

Interview question 1

Integer promotion

"Two uint8_t variables hold 200 and 100. What does a + b give, and why?"

Show the solution

"300. Anything narrower than int is promoted to int before the arithmetic, so the addition happens in a type wide enough to hold the answer. You only lose the top bits if you store the result back into a uint8_t, which would leave 44. It is worth saying that this is a good thing: promotion is what stops intermediate sums from overflowing."

Interview question 2

Float or fixed point?

"You need to work with temperatures to a hundredth of a degree on a Cortex-M0. How would you store them?"

Show the solution

"As fixed point: an int16_t in hundredths, so 23.45 degrees is 2345. The M0 has no floating-point unit, so every float operation becomes a software library call, costing hundreds of cycles and a few kilobytes of flash. Fixed point is exact, half the size, and fast. I would write the scale in a comment, and keep intermediate sums in int32_t. The value only becomes a human string at the point where it is printed."

Next, Volume 04 is the one firmware runs on: bit manipulation. Setting, clearing, toggling and testing bits, masks and fields, and the puzzles interviewers ask about them.