Embedded

View Original

Embedded Wednesdays: Functions Part 2

This week we continue with functions. Previously I explained how functions can be used to give structure to code by grouping multiple lines of code into a function, but that is the most basic capability.

If you ever studied the Pascal programming language, it has procedures and functions. Functions return a value to the caller and procedures don’t. C doesn’t make that distinction and only has functions. C’s functions can return a value or not.

Return values

Back when you took algebra you had functions of the form y = f(x), where f is a function that takes a value x, does something to it and returns a value y. The two parts that we are missing are x and y, otherwise known as the parameter, the x, and the return value, the y.

Every function in C returns a value of a type that you specify. The sine, cosine, or tangent functions would return a double precision float value. The system tick counter returns a 32 bit unsigned integer. In our example we tell the compiler that our functions will be returning a value of type void. Void is a special keyword in C that means that the compiler should return nothing.

So:

void Find42(void) {
    int32_t randomNumber;
    uint32_t i;

    do {
        randomNumber = rand();
        i++;
    } while (randomNumber != 42);
    printf("It took %d calls to the random number generator to get the value 42\n", i);
}

Tells the compiler that Find42 isn’t expected to return anything. Let’s munge the code slightly so that it gives us a true/false indicator.

The type that will be returned by the function is stated in the first line, just before the name of the function. In this case, Find42 will return a boolean value.

bool Find42(void) {
    bool returnValue;

    if (rand() == 42) {
        returnValue = true;
    } else {
        returnValue = false;
    }

    return (returnValue);
}

If we go back to our example from last week, the main function looks like:

#include <stdio.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>

void Initialize( void);
bool Find42( void);

int main( void) {
    uint32_t i;
    bool isDone;

    Initialize();
    i = 0;
    isDone = false;

    do {
        isDone = Find42();
        if (isDone) {
            printf("It took %d calls to the random number generator to get the value 42\n", i);
        }
    } while( !isDone);
}

But what is that weird thing at the beginning that looks like the function?

bool Find42( void);

This is known as a function prototype, it gives the compiler hints as to what type is supposed to be returned by the function, and what parameters should be expected. It looks just like the first line of your function, except it has a semicolon on the end.

This exists because C has a stupid rule that you have to declare a function before you can use it. This means that you either have to position your function higher in the file than where it is called, or provide a prototype above where it is called. If you don’t declare your function, it will give you a warning and assume that you will be returning an integer and will be supplying an integer as a parameter, like this:

int FunctionName( int parameter);

Once you fill in your function body, if it doesn’t match this pattern, you get a compiler error.

By providing a function prototype, you give the compiler hints as to how to link your function call to the function body, also it gives information that is used to detect errors early in the development of your program.

It is very common for a function to return a value indicating whether or not the function succeeded, or failed. Returning a boolean indicator is quite common, but you can also do something like returning -1 as an error indication in a function that normally returns positive numbers.

C requires that you provide a return type. If you don’t want to return anything, declare your function as type void, then you also don’t need to have a return statement.

Parameters

Parameters are used to feed values into the functions that can then be used to change the system, or be used in a calculation, altering the return value.

A function can have many parameters, and each one has a type.

float SensorLinearize( uint8_t channel, uint16_t reading, float temperature);

The compiler will allocate space for the parameters inside the function so you can use the values by the parameter names.

The parameters are separated by commas, and the order that they appear is used in the function call:

linearizedReading = SensorLinearize( 1, currentPressure, currentTemperature);

This starts executing the function SensorLinearize. When the function runs, the value 1 will be assigned to a variable called channel, the value of our variable currentPressure will be assigned to a variable called reading, and the value of our variable currentTemperature will be assigned to a variable called temperature.

If you don’t want to provide any parameters, just put in a parameter of void, as we did with our Find42 function.

Header Files

I mentioned that functions have to be declared before you can use them, and function prototypes are the preferred way. But what happens if you break up your program into multiple files? You could call a function from one file, but the function is in a different file. Do you still need a prototype? Yes, the trick is to use header files.

A header file is just a text file, containing C statements, that is copied into another C program file by using a #include statement.

Way back in our first example we saw:

#include <stdlib.h>

this tells the C compiler to bring in a file called stdlib.h. In this file, there is a bunch of complex data type declarations, defines and, among other things, the function prototypes for the random number generating functions:

