UBMP4.1 Introductory Programming Activity 4

This activity was designed for UBMP4.1 and is now superseded by a new version for UBMP4.2.

Functions

Functions are self-contained blocks of code created to perform a specific task within a program. The function source code can be located in the main program file or in any program file that is a part of the programming project. Functions help to simplify and organize code by moving blocks of complex code out of the main program and replacing them with a shorter, descriptive, function call statement.

Functions are also very useful and important in reducing the size of microcontroller programs by allowing a single block of function code to be repeatedly called from different parts of the program, instead of having to duplicate the same code in multiple locations in a program.

Activity 3 introduced the concepts of PWM (pulse width modulation). In this activity, we will recreate the Activity 3 PWM program using two functions: one function to read the pushbuttons, and a second function to generate the PWM output waveform. While this example will not reduce the overall size of the program code, it should help to simplify the main code. First, let’s learn more about functions.

Function declarations

All functions begin with a function declaration statement, which is followed by the program instructions that make up the function grouped inside curly braces:

    return-type name(parameter 1, parameter 2, ...)
    {
        ...
    }

The function declaration consists of three parts. The return-type specifies the numeric type or number format of any data returned from the function, and can either be any one of the supported data types described in Introductory Programming Activity 2 – Variables, or no data. If no data is to be returned, the return-type is specified as void.

The function name is a unique name given to each function which is used by the the currently running program code to ‘call’ the function. In other words, when the main program code calls a function, it temporarily diverts program execution to run the code inside the function before continuing the next program statement in line in the program code.

Optional parameters can be supplied to a function by the code making the function call. These parameters pass data values that the function needs to perform its operation. The form of each parameters is the same as the statements used to create variables – each consists of a numeric type, and a local variable name.

Function calls

Once a function is defined, it can be called from other parts of the program. Function call statements are easily identified by the brackets meant to hold the parameters, and previous programs have implemented both function calls, and functions. Every introductory program so far, including this one, includes this code:

int main(void)
{
    OSC_config();               // Configure internal oscillator for 48 MHz
    UBMP4_config();             // Configure on-board UBMP4 I/O devices
    ...

Both OSC_config( ); and UBMP4_config( ); are function calls. When the microcontroller gets to each function call in the program, it executes the code in the function itself. When the code in each function finishes, the microcontroller continues from the next line of code below the function call.

As can be seen from the function calls, above, the parameter brackets are empty – no data is passed to the functions. No data is returned from the functions either, but that is less easy to see from the function call. It is, however, apparent when looking at the actual function declaration statements.

Here is the complete OSC_config( ) function (located in the UBMP410.c project file):

void OSC_config(void)
{
    OSCCON = 0xFC;              // Set 16MHz HFINTOSC with 3x PLL enabled
    ACTCON = 0x90;              // Enable active clock tuning from USB clock
    while(!PLLRDY);             // Wait for PLL lock (disable for simulation)
}

Note that the function declaration begins with the void return type, indicating that no data will be returned. The second void, as a parameter specifier, indicates that no parameters are accepted into the function.

When the function is called, all three program statements in the function run. When they finish, the function implicitly returns to the calling code. In our example program, we will also explore an example that explicitly returns data from a function.

 

New Concepts

Functions are stand-alone blocks of code. Here are some of the concepts you will learn about in this activity:

  • function declaration - the first line of the function program code which serves three purposes: declares the function name, lists any need parameters required by the function, and identifies the type of data returned by the function

  • function prototype - identifies a function in the same way as a function declaration, but without the actual function program code, to reserve the function names and variables in preparation for compiling the program

  • function call - a statement that causes the program to begin to run the code inside the function

  • function name - a unique name given to each function (function names often describe the purpose of the function, or what it is designed to accomplish)

  • function parameters - data variables that can be transferred or passed from the code calling the function to the code in the function

  • return statement - a statement that explicitly ends the function and may return data to the calling code (functions can end without return statements)

  • return type - the type of data or variables returned from a function to the calling code

The main( ) function is special

Did you notice the main( ) function declaration in the code snippet, above? The main function is required in every C language program – main is actually the starting location of every C program, and every C program is coded into a main( ) function.

Since main is the first part of a program to run, it makes sense that the parameter specifier in the declaration is void, as there is no calling code that could run before main and send main any parameters. But, if main is the first function to run, isn’t it odd that it is coded with an int return type? Again, as main is the first thing to run, there is no code running before it to return data to! Nonetheless, the int type is required by MPLAB, and every PICmicro C program must begin with the int main(void) function declaration.

The main program

Have you noticed that the main program file in MPLAB is the one that contains the main function? Many C projects name the file containing the main function ‘main’, to indicate that it contains the program starting/main code. We have elected not to do so in these programming activities since having multiple programs’ main files open in MPLAB X for reference would result in a number of tabs all labelled ‘main’, making it confusing to determine which program tab is which.

Download the Intro-4-Functions program files (.zip) to create the MPLAB project for this activity, or import the files into MPLAB from its Github repository. Let’s examine the contents of the main file, Intro-4-Functions.c, below:

/*==============================================================================
 Project: Intro-4-Functions
 Date:    March 18, 2022
 
 This program demonstrates the use of functions, and variable passing between
 the main and function code.
 
 Additional program analysis and programming activities examine function code
 location, function prototypes, and reinforce the concepts of global and local
 variables.
==============================================================================*/

#include    "xc.h"              // Microchip XC8 compiler include file
#include    "stdint.h"          // Include integer definitions
#include    "stdbool.h"         // Include Boolean (true/false) definitions

#include    "UBMP410.h"         // Include UBMP4.1 constant and function definitions

// TODO Set linker ROM ranges to 'default,-0-7FF' under "Memory model" pull-down.
// TODO Set linker code offset to '800' under "Additional options" pull-down.

// Button constant definitions
const char noButton = 0;
const char UP = 1;
const char DOWN = 2;

// Program variable definitions
unsigned char LED5Brightness = 125;
unsigned char button;

unsigned char read_button(void)
{
    if(SW4 == 0)
    {
        return(UP);
    }
    else if(SW5 == 0)
    {
        return(DOWN);
    }
    else
    {
        return(noButton);
    }
}

void pwm_LED5(unsigned char pwmValue)
{
    for(unsigned char t = 255; t != 0; t --)
    {
        if(pwmValue == t)
        {
            LED5 = 1;
        }
        __delay_us(20);
    }
    // End the pulse if pwmValue < 255
    if(pwmValue < 255)
    {
        LED5 = 0;
    }
}

int main(void)
{
    OSC_config();               // Configure internal oscillator for 48 MHz
    UBMP4_config();             // Configure on-board UBMP4 I/O devices
	
    while(1)
    {
        // Read up/down buttons and adjust LED5 brightness
        button = read_button();
        
        if(button == UP && LED5Brightness < 255)
        {
            LED5Brightness += 1;
        }

        if(button == DOWN && LED5Brightness > 0)
        {
            LED5Brightness -= 1;
        }

        // PWM LED5 with current brightness
        pwm_LED5(LED5Brightness);
        
        // Activate bootloader if SW1 is pressed.
        if(SW1 == 0)
        {
            RESET();
        }
    }
}

// Move the function code to here in Program Analysis, step 5.

Program Operation

The program begins with the declarations and code for the two new functions, read_button( ) and pwm_LED5( ), followed by the required main( ) function. To anyone reading the program for the first time, these descriptive function names hopefully provide some insight into what the functions do, and how the functions might be used in the program.

Even though the two new functions appear above the main( ) function in the code, the compiler builds the code so that the main( ) function is first to run. Reading further into the main( ) function, it should also become apparent that the readability of the program, and potentially, comprehension of its operation, has been improved by replacing the longer function code with shorter, descriptive function call statements to the code in the functions.

The read_button( ) function

The read_button( ) function reads the state of the pushbuttons and returns a unique value representing the first pressed button, or a different value if none of the buttons are pressed. Let’s take a look at the function to see how it does that:

unsigned char read_button(void)
{
    if(SW4 == 0)
    {
        return(UP);
    }
    else if(SW5 == 0)
    {
        return(DOWN);
    }
    else
    {
        return(noButton);
    }
}

The function declaration begins with the unsigned char return type indicating that this function is expected to return a piece of data, in the form of a number from 0-255 in value, to the calling code. Where does this number come from?

The code that makes up the function is an if-else condition, so one of its options will always be chosen. Each conditional block includes a return( ) statement, which explicitly returns from the function back to the calling program, even before the end of the function code is reached. Importantly, the value in the brackets – set to a number by previously-defined constants – is returned from the function to the function call. We can examine the function call statement in the main code to see how the variable is assigned a number from the read_button( ) function:

        button = read_button();

The function call statement is in the form of a variable assignment expression – the variable button will be assigned the value returned by the read_button( ) function. The value assigned to the button variable is used in the conditional statements that follow to increment or decrement the PWM value.

The pwm_LED5( ) function

After reading the pushbuttons, the conditional statements that follow might adjust the PWM value. Below the conditions is a statement that calls a PWM function and passes it the desired LED5Brightness value:

        pwm_LED5(LED5Brightness);

The actual code that performs the PWM operation, using the supplied brightness value, is shown in the pwm_LED5( ) function, below:

void pwm_LED5(unsigned char pwmValue)
{
    for(unsigned char t = 255; t != 0; t --)
    {
        if(pwmValue == t)
        {
            LED5 = 1;
        }
        __delay_us(20);
    }
    // End the pulse if pwmValue < 255
    if(pwmValue < 255)
    {
        LED5 = 0;
    }
}

The two important things to notice in the function declaration are the void return type, and the parameter variable assignment. The void return type shows that no data value is returned from this function. The variable assignment copies the value of the LED5Brightness variable, passed as a parameter in the function call, and saves it in the newly-created pwmValue local variable.

Variables created in a function declaration are local, meaning they are only available to the code within the function itself. This is in contrast to variables created in the main program which are global, or available to all of the program code. Although not used here, making a local copy of a global variable in a function offers the advantage that the copy of the variable can be modified without altering the value of the global variable for the rest of the code.

Learn more – program analysis activities

Hopefully, you have built and run this program to verify its operation. If not, do that now, as the next steps will break the building of the program by relocating the function code before fixing the program with function prototypes.

While this program is perfectly functional, some people may not like that the beginning of the main part of the program is below the function code at the top. You can imagine that in a program containing many more functions, anyone reading or editing the code would have to scroll down past them to find the main code. So, why not just move the functions below the main code?

Let’s give that a try. Cut the two functions out of the code at the top of the program and paste them below the comment at the end of the code (below the three closing curly braces). Then, build the program. During compilation three errors and two warnings were produced, with the first error shown below:

make[2]: *** [nbproject/Makefile-default.mk:134: build/default/production/Intro-4-Functions.p1] Error 1
Intro-4-Functions.c:39:18: warning: implicit declaration of function 'read_button' is invalid in C99 [-Wimplicit-function-declaration]
        button = read_button();
                 ^

What is the error trying to tell us? The compiler sees read_button as a function, but one that has not been declared yet. The error warns that implicitly declaring the function during its first use is not possible.

The reason for the error is that the C compiler generates the program’s machine code during what is know as a single pass through the program, reading all of our source code once, from top to bottom. When the compiler read our original program, the function declarations provided it with all of the code and variables it needed to know about in order to make the function code, and then use the code during the function calls.

Moving the functions below the main code means that the compiler is expected to try to generate the program code without having any knowledge of the function code, or the variables required by the function. Clearly, this makes it impossible for the compiler to generate the program. But, we can use function prototypes to tell the compiler all it needs to know to successfully build the program.

Function prototypes

Function prototypes are like empty function declarations. Function prototypes provide the important details of the functions, namely the return-type, the function name, and the parameter types, but leave out the actual function code. Add these two function prototypes above the main( ) function in the code, where the functions were originally located, and then build the program again.

unsigned char read_button(void);

void pwm_LED5(unsigned char);

The addition of these two prototypes above the main code allow the program to build successfully, even with the function code moved below the function calls in the main code. But, how can this work? The function code is still missing, and the local variable used in the pwm_LED5( ) function isn’t even named. The answer is that C compilers have a companion helper that takes care of these situations – the linker.

The linker runs after the C compiler, and allows the C compiler to compile each program function independently as well as reserve variables with abstracted names in program memory. In this program, one byte of memory will be reserved to hold the value of the button variable in the read_button( ) function, and another byte of memory will hold the LED5Brightness variable in the pwm_LED5( ) function. When the linker runs, it will link all of the variables to their functions, and may relocate program code (or just stitch it together), completing the building the final program.

While this represents a complex process, you don’t need to understand these concepts to appreciate the benefits they provide. Function prototypes provide the compiler with the important information it needs to prepare to use the functions, and the function code can be located anywhere in the program, including in totally separate files. The UBMP410.h and UBMP410.c files for all of these activities are examples of this. The top of this program includes the directive:

#include    "UBMP410.h"         // Include UBMP4.1 constant and function definitions

This #include statement add the contents of the UBMP410.h file to our program, invisibly, at compile time. That file contains prototypes for functions in the UBMP410.c file, which the compiler will also compile separately. The linker will stitch all of the required functions from all of the files together to build our program.

Activity 4 learning summary

Functions provide a number of advantages in most programs. First, functions simplify the main structure of the program by moving larger blocks of program code out of the main code and into separately located blocks of function code.

If these functions are logically named, they can aid code readability by acting as meaningful descriptions in the code itself, and abstracting away hardware interactions.

Of particular importance in small, memory constrained microcontrollers, is the ability of function code to be reused by being called from multiple locations in a program instead of being duplicated in each of those locations. This feature alone will make your programs smaller and easier to maintain, making you a more productive programmer.

Programming Activity 4 Challenges

1. It might be useful to have a button that instantly turns LED D5 fully on or off instead of waiting for it to brighten and dim while the UP and DOWN buttons are held to change the PWM duty cycle. But, PWM dimming dimming capability is still a useful feature that should be retained.

Modify the read_button( ) and main( ) functions to use SW3 as an instant-on button, and SW2 as an instant-off button. Pressing either of these buttons will over-write the current LED5Brightness value with either 255 or 0, while still allowing SW4 and SW5 to adjust the brightness in smaller increments when pressed.

2. Create a function that will return a number from 1-4 corresponding to which of the SW2 to SW5 switches is pressed, or return 0 if no switches are pressed. Then, create a function that will accept a number from 1 to 4 that lights the corresponding LED beside each button.

3. Create a sound function that receives a parameter representing a tone's period. Modify your button function, above, to return a variable that will be passed to the sound function to make four different tones.

4. A function that converts an 8-bit binary value into its decimal equivalent would be useful for helping us to debug our programs. Create a function that converts an 8-bit binary number into three decimal character variables representing the hundreds, tens and ones digits of the binary number passed to it. For example, passing the function a value of 142 will result in the hundreds variable containing the value 1, the tens variable containing 4, and the ones variable 2. How could you test this function to verify that it works? Try it! (Hint: the BCD number system represents the decimal digits 0-9 using 4 bits.)