Jonathan Valvano and Ramesh Yerraballi
This chapter covers the C99 Programming language starting with the structure, constants and variable declarations, the main subroutine, simple input/output, arithmetic expressions, Boolean expressions, the assignment statement, the while loop and lastly simple functions with at most one input and one output. Arrays are introduced in this chapter and covered in more detail in Chapter 4.
Table of Contents:
Video 2.0. Introduction to Programming in C
Reference material relative to this chapter:
This course presents the art and science of designing embedded systems. In this module we will introduce C programming. If you need to write a paper, you decide on a theme, and then begin with an outline. In the same manner, if you design an embedded system, you define its specification (what it does) and begin with an organizational plan. In this book, we will present three graphical tools to describe the organization of an embedded system: flowcharts, data flow graphs, and call graphs. You should draw all three for every system you design. In this section, we introduce the flowchart syntax that will be used throughout the class. Programs themselves are written in a linear or one-dimensional fashion. In other words, we type one line of software after another in a sequential fashion. Writing programs this way is a natural process, because the computer itself usually executes the program in a top-to-bottom sequential fashion. This one-dimensional format is fine for simple programs, but conditional branching and function calls may create complex behaviors that are not easily observed in a linear fashion. Flowcharts are one way to describe software in a two-dimensional format, specifically providing convenient mechanisms to visualize conditional branching and function calls. Flowcharts are very useful in the initial design stage of a software system to define complex algorithms. Furthermore, flowcharts can be used in the final documentation stage of a project, once the system is operational, in order to assist in its use or modification.
Where does one begin when learning a new skill? To us, we begin software development with the question, “What is it the program is supposed to do?” Next, we think of how we will test it. Testing is a mechanism to see if the program does exactly what it is supposed to do, no more no less. Next, we list what are the inputs, and what are the outputs. Inside the system we have data. The data defines what do we know, so we define the data and give some examples. The software algorithm connects the inputs to the data, and software must connect the data to the outputs. Lastly, we test it. So you see we begin with testing and end with testing.
We will use flowcharts to illustrate what the software does (Figure 2.1). The oval shapes define entry and exit points. The main entry point is the starting point of the software. Each function, or subroutine, also has an entry point. The exit point returns the flow of control back to the place from which the function was called. When the software runs continuously, as is typically the case in an embedded system, there will be no main exit point. We use rectangles to specify process blocks. In a high-level flowchart, a process block might involve many operations, but in a low-level flowchart, the exact operation is defined in the rectangle. The parallelogram will be used to define an input/output operation. Some flowchart artists use rectangles for both processes and input/output. Since input/output operations are an important part of embedded systems, we will use the parallelogram format, which will make it easier to identify input/output in our flowcharts. The diamond-shaped objects define a branch point or conditional block. Inside the diamond we can define what is being tested. Each arrow out of a condition block must be labeled with the condition causing flow to go in that direction. There must be at least two arrows out of a condition block, but there could be more than two. However, the condition for each arrow must be mutually exclusive (you can’t say “if I’m happy go left and if I’m tall go right” because it is unclear what you want the software to do if I’m happy and tall). Furthermore, the complete set of conditions must define all possibilities (you can’t say “if temperature is less than 20 go right and if the temperature is above 40 go left” because you have not defined what to do if the temperature is between 20 and 40). The rectangle with double lines on the side specifies a call to a predefined function. In this book, functions, subroutines, and procedures are terms that all refer to a well-defined section of code that performs a specific operation. Functions usually return a result parameter, while procedures usually do not. Functions and procedures are terms used when describing a high-level language, while subroutines are often used when describing assembly language. When a function (or subroutine or procedure) is called, the software execution path jumps to the function, the specific operation is performed, and the execution path returns to the point immediately after the function call. Circles are used as connectors. A connector with an arrow pointing out of the circle defines a label or a spot in the algorithm. There should be one label connector for each number. Connectors with an arrow pointing into the circle are jumps or goto commands. When the flow reaches a goto connector, the execution path jumps to the position specified by the corresponding label connector. It is bad style to use a lot of connectors.
Figure 2.1. Flowchart symbols.
There are a seemingly unlimited number of tasks one can perform on a computer, and the key to developing great products is to select the correct ones. Just like hiking through the woods, we need to develop guidelines (like maps and trails) to keep us from getting lost. One of the fundamentals when developing software, regardless whether it is a microcontroller with 1000 lines of assembly code or a large computer system with billions of lines of code, is to maintain a consistent structure. One such framework is called structured programming. C is a structured language, which means we begin with a small number of simple templates, as shown in Figure 2.2. A good high-level language will force the programmer to write structured programs. Structured programs in C are built from three basic templates: the sequence, the conditional, and the while-loop. At the lowest level, the “block” contains simple and well-defined commands, like Area = Height*Width; I/O functions are also low-level building blocks. To program in C, we combine existing structures into more complex structures. Each of the “blocks” in Figure 2.2 is either a simple well-defined command or another structure.
Figure 2.2. Flowchart showing the basic building blocks of structured programming.
Example 2.1: Using a flowchart describe the control algorithm that a toaster might use to cook toast. There will be a start button the user pushes to activate the machine. There is other input that measures toast temperature. The desired temperature is preprogrammed into the machine. The output is a heater, which can be on or off. The toast is automatically lowered into the oven when heat is applied and is ejected when the heat is turned off.
Solution: This example illustrates a common trait of an embedded system, that is, they perform the same set of tasks over and over forever. The program starts at main when power is applied, and the system behaves like a toaster until it is unplugged. Figure 2.3 shows a flowchart for one possible toaster algorithm. The system initially waits for the operator to push the start button. If the switch is not pressed, the system loops back reading and checking the switch over and over. After the start button is pressed, heat is turned on. When the toast temperature reaches the desired value, heat is turned off, and the process is repeated.
Figure 2.3. Flowchart illustrating the process of making toast.
Safety tip: When dealing with the potential for fire, you may want to add some safety features such as a time out or an independent check for temperature overflow.
Observation: Notice in Figure 2.3 we defined a function Cook even though it was called from only one place. You might be tempted to think it would have been better to paste the code for the function into the one place it was called. There are many reasons it would be better to define the function as a separate software object: it will be easier to debug because there is a clear beginning and end of the function, it will make the overall system simpler to understand, and in the future we may wish to reuse this function for another purpose.
Example 2.2. The system has one input and one output. An event should be recognized when the input goes from 0 to 1 and back to 0 again. The output is initially 0, but should go 1 after four events are detected. After this point, the output should remain 1. Design a flowchart to solve this problem.
Solution: This example also illustrates the concept of a subroutine. We break a complex system into smaller components so that the system is easier to understand and easier to test. In particular, once we know how to detect an event, we will encapsulate that process into a subroutine, called Event. In this example, the main program first sets the output to zero, calls the function Event four times, then it sets the output to one. To detect the 0 to 1 to 0 edges in the input, it first waits for 1, and then it waits for 0 again. The letters A through H in the above flowchart specify the software activities in this simple example. In this example, execution is sequential and predictable.
The following tool allows you to see the ordered sequence of steps (labeled A-H) executed as you interact with the Switch to give an input. When the code is executing in the Event handler subroutine you can click on the switch to turn it on and off and see the timeline (at the bottom) reflect this.
C is a general-purpose programming language initially developed by Dennis Ritchie between 1969 and 1973 while at AT&T Bell Labs. At the time, there were programming languages called A and another named B, so Ritchie decided to name his language C. Dennis Ritchie and Brian Kernighan wrote the first book on C, The C Programming Language. Ritchie was also one of the developers of the Unix operating system. As C became more popular, many derivative languages were introduced. C++ was developed by Bjarne Stroustrup 1979-1983 also at Bell Labs. C++ is a language originally called “C plus classes”. In 1999, a professional standard version of C, called C99, was defined. If you download Tivaware (http://www.ti.com/tool/sw-tm4c) from Texas Instruments, you will notice TI’s example code for the TM4C123 has been written in C99.
A compiler is system software that converts a high-level language program (human readable format) into object code (machine readable format). It produces software that is fast, but to change the software we need to edit the source code and recompile.
C code (z = x+y;) → Assembly code (ADD R2,R1,R0) → Machine code (0xEB010200)
An assembler is system software that converts an assembly language program (human readable format) into object code (machine readable format).
Assembly code (ADD R2,R1,R0) → Machine code (0xEB010200)
An interpreter executes directly the high level language. It is interactive but runs slower than compiled code. Many languages can be compiled or interpreted. The original BASIC (Beginner's All-purpose Symbolic Instruction Code) was interpreted. This means the user typed software to the computer, and the interpreter executed the commands as they were typed. In this class, an example of the interpreter will be the command window while running the debugger. For more information on this interpreter, run Keil uVision and execute Help->uVisionHelp. Next, you need to click the Contents tab, open the uVisionIDEusersGuide, and then click DebugCommands. It will show you a list of debugger commands you can type into the command window.
A linker builds software system by connecting (linking) software components. In Keil uVision, the build command (Project->BuildTarget) performs both a compilation and a linking. The example code in this module has three software components that are linked together. These components are
startup.s
uart.c
main.c
A loader will place the object code in memory. In an embedded system, the loader will program object code into flash ROM. In Keil uVision, the download command (Flash->Download) performs a load operation.
A debugger is a set of hardware and software tools we use to verify system is operating correctly. The two important aspects of a good debugger are control and observability.
Before we write software, we need to develop a plan. Software development is an iterative process. Even though we list steps the development process in a 1,2,3,4 order, in reality we cycle through these steps over and over. I like to begin with step 4), deciding how I will test it even before I decide what it does.
1) We begin with a list of the inputs and outputs. This usually defines what the overall system will do. We specify the range of values and their significance.We think about how we will test the system.
2) Next, we make a list of the required data. We must decide how the data is structured, what does it mean, how it is collected, and how it can be changed.
3) Next we develop the software algorithm, which is a sequence of operations we wish to execute. There are many approaches to describing the plan. Experienced programmers can develop the algorithm directly in C language. On the other hand, most of us need an abstractive method to document the desired sequence of actions. Flowcharts and pseudo code are two common descriptive formats. There are no formal rules regarding pseudo code, rather it is a shorthand for describing what to do and when to do it. We can place our pseudo code as documentation into the comment fields of our program. Next we write software to implement the algorithm as define in the flowchart and pseudo code.
4) The last stage is debugging. Learning debugging skills will greatly improve the quality of your software and the efficiency at which you can develop code.
Documentation is important, so we begin with comments. The token // specifies the remainder of the line is a comment. Comments can also be placed between the token /* and the token */. There are two types of comments. The first type explains how to use the software or what the software does. These comments are usually placed at the top of the file, within the header file, or at the start of a function. The reader of these comments will be writing software that uses or calls these routines. The second type of comments explains how the software works, assisting a future programmer in changing, debugging or extending these routines. We usually place these comments within the body of the functions.
There are four sections of a C program as shown in Program 2.1. The first section is the documentation section, which includes the purpose of the software, the authors, the date, and any copyright information. When the software involves external hardware we will add information about how the external hardware is connected. The second section is the preprocessor directives. We will use the preprocessor directive #include to connect this software with other modules. We use diamond braces to include system libraries, like the standard I/O, and we use quotes to link up with other user code within the project. In this case the tm4c123ge6pm.h contains all the I/O port definitions for the TM4C123. We will discuss modular programming in great detail in this class. The third section is global declarations section. This section will include global variables and function prototypes for functions defined in this module. The last section will be the functions themselves. In this class we will use the terms subroutine, procedure, function, and program interchangeably. Every software system in C has exactly one main program, which defines where it begins execution, defined as int main(void). The void means main takes no input parameters, and the int means it would return a signed 32-bit result if it were to finish. However with embedded systems, the main program should never finish because the software is loaded into ROM and the system should continue to run as long as power is supplied to the microcontroller. This means the return parameter for main is never needed.
//****
0.
Documentation Section
// This program demonstrates the sections of a C program
// Author: Ramesh Yerraballi & Jon Valvano
// Date: 7/20/2022
//
// 1. Pre-processor Directives Section
#include <stdint.h> // C99 definitions
#include "tm4c123ge6pm.h" //TM4C123 I/O ports
//
2.
Global Declarations section
// 3. Subroutines Section
// MAIN: Mandatory routine for a C program to be executable
int
main(void)
{
PortF_Init(); // initialize PortF
while(1);
GPIO_PORTF_DATA_R =
GPIO_PORTF_DATA_R^0x08; // toggle PF3
}
}
Program 2.1. Software to toggle PF3.
Video 2.2. Your first C Program (in Keil) from scratch
In assembly language, symbols placed at the beginning of each line have special meaning. On the contrary, C is a free field language. Except for preprocessor lines that begin with #, spaces, tabs and line breaks have the same meaning. This means we can place more than one statement on a single line, or place a single statement across multiple lines. Special characters include punctuation marks (Table 2.1) and operations (Table 2.2).
Punctuation |
Meaning |
; |
End of statement |
: |
Defines a label |
, |
Separates elements of a list |
( ) |
Start and end of a parameter list |
{ } |
Start and stop of a compound statement |
[ ] |
Start and stop of a array index |
" " |
Start and stop of a string |
' ' |
Start and stop of a character constant |
Table 2.1. Special characters can be punctuation marks.
Punctuation marks (semicolons, colons, commas, apostrophes, quotation marks, braces, brackets, and parentheses) are very important in C. It is one of the most frequent sources of errors for both the beginning and experienced programmers. Semicolons are used as statement terminators. Strange and confusing syntax errors are generated when you forget a semicolon, so this is one of the first things to check when trying to remove syntax errors.
Logical operations are performed on all bits independently. Conversely, Boolean operations take true/false inputs and return a true/false output.
Operation |
Meaning |
|
Operation |
Meaning |
= |
Assignment statement |
|
== |
Equal to comparison |
? |
Selection |
|
<= |
Less than or equal to |
< |
Less than |
|
>= |
Greater than or equal to |
> |
Greater than |
|
!= |
Not equal to |
! |
Boolean not (T to F, F to T) |
|
<< |
Shift left |
~ |
1’s complement (flip all bits) |
|
>> |
Shift right |
+ |
Addition |
|
++ |
Increment |
- |
Subtraction |
|
-- |
Decrement |
* |
Multiply or pointer reference |
|
&& |
Boolean and |
/ |
Divide |
|
|| |
Boolean or |
% |
Modulo, division remainder |
|
+= |
Add value to |
| |
Logical or |
|
-= |
Subtract value to |
& |
Logical and, or address of |
|
*= |
Multiply value to |
^ |
Logical exclusive or |
|
/= |
Divide value to |
. |
Used to access parts of a structure |
|
|= |
Or value to |
|
|
|
&= |
And value to |
|
|
|
^= |
Exclusive or value to |
|
|
|
<<= |
Shift value left |
|
|
|
>>= |
Shift value right |
|
|
|
%= |
Modulo divide value to |
|
|
|
-> |
Pointer to a structure |
Table 2.2. Special characters can be operators; operators can be made from 1, 2, or 3 characters.
C has predefined tokens, called keywords, which have specific meaning in C programs, as listed in Table 2.3. It is a good programming practice not to use these keywords for your variable or function names
Keyword |
Meaning |
__asm |
Specify a function is written in assembly code (specific to ARM Keil™ uVision®) |
auto |
Specifies a variable as automatic (created on the stack) |
break |
Causes the program control structure to finish |
case |
One possibility within a switch statement |
char |
Defines a number with a precision of 8 bits |
const |
Defines parameter as constant in ROM, and defines a local parameter as fixed value |
continue |
Causes the program to go to beginning of loop |
default |
Used in switch statement for all other cases |
do |
Used for creating program loops |
double |
Specifies variable as double precision floating point |
else |
Alternative part of a conditional |
extern |
Defined in another module |
float |
Specifies variable as single precision floating point |
for |
Used for creating program loops |
goto |
Causes program to jump to specified location |
if |
Conditional control structure |
int |
Defines a number with a precision that will vary from compiler to compiler |
long |
Defines a number with a precision of 32 bits |
register |
Specifies how to implement a local |
return |
Leave function |
short |
Defines a number with a precision of 16 bits |
signed |
Specifies variable as signed (default) |
sizeof |
Built-in function returns the size of an object |
static |
Stored permanently in memory, accessed locally |
struct |
Used for creating data structures |
switch |
Complex conditional control structure |
typedef |
Used to create new data types |
unsigned |
Always greater than or equal to zero |
void |
Used in parameter list to mean no parameter |
volatile |
Can change implicitly outside the direct action of the software. |
while |
Used for creating program loops |
Table 2.3. Keywords have predefined meanings.
: What does the semicolon mean in C?
: What is the difference between the operators ! and ~ ?
Variables are used to hold information. There are six aspects of variables:
Allocation may be permanent or temporary. Public scope means it can be accessed anywhere. Private scope means there is limited places from which the variable can be accessed. We can limit the scope to software in one file, or limit access to code within the same function. It is good design to limit the scope as much as possible, making it easier to debug. Variables declared outside of a function are called global variables. Global variables are allocated permanently and can be accessed from anywhere in the system. We further classify a global variable that is defined in one file but accessed in another file as an external variable. In general, the use of external variables is considered very poor style, because it makes understanding and debugging difficult. One the other hand, there are two reasons to employ global variables defined and accessed within the same file. The first reason is data permanence. For example, we will use global variables to manage time. The other reason is information sharing. Normally we pass information from one program to another explicitly using input and output parameters, but with interrupts we cannot pass data with parameters. When programming with interrupts, one place in the software can store data into a global while another place in the software can view the shared global In C, we define a variable by specifying the name of the variable and the type. Table 2.4 lists the possible data types.
Data type |
C99 Data type |
Precision |
Range |
unsigned char |
uint8_t |
8-bit unsigned |
0 to +255 |
signed char |
int8_t |
8-bit signed |
-128 to +127 |
unsigned int |
|
compiler-dependent |
|
int |
|
compiler-dependent |
|
unsigned short |
uint16_t |
16-bit unsigned |
0 to +65535 |
short |
int16_t |
16-bit signed |
-32768 to +32767 |
unsigned long |
uint32_t |
unsigned 32-bit |
0 to 4294967295L |
long |
int32_t |
signed 32-bit |
-2147483648L to 2147483647L |
float |
|
32-bit float |
±10-38 to ±10+38 |
double |
|
64-bit float |
±10-308 to ±10+308 |
Table 2.4. Data types in C. C99 includes the C types.
Observation: It is good design to limit the scope as much as possible, making it easier to debug.
On the Keil compiler, there is an option to specify whether char all by itself without a signed or unsigned before it is considered signed or unsigned. Keil considers int as 32 bits. In this class we will avoid int and use long for 32-bit variables so there is no confusion. We will assume char is signed, but it is good practice to see exactly how char and int are treated by your compiler.
Program 2.2 shows the creation of global variables of different types.When accessing 32-bit global variables, the compiler will generate code using LDR and STR. When accessing 16-bit unsigned global variables, the compiler will generate code using LDRH and STRH. When accessing 16-bit signed global variables, the compiler will generate code using LDRSH and STRH. When accessing 8-bit unsigned global variables, the compiler will generate code using LDRB and STRB. When accessing 8-bit signed global variables, the compiler will generate code using LDRSB and STRB.
#include
<stdint.h> // C99 definitions
//
2.
Global Declarations section
uint32_t U32; // 0 to 4,294,967,295
int32_t
S32; // -2,147,483,648 to +2,147,483,647
uint16_t
U16; // 0 to 65,535
int16_t
S16; // -32,768 to +32,676
uint8_t
U8; // 0 to 255
int8_t
S8; // -128 to +127
// 3. Subroutines Section
int
main(void)
{
U32
= 5;
; // initialize U32
while(1);
}
}
Program 2.2. Software that defines some global variables.
The volatile qualifier modifies a variable disabling compiler optimization, forcing the compiler to fetch a new value each time. We will use volatile when defining I/O ports because the value of ports can change outside of software action. We will also use volatile when sharing a global variable between the main program and an interrupt service routine. For example if an interrupt sets Flag and the main program reads Flag, we define it as.
volatile int32_t Flag;
Constants are defined in a manner similar to variables, but with the const modifier. The software is allowed to read the value of a constant, but not allowed to change it during execution. Constants must include a defined value. In an embedded system, constants are stored in ROM. For example
const int32_t Size=1000;
Local variables have temporary allocation and private scope. They contain temporary information that is accessible only within a narrow scope. We can define local variables at the start of a compound statement. We call these local variables since they are known only to the block in which they appear, and to subordinate blocks. In C, local variable must be declared immediately after a brace { that begins a compound statement. Unlike globals, which are permanent, locals are created dynamically when their block is entered, and they cease to exist when control leaves the block. Furthermore, local names supersede the names of globals and other locals declared at higher levels of nesting. Therefore, locals may be used freely without regard to the names of other variables. Although two global variables cannot use the same name, a local variable of one block can use the same name as a local variable in another block. Program 2.3 shows the creation of local variables of different types. When accessing local variables, the compiler will use registers or temporarily allocate them on the stack. Local variables are not initialized to 0 automatically, so they must be initialized explicitly. We will study variables in more detail later in Chapter 7.
#include
<stdint.h> // C99 definitions
//
2.
Global Declarations section
//
3. Subroutines Section
int
main(void)
{
uint32_t
u32; // 0 to 4,294,967,295
int32_t
s32; // -2,147,483,648 to +2,147,483,647
uint16_t
u16; // 0 to 65,535
int16_t
s16; // -32,768 to +32,676
uint8_t
u8; // 0 to 255
int8_t
s8; // -128 to +127
u32
= 5;
; // initialize u32
while(1);
}
}
Program 2.3. Software that defines some local variables.
: Which C99 data type does one use for numbers in the range of 0 to 200?
: Which C99 data type does one use for numbers in the range of -10 to +10?
: Which C99 data type does one use for numbers in the range of -1000 to +1000?
: Which C99 data type does one use for numbers in the range of zero to a million?
: What is the range of values possible with a C data type of int?
Program 2.4 illustrates the assignment operator. Notice that in the line delay=100; the delay is on the left hand side of the = . The left side of the assignment specifies the address into which the data transfer will occur. On the other hand, if the variable is the right side of the =, it will evaluate into the contents of the variable. The right side of an assignment statement will evaluate into a value.
void
PortF_Init(void) { volatile uint32_t delay;
SYSCTL_RCGCGPIO_R = SYSCTL_RCGCGPIO_R | 0x10; // Turn
clock on PortE
delay = 100; // Wait
GPIO_PORTF_DIR_R = 0x0E; // PF4,PF0 input, PF3-1 output
GPIO_PORTF_DEN_R = 0x1F; // PF4-PF0 enabled
GPIO_PORTF_PUR_R = 0x11; // PF4,PF0 pull up resistor
}
Program 2.4. Simple program initializing all five pins on Port F.
Video 2.3. Variables and Assignment operation
In the operation z=x+4*y, multiplication has precedence over addition. Table 2.2 listed the operators available in the C language. As with all programming languages the order of the tokens is important. There are two issues to consider when evaluating complex statements. The precedence of the operator determines which operations are performed first. In expression z=x+4*y, the 4*y is performed first because * has higher precedence than + and =. The addition is performed second because + has higher precedence than =. The assignment = is performed last. Sometimes we use parentheses to clarify the meaning of the expression, even when they are not needed. Therefore, the line z=x+4*y; could have been written as z=(x+4*y); z=x+(4*y); or z=(x+(4*y)); The second issue is the associativity. Associativity determines the left to right or right to left order of evaluation when multiple operations of equal precedence are combined. For example + and - have the same precedence, so how do we evaluate the following?
z = y-2+x;
We know that + and - associate the left to right, this function is the same as z=(y-2)+x;. Meaning the subtraction is performed first because it is more to the left than the addition. Most operations associate left to right, but the Table 2.5 illustrates that some operators associate right to left.
Observation: When confused about precedence (and aren't we all) add parentheses to clarify the expression. Even if you are clear about precedence, programmers who use or change your code may not be. Be a parentheses zealot!
Precedence |
Operators |
Associativity |
Highest |
() []. -> ++(postfix) --(postfix) |
Left to right |
|
++(prefix) --(prefix) ! ~ sizeof(type) +(unary) -(unary) &(address) *(dereference) |
Right to left |
|
* / % |
Left to right |
|
+ - |
Left to right |
|
<< >> |
Left to right |
|
< <= > >= |
Left to right |
|
== != |
Left to right |
|
& |
Left to right |
|
^ |
Left to right |
|
| |
Left to right |
|
&& |
Left to right |
|
|| |
Left to right |
|
? : |
Right to left |
|
= += -= *= /= %= <<= >>= |= &= ^= |
Right to left |
Lowest |
, |
Left to right |
Table 2.5. Precedence and associativity determine the order of operation.
: The expression x&1&&y+1<z*4 is poor style because it is unclear. Add parentheses to clarify this expression.
: The expression x==1||y<z is poor style because it is unclear. Add parentheses to clarify this expression.
: The expression a << b << c is poor style because it is unclear. Add parentheses to clarify this expression.
If-Then Statement - The
statements inside an if statement will execute once if the condition is
true. If the condition is false, the program will skip those
instructions. Choose two unsigned integers as variables a and b, press
run and follow the flow of the program and examine the output screen.
You can repeat the process with different variables.
variable a: variable b:
C Code | |
---|---|
int32_t a; int32_t b; int main () { printf("Starting the Construct ...\n"); if (a<b){ printf("a is less than b\n"); } printf("Ending the Construct ...\n"); return 0; } |
|
Output Screen | |
|
In Program 2.5 we will introduce a simple conditional control structure. Assume the global variable error is initialized to zero. The goal is to make sure the function is being used properly. An effective software design approach is to test the input parameters of a function to make sure the values make sense. An unsigned long can represent a number up to 4 billion. Clearly the system is not operating properly if we are trying to calculate the size of a room with 4 billion meter sides. In this case, we define the largest possible room to have a side of 25 meters. The expression s<=25 will return true if the side is less than or equal to 25 and will return a false if the side is strictly greater than 25. The statement immediately following the if will be executed if the condition is true. The statement immediately following the else will be executed if the condition is false.
uint32_t
error;
//
Calculates
area
//
Input:
side of a room (unsigned long)
//
Output:
area of the room (unsigned long)
//
Notes:
...
uint32_t
Calc_Area(unsigned long s) {
uint32_t
result;
if(s
<= 25){
result
= s*s;
}else{
result
= 0; // mistake
error
= error +1;
}
return(result);
}
Program 2.5. Simple program illustrating the C if else control structure.
If-then-else - If
statements can be expanded by an "else" statement. If the condition is
false, the program will execute the statements under the "else"
statement. Choose two unsigned integers as variable a and b, press run
and follow the flow of the program and examine the output screen. You
can repeat the process with different variables.
variable a: variable b:
C Code | |
---|---|
uint32_t a; uint32_t b; int main () { printf("Starting the Construct ...\n"); if (a<b){ printf("a is less than b\n"); } else { printf("a is not less than b\n"); } printf("Ending the Construct ...\n"); return 0; } |
|
Output Screen | |
|
Quite often the microcomputer is asked to wait for events or to search for objects. Both of these operations are solved using the while or do-while structure. A simple example of while loop is illustrated in the Figure 2.4 and presented in Program 2.6. Assume Port A bit 3 is an input. The function Body() will be executed over and over as long as Port A bit 3 is high. .
Figure 2.4. Flowchart of a while structure. Execute Body() over and over bit 3 of G1 is high.
Program 2.6 begins with a test of Port A bit 3. If bit 3 is low then the body of the while loop is skipped. The unconditional branch (B loop) after the body causes Port A bit 3 to be tested each time through the loop. In this way, the body is executed repeatedly until Port A bit 3 is low.
LDR R4, =GPIO_PORTA_DATA_R loop LDR R0, [R4] ; R0 = Port A AND R0, #0x08 ; test bit 3 BEQ next ; if so, quit BL Body ; body of the loop B loop next |
while(GPIO_PORTA_DATA_R&0x08){ Body(); } |
Program 2.6. A while loop structure.
The while loop - The
statements inside a while statement, will continuously be executed if
the while condition is true. If the condition is/becomes false, the
program will skip the loop and continue with the execution of the
remaining statements. Choose an unsigned integer less than 100000 as
variable a, press run and follow the flow of the program and examine the
output screen. The loop continuously divides the variable a by 10 and
outputs the result. You can repeat the process with different variables.
variable a:
C Code | |
---|---|
uint32_t a; // a is always less than 100000 int main () { printf("Starting the loop ..., a = %d\n",a); while (a>0){ a = (a/10); printf("current a = %d\n",a); } printf("Ending the loop ...\n"); return 0; } |
|
Output Screen | |
|
The goal in Program 2.6 is to count (touchCount) the number of times the switch connected to PF4 is touched and released. Like the if statement, the while statement has a conditional test (i.e., returns a true/false). The statement immediately following the while will be executed over and over until the conditional test becomes false. Using a while loop is a simple way to wait for an event in an embedded system.
int
main(void) {int32_t touchCount,count;
PortF_Init();
touchCount
= count = 0;
while(1){
while((GPIO_PORTF_DATA_R&0x10)==0){
count--; // decrement while PF4 is
low
}
while((GPIO_PORTF_DATA_R&0x10)==0x10){
count++; // increment while
PF4 is high
}
touchCount++;
}
}
Program 2.6. Simple program illustrating the C while control structure.
: The pin PF4 will be high if the switch on the LaunchPad is not pressed. Assume the operator never pushes the switch (PF4 is always high). What will be the behavior of Program 2.6?
A do-while loop performs the body first, and the test for completion at the end. It will execute the body at least once. Assume PF1 is an output and PA5 is an input. Program 2.7 will toggle the output PF1 as long as input PA5 is low.
LDR R1, =GPIO_PORTF_DATA_R LDR R5,
=GPIO_PORTA_DATA_R loop LDR R0, [R1] EOR R0, #2 ; toggle bit 1 STR R0, [R1] LDR R2,
[R5] ANDS R2, #0x20 ; bit 5 set? BEQ loop ; spin while low next |
// toggle PF1 while PA5 low do{ GPIO_PORTF_DATA_R=GPIO_PORTF_DATA_R^0x02; }
|
Program 2.7. A do-while loop structure.
: The pin PA5 will be high if the switch is not pressed. Assume the operator never pushes the switch (PA5 is always high). What will be the behavior of Program 2.7?
The for control structure has three parts and a body.
for(part1;part2;part3){body;}
The assembly version in Program 2.8 places the loop counter in the Register R4. According to AAPCS, we assume the subroutine Process preserves the value in R4.
MOV R4, #0 ; R4 = 0 loop CMP R4, #10 ; index >= 10? BHS done ; if so, skip to done BL Process ; process function ADD R4, R4, #1 ; R4 = R4 + 1 B loop done |
for(i=0; i<10; i++){ Process(); } |
Program 2.8. A simple for-loop.
If we assume the body will execute at least once, we can execute a little faster, as shown in Program 2.9, by counting down. Counting down is one instruction faster than counting up.
MOV R4, #10 ; R4 = 10 loop BL Process ; body SUBS R4, R4, #1 ; R4 = R4-1 BNE loop done |
MOV R4, #0 ; R4 = 0 loop BL Process ; body ADD R4, R4, #1 ; R4 = R4+1 CMP R4, #10 ; done? BLO loop ; if not,repeat |
Program 2.9. Optimized for-loops.
The for loop - A for
loop functionality is similar to a while loop, with automatic
initialization and update. It is usually used when the number of
iterations is known. One variable is used as a counter of iterations.
The first field of a "for loop" is the initialization, which is always
executed once. The second field is the condition to be checked. The loop
will continue being executed until the condition becomes false. The
third field (update) is executed at the end of each iteration and is
usually used for incrementing/decrementing the counter. Choose a number
less than 6 for variable a, and observe the flow of the following
program. You can repeat the process with different variables.
variable a:
C Code | |
---|---|
int32_t a; // a must be less than 6 in this example. int main () { int32_t i; printf("Starting the loop ...\n"); for (i=0;i<a;i++){ printf("current i = %d\n",i); } printf("Ending the loop ...\n"); return 0; } |
|
Output Screen | |
|
In Program 2.10, the first part side=1 is executed once at the beginning. Before the body is executed, the end-condition part 2 is executed. If the condition is true, side<50 then the body is executed. After the body is executed, the third part is executed, side=side+1. The second part is always a conditional that results in a true or a false. The body and third part are repeated until the conditional is false.
int
main(void) {
uint32_t side; // room wall meters
uint32_t area; // size squared meters
UART_Init(); // call subroutine to
initialize the uart
printf("This program calculates areas of
square-shaped rooms\n");
for(side = 1; side < 50; side =
side+1){
area = Calc_Area(side);
printf("\nArea of the room
with side of %ld m is %ld sqr m\n",side,area);
}
}
Program 2.10. Simple program illustrating the C for-loop control structure.
: How many times will the function Calc_Area be executed in Program 2.10?
Video 2.5. if-then conditional and while loop
: What happens when the following code is executed? if(n1>100) n2=100; n3=0;
A function is a sequence of operations that can be invoked from other places within the software. We can pass zero or more parameters into a function. A function can have zero or one output parameter. It is important for the C programmer to distinguish the two terms declaration and definition. A function declaration specifies its name, its input parameters and its output parameter. Another name for a function declaration is prototype. A data structure declaration specifies its type and format. On the other hand, a function definition specifies the exact sequence of operations to execute when it is called. A function definition will generate object code, which are machine instructions to be loaded into memory that perform the intended operations. A data structure definition will reserve space in memory for it. The confusing part is that the definition will repeat the declaration specifications. The C compiler performs just one pass through the code, and we must declare data/functions before we can access/invoke them. To run, of course, all data and functions must be defined. A function to calculate the area of a square room is shown in Program 2.11. We can see that the declaration shows us how to use the function, not how the function works. Because the C compilation is a one-pass process, an object must be declared or defined before it can be used in a statement. Actually the preprocessor performs the first pass through the program that handles the preprocessor directives. A top-down approach is to first declare a function, use the function, and lastly define the function as illustrated in Program 2.11.
uint32_t
Calc_Area(uint32_t s);
int main(void) {
uint32_t
side; // room wall meters
uint32_t
area; // size squared meters
UART_Init(); // call subroutine to
initialize the uart
printf("This program calculates areas of
square-shaped rooms\n");
side = 3;
area = Calc_Area(side);
printf("\nArea of the room with side of
%ld m is %ld sqr m\n",side,area);
side = side+2;
area = Calc_Area(side);
printf("\nArea of the room with side of
%ld m is %ld sqr m\n",side,area);
}
// Calculates area
// Input: side of a room (32 bit
unsigned) in meters
// Output: area of the room (unsigned long) in
square meters
uint32_t
Calc_Area(uint32_t
s) {
uint32_t
result;
result = s*s;
return(result);
}
Program 2.11. A main program that calls a function. In this case the declaration occurs first.
A bottom-down approach is to first define a function, and then use the function as illustrated in Program 2.12. In the bottom up approach, the definition both declares its structure and defines what it does.
//
Calculates area
// Input: side of a room (unsigned long)
in meters
// Output: area of the room (unsigned long) in
square meters
unsigned long Calc_Area(unsigned long s) {
unsigned long result;
result = s*s;
return(result);
}
int main(void) {
uint32_t
side; // room wall meters
uint32_t
area; // size squared meters
UART_Init(); // call subroutine to
initialize the uart
printf("This program calculates areas of
square-shaped rooms\n");
side = 3;
area = Calc_Area(side);
printf("\nArea of the room with side of
%ld m is %ld sqr m\n",side,area);
side = side+2;
area = Calc_Area(side);
printf("\nArea of the room with side of
%ld m is %ld sqr m\n",side,area);
}
Program 2.12. A main program that calls a function. In this case the definition occurs before its use.
The sum
function (aka subroutine) in Program 2.13 has two 32-bit signed input
parameters, and one 32-bit signed output parameter. The interesting part
is that (in assembly) after the operations within the subroutine are
performed, control returns to the place right after where the subroutine
was called. It is the same in C. You will also see how the registers are
manipulated as the code flows.
Assembly Code | Registers | ||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Address Machine Code Label Instruction Comment 0x00000660 EB010200 sum ADD R2,R1,R0 ;z=x+y 0x00000664 4610 MOV R0,R2 ;return value 0x00000666 4770 BX LR 0x00000668 F44F60FA main MOV R0,#2000 ;first parameter 0x0000066C F44F61FA MOV R1,#2000 ;second parameter 0x00000670 F7FFFFF6 BL sum ;call function 0x00000674 4603 MOV R3,R0 ;a=sum(2000,2000) 0x00000676 F04F0400 MOV R4,#0x00 ;b=0 0x0000067A 4620 loop MOV R0,R4 ;first parameter 0x0000067C F04F0101 MOV R1,#0x01 ;second parameter 0x00000680 F7FFFFEE BL sum ;call function 0x00000684 4604 MOV R4,R0 ;b=sum(b,1) 0x00000686 E7F8 B loop |
|
||||||||||||||||||||||||||||||||||
C Code | |||||||||||||||||||||||||||||||||||
int32_t sum(int32_t x, int32_t y){
int32_t z; z = x+y; return(z); } void main(void){ int32_t a,b; a = sum(2000,2000); b = 0; while(1){ b = sum(b,1); } } |
Program 2.13. A function with two inputs and one output.
To specify the absence of a parameter we use the expression void. The body of a function consists of a statement that performs the work. Normally the body is a compound statement between a {} pair. If the function has a return parameter, then all exit points must specify what to return.
: What does it mean to say a function as one input parameter?
: What does it mean to say a function as one output parameter?
Video 2.5. Introduction to Arrays
An array is made of elements of equal precision. The precision is the size of each element. Typically, precision is expressed in bits or bytes. The length is the number of elements. The origin is the index of the first element. A data structure with the first element existing at index zero is called zero-origin indexing. In C, zero-origin index is the default. For example, Data[0] is the first element of the array Data.
Random access means one can read and write
any element in any order. Random access is allowed for all indexable
data structures. An indexed data structure has elements of the same size
and can be accessed knowing the name of the structure, the size of each
element, and the element number. In C, we use the syntax []
to access an indexed structure. Arrays, matrices, and tables are
examples of indexed structures available in C.
Sequential access means one reads and writes the elements in order. Pointers are usually employed in these types of data structures. Strings, linked-lists, stacks, queues, and trees are examples of sequential structures. The first in first out circular queue (FIFO) is useful for data flow problems.
Just like any variable, arrays must be declared before they can be accessed, see Figure 2.4. The number of elements in an array is determined by its declaration. Appending a constant expression in square brackets to a name in a declaration identifies the name as the name of an array with the number of elements indicated. Multi-dimensional arrays require multiple sets of brackets. These examples illustrate valid declarations of arrays. Because there is no const modifier, the arrays will be defined in RAM. The C compiler will add code that runs before your main, which will initialize all RAM-based variables to zero.
int16_t Data[5]; // allocate space for 5 16-bit integers
int32_t scores[20]; // allocate space for 20 32-bit integers
int Width[6]; // 6 signed, precision depends on compiler
int8_t Image[5][10]; // allocate space for 50 8-bit integers
int16_t Points[5][5][5]; // allocate space for 125 16-bit ints
If you would like the compiler to initialize to something other than zero, you can explicitly define the initial values. Again, this initialization will occur before your main is executed. Even though they have nonzero initial values, these arrays are in RAM, and thus can be modified at run time.
int16_t Data[5]={1,-1,2,-2,6};
int32_t scores[20]={-2,-1,0,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2}
int Width[6]={-10,-20,10,100,2000,40};
int16_t Image[5][10]={
{0,0,1,0,1,1,1,0,1,0},
{0,0,1,0,0,1,0,0,1,0},
{0,0,1,0,0,1,0,0,1,0},
{0,0,1,0,0,0,0,0,1,0},
{0,0,0,1,1,1,1,1,0,0}};
Figure 2.4. The Data array has 5 16-bit elements, and the scores array has 20 32-bit elements.
If the array contains constants, and we know the values at compile time, we can place the data in ROM using the const modifier. For ROM-based data, we must define the value explicitly in the software. These arrays are in ROM, and thus cannot be modified at run time.
const int16_t Data2[5]={1,2,3,4,5};
const int32_t scores2[20]={-2,-1,0,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2}
const int Width2[6]={-10,-20,10,100,2000,40};
const int16_t Image2[5][10]={
{0,0,1,0,1,1,1,0,1,0},
{0,0,1,0,0,1,0,0,1,0},
{0,0,1,0,0,1,0,0,1,0},
{0,0,1,0,0,0,0,0,1,0},
{0,0,0,1,1,1,1,1,0,0}};
In a formal way, const actually means “constant”. More specifically it means the executing software cannot change the value at run time. This means in C programs not implemented on an embedded system, const data could be stored in RAM, initialized at startup. In this case, the const modifier simply means the running program cannot change the values. However, in the class we are writing C code that will be deployed into a microcontroller as part of an embedded system, so const data will be stored in ROM.
When an array element is referenced, the subscript expression designates the desired element by its position in the data. The first element occupies position zero, the second position one, and so on. It follows that the last element is subscripted by [N-1] where N is the number of elements in the array. The statement
scores[9] = 0;
for instance, sets the tenth element of the array to zero. The array subscript can be any expression that results in an integer. The following for-loop clears 20 elements of the array data to zero.
for(j=0;j<20;j++) scores[j] = 0;
If the array has two dimensions, then two subscripts are specified when referencing. As programmers we may any assign logical meaning to the first and second subscripts. For example we could consider the first subscript as the row and the second as the column. Then, the statement
TheValue = Image[3][5];
copies the information from the 4th row 6th column into the variable TheValue. If the array has three dimensions, then three subscripts are specified when referencing. Again we may assign any logical meaning to the various subscripts. For example we could consider the first subscript as the x coordinate, the second subscript as the y coordinate and the third subscript as the z coordinate. Then, the statement
Points[2][3][4]=100;
sets the values at position (2,3,4) to 100. Array subscripts are treated as signed integers. It is the programmer's responsibility to see that only positive values are produced, since a negative subscript would refer to some point in memory preceding the array. One must be particularly careful about assuming what existing either in front of or behind our arrays in memory.
Example 2.3. Assume there are 50 students in the class, and their grades on the first exam are stored in an array. Write a function that calculates and returns class average. Write a second function that finds and returns the highest grade on the exam.
Solution: Assuming the grades vary from 0 to 100, the data could be stored in char, short or long format. To be compatible with the ARM architecture, we will create a 32-bit array. In this example, we will not worry about how grades are entered, but assume there is data in this array.
long Grades[50];
To calculate average, we sum the values and divide by the number of students. The return statement will return the 32-bit calculation of sum/50. This will never create an error, because the sum of 50 numbers ranging from 0 to 100 will always be less than 4 trillion (232) and the average of these numbers will never go below 0 or above 100.
long Average(void){long sum,i;
sum = 0;
for(i=0; i<50; i++){
sum = sum+Grades[i];
}
return (sum/50);
}
Program 2.14. A function that calculates the average of data stored in an array.
To make this more general, we will pass in the array and the size.
long Average(long class[],long size){
long sum,i;
sum = 0;
for(i=0; i<size; i++){
sum = sum+class[i]; // add up all values
}
return (sum/size);
}
The array parameter is actually a pointer to the array. So to use this new function we call
MyClassAverage = Average(Grades, 50);
Similarly to find the largest we will search the array. We initialized the result parameter (largest) to the smallest possible answer, and then every time we find one larger, we replace the result parameter.
long Max(long class[],long size){
long largest,i;
largest = 0; // smallest possible value
for(i=0; i<size; i++){
if(class[i] > largest){
largest = class[i]; // new maximum
}
}
return (largest);
}
To use this new function we call
HighestsScore = Max(Grades, 50);
Example 2.4. Design an exponential function, y = 10x, with a 32-bit output.
Solution: Since the output is less than 4,294,967,295, the input must be between 0 and 9. One simple solution is to employ a constant word array, as shown in Figure 2.5. Each element is 32 bits. In assembly, we define a word constant using DCD, making sure in exists in ROM.
In C, the syntax for accessing all array types is independent of precision. See Program 2.15. The compiler automatically performs the correct address correction. We will assume the input is less than or equal to 9. If x is the index and Base is the base address of the array, then the address of the element at x is Base+4*x. In assembly, we can access the array using indexed addressing. We will assume the input (which is in Register R0) is less than or equal to 9. We show the asembly equivalent of the C code below (and examples that follow) just to pique your curiosity, you may ignore it if you choose.
0x00000134 |
1 |
0x00000138 |
10 |
0x0000013C |
100 |
0x00000140 |
1,000 |
0x00000144 |
10,000 |
0x00000148 |
100,000 |
0x0000014C |
1,000,000 |
0x00000150 |
10,000,000 |
0x00000154 |
100,000,000 |
0x00000158 |
1,000,000,000 |
Figure 2.5. A word array with 10 elements. Addresses illustrate the array is stored in ROM as 4 bytes each.
;assembly
version
AREA |.text|,CODE,READONLY,ALIGN=2
Powers DCD 1, 10, 100, 1000, 10000
DCD 100000, 1000000, 10000000
DCD 100000000, 1000000000
; Input: R0=x Output: R0=10^x
power LSL R0, R0, #2 ; x = x*4
LDR R1, =Powers ; R1 = &Powers
LDR R0, [R0, R1] ; y=Powers[x]
BX
LR
//
C version
const uint32_t Powers[10]
={1,10,100,1000,10000,
100000,1000000,10000000,
100000000,1000000000};
uint32_t power(uint32_t x){
return Powers[x];
}
Program 2.15. Array implementation of a nonlinear function.
In general, let n be the precision of a zero-origin indexed array in bytes. If I is the index and Base is the beginning address of the array, then the address of the element at I is
Base+n*I
The origin of an array is the index of the first element. The origin of a zero-origin indexed array is zero. In general, if o is the origin of the array, then the address of the element at I is
Base+n*(I-o)
A string is simply an array of ASCII characters. In C, strings are automatically null-terminated by the compiler:
const char Hello[] = "Hello world\n\r";
const char Name[] = "Yerraballi";
When defining constant strings or arrays we must specify their value because the data will be loaded into ROM and cannot be changed at run time. In the previous constant arrays we specified the size; however for constant arrays the size can be left off and the compiler will calculate the size automatically. Notice also the string can contain special characters, some of which are listed in Table 2.6. The Hello string has 13 characters followed by a null (0) termination.
Character |
Escape Sequence |
alert (beep) |
\a |
backslash |
\\ |
backspace |
\b |
carriage return |
\r |
double quote |
\" |
form feed |
\f |
horizontal tab |
\t |
newline |
\n |
null character |
\0 |
single quote |
\' |
vertical tab |
\v |
question mark |
\? |
Table 2.6. Escape sequences.
Note, that in C, ASCII strings are stored with null-termination. So, the C compiler automatically adds the zero at the end, but in assembly, the zero must be explicitly defined.
Reprinted with approval from Introduction to Embedded Systems, 2022, ISBN: 978-1537105727