Volume 01 Beginner 5 sub-modules ~15 min read

Your First C Programs

Now you write C. This volume takes the five pieces every C program is made of - printing, variables, decisions, loops and functions - and shows each one in a program short enough to type in a minute. Every program here was compiled and run, so the output on the page is the output you will get.

You will learn
  • What every line of a hello world program is for
  • How to declare variables, and how to print them correctly
  • How to make decisions with if, else if and switch
  • How for, while and do-while differ, and when to use each
  • How to write your own functions, and why a function cannot change its caller’s variables
You need
  • Volume 00, or any way to compile and run a C program

1.1 Hello, world - every line explained

Every C program is made of functions, and one of them is special: main. That is where the program starts.

Here is the whole program. Type it, run it, then read the table under it.


#include <stdio.h>

int main(void)
{
    printf("Hello, embedded world!\n");
    return 0;
}

Hello, embedded world!

Every line, one at a time

Line What it is for
#include <stdio.h> Brings in a header file that describes printf. Without it, the compiler has never heard of printf.
int main(void) Declares the function where the program starts. int is the kind of value it hands back; void says it takes nothing.
{ and } Mark the start and end of the function's body. Everything between them is main's work.
printf("..."); Calls a ready-made function from the standard library to print text.
\n Inside the text, this means "new line". Without it, the next print would carry on the same line.
return 0; Ends main and hands back 0, which by tradition means "all went well".
; Ends a statement. C needs one after every instruction.
In plain words

A program is a list of instructions. Functions are named groups of instructions. When the program starts, the computer looks for main and runs the instructions inside it, in order, from top to bottom.

Three rules that catch everyone

Common mistake

Forgetting the semicolon. Leave out the one after printf and gcc says:


hello.c: In function 'main':
hello.c:5:39: error: expected ';' before 'return'
    5 |     printf("Hello, embedded world!\n")
      |                                       ^
      |                                       ;
    6 |     return 0;
      |     ~~~~~~

Read it like an address: the file, then the line, then the column, then what it wanted. The message points at line 5 - one line above where it noticed - because that is where the semicolon should have been. That is normal for a missing semicolon, and worth remembering.

Quick check

What does return 0 at the end of main mean?

Show the answer

Answer: B. main hands a number back to whatever started it. By tradition 0 means success and anything else means a problem. On a bare-metal chip nothing is waiting for that number, but the shape of main stays the same.

1.2 Variables and types

A variable is a named box in memory. Its type says how big the box is and how to read the bits inside.

Declaring and using them


#include <stdio.h>
#include <stdint.h>

int main(void)
{
    int      count      = 12;      /* a whole number, positive or negative */
    unsigned tries      = 3u;      /* a whole number, never negative       */
    char     grade      = 'B';     /* one character                        */
    float    volts      = 3.3f;    /* a number with a fractional part      */
    uint8_t  brightness = 255;     /* exactly 8 bits, 0 to 255             */

    printf("count      = %d\n", count);
    printf("tries      = %u\n", tries);
    printf("grade      = %c\n", grade);
    printf("volts      = %.2f\n", (double)volts);
    printf("brightness = %u\n", brightness);

    count = count + 1;             /* variables can change */
    brightness = 0;                /* the LED goes dark    */
    printf("after: count = %d, brightness = %u\n", count, brightness);

    printf("an int takes %zu bytes here\n", sizeof(int));
    return 0;
}

count      = 12
tries      = 3
grade      = B
volts      = 3.30
brightness = 255
after: count = 13, brightness = 0
an int takes 4 bytes here

Each line declares a variable: first the type, then the name, then an initial value. The text between /* and */ is a comment, written for people and ignored by the compiler.

The types you will use

Type Holds Typical size
char one character, or a small number 1 byte
int a whole number, positive or negative 4 bytes on a computer, sometimes 2 on a small chip
unsigned a whole number that is never negative same size as int
float a number with a fractional part 4 bytes
double the same, with more digits 8 bytes
uint8_t, uint16_t, uint32_t exactly that many bits, never negative 1, 2 and 4 bytes, on every machine
Remember

In embedded C, prefer the fixed-width types: uint8_t, int16_t, uint32_t. They say exactly what you mean, on every chip. Plain int changes size between machines, and that difference has broken a lot of firmware. Volume 02 takes this apart properly.

Printing them

printf needs a format specifier for each value: a small code saying how to read it.

Specifier Prints
%d a signed whole number
%u an unsigned whole number
%c one character
%f a number with a fractional part
%s text
%x a whole number in hexadecimal
%zu a size, as sizeof gives
Common mistake

Using the wrong specifier, such as %d for a float. The compiler may not stop you, and the output is nonsense, because printf reads the bytes the way the specifier told it to. Turn warnings on (-Wall) and the compiler will point at it.

