ARM? Cortex? M4 Cookbook
上QQ阅读APP看书,第一时间看更新

Writing a function

Functions (sometimes called subroutines) are used to hide the complexity of underlying program statements, thereby presenting a more abstract view of the program. Abstraction is commonplace in engineering; for example, we can think of a car as comprising subassemblies that include body, engine, gearbox, suspension, and so on. The complexity within these subassemblies is only important to those specialists such as designers, test engineers, and technicians who need to interact with them. For example, the designers of the gearbox don't need to concern themselves with the intricacies of the engine, they just need to know a few important parameters. Functions provide a similar abstraction mechanism. We already met the functions LED_Initialize(); LED_On(), and LED_Off() used to initialize and switch the LEDs. We don't need to know exactly how these functions do their job but only how to use them. C provides functions as a mechanism of achieving hierarchical decomposition. For example, our main() function of helloBinky_c2v1 is becoming a bit cluttered and difficult to follow. To simplify the structure, the two for loops that simply introduce a delay could be repackaged as a function called delay() that accepts one input arg (that determines the length of the delay) and returns no output args (that is, void).

How to do it

  1. Clone the helloBlinky_c2v1 project to give helloBlinky_c2v2.
  2. Edit Blinky.c, and define the function delay() by adding the following:
    void delay (unsigned integer d) {
      unsigned integer i;
    
      for (i=0; i < d; i++)
        /* empty statement */ ;
    }
  3. It doesn't matter if the definition is placed before or after main ( ), but it shouldn't be nested inside main() (Note: functions defined inside other functions are called nested functions). Declare the function by including a function prototype declaration at the start of the program (that is, before the function is defined).
    void delay (unsigned int);
  4. Replace the statements:
    for (i = 0; i < 1000000; i++) 
       /* empty statement */ ;              /* Wait */
  5. Call the following function:
    delay (num_ticks);
  6. Declare a new variable in main() and initialize it.
    const unsigned int num_ticks = 500000;
  7. The relevant changes are shown as follows (omitting boilerplate code):
    void delay (unsigned int);        /* Func Prototype */
    
    int main (void) {
      const unsigned int max_LEDs = 8;
      const unsigned int num_ticks = 500000;
      unsigned int num = 0;
    
      HAL_Init ( );   /* Init Hardware Abstraction Layer */
      SystemClock_Config ( );           /* Config Clocks */
      LED_Initialize ( );                    /* LED Init */
    
      for (;;) {                         /* Loop forever */
        LED_On (num);                    /* Turn LEDs on */
        delay (num_ticks);
        LED_Off (num);                  /* Turn LEDs off */
        delay (num_ticks);  
        num = (num+1)%max_LEDs;  /* increment num (mod-8) */
      } /* end for */
    } /* end main ( ) */
    
    void delay (unsigned int d){         /* Function Def */
      unsigned int i;
      
      for (i = 0; i < d; i++)
      /* empty statement */ ;                   /* Wait */   
    } /* end delay ( ) */  

How it works…

Essentially, we've moved the for loop which implements the delay to within the function. The for loop itself is very similar to that used by helloBlinky_c2v1, except that the compare instruction used to terminate the loop now references the input argument d rather than a literal value (that is, 1000000).

for (i=0; i < d; i++) {
  ;
}

This is advantageous because it parameterizes the delay function, thereby allowing it to be used to implement different length delays, determined by the value of input argument d. An important feature of all programming languages is the mechanism they use to pass arguments to a function when it is called. There are two general models, called pass-by-value and pass-by-reference. The delay function call we've used here:

delay (num_ticks);

adopts a pass-by-value model. In this case, a copy of the variable num_ticks is passed to the delay function, and this copy can be referenced through the variable d. The statements inside the function can only access the variables declared within the function (that is, local to the function) and the input arguments. The function may change the value of the copy, but when the function terminates the copy (and the so-called automatic variables declared inside the function cease to exist). This model works fine in this case, because the function doesn't need to change the value of the variable num_ticks declared in main() (that is, the calling function).

All identifiers in C need to be declared before they are used. This is true of functions as well as variables (you may be catching onto the idea that C compilers don't tolerate surprises!), so functions should be declared before they are defined or called. A function declaration (also called a function prototype) includes the type of variable returned by the function, and the types of all the input args. C compilers accept the function definition as an implicit declaration and lazy programmers sometimes take advantage of this and omit the function prototype. But in this case, it must occur before the function is called. Nevertheless, it is considered good practice to include prototypes for all functions used. Function prototypes are usually placed at the beginning of the program or in a separate #include file. The prototype for our delay function looks like this:

void delay (unsigned integer);
Tip

White space characters are ignored by the compiler; we only include them to make our code more readable.

There's more…

If the delay function did need access to main functions variable, num_ticks, then it would need to access the memory location where num_ticks was stored. In this case, rather than passing a copy, we need to pass a reference (or so-called pointer) to the variable. C includes two special operators (* and &) for handling memory references. The ability to manipulate pointers as well as variables makes C a very powerful language, and it is a feature that is particularly useful for embedded systems programming. Consider the declaration:

unsigned int *ptr;

Here, ptr is the name of our variable, but in this case, it is preceded by the dereferencing operator * which tells the compiler it's a pointer variable, and so, the compiler must reserve enough memory to store an address. It also says the address will reference (that is, point to) an unsigned integer. When the pointer is declared and hasn't been assigned, we say the pointer is NULL (that is, its value cannot be guaranteed). To assign the pointer, we need to find the address of the variable num_ticks; the & operator achieves this. For example:

ptr = &num_ticks;

Let's consider another version of the delay function that doesn't declare the local variable i, but instead, employs a while loop that decrements the variable num_ticks declared in main. To do this, the function call to delay (within main) will need to pass a reference (or pointer) to num_ticks, and the delay() function will need to be told to expect a pointer to an unsigned integer as an input arg. Therefore, the function prototype will need to be changed to

void delay (unsigned int *);

and the function declaration itself becomes:

void delay (unsigned int *ptr) {
    
    while (*ptr > 0 ) 
    *ptr = (*ptr)-1;       /* Wait */  
}

The delay function uses the dereferencing operator * whenever it needs to access the value pointed to by ptr. The following recipe (helloBlinky_c2v3) represents a version of helloBlinky that uses pointers:

void delay (unsigned int *);      /* Func Prototype */

int main (void) {
  const unsigned int max_LEDs = 8;
  const unsigned int wait_period = 500000;
  unsigned int *ptr;
  unsigned int num_ticks;
  unsigned int num = 0;

  HAL_Init ( );   /* Init Hardware Abstraction Layer */
  SystemClock_Config ( );           /* Config Clocks */
  LED_Initialize();                      /* LED Init */

  for (;;) {                         /* Loop forever */
    LED_On (num);                          /* LED on */
    num_ticks = wait_period;        /* (re)set delay */
    ptr = &num_ticks;              /* assign pointer */
    delay (ptr);              /* call delay function */
    LED_Off (num);                        /* LED off */
    num_ticks = wait_period;        /* (re)set delay */
    delay (ptr);              /* call delay function */
    num = (num+1)%max_LEDs;     /* increment num (mod-8) */
  } /* end for */
} /* end main ( ) */

void delay (unsigned int *p){        /* Function Def */
  
  while (*p > 0 ) 
    *p = *p-1;                              /* Wait */
  
} /* end delay ( ) */

The preceding version of helloBlinky is just a vehicle for illustrating pointers, and the earlier recipe is preferable and easier to understand. So why are pointers used? Well, if our delay function needed access to many values, making the copies needed for pass-by-value would be time-consuming and impractical. This is particularly true when we come to consider passing arrays of data, strings (arrays of characters), and so on.