Lecture: 6
Objectives:Understand the syntatic structures needed to make a function call, passing input arguments and returning values.

Functions

Since the early days of programming, functions were a way programmers got a handle on code complexity. A frequently performed task was removed from its various places in the program, put into its own code structure, called a function. At its most elemental, a function encompass program functionality in a single callable format. When you define a function there are several things you need to consider:

Simple functions

Lets start our discussion of functions by illustrating functions without input arguments and no return values. This will allow us to focus on function prototypes and local variables.

Imagine that for some reason you were asked to write a function that printed "foo" 10 times and call this function from main. The following code illustrates how you might accomplish this.
void foo(void);		// This is a function prototype

//--------------------
//--------------------
main() {
    uint8_t i;		// this variable i is local to main and not related in anyway to the one in foo
    for (i=0; i < 5; i++) 
        foo();		// main is the caller, foo is the callee
} // end main

//--------------------
//--------------------
void foo(void) {
    uint8_t i;		// this variable i is local to foo, different from the one in main
    for (i=0; i < 5; i++) printf("foo ");
    printf("\r\n");
}
The first thing you encounter in this program (when reading from the top to the bottom) is the function prototype for the function "foo". A function prototype allows the compiler a chance to understand what "foo" means when it encounters it later on. Note that the function prototype is identical to the first line of the function definition except that one has a semicolon and the other a curly brace.

Inside main, we can execute the code inside the function by calling it. This means writing the name of the function followed by parenthesis. From an execution perspective you leave main and begin running the code in the function. When the last line of code in the function executes (or a return is encountered, but more on that later), execution resumes in main on the line of code following the function call.

After the body of main, the definition of the functions are provided. The first line of the function declaration "void foo(void)" contains information about three things; the return value, the name of the function and the input arguments of the function respectively. The use of "void" means that, in this case, there are no return values and no input arguments. Hence this function has the same behavior every time it runs.

In this example, main is called the caller of the function foo, and foo is called the callee.

Variable scope is a concept that often confuses new programmers, so let's take a moment to discuss it. You should have noticed that we have two separate declarations for the variable i, one in main and one in foo. The scope of a variable are the program regions where it can be read and written. The scope of a local variable is limited to the function in which it's defined. A variable defined in a function cannot be access in any other function. Thus, you can give your local variables names without any consideration to the variable names that may exit in the caller function.

The output behavior of this program is to print the word foo a lot of times in a particular pattern. Each call to the foo function prints five foos in the for-loop, one next to another with a space between each. After the for-loop exits, the foo function prints a carriage-return and line feed, moving the cursor to the beginning of the next line.
Main calls the foo function five times in its loop. Consequently the word foo is printed 25 times as shown below.
foo foo foo foo foo
foo foo foo foo foo
foo foo foo foo foo
foo foo foo foo foo
foo foo foo foo foo

Input Arguments

Lets extend the previous discussion to make the foo function more flexible by incorporating a input parameter which determines the number of times the foo function prints the word "foo".
void foo(uint8_t loops);	// This is a function prototype

//--------------------
//--------------------
main() {
    uint8_t i;		// this variable i is local to main and not related in anyway to the one in foo
    for (i=0; i < 5; i++) 
        foo(i);		// main is the caller, foo is the callee
} // end main

//--------------------
//--------------------
void foo(uint8_t loops) {
    uint8_t i;		// this variable i is local to foo, different from the one in main
    for (i=0; i < loops; i++) printf("foo ");
    printf("\r\n");
}
There are difference in this version of the program in the call, function definition and the function prototype.

The difference in the function prototype is trivial; a result of the change in the definition, the two have to always be the same. So, let's start our detailed discussion with an examination of the function definition for foo.

By adding the statement uint8_t loops in the declaration of the foo function we are telling the compiler that we will be sending an 8-bit integer from the caller to and that inside the foo function the value of this number is associated with the variable "loop". Inside the function, the loop variable is used to determine the number of times "foo" is printed. It is not a common practice, but you could change the value of loop inside foo, but do not expect this value to be sent back to the caller, it will not. In other words if you modified the value of loop inside foo, the value of i in main would not be changed. In programming we say that this is a call by value.

New programmers who are coming to terms with functions are often under the mistaken notion that the variable name for the input parameter in the function declaration ("loop" in our case) must be the same as the variable name used by the caller ("i" in our case). So for example, it would not surprise me it see foo(loop); in main. However, this is a misconception that I hope is dispelled with this example. There is absolutely no need to have the name of the input argument in the caller be the same as the name of the variable name in the declaration of the callee.

Like its predecessor, the output behavior of this program is to print the word "foo" a lot of times in a particular pattern. Each call to the foo function prints "foo" the number of times given by the input argument. Each "foo" is printed with a trailing space so that consecutive "foo" are distinguishable from one another with a space. After printing zero or more "foo"'s, the function prints a carriage-return and line-feed so that the next call to foo prints on a fresh new line.
main determines the number of "foo"'s printed using its for-loop. It starts by calling foo with i=0, so the first line of output from this program is a blank line. The next call to foo has i=1, so the foo function prints a single "foo". This continues, with each call to foo being one larger than the last causing each line of output to contain one more "foo" than the previous line. This generates a pattern of output as follows.

