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

Writing a C program to blink each LED in turn

This recipe extends the helloBlinky_c2v0 recipe introduced in the previous section, and includes a few more C programming statements. We'll call our new recipe helloBlinky_c2v1. uVision5's IDE features a so-called folding editor that allows blocks of code and comments to be hidden or expanded. This is quite useful for hiding complexity, allowing us to focus on the important details.

Getting ready…

First, we'll draw a flowchart describing what our program will do. Don't worry about the details at this stage, we just need to describe the behavior. A flowchart describing helloBlinky_c2v1 is shown as follows:

Our program will need to change the value of a number stored in memory that determines the LED that is illuminated. Numbers coded in this way are called variables. The name of the variable is chosen by the programmer (usually programmers try to pick meaningful names); in this case, it's referred to by the identifier num. Since there are only eight LEDs, the values we assign to num are 0,1,2,3,4,5,6, and 7. The subroutines LED_On and LED_Off use the variable to determine which LED is switched.

The flowchart illustrates several different types of operation, identified by the geometrical shapes shown in the preceding diagram as follows:

  • Diamond: A decision operation with two outcomes Yes (True) or No (False)
  • Rectangle: A process operation
  • Parallelogram: A data operation
  • Rounded rectangle: Start/End

Within the flowchart, we can identify processes that are executed within a loop, and so are repeated until a certain condition is fulfilled. Structures such as this are a common feature in algorithms, and high-level programming languages have evolved to enable such operations to be efficiently coded.

How to do it…

  1. Clone helloBlinky_c2v0 to create helloBlinky_c2v1.
  2. Modify main() as follows (keep the boilerplate unchanged):
    int main (void) {
      unsigned int i;
      unsigned int num;
      
    HAL_Init ( );                /* Init Hardware Abstraction Layer */
    SystemClock_Config ( );      /* Config Clocks */
    LED_Initialize ( );          /* LED Init */
    
      for (;;) {                       /* Loop forever */
        LED_On (num);                  /* Turn LEDs on */   
        for (i = 0; i < 1000000; i++) 
         /* empty statement */ ;              /* Wait */
         LED_Off (num);           /* Turn LEDs off */
        for (i = 0; i < 1000000; i++)
          /* empty statement */ ;              /* Wait */   
        num = (num+1)%8;   /* increment num (modulo-8) */
      } /* end for */
    }
  3. Once we have entered the code, we build it and download it to the evaluation board in exactly manner as we did for the helloBlinky_c2v0 recipe.
  4. Run the program by pressing RESET on the evaluation board.

How it works…

The program starts with two statements beginning with a # character. These are not program statements but directives for the C preprocessor. The preprocessor resolves all these directives before the C compiler parses the rest of the code. It is considered good practice to group these together at the start of the program. Preprocessor directives can only extend over one line, and they are not terminated by a semicolon. However, to aid readability, longer directives can be split over several lines by using a \ character to terminate each block of text. There are six types of directives:

  • Macro definition: #define and #undef
  • Conditional inclusion: #ifdef, #ifndef, #if, #endif, #else, and #elsif
  • Line control directive: #line
  • Error directive: #error
  • File inclusion: #include
  • Pragma directive: #pragma

We'll briefly explain these directives as they are introduced in the recipes we consider. However, there are plenty of online resources available for those who feel they need more detail (for example, http://gcc.gnu.org/onlinedocs/cpp/). The preprocessor parses the headers:

#include "stm32f4xx_hal.h"
#include "Board_LED.h"
#include "cmsis_os.h"

replacing each #include directive with the contents of the files stm32f4xx_hal.h , Board_LED.h. and cmsis_os.h. By convention, include files adopt .h file extensions, while those not included in other files are given a .c file extension. Later on, we'll meet another style of #include directive:

#include <stdio.h>

In this case, the filename is enclosed in angled brackets. This syntax is used to indicate that the compiler's standard include path is to be searched. When the filename is enclosed in double quotes, the search path includes the current directory. We can add folders in the include path, and select compiler options using the C/C++ tab in the project options window.

The next statement declares a function called main(). Every C program must include one (but only one) function named main(). The structure of the main() function of all the embedded C programs that we'll meet is as follows:

int main (void) {

  ...

}

We identify the input arguments (args) of main() inside the brackets; in this case, there are none, and so we use the reserved word void to indicate none are to be expected. Before main() we see (primitive data type) int, indicating that main() returns an integer. Conventionally, main() returns a value 0 to indicate to the program that called main() (that is, the operating system) that the program terminated successfully. But since our program doesn't run under an operating system and typically declares an infinite loop (called a superloop), there is no need to include a return statement at the end of main() (if we do, the compiler will warn us that it's not reachable). The other feature of main() are the braces, { and }, that are used to identify the beginning and end of the block of statements that comprise main(). Note that the curly bracket (opening brace) immediately following main() is paired with the closing brace that terminates the statements within main(). These braces mark the beginning and end of the main() function; the statements inside the braces belong to main(). We indent these statements to make this clearer. The first two statements in main() are variable declarations. Because C is a strongly-typed language, we must declare all our variables before we use them. In so doing, we're telling the compiler how many bits to use to represent the number so that it can determine the size of the memory space needed to store them.

