UBMP4 Introductory Programming Activity 4

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 other source code 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, and often more descriptive, function call statement.

Functions are also 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 has been 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( ); statements are function calls. When the microcontroller gets to each function call statement in the program, it executes the program 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 these functions. No data is returned from the functions either, but that is less easy to see from the function call statement. It is, however, apparent when looking at the actual function declaration statements.

Here is the complete OSC_config( ) function (located in the UBMP420.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 by the function. 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, and every C program starts by running the first line of code inside the main( ) function.

Since main is the first part of a program to run, it makes sense that the parameter specifier in the int main(void) declaration is void, as there is no calling code that could run before main to 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 same int main(void) function declaration.

The main program

The main program file in in any C program is the one that contains the main function. Many C projects name the file containing the main function, ‘main.c’, to indicate that it contains the program’s starting and main code. We have elected not to do that in these programming activities, since each open file’s name is shown in a tab in MPLAB X. Having multiple programs’ main files open for reference would result in a number of tabs all labelled ‘main.c’, making it more confusing to determine which program tab is which. Instead, each of our programs is labelled with its activity name.

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             Activity: mirobo.tech/ubmp4-intro-4
 Date:    May 9, 2023
 
 This introductory programming activity for the mirobo.tech UBMP4 demonstrates
 the use of functions and the ability to pass variables between the program's
 main code and the function code.
 
 Additional program analysis and programming activities examine the location of
 function code within the program, introduce function prototypes, and reinforce
 the concepts of global and local variables and making sounds using loops.
==============================================================================*/

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

#include    "UBMP420.h"         // Include UBMP4.2 constants and functions

// 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
#define NOBUTTON    0
#define UP          1
#define DOWN        2

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

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

void pwm_LED4(unsigned char pwmValue)
{
    for(unsigned char t = 255; t != 0; t --)
    {
        if(pwmValue == t)
        {
            LED4 = 1;
        }
        __delay_us(20);
    }
    // End the pulse if pwmValue < 255
    if(pwmValue < 255)
    {
        LED4 = 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 LED4 brightness
        button = read_button();
        
        if(button == UP && LED4Brightness < 255)
        {
            LED4Brightness += 1;
        }

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

        // PWM LED4 with current brightness
        pwm_LED4(LED4Brightness);
        
        // 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_LED4( ), 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 put to use 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 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(SW3 == 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 an 8-bit binary 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_LED4( ) function

After reading the pushbuttons, the conditional statements that follow will adjust the PWM value if the up or down buttons are pressed. Below that is a statement that calls a PWM function and passes it the desired LED4Brightness value:

        // PWM LED4 with current brightness
        pwm_LED4(LED4Brightness);

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

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

Two important things to notice in the function declaration are the void return type, and the parameter variable assignment. First, the void return type shows that no data value is returned from this function. The second thing to notice is that the variable assignment copies the value of the variable passed to it – in this case the LED4Brightness value passes 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 build process by relocating the function code. Then, function prototypes will be added to the program to allow it to build again.

While this program is perfectly functional as is, some people may not like that the beginning of the main( ) function is below the new function code at the top of the program. You can imagine that in a program containing many more functions, anyone reading or editing the code would have to scroll all the way down past them to find the main( ) function 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 warnings and two errors were produced, with a part of the error output shown below:

Intro-4-Functions.c:40:18: warning: implicit declaration of function 'read_button' is invalid in C99 [-Wimplicit-function-declaration]
        button = read_button();
                 ^
Intro-4-Functions.c:40:18: warning: implicit conversion loses integer precision: 'int' to 'unsigned char' [-Wconversion]
        button = read_button();
               ~ ^~~~~~~~~~~~~
Intro-4-Functions.c:53:9: warning: implicit declaration of function 'pwm_LED4' is invalid in C99 [-Wimplicit-function-declaration]
        pwm_LED4(LED4Brightness);
        ^
Intro-4-Functions.c:64:15: error: conflicting types for 'read_button'
unsigned char read_button(void)
              ^
Intro-4-Functions.c:40:18: note: previous implicit declaration is here
        button = read_button();
                 ^
Intro-4-Functions.c:80:6: error: conflicting types for 'pwm_LED4'
void pwm_LED4(unsigned char pwmValue)
     ^
Intro-4-Functions.c:53:9: note: previous implicit declaration is here
        pwm_LED4(LED4Brightness);
        ^
3 warnings and 2 errors generated.
(908) exit status = 1
make[2]: Leaving directory '/Users/johnrampelt/mirobo/UBMP420/MPLABX/UBMP4.2-Intro-4-Functions/UBMP420-Intro-4-Functions.X'
make[1]: Leaving directory '/Users/johnrampelt/mirobo/UBMP420/MPLABX/UBMP4.2-Intro-4-Functions/UBMP420-Intro-4-Functions.X'

BUILD FAILED (exit value 2, total time: 262ms)

What are these warnings and errors trying to tell us? The first warning shows that the compiler sees read_button as a function, but one that has not been declared yet. The warning lets us know 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 the top to the bottom of the program code. When the compiler read our original program, with the function code at the top, the function declarations provided the compiler with all of the information about the code and variables it needed to know about in order to build the function code. The compiler could then easily build the the later function call, because it already knew about the functions.

Moving the functions below the main code means that the compiler tries to generate the program code without having any knowledge of the function 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 compiler with important details about 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_LED4(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 adding these two lines possibly work? The function code is still effectively missing, since it is still located below the main( ) function, and the compiler will not know what it does, or what variables are required. How will the read_button( ) function even return data if the compiler doesn’t know about it?

The answer is that C compilers have a companion helper that takes care of these situations – the linker. The C compiler generates blocks of code for each function, and can build all of the functions separately. The function prototypes inform the compiler about the functions and variables that it will encounter, allowing the C compiler to reserve variables of the proper type (and with abstracted names) in program memory. In this program, one 8-bit byte of memory will be reserved to hold the value returned by the read_button( ) function, and another byte of memory will be reserved hold the value of the variable passed to the pwm_LED4( ) 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 build of the final program.

While the compiling and linking steps 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 UBMP420.h and UBMP420.c files for all of these activities are examples of this. The top of this program includes the directive:

#include    "UBMP420.h"         // Include UBMP4.2 constants and functions

This #include statement adds the contents of the UBMP420.h file to our program, invisibly, at compile time. That file contains prototypes for functions in the UBMP420.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 actual 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. These functions can remain in the main program source code file or can be part of other source code files in a programming project.

When functions are logically named, they can aid in code readability by acting as meaningful descriptions of the purpose of the function code, and abstracting away the complexity of the code in the function.

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 help you make your programs smaller and easier to maintain, making you more productive.

Programming Activity 4 Challenges

1. It might be useful to have a button that instantly turns LED D4 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 also a useful feature that should be retained.

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

2. Create a new program that uses functions to turn on one LED for each pushbutton while the button is being pressed, and to turn off the LEDs when all of the buttons are released.

Start by creating a function that will return a number from 1-4 corresponding to which of the SW2 to SW5 pushbuttons is being pressed, or that returns a value of 0 if no buttons are pressed. Then, create a second function that will accept a number from 0 to 4 which will turn off all of the LEDs in response to a 0 input or light the corresponding LED for each pushbutton when receiving the numbers 1-4.

3.Create a four note music player using two functions, one to read the pushbutons (similar to Activity 2, above), and a second to play a sound on the piezo beeper using a loop.

Start by creating a sound function that will receive a parameter representing a tone's period or pitch. Then, modify the pushbutton function to return a different period or pitch in response to each button being pressed. You could even modify your pushbutton function to respond to multiple, simultaneous button presses so that your program is able to play more than four notes using just the four buttons!

4. A function that converts an 8-bit binary value into its equivalent 3-digit decimal number might be useful for helping us to debug our programs (as you will see in the next activity). Create a function that converts an 8-bit binary number into three decimal character variables representing the hundreds, tens, and ones digits of a (binary) number passed to it. For example, passing the function the 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 only 4 bits. Can you display the equivalent BCD digits on the LEDs?)

 

Next: UBMP4 Introductory Programming Activity 5 - Analog Input