foo 
foo foo 
foo foo foo 
foo foo foo foo

Return Values

There are times where a function call is like asking a question and we would like an meaningful answer. A return value provides a mechanism for this type of interaction. Let's modify the previous definition of the foo function so that it answers the question, "how many characters were printed to the terminal?"
uint8_t foo(uint8_t loops);	// This is a function prototype

//--------------------
//--------------------
main() {
    uint8_t i;		// this variable i is local to main and not related in anyway to the one in foo
    for (i=0; i < 5; i++) {
        charsPrinted = foo(i);	// use the charsPrinted variable as you want
    }
} // end main

//--------------------
//--------------------
uint8_t foo(uint8_t loops) {
    uint8_t i;		// this variable i is local to foo, different from the one in main
    uint8_t sum = 0;	// this will hold the number of characters printed
    for (i=0; i < loops; i++) {
	printf("foo ");
	sum += 4;	// each time you print foo with a space is 4 characters
    }
    printf("\r\n");
    return(sum);	// this is what sends the value back to the caller
}
There are difference in this version of the program in the call, function definition and the function prototype.

The difference in the function prototype are trivial; a result of the change in the definition, the two have to always be the same. So, let's start our detailed discussion with an examination of the function definition for foo.

By adding the statement uint8_t in front of the declaration of the foo function we are telling the compiler that we will be returning an 8-bit integer to the caller using the return(sum); statement inside the function. You need to make sure that the datatype of the variable inside the return statement is the same as the data type given in the function definition. In our case since sum has type uint8_t (declared inside the function), it matches the return type given in the function declaration. In order to keep track of the number of characters printed as they are being printed, I decided to add 4 to sum on every iteration of the for-loop.

In the caller, the call to foo can now be used just like a variable, that is in fact what it means to return a value. So, I've assigned the return value from foo to the variable "charsPrinted". While nothings is done with this value in this example, you can use charsPrinted like any other local variable in main.

1D Arrays as input arguments

Let's add a pair of related and important final items to our discussion about functions and that is passing arrays to functions. While conceptually passing an array to a function is not dissimilar to passing a regular uint8_t, the syntax is different.
#define N	8
uint8_t foo(uint8_t array[], uint8_t length);

//--------------------
//--------------------
main() {
    uint8_t i, array[N];
    uint8_t average;

    for (i=0; i < N; i++) array[i] = i;
    average = foo(array, N)/N;

} // end main

//--------------------
//--------------------
uint8_t foo(uint8_t array[], uint8_t length) {
    uint8_t i, sum = 0;
    for (i=0; i < length; i++) {
	sum += array[i];
    }
    return(sum);
}
Most of the syntax changes become familiar with use, but are quirky at first: Now a subtle point, you could if you wanted change the values of the array inside the function and these changes would be communicated back to main. This is because we are calling the function with the array declared as a call by reference.

2D Arrays as input arguments

Working with 2D arrays requires a slightly different syntax than working with 1D arrays. Lets look at the syntax differences with an example and then we can explore why these changes are necessary.
#define N	8
uint8_t foo(uint8_t array[N][N], uint8_t rows, uint8_t cols);

//--------------------
//--------------------
main() {
    uint8_t i, array[N][N];
    uint8_t average;

    for (i=0; i < N; i++) 
        for (i=0; i < N; i++) 
	    array[i][j] = i*N + j;
    average = foo(array, N)/(N*N);

} // end main

//--------------------
//--------------------
uint8_t foo(uint8_t array[N][N], uint8_t rows, uint8_t cols) {
    uint8_t i, sum = 0;
    for (i=0; i < rows; i++) {
        for (i=0; i < cols; i++) {
	    sum += array[i][j];
    }
    return(sum);
}
Most of the syntax changes become familiar with use, but are quirky at first: Now a subtle point, you could if you wanted change the values of the array inside the function and these changes would be communicated back to main. This is because we are calling the function with the array declared as a call by reference. This is directly related to the topic of pointers, something that we will take up later in the semester.

Now for an explanation of why we need to define the dimensions of a 2D array, but not a 1D array. The reason is that compiler has to maps the indecies of the 2D array is into the linearly address space of the computer memory. Imagine that you had a 4X4 array of uint8_t called "samples" The computer would actually store the 16 uint8_t in continuous memory locations. Lets call the starting memory location of the sample array 0x4000. Each of the 16 element occupies 1 byte of memory so the sample array is store at memory locations 0x4000 to 0x400F. The first row of the sample array is stores in memory locations 0x4000 - 0x4003. The second row of the sample array is stores in memory locations 0x4004 - 0x4007. The third row of the sample array is stores in memory locations 0x4008 - 0x400B. The fourth row of the sample array is stores in memory locations 0x400C - 0x400F. When you the computer sees sample[2][1] it needs to convert this into address 0x4009. In general a reference to sample[row][column] references address 0x4000 + row*4 + column. It's at this point you can start to appreciate why we need to provide the subroutine with the dimensions of the array, so that it can convert the row and column references into an address. You would be correct in assuming that you only need to provide the number of columns per row to the subroutine, but we will use the convention that we will provide both.

Test your understanding

You can find the solutions embedded in the "source code" for this web page by right mouse clicking on this web page and selecting "view source". The solutions are in HTML comments.
  1. Write a function which takes as input:
    • an array of uint8_t's
    • the length of the array
    and returns the integer average of the elements. The array will never have more than 255 elements. Take care when accumulating the sum that it does not overflow.
  2. Write a function which takes as input:
    • an array of uint8_t's
    • the length of the array
    • an integer threshold
    and returns the number of array entries greater than the threshold. The array will never have more than 255 entries.
  3. Write a function that returns the period of a sine wave stored in an array. The sine wave has an average value of 128. You are to determine the period by looking for the for the first time the values in the array cross the average value of 128. For example, in the array below, this happens when we go from array[4]=168 to array[5]=122. Note that this is a negative transition, meaning that we decreased our way through 128.
        uint8_t array[16] = {233, 242, 233, 207, 168, 122, 76, 37, 11, 2, 11, 37, 76, 122, 168, 207}
    
    We then look for the second time the array crosses 128. In the array above this occurs we we go from array[13]=122 to array[14]=168. This is a positive transition, which should make sense because the sine wave is now increasing. Thus there are 9 indicies (index 13 minus index 4) in half the waveform. Thus this waveform has a period of 2*9 = 18 indicies.

    The function takes as input:
    • an array of uint8_t's
    • the length of the array
    and returns the period in terms of indicies. The array will never have more than 127 entries. If the period cannot be calculated, return 0.
  4. Given the function declaration below, answer the following questions.
    //----------------------------------------------
    // Computed the integer square root of the input.
    //----------------------------------------------
    uint8_t SQRT(uint8_t x) {
    	...
    } // end SQRT
    
    1. Make a call to SQRT with input 0xAB
    2. Make a call with SQRT with 8-bit var arg
    3. Add SQRT of arg to 8-bit variable sum
    4. Check if SQRT of 0xEF is greater than 0xDE
  5. In the backcountry of Colorado, knowledge of the barometric pressure is a useful tool for the predication of thunderstorms that generate lightening strikes which kill an average of 3 Coloradans a year and injure 15. Barometric pressure is the force per unit area of the atmosphere above a point on the earth's surface. We will measure air pressure Pascals; an increase in air pressure of 200 Pascals or more in an hour is a strong indicator of downdrafts from a developing thunderstorm. As a consequence, manufactures of GPS watches often include a barometric pressure sensor like the Bosch Sensortec BMP280. The BMP280 measures the absolute barometric pressure and returns it as a 16-bit values. Since the amount of atmosphere above a point on the earth's surface changes depending on your altitude, barometric pressure depends both on the local weather and the altitude.

    For example, in the image below, an imaginary column of atmosphere is shown above four points on the earth's surface. Each of the columns has a 1 square meter cross section. The column's height extends through the atmosphere into outer space so that all the possible atmosphere is accounted for. If you went out and were able to measure the force (weight) of this column at these points you would collect the information shown in the table below.
    Location Altitude Force of 1 m2 air column (Newtons)
    San Diego 0 m 101,325 N
    Idaho Springs 2,200 m 78,062 N
    Mt. Evans 4,400 m 60,140 N
    Mt. Everest 8,800 m 35,695 N



    In order to simplify interpreting barometric pressure, the effect of altitude is removed by adding an altitude correction factor to the absolute barometric pressure, yielding the sea-level adjusted barometric pressure. Note, all pressures that you read in weather reports are sea-level adjusted barometric pressures. The altitude correction factor (acf)depends on the altitude, a and is given by the equation:
    acf = 101,325*(1 - e-0.00012*a)

    For example, the altitude correct factor for Mt. Evans (an altitude of 4,400 m) is 60,140 Pa. Thus if the BMP280 sensor reported an absolute pressure of 42,000 Pa, the sea-level adjusted barometric pressure would be 42,000 Pa + 60,140 Pa = 102,140 Pa. Since 101,325 Pa is considered to be standard pressure, the air pressure at the top of Mt. Evans would be above average making for a blue bird day.

    Since all barometric pressures reported on the weather forecast or on a GPS watch are sea-level adjusted barometric pressures, a GPS watch must be able to calculate the altitude correction factor. Conveniently, a GPS watch has access to its altitude through the GPS receiver. Unfortunately, the microcontroller in your GPS watch cannot compute floating point numbers nor exponential functions. Instead you are going to have to use some of the programming constructs presented in this first unit to compute the altitude correction factor.

    1. Use the acf equation to determine the afc at 1,000 meter intervals starting from sea level to the highest peak in the world.
    2. Create an array of these values using the correct data type. You should assume all altitudes are positive.
    3. Write a function that takes in the alititude and returns the closest acf stores in the array.