This article will help you to understand what functions are, why they are used, and how they are implemented in embedded hardware.

Supporting Information

Every C program has a main() function. It is certainly possible to write a successful program in which the only function is main(). My guess is that this has been done many times, and it’s true that in certain simple applications no other function is needed.

However, extensive use of functions is an indication that the person writing the code is an experienced firmware developer. Why? Because functions allow us to write better code more quickly, with less work and fewer bugs. For those who spend a significant portion of their professional life writing firmware, these are the sort of advantages that cannot be ignored. Even if we initially resist the use of functions because they seem to require more work, experience gradually teaches us that the benefits greatly outweigh the costs.

What Is a Function?

A C function is a group of instructions that work together to implement a specific type of processor activity. In many cases, a function will perform one specific task, such as retrieving data from an SPI buffer, configuring a timer so that it generates a specified delay, or reading a value from memory and loading it into a DAC register.

However, there is certainly no law stating that a function can perform only one task. You might find it convenient to have one function that updates three unrelated state machines, or you could write a function that transmits a byte via UART, then checks the status bit until a byte is received, then incorporates the value of the received byte into some mathematical computations.

One thing I like about functions is that they enable a fairly direct translation between a flowchart and code.

The “Components” of a Function

A function consists of a name, a list of input parameters, the code statements that implement the required functionality, and a return type.

The following code snippet gives you an example.

char Convert_to_Lowercase(char UppercaseLetter) { if(UppercaseLetter < 65 || UppercaseLetter > 90) return 0x00; else return (UppercaseLetter + 32); }

I like to make my function names very descriptive. This makes code more readable and helps you to keep your thoughts organized.

The instructions are enclosed in curly brackets; this portion of the function definition is called the body of the function. The “return” keyword is used to exit the function and to identify which data should be delivered to the previously executing portion of code.

The return type, placed before the function name, identifies the data type of the information that will be returned. It is perfectly acceptable to have a function that simply performs a task, without any need to return data. In this case you would use the keyword “void” instead of a data type.

Passing Data to a Function

The input parameters, also called arguments, are enclosed in parentheses and placed after the function name. A C function can have multiple arguments, in which case they are separated by commas. Each argument must be accompanied by a data type.

In embedded applications, it is often not necessary to use arguments. I can think of two reasons for this.

First, embedded firmware frequently interacts directly with the device’s hardware, and consequently a function can get the information it needs from configuration registers, communication registers, or port pins.

Second, simple C programs written for microcontrollers can use global variables, i.e., variables that are present throughout the program and can be accessed by any function. As I understand it, the use of global variables in application programming is discouraged, or maybe even “condemned” would be the appropriate word. But in my opinion many firmware projects, especially those written entirely by one programmer, can benefit from the simplicity of global variables.

When defining a function that has no arguments, you can leave the parentheses empty or insert the keyword “void.” Theoretically the “void” approach is better than the empty parentheses, but in the context of embedded development—especially considering how clever modern compilers are—I don’t know how much it really matters.

Walkthrough

Let’s briefly examine the function definition shown above.

The function name, Convert_to_Lowercase, clearly indicates the purpose of the function: it accepts an eight-bit value corresponding to an uppercase ASCII letter, and it returns an eight-bit value corresponding to the lowercase version of that same letter.

There is one input parameter. It has a data type of char and uses a descriptive identifier.

The return value, like the input argument, is an ASCII character, and consequently the return type is char.

If the input value is outside of the range corresponding to uppercase ASCII letters, the function returns 0x00, which indicates an error. Otherwise, it adds 32 to the input value and returns the sum. If you’re not familiar with ASCII values, the table shown below will help you to understand why I am using the numbers 65, 90, and 32.

Functions in Hardware

Just as a processor’s data memory doesn’t directly support the details attached to a C variable, a processor’s code memory is much simpler than a C function. Code memory is a long sequence of storage locations that are not categorized or organized in any helpful way. The only thing that identifies a particular location in code memory is the address.

A C function, then, is an elaborate and programmer-friendly way to place blocks of code in memory and direct the processor to the block that needs to be executed. If you’ve worked with assembly language, you are familiar with the low-level reality of code execution: Each instruction has an address. We use a text label to represent a given address, and if we want the processor to execute the instructions at this address, we tell it to jump to the label.

C functions are a major improvement over the basic subroutines used in assembly language, but they are not fundamentally different. When you call, or “invoke,” a function, the processor’s program counter receives the address of the first machine-language instruction associated with that function.

The Call Stack

A section of memory known as a call stack is used to store the code-memory address to which the processor should return after the function has been executed. The stack also provides memory locations for local variables, i.e., variables that are created when the function is called and used only within the function.

An example of a call stack. Note that the diagram doesn’t indicate how many bytes are required for each item.

The call stack can serve as a place to store data that is passed to the function as an input parameter, but as I understand it, compilers will use registers for this instead of memory whenever possible (because registers are faster).

Conclusion

I hope that this article has given you a good overview of the structure and behavior of a function, both within a C program and in the processor’s actual hardware. We’ll continue our discussion of C functions in the next article.

C Language Series