Quick check

Which type should you choose for a value that is always between 0 and 255, on any chip?

Show the answer

Answer: C. uint8_t is exactly 8 bits and never negative, on every machine. Plain char may be signed or unsigned depending on the compiler, and int is bigger than you need.

1.3 Making decisions: if and switch

if runs a piece of code only when something is true. switch picks one path out of many, when you are choosing on a single value.


int temperature = 42;

if (temperature > 60) {
    printf("too hot: turn the heater off\n");
} else if (temperature < 20) {
    printf("too cold: turn the heater on\n");
} else {
    printf("just right: do nothing\n");
}

just right: do nothing

The tests run in order, and the first true one wins. else catches everything left over.

The comparisons

You write It asks
a == b are they equal?
a != b are they different?
a > b, a >= b is a bigger (or equal)?
a < b, a <= b is a smaller (or equal)?
a && b are both true?
a \|\| b is either true?
!a is a false?

In C there is no separate true or false type to learn first: 0 is false, and anything else is true. That is why if (count) means "if count is not zero".

Common mistake

Writing = where you meant ==:


if (mode = 3) {        /* assigns 3 to mode, and is always true */

= puts a value in; == compares. The buggy line sets mode to 3, then tests 3, which is not zero, so the branch always runs. Compilers warn about this, and the extra brackets in if ((mode = 3)) are how you tell the compiler you meant it.

switch: many fixed choices


switch (button) {
case 1:
    printf("button 1: start\n");
    break;
case 2:
    printf("button 2: stop\n");
    break;
case 3:
    printf("button 3: reset\n");
    break;
default:
    printf("no such button\n");
    break;
}

With button set to 2, that prints button 2: stop.

break leaves the switch. Without it, C carries straight on into the next case - which is sometimes useful and usually a bug:


fell into case 4
...and kept going into case 5
Remember

Give every switch a default, even when you think the other cases cover everything. In firmware, a value you did not expect is exactly the case you want to catch.

Quick check

What does a missing break at the end of a case do?

Show the answer

Answer: A. Cases fall through by design: without break, control continues into the next case's code. It is occasionally what you want, and far more often a bug.

1.4 Repeating: for, while and do-while

All three loops do the same job - repeat while something stays true. They differ only in where the check happens, and how much bookkeeping they do for you.

for: when you know how many times


for (int i = 1; i <= 5; i++) {
    printf("blink %d\n", i);
}

blink 1
blink 2
blink 3
blink 4
blink 5

The brackets hold three parts, separated by semicolons.

The three parts of a for loop, and the order in which C runs them for (int i = 1; i <= 5; i++) { ... } 1. set up 2. check 3. body 4. step once, at the start before every turn the work you want after every turn when the check is false, the loop ends here back to the check
Figure 1.1 - A for loop in the order it really happens: set up once, then check, body, step, check, body, step, until the check is false. The body may never run at all, if the check is false the first time.

while: when you do not know how many times


int level = 40;
while (level > 0) {
    level = level - 15;
}
printf("level ended at %d\n", level);     /* level ended at -5 */

int tries = 0;
do {
    tries++;
} while (tries < 3);
printf("tried %d times\n", tries);        /* tried 3 times */

A while loop checks first, so it can run zero times. A do-while runs the body first and checks afterwards, so it always runs at least once - even when the condition was false from the start:


this line runs once, although 99 > 100 is false

Leaving early: break and continue

break leaves the loop completely. continue skips the rest of this turn and goes to the next one. Adding up 1 to 10, skipping 4 and stopping at 7, gives:


sum of 1,2,3,5,6 = 17
Remember

Firmware ends with a loop that never finishes:


for (;;) {        /* or: while (1) */
    do_the_work();
}

An empty for (;;) means "no set-up, no check, no step" - so it repeats for ever. On a chip, this infinite loop is not a bug. It is the design.

Common mistake

The off-by-one. for (int i = 0; i <= 5; i++) runs six times, not five: 0, 1, 2, 3, 4, 5. For an array of five items, that last turn reads past the end. The usual shape is i < count, starting at 0, which runs exactly count times.

Quick check

How many times does the body of a do-while loop run if its condition is false from the very start?

Show the answer

Answer: B. A do-while checks after the body, so the body always runs at least once. That is the one thing it does differently from while.

1.5 Functions

A function is a named piece of work. You hand it values, it does its job, and it can hand one value back. Firmware is mostly a set of small functions called from one loop.


#include <stdio.h>

/* declarations first, so main can call them */
int  add(int a, int b);
int  larger(int a, int b);
void print_banner(void);
void try_to_change(int value);

int main(void)
{
    print_banner();

    printf("add(2, 3)      = %d\n", add(2, 3));
    printf("larger(9, 4)   = %d\n", larger(9, 4));

    int reading = 100;
    try_to_change(reading);
    printf("reading is still %d\n", reading);
    return 0;
}

int add(int a, int b)
{
    return a + b;
}

int larger(int a, int b)
{
    if (a > b) {
        return a;
    }
    return b;
}

void print_banner(void)
{
    printf("--- sensor board ---\n");
}

/* value is a copy, so changing it changes nothing in main */
void try_to_change(int value)
{
    value = 0;
    printf("inside the function, value = %d\n", value);
}

--- sensor board ---
add(2, 3)      = 5
larger(9, 4)   = 9
inside the function, value = 0
reading is still 100

The parts of a function

Part In int add(int a, int b)
Return type int - the kind of value it hands back. void means it hands nothing back
Name add - how you call it
Parameters int a, int b - the values it works on. (void) means none
Body the code between { and }
Return return a + b; ends the function and hands the value back

The lines at the top of the file, ending in a semicolon, are declarations. They tell the compiler what exists before it reaches the full definitions. Volume 15 shows how headers do this across several files.

The one surprise: C copies your arguments

Look at the last two lines of the output. The function set value to 0, yet reading in main is still 100. That is pass by value: the function received a *copy*, and changed the copy.

Think of it like this

You photocopy a page and hand over the copy. Whatever they scribble on it, your original is untouched.

Common mistake

Expecting a function to change the variable you passed. It cannot, not like this. To let a function change something of yours, you hand over its address instead - and that is exactly what pointers are for, in Volume 07.

Quick check

After calling try_to_change(reading), why is reading still 100?

Show the answer

Answer: C. C passes arguments by value. The parameter is a separate variable holding a copy, so assigning to it inside the function leaves the caller's variable exactly as it was.

What you learned

Key words from this volume

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

Practice

Practice 1

Count the turns

How many times does each loop run its body?


for (int i = 0; i < 5; i++)   { /* A */ }
for (int i = 1; i <= 5; i++)  { /* B */ }
for (int i = 0; i <= 5; i++)  { /* C */ }
Show the solution

A: five times (i is 0, 1, 2, 3, 4). B: five times (1 to 5). C: six times (0 to 5) - the off-by-one.

For an array of five items, only A is safe: array places are numbered from 0 to 4, so i < 5 is the shape to learn.

Practice 2

Predict the output

What does this print?


int x = 0;
do {
    printf("%d ", x);
    x++;
} while (x < 3);
printf("| x = %d\n", x);
Show the solution

0 1 2 | x = 3

The body runs with x at 0, 1 and 2. After the turn that printed 2, x becomes 3, the condition 3 < 3 is false, and the loop ends. x keeps its final value, 3.

Practice 3

Write a function

Write a function called clamp that limits a reading to the range 0 to 100. Below 0 becomes 0, above 100 becomes 100, and anything else is handed back unchanged.

Show the solution

int clamp(int value)
{
    if (value < 0) {
        return 0;
    }
    if (value > 100) {
        return 100;
    }
    return value;
}

Returning early, as here, is usually clearer than building one long chain of if and else. This little function appears in real firmware constantly: sensor readings arrive noisy, and a driver should never be handed a value outside the range it expects.

Practice 4

Find the bug

This is meant to print "unlocked" only when code is 42. It prints it every time. Why?


if (code = 42) {
    printf("unlocked\n");
}
Show the solution

= assigns; == compares. The line stores 42 in code, and the value of that assignment is 42. In C anything that is not 0 counts as true, so the branch always runs - and the real code is thrown away as a bonus bug.

The fix is if (code == 42). Compilers warn about this when you turn warnings on, which is one reason to always build with -Wall.

Interview corner

Interview question 1

while or do-while?

"When would you choose a do-while loop over a while loop?"

Show the solution

"When the body has to run at least once before you can tell whether to repeat it. A good example is reading a status register: you have to read it once before you can test the busy bit. A while loop checks first, so it can run zero times; a do-while checks afterwards, so it always runs at least once."

Interview question 2

Pass by value

"I call swap(a, b) and my variables are unchanged. What is going on?"

Show the solution

"C passes arguments by value, so the function is swapping its own copies. The caller's variables never move. To swap them for real, the function has to take their addresses - void swap(int *a, int *b) - and work through the pointers. This is the classic first pointer exercise, and it is why scanf needs an ampersand in front of its argument."

Next, Volume 02 goes under the surface: bits, bytes and addresses, hexadecimal, the fixed-width types, two's complement, overflow, and why endianness bites when bytes travel between chips.