int     rand(void);
void    srand(unsigned);

Which is how we got away with using srand and rand before declaring them. By including stdlib.h, we give the compiler the information that it needs to properly call srand and rand.

We can split our example code into 3 files:

File 1: main.c

main.c contains our main function.

#include <stdio.h>   // gives us the prototype for printf
#include <stdint.h>  // gives us uint32_t
#include <stdbool.h> // gives us the boolean data type
#include "rando.h"   // gives us access to Initialize and Find42

int main(void) {
    uint32_t i;
    bool isDone;

    Initialize();
    i = 0;
    isDone = false;

    do {
        i = i + 1;
        isDone = Find42();
        if (isDone) {
            printf("It took %d calls to the random number generator to get the value 42\n", i);
        }
    } while(! isDone);
}

File 2: rando.h

rando.h - this is the header file associated with rando.c. It provides the prototypes to main.c so that it correctly calls the functions in rando.c.

void Initialize( void);
bool Find42( void);

File 3: rando.c

And finally, rando.c; the functions. Note that we include rando.h so that the compiler will give us an error if our prototypes don’t match our functions, avoiding subtle errors.

#include <stdint.h>     // gives us uint32_t
#include <stdlib.h>     // gives us srand and rand
#include "rando.h"

void Initialize( void) {
    uint32_t nowTime;

    nowTime = HAL_GetTick();
    srand( nowTime);
}

bool Find42(void) {
    bool returnValue;

    if (rand() == 42) {
        returnValue = true;
    } else {
        returnValue = false;
    }

    return (returnValue);
}

Good Habits

Return Statements

You should only have one return statement in a function. Some people feel that having multiple return statements is efficient, but this choice often makes debugging more difficult. 

If you use multiple return statements and miss some condition that lets the code exit out of the bottom of your function, the result isn’t predictable. The compiler will not complain, since you do have a return statement, but your code will occasionally silently give incorrect results.

Multiple Files

Break your program up into multiple files, each file containing the code to control a chunk of your system. This forms a set of modules that make up the system, rather than a single monolithic file that contains everything.

For example, if you had a balancing robot, you may have an accelerometer attached to an SPI bus. I would have one file that deals with the SPI. Another that deals with the accelerometer. Another that implements the balancing algorithm.

The balancing algorithm calls the accelerometer code to get the latest values, the accelerometer code uses the SPI functions to talk to the chip. The balancing algorithm doesn’t need to know about the SPI functions. Therefore when you change the way that the SPI functions talk to the chip, you don’t have to rework the balancing algorithm.

The SPI module is made of spi.c and spi.h. spi.c has the functions and all of the data that are needed to take care of working with the SPI bus. spi.h has the function prototypes.

Naming consistency

You should come up with some rules to naming your functions. What I do is put the name of the module before the name of the function. I also capitalize all of my function names so I can differentiate them from variables, defines, and macros.

In the balancing robot, I would have the functions AccelInit, and AccelGetData. AccelInit might call SPIInit.

The function name follows the same rules as variable names, the first character must be an upper or lower case letter or the underscore character (I choose uppercase). After that, it can be any combination of upper or lower case letters, underscores, or decimal digits.

Initialize Variables

Any variables that you declare inside of a function can only be accessed inside of that function. When the function begins, they are created and have a random value. When the function terminates the space they used is released and reused by other functions. Always remember that variables must be manually initialized at the beginning of a function and disappear when your function returns.

Parameters to Control Behaviour

Functions are a great way to structure your programs, by aggregating pieces of code into logical, larger, pieces. Once you use parameters, you can change the behaviour of the code inside of the function without rewriting it. And using return values, you can change the behaviour of the calling code based on what happened in the function.  

Welcome to the language!

Here in Canada, all of our consumer products are labeled in English and French. A lot of English speaking kids pick up the rudiments of French by reading cereal boxes at the breakfast table. Cereal box French. Now, I have not covered all of the niggly intricacies of C; you need to get some hands on experience, a good reference book[1], and time, to become fluent. What you have now is cereal box C, you have enough knowledge to look at a piece of well written code and probably understand what is happening. You might not feel comfortable writing programs yet, but that will come with practice.

[1] One book I like is the C Reference Manual 5th Ed by Harbison and Steel.


This post is part of a series. Please see the other posts here.


MMMMmmmm très bon goût.