The values that a computer manipulates are stored in binary. In the binary system, number values are represented by a sequence of digits, just like the decimal system. However, whereas the decimal system uses digits 0,1,2,3,4,5,6,7,8, and 9, the binary system uses only 0 and 1. Digits 0 and 1 in the binary number system are called bits.

The decimal system is a positional number system, where the value of the number is determined by the position of the digits relative to the decimal point. Conventionally, when we write whole numbers, we assume the decimal point is immediately to the right of the least significant digit. Hence, if there are three digits, each represents (from left to right) the number of hundreds (102), tens (101), and units (100), for example:

36510 = (3 x 102) + (6 x 101) + (5 x 100)

Consider a similar 3-bit binary number. Here, each bit represents (from left to right) multiples of 22, 21, and 20, for example:

1012 = (1 x 22) + (0 x 21) + (1 x 20) = 510

In the preceding examples, we are using a subscript to represent the base (or radix) of the number system just to avoid any confusion.

Inside a computer, each bit is represented as an electrical signal; typically a +ve signal voltage represents a '1' and no voltage (0 v) represents '0'. To manipulate a 3-bit binary number, a computer must provide three signal transmission paths, and the registers within the Central Processing Unit (CPU) must be capable of storing 3 bits. You have probably already spotted that three bits isn't going to be of much use, as a 3-bit computer can only manipulate quantities between 010 and 710. Historically, some simple 3-bit computers have been used for elementary control tasks, but many more have been designed to manipulate 8, 16, 32, and 64 bits. The number of bits that a computer has been designed to manipulate is called its word length. As we've seen, the ARM Cortex has been designed with 32-bit registers (that is, a 32-bit word length). A typical ARM Cortex register can be visualized as 32 cells, each able to store 1 bit of data:

Hence, 110010000010001000000000000010012 = 335767136310 = C822000916. We identify hexadecimal (hex) numbers in C programs using the syntax 0xC8220009. In this case, since there are 8 hex digits, we have an 8 x 4 = 32-bit binary word.

The number of bits used to represent a number is determined by its data type. Some of the more common basic (also called primitive) C data types are:

  • char (8-bit)
  • short int (16 bits)
  • unsigned short int (16 bits)
  • int (32 bits)
  • unsigned int (32 bits )
  • long int (64 bits )
  • unsigned long int (64 bits )

A full list of basic types is available at https://en.wikipedia.org/wiki/C_data_types. Data types qualified by the identifier unsigned indicate that the value should be interpreted as representing only positive quantities. Sometimes, embedded developers define aliases for the basic data types, such as int32_t, uint32_t, and so on. We'll explain the purpose of this in Chapter 3, Assembly Language Programming but for the time being, don't be concerned if you see these identifiers used in library functions.

The helloBlinky_c1v1 recipe of Chapter 1, A Practical Introduction to ARM® CORTEX® declares two variables, both 32 bits in length:

const unsigned int num = 0;
unsigned int i;

The first variable declaration is preceded by the qualifier const and assigned a value 0. The const qualifier tells the compiler to treat the variable as a constant, and so, if we attempt to change its value in a subsequent assignment statement, then the compiler will issue an error. When a variable is declared, the compiler just reserves somewhere to store it; this might be in a register (registers are places that data can be stored in the processor) or in memory. Values are assigned to variables by assignment statements; for example,

 p = 0;

places 0 in the memory location or register referenced by the identifier p.

To generate a more interesting LED lightshow, we'll need to write to a different LED each time we execute the superloop. We use the functions LED_On() and LED_Off() to switch the LEDs (as we did in helloBlinky_c1v1), but this time, we increment that value of the variable (num) that controls the LED that we switch each time we iterate the superloop. Since there are 8 LEDs (num = 0 represents the Least Significant LED and num = 7 the Most Significant), we need num to behave as a modulo-8 counter (that is, 7+1 = 0). The statement

num = (num+1)%8;

achieves this. The % operator performs modulo pision. Of course, we don't need the const qualifier in the declaration for num, as its value is changed within main(). Variable i is used by the for loop to implement a delay in exactly the same way as it was in our helloBlinky_c1v1 recipe.

There's more…

High-level languages such as C typically provide mechanisms that allow the programmer to express decisions and iterations within the algorithm by means of IF, FOR, and WHILE structures shown in the following diagram (a). uVision5 provides common templates shown in (b) to help the programmer include these structures in their code.

The helloBlinky_c1v1 folder we developed in Chapter 1, A Practical Introduction to ARM® CORTEX® was quite small and could easily be described by a flowchart (try to sketch it), but as programs become larger, their flowcharts become large and unwieldy. Handling complexity is a common problem in all engineering disciplines and one that is solved by a technique called hierarchical decomposition. This is a long name for something quite simple. It just means we keep on subpiding complex designs into smaller and smaller parts until they become simple enough to handle.