Chapter 10: Functions
What's in Chapter 10?
Function Declarations
Function Definitions
Function Calls
Parameter Passing
Making our C programs "look like" C++
Finite State Machine using Function
Pointers
Linked list interpreter
We have been using functions throughout this document, but have put off formal presentation until now because of their immense importance. The key to effective software development is the appropriate division of a complex problem in modules. A module is a software task that takes inputs, operates in a well-defined way to create outputs. In C, functions are our way to create modules. A small module may be a single function. A medium-sized module may consist of a group of functions together with global data structures, collected in a single file. A large module may include multiple medium-sized modules. A hierarchical software system combines these software modules in either a top-down or bottom-up fashion. We can consider the following criteria when we decompose a software system into modules:
1) We wish to make the
overall software system easy to understand;
2) We wish to minimize the coupling or interactions between modules;
3) We wish to group together I/O port accesses to similar devices;
4) We wish to minimize the size (maximize the number) of modules;
5) Modules should be able to be tested independently;
6) We should be able to replace/upgrade one module with effecting the
others;
7) We would like to reuse modules in other situations.
As a programmer we must take special case when dealing with global variables and I/O ports. In order to reduce the complexity of the software we will limit access to global variables and I/O ports. It is essential to divide a large software task into smaller, well-defined and easy to debug modules. For more information about modular programming see either Chapter 5 of Embedded Systems: Introduction to ARM Cortex M Microcontrollers by Jonathan W. Valvano, or Chapter 3 of Embedded Systems: Real-Time Interfacing to ARM Cortex M Microcontrollers by Jonathan W. Valvano.
The term function in C is based on the concept of mathematical functions. In particular, a mathematical function is a well-defined operation that translates a set of input values into a set of output values. In C, a function translates a set of input values into a single output value. We will develop ways for our C functions to return multiple output values and for a parameter to be both an input and an output parameter. As a simple example consider the function that converts temperature in degrees F into temperature in degrees C.
short
FtoC(short TempF){
short TempC;
TempC=(5*(TempF-32))/9; //
conversion
return TempC;}
When the function's name is written in an expression, together with the values it needs, it represents the result that it produces. In other words, an operand in an expression may be written as a function name together with a set of values upon which the function operates. The resulting value, as determined by the function, replaces the function reference in the expression. For example, in the expression
FtoC(T+2)+4; //
T+2 degrees Fahrenheit plus 4 degrees Centigrade
the term FtoC(T+2) names the function FtoC and supplies the variable T and the constant 2 from which FtoC derives a value, which is then added to 4. The expression effectively becomes
((5*((T+2)-32))/9)+4;
Although FtoC(T+2)+4 returns the same result as ((5*((T+2)-32))/9)+4, they are not identical. As will we see later in this chapter, the function call requires the parameter (T+2) to be passed on the stack and a subroutine call will be executed.
Similar to the approach with variables, C differentiates between a function declaration and a function definition. A declaration specifies the syntax (name and input/output parameters), whereas a function definition specifies the actual program to be executed when the function is called. Many C programmers refer to function declaration as a prototype. Since the C compiler is essential a one-pass process (not including the preprocessor), a function must be declared (or defined) before it can be called. A function declaration begins with the type (format) of the return parameter. If there is no return parameter, then the type can be either specified as void or left blank. Next comes the function name, followed by the parameter list. In a function declaration we do not have to specify names for the input parameters, just their types. If there are no input parameters, then the type can be either specified as void or left blank. The following examples illustrate that the function declaration specifies the name of the function and the types of the function parameters.
// declaration input
output
void
Ritual(void); //
none
none
char
InChar(void); //
none
8-bit
void
OutChar(char); //
8-bit
none
short
InSDec(void); //
none
16-bit
void
OutSDec(short); //
16-bit none
char
Max(char,char); //
two 8-bit 8-bit
int
EMax(int,int); //
two 32-bit 32-bit
void
OutString(char*); //
pointer to 8-bit none
char
*alloc(int); //
32-bit pointer
to 8-bit
int Exec(void(*fnctPt)(void)); // function pointer 32-bit
Normally we place function declarations in the header file. We should add comments that explain what the function does.
void
InitSCI(void); // Initialize 38400 bits/sec
char InChar(void); // Reads in a
character, gadfly
void OutChar(char); // Output a character,
gadfly
char UpCase(char); // Converts
lower case character to upper case
void InString(char *, unsigned int); // Reads in a String of max length
To illustrate some options when declaring functions, we give alternative declarations of these same five functions:
InitSCI();
char InChar();
void OutChar(char letter);
char UpCase(char letter);
InString(char *pt, unsigned int MaxSize);
Sometimes we wish to call a function that will be defined in another module. If we define a function as external, software in this file can call the function (because the compiler knows everything about the function except where it is), and the linker will resolve the unknown address later when the object codes are linked.
extern
void InitSCI(void);
extern char InChar(void);
extern void OutChar(char);
extern char UpCase(char);
extern void InString(char *, unsigned int);
One of the power features of C is to define pointers to functions. A simple example follows:
int
(*fp)(int); // pointer to a function with input and
output
int fun1(int input){
return(input+1); //
this adds 1
};
int fun2(int input){
return(input+2); //
this adds 2
};
void Setp(void){ int data;
fp = &fun1; //
fp points to fun1
data = (*fp)(5); // data=fun1(5);
fp = &fun2; //
fp points to fun2
data = (*fp)(5); // data=fun2(5);
};
The declaration of fp looks a bit complicated because it has two sets of parentheses and an asterisk. In fact, it declares fp to be a pointer to any function that returns integers. In other words, the line int (*fp)(int); doesn't define the function. As in other declarations, the asterisk identifies the following name as a pointer. Therefore, this declaration reads "fp is a pointer to a function with a 32-bit signed input parameter that returns a 32-bit signed output parameter." Using the term object loosely, the asterisk may be read in its usual way as "object at." Thus we could also read this declaration as "the object at fp is a function with an int input that returns an int."
So why the first set of parentheses? By now you have noticed that in C declarations follow the same syntax as references to the declared objects. And, since the asterisk and parentheses (after the name) are expression operators, an evaluation precedence is associated with them. In C, parentheses following a name are associated with the name before the preceding asterisk is applied to the result. Therefore,
int
*fp(int);
would be taken as
int
*(fp(int));
saying that fp is a function returning a pointer to an integer, which is not at all like the declaration in Listing 10-1.
The second way to declare a function is to fully describe it; that is, to define it. Obviously every function must be defined somewhere. So if we organize our source code in a bottom up fashion, we would place the lowest level functions first, followed by the function that calls these low level functions. It is possible to define large project in C without ever using a standard declaration (function prototype). On the other hand, most programmers like the top-down approach illustrated in the following example. This example includes three modules: the LCD interface, the UART functions, and some SysTick timer routines. Notice the function names are chosen to reflect the module in which they are defined. If you are a C++ programmer, consider the similarities between this C function call LCD_clear() and a C++ LCD class and a call to a member function LCD.clear(). The *.H files contain function declarations and the *.C files contain the implementations.
#include
"LCD.h"
#include "UART.H"
#include "SysTick.H"
void main(void){ char letter; short n=0;
UART_Init();
LCD_Init();
SysTick
_Init()
LCD_String("This is a LCD");
SysTick_Wait10ms
(1000);
LCD_clear();
letter='a'-1;
while(1){
if
(letter=='z')
letter='a';
else
letter++;
LCD_putchar(letter);
SysTick_Wait10ms
(250);
if(++n==16){
n=0;
LCD_clear();
}
}
}
C function definitions have the following form
type Name(parameter
list){
CompoundStatement
};
Just like the function declaration, we begin the definition with its type. The type specifies the function return parameter. If there is no return parameter we can use void or leave it blank. Name is the name of the function. The parameter list is a list of zero or more names for the arguments that will be received by the function when it is called. Both the type and name of each input parameter is required. .
Although a character is passed in a 32-bit register, we are free to declare its formal argument as either character or word. If it is declared as a character, only the low-order byte of the actual argument will be referenced. If it is declared as an integer, then all 32 bits will be referenced.
It is generally more efficient to reference integers than characters because there is no need for a machine instruction to set the high-order byte. So it is common to see situations in which a character is passed to a function which declares the argument to be an integer. But there is one caveat here: not all C compilers promote character arguments to integers when passing them to functions; the result is an unpredictable value in the high-order byte of the argument. This should be remembered as a portability issue.
Since there is no way in C to declare strings, we cannot declare formal arguments as strings, but we can declare them as character pointers or arrays. In fact, as we have seen, C does not recognize strings, but arrays of characters. The string notation is merely a shorthand way of writing a constant array of characters.
Furthermore, since an unsubscripted array name yields the array's address and since arguments are passed by value, an array argument is effectively a pointer to the array. It follows that, the formal argument declarations arg[] and *arg are really equivalent. The compiler takes both as pointer declarations. Array dimensions in argument declarations are ignored by the compiler since the function has no control over the size of arrays whose addresses are passed to it. It must either assume an array's size, receive its size as another argument, or obtain it elsewhere.
The last, and most important, part of the function definition above is CompoundStatement. This is where the action occurs. Since compound statements may contain local declarations, simple statements, and other compound statements, it follows that functions may implement algorithms of any complexity and may be written in a structured style. Nesting of compound statements is permitted without limit.
As an example of a function definition consider
int
add3(int z1, int z2, int z3){ int y;
y=z1+z2+z3;
return(y);}
Here is a function named add3 which takes three input arguments.
A function is called by writing its name followed by a parenthesized list of argument expressions. The general form is
Name (parameter list)
where Name is the name of the function to be called. The parameter list specifies the particular input parameters used in this call. Notice that each input parameter is in fact an expression. It may be as simple as a variable name or a constant, or it may be arbitrarily complex, including perhaps other function calls. Whatever the case, the resulting value is pushed onto the stack where it is passed to the called function.
C programs evaluate arguments from left to right, pushing them onto the stack in that order. On return, the return parameter is located in Reg R0. The input parameters are removed from the stack at the end of the program.
When the called function receives control, it refers to the first actual argument using the name of the first formal argument. The second formal argument refers to the second actual argument, and so on. In other words, actual and formal arguments are matched by position in their respective lists. Extreme care must be taken to ensure that these lists have the same number and type of arguments.
It was mentioned earlier, that function calls appear in expressions. But, since expressions are legal statements, and since expressions may consist of only a function call, it follows that a function call may be written as a complete statement. Thus the statement
add3(--counter,time+5,3);
is legal. It calls add3(), passing it three arguments --counter, time+5, and 3. Since this call is not part of a larger expression, the value that add3() returns will be ignored. As a better example, consider
y=add3(--counter,time+5,3);
which is also an expression. It calls add3() with the same arguments as before but this time it assigns the returned value to y. It is a mistake to use an assignment statement like the above with a function that does not return an output parameter.
The ability to pass one function a pointer to another function is a very powerful feature of the C language. It enables a function to call any of several other functions with the caller determining which subordinate function is to be called.
int
fun1(int input){
return(input+1); //
this adds 1
};
int fun2(int input){
return(input+2); //
this adds 2
};
int execute(int (*fp)(int)){ int data;
data = (*fp)(5); // data=fun1(5);
return (data);
};
void main(void){ int result;
result = execute(&fun1); //
result=fun1(5);
result = execute(&fun2); //
result=fun2(5);
};
Notice that fp is declared to be a function pointer. Also, notice that the designated function is called by writing an expression of the same form as the declaration.
Now let us take a closer look at the matter of argument passing. With respect to the method by which arguments are passed, two types of subroutine calls are used in programming languages--call by reference and call by value.
The call by reference method passes arguments in such a way that references to the formal arguments become, in effect, references to the actual arguments. In other words, references (pointers) to the actual arguments are passed, instead of copies of the actual arguments themselves. In this scheme, assignment statements have implied side effects on the actual arguments; that is, variables passed to a function are affected by changes to the formal arguments. Sometimes side effects are beneficial, and some times they are not. Since C supports only one formal output parameter, we can implement additional output parameters using call by reference. In this way the function can return parameters back using the reference. As an example recall the fifo queue program shown earlier in Listing 8-7. The function GetFifo, shown below, returns two parameters. The regular formal parameter is a boolean specifying whether or not the request was successful, and the actual data removed from the queue is returned via the call by reference. The calling program InChar passes the address of its local variable data. The assignment statement *datapt=Fifo[GetI++]; within GetFifo will store the return parameter into a local variable of InChar. Normally GetFifo does not have the scope to access local variables of InChar, but in this case InChar explicitly granted that right by passing a pointer to GetFifo.
int GetFifo (char *datapt) {
if(Size == 0 )
return(0); /* Empty if Size=0 */
else{
*datapt=Fifo[GetI++]; Size--;
if (GetI == FifoSize) GetI = 0;
return(-1);
}
}
char InChar(void){ char data;
while(GetFifo(&data)){};
return (data);}
When we use the call by value scheme, the values, not references, are passed to functions. With call by value copies are made of the parameters. Within a called function, references to formal arguments see copied values on the stack, instead of the original objects from which they were taken. At the time when the computer is executing within PutFifo() of the example below, there will be three separate and distinct copies of the 0x41 data (main, OutChar and PutFifo).
int PutFifo(char data) {
if(Size == FifoSize ) {
return(0);}
/* Failed, fifo was full */
else{
Size++;
*(PutPt++)=data;
/* put data into fifo */
if (PutPt
== &Fifo[FifoSize]) PutPt = &Fifo[0]; /* Wrap */
return(-1);
/* Successful */
}
}
void OutChar(char data){
while(PutFifo(data)){};
}
void main(void){ char data=0x41;
OutChar(data);
}
The most important point to remember about passing arguments by value in C is that there is no connection between an actual argument and its source. Changes to the arguments made within a function, have no affect what so ever on the objects that might have supplied their values. They can be changed with abandon and their sources will not be affected in any way. This removes a burden of concern from the programmer since he may use arguments as local variables without side effects. It also avoids the need to define temporary variables just to prevent side effects.
It is precisely because C uses call by value that we can pass expressions, not just variables, as arguments. The value of an expression can be copied, but it cannot be referenced since it has no existence in global memory. Therefore, call by value adds important generality to the language.
Although the C language uses the call by value technique, it is still possible to write functions that have side effects; but it must be done deliberately. This is possible because of C's ability to handle expressions that yield addresses. And, since any expression is a valid argument, addresses can be passed to functions.
Since expressions may include assignment, increment, and decrement operators (Chapter 9), it is possible for argument expressions to affect the values of arguments lying to their right. (Recall that C evaluates argument expressions from left to right.) Consider, for example,
func
(y=x+1, 2*y);
where the first argument has the value x+1 and the second argument has the value 2*(x+1). What would be the value of the second argument if arguments were evaluated from right to left? This kind of situation should be avoided, since the C language does not guarantee the order of argument evaluation. The safe way to write this is
y=x+1;
func (y, 2*y);
It is the programmer's responsibility to ensure that the parameters passed match the formal arguments in the function's definition. Some mistakes will be caught as syntax errors by the compiler, but this mistake is a common and troublesome problem for all C programmers.
Occasionally, the need arises to write functions that work with a variable number of arguments. An example is printf() in the library. In C, this feature is implemented using macros defined in the library file STDARG.C. To use these features you include STDARG.H in your file.
Private versus Public Functions
For every function definition, the compiler generates an assembler directive declaring the function's name to be public. This means that every C function is a potential entry point and so can be accessed externally. One way to create private/public functions is to control which functions have declarations. Consider again the main program in Listing 10-2 shown earlier. Now lets look inside the Timer.H and Timer.C files. To implement Private and Public functions we place the function declarations of the Public functions in the Timer.H file.
void
SysTick_Init
(void);
void SysTick_Wait(unsigned long delay)
;
The implementations of all functions are included in the SysTick.c file. The function, SysTick_Wait, is private and can only be called by software inside the SysTick.c file. We can apply this same approach to private and public global variables. Notice that in this case the global variable, TimerClock, is private and can not be accessed by software outside the SysTick.c file.
unsigned long
static TimerClock; // private global
void SysTick_Init(void){
NVIC_ST_CTRL_R =
0;
// 1) disable SysTick during setup
NVIC_ST_RELOAD_R = 0x00FFFFFF; // 2) maximum
reload value
NVIC_ST_CURRENT_R =
0; //
3) any write to current clears it
NVIC_ST_CTRL_R = 0x00000005; // 4)
enable SysTick with core clock
}
// The delay parameter is in units of the 80 MHz core clock. (12.5 ns)
void static SysTick_Wait(unsigned long delay){
NVIC_ST_RELOAD_R = delay-1; // number of counts to wait
NVIC_ST_CURRENT_R = 0; //
any value written to CURRENT clears
while((NVIC_ST_CTRL_R&0x00010000)==0){ // wait for count flag
}
} // 10000us equals 10ms
void SysTick_Wait10ms(unsigned long
delay){
unsigned long
i;
for(i=0; i<delay; i++){
SysTick_Wait(800000); // wait 10ms
}
}
For more information about modular programming see either Chapter 5 of Embedded Systems: Introduction to ARM Cortex M Microcontrollers by Jonathan W. Valvano, or Chapter 3 of Embedded Systems: Real-Time Interfacing to ARM Cortex M Microcontrollers by Jonathan W. Valvano.
Finite State Machine using Function Pointers
Now that we have learned how to declare, initialize and access function pointers, we can create very flexible finite state machines. In the finite state machine presented in Listing 9-2 and Listing 9-4, the output was a simple number that is written to the output port. In this next example, we will actually implement the exact same machine, but in a way that supports much more flexibility in the operations that each state performs. In fact we will define a general C function to be executed at each state. In this implementation the functions perform the same output as the previous machine.
Figure 10-6: Finite State Machine (same as Figure 9-1)Compare the following implementation to Listing 9-2, and see that the unsigned char Out; constant is replaced with a void (*CmdPt)(void); function pointer. The three general function DoStop() DoTurn() and DoBend() are also added.
struct State{
void
(*CmdPt)(void);
/* function to
execute */
unsigned short Wait; /* Time,
10ms to wait */
unsigned char AndMask[4];
unsigned char EquMask[4];
const struct State *Next[4];}; /* Next states */
typedef const struct State state_t;
typedef state_t
* StatePtr;
void DoStop(void){
#define stop &fsm[0]
#define turn &fsm[1]
#define bend &fsm[2]
PORTA
= 0x34;}
void DoTurn(void){
PORTA
= 0xB3;}
void DoBend(void){
PORTA
= 0x75;}
state_t
fsm[3]={
&DoStop
{, 2000, // stop 1 ms
&DoTurn
{0xFF, 0xF0, 0x27, 0x00},
{0x51, 0xA0, 0x07, 0x00},
{turn, stop, turn, bend}},
{,5000, // turn 2.5 ms
&DoBend
{0x80, 0xF0, 0x00, 0x00},
{0x00, 0x90, 0x00, 0x00},
{bend, stop, turn, turn}},
{,4000, // bend 2 ms
{0xFF, 0x0F, 0x01, 0x00},
{0x12, 0x05, 0x00, 0x00},
{stop, stop, turn, stop}}};
Compare the following implementation to Listing 9-4, and see that the PORTH=pt-Out; assignment is replaced with a (*Pt->CmdPt)(); function call. In this way, the appropriate function DoStop() DoTurn() or DoBend() will be called.
void control(void){ StatePtr Pt;
unsigned char Input; unsigned
short startTime; unsigned int i;
SysTick_Init();
Port_Init();
Pt =
stop; // Initial State
while(1){
(*Pt->CmdPt)();
//
1)
execute
function
SysTick_Wait10ms(
Pt->Wait); // 2) wait
Input =
PORTB; // 3) input
for(i=0;i<4;i++)
if((Input&Pt->AndMask[i])==Pt->EquMask[i]){
Pt=Pt->Next[i]; // 4) next depends on input
i=4; }}};
Linked List Interpreter using Function Pointers
In the next example, function pointers are stored in a listed-list. An interpreter accepts ASCII input from a keyboard and scans the list for a match. In this implementation, each node in the linked list has a function to be executed when the operator types the corresponding letter. The linked list LL has three nodes. Each node has a letter, a function and a link to the next node.
//
Linked List Interpreter
struct Node{
unsigned char Letter;
void (*fnctPt)(void);
const struct Node *Next;};
typedef const struct Node node_t;
typedef node_t
* NodePtr;
void CommandA(void){
OutString("\nExecuting Command
a");
}
void CommandB(void){
OutString("\nExecuting Command
b");
}
void CommandC(void){
OutString("\nExecuting Command
c");
}
node_t
LL[3]={
{ 'a', &CommandA,
&LL[1]},
{ 'b', &CommandB,
&LL[2]},
{ 'c', &CommandC, 0 }};
void main(void){ NodePtr Pt; char string[40];
UART_Init(); // Enable SCI port
UART_OutString("\nEnter a single
letter command followed by <enter>");
while(1){
UART_
OutString("\n>");
UART_
InString(string,39); //
first character is interpreted
Pt=&LL[0];
// first node to check
while(Pt){
if(string[0]==Pt->Letter){
Pt->fnctPt(); //
execute function
break;} //
leave while loop
else{
Pt=Pt->Next;
if(Pt==0)
UART_
OutString(" Error");}}}}
Compare the syntax of the function call, (*Pt->CmdPt)();, in Listing 10-13, with the syntax in this example, Pt->fnctPt();.These two expressions both generate code that executes the function.
Go to Chapter 11 on Preprocessor Directives Return to Table of Contents