Chapter 3: Introduction to C and debugging

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 used to create an effective debugging tool.


Table of Contents:

Return to book table of contents

 

                  

Video 3.0.0 Introduction to Programming in C


 Reference material relative to this chapter:


 

3.1. Organization of C

                  

Video 3.1.1. What is C?

Code Composer Studio (CCS) is an integrated development environment, which includes these tools.


The following video shows the editor, compiler, assembler, linker, loader and debugger

                  

Video 3.1.2. Code Composer Studio IDE showing the editor, compiler and loader

There are four sections of a C program described in the next video. There is always one function called main, in which the system will begin execution on power up or reset.

  1. Documentation defines purpose, authors, date, and copyright
  2. Preprocessor directives are #include and #define
  3. Declarations creates global variables and function declarations
  4. Functions are actual programs.

                  

Video 3.1.3. Documentation, Preprocessor, Declarations, and Functions

/* 1) Documentation
 * Input from Switch PB0, output to LED on PB1
 * Jonathan Valvano
 * September 9, 2023
 * Derived from gpio_toggle_output_LP_MSPM0G3507_nortos_ticlang
 * This is a simple C language project that creates a not gate
 * If SW is pressed, the LED is off.
 * If SW is not pressed, the LED is on. */
// 2) Preprocessor directives
#include <ti/devices/msp/msp.h>
#include "../inc/Clock.h"
#define PB0INDEX 11
#define PB1INDEX 12
// 3) Declarations
uint32_t Input,Output; // Globals
void Init(void); // Prototype
// 4) functions
int main(void){
  Init();
  while(1){
    Input = GPIOB->DIN31_0 & 0x01; // read input
    Output = (Input^0x01)<<1; // not gate, shift into bit 1
    GPIOB->DOUT31_0 = Output;
  }
}
void Init(void){
// See starter projects for details of how init works
}

Program 3.1. Software to implement a NOT gate.


: In CCS, what happens when I execute the build command?

: In CCS, what happens when I execute the debug command?

: Why did Dennis Richie call his language C?

: In CCS, how does a project help us manage software?



3.2. Variables and Constants

Variables are used to hold information. There are six aspects of variables:

                  

Video 3.2.1. Global variable

                  

Video 3.2.2. Local variable

uint32_t Time;   // global, automatically initialized to 0
int main(void){
uint32_t delta; // local
  delta = 100;  // manually initialized to 100
  while(1){
    Time = Time+delta;
  }
}

Program 3.2. Demonstration of global and local variables.

Allocation may be permanent or temporary. Permanent variables will go in RAM and exist forever. Temporary variables will go on the stack or in registers and exist for a finite amount of time.

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.

We declare global variables outside of a function. Global variables are allocated permanently and have public scope. One reason to use globals is data permanence. For example, we will use global variables to manage time. The other reason is information sharing. When programming with interrupts, one part of a function in the software can store data into a global while in an other function in the software the shared global can be viewed.

We declare local variables inside of a function. Local variables are allocated temporarily and have private scope. We use local variable to make information private and to conserve memory.

In C, we define a variable by specifying the name of the variable and the type. Table 3.2.1 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 3.2.1. 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.

: 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?

: What is the scope of global variable?

: What is the scope of local variable?

The variable type char should be used only for ASCII characters. If we were to use char for numbers, there will be an ambiguity as whether it will be compiled as signed or unsigned. On the other hand, signed char unambiguously represents -128 to +127, and unsigned char unambiguously represents 0 to +255. Using char without qualifying it with signed or unsigned is not specified in the C language, but in a configuration within the compiler. Figure 3.2.1 shows the compiler options dialog in CCS.

Figure 3.2.1. Whether char is considered as signed or unsigned is specified within the compiler settings, and not prescribed by the C language itself.

Maintanence Tip: It is good design to use char type only for ASCII characters, and not integers.

The precision of the type int is not specified in the C language. The CCS compiler for the MSPM0 implements int as 32-bits. The C language standard allows int to be implementation-specific, meaning the compiler can choose the precision which runs most efficiently on that specific processor. Consequently, compilers may choose to implement int as 8, 16, 32, or 64 bits, whichever runs fastest. To make our code more portable (meaning it will run on other microcontrollers and built with other compilers), we should use long or int32_t to create 32-bit signed integers.

Maintanence Tip: We can use int for situations where the precision does not matter. For example, we could use int to hold true as 1, and false as 0.

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.

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, because ROM is typically larger than RAM and is nonvolatile. In a system that is not embedded, like your phone or laptop, constants are stored in RAM, initialized on startup to the specified value. In all cases, software may not change the value of a constant during execution. For example

const int32_t Size=1000;

 

Quiz 3.2

 

3.3. Expressions and statements

In the last section we created variables to hold data. In this section, we will add expressions and statements that manipulate the data. An assignment statement has an equal sign sign. On the right of the equal sign will be an expression that results in a value, and on the left of the equal sign will be a variable into which the value is stored. All statements end in a semicolon. For example, this statement assigns the variable delta to the value 100.


    delta = 100;


An expression is comprised of values and operations. An operation takes one two or three values and produces a result value. If a variable is part of an expression, when evaluated the variable is replaced with its current value. There are many types operators:

The next video illustrates the operators and assignment expressions using Program 3.3.1 as an example.

                  

Video 3.3.1. Assignment statement

int main(void){
  Init();
  while(1){
    Input = GPIOB->DIN31_0 & 0x01; // read input
    Output = (Input^0x01)<<1; // not gate, shift into bit 1
    GPIOB->DOUT31_0 = Output;

  }
}

Program 3.3.1. Demonstration of assignment statement.

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 3.3.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.

Arithmetic operations are performed using integer arithmetic. Logical operations are performed on all bits independently. Conversely, Boolean operations take true/false inputs and return a true/false output.

Observation: Arithmetic operations must be performed on data of the same type. I.e., both values must be signed or both values must be unsigned. However, if the values have a different number of bits, the smaller value is promoted to match the value with more bits.

 

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 3.3.2. Special characters can be operators; operators can be made from 1, 2, or 3 characters.



: What does the semicolon mean in C?

: What is the difference between the operators ! and ~ ?

: What is the difference between the operators & and && ?

: Assuming x is a variable, what does this expression do? x++;

: Assume n is a signed variable. Assume m is an unsigned variable. What does this expression do? n+m

: Assume y is a signed 8-bit variable. Assume z is a signed 32-bit variable. What does this expression do? y+z


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 the operation z=x+4*y, multiplication has precedence over addition, so the 4*y is performed first because multiplication has higher precedence than addition. 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);

just to make it more clear to everyone. 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 3.3.3 illustrates that some operators associate right to left.

Observation: When confused about precedence and associativity (and aren't we all) add parentheses to clarify the expression. Even if you are clear about precedence and associativity, 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 3.3.3. 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.

Assume x y z r are variables of the same type. Write C code to set r to be the average of x y z.

Assume F C are signed integer variables of the same type. The following conversion from Centigrade to Fahrenheit doesn't seem to work F=C*(9/5)+32; What is the bug? Rewrite the assignment expression to be more accurate.

 

Quiz 3.3

 

3.4. Conditional Branching


Interactive Tool 3.4.1

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 Start Output Start Output End True Output Taken a<b False End
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


Program 3.4.1 introduces an if-then-else conditional control statement. Assume the global variable Score is initialized the numerical value obtained in the course. The goal is to assign a ASCII letter grade based on a cutoff value of 70. The expression Score>=CUTOFF will return true if the Score is greater than or equal to 70 and will return a false if the Score is strictly less than 70. 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.

#define CUTOFF 70
uint32_t Score = 95;
char Grade;
int main(void){
  if(Score >= CUTOFF){
    Grade = 'P'; // true
  }else{
    Grade = 'F'; // false
  }
  while(1){
  }
}

Program 3.4.1. Demonstration of an if-then-else statement.

                  

Video 3.4.1. If-then-else statement


Interactive Tool 3.4.2

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 True False End Output Not Taken Start Output Start Output End Output Taken a<b
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

What happens when the following code is executed?   if(n1>100) n2=100; n3=0;

Assume x y z are unsigned variables. You may assume x y bounded within 0 to 100. Write C code that makes z equal to x plus y. However, add code to guarantee z is also bounded within 0 to 100. For example, 90+20 should result in 100.

Assume n is an unsigned variable, and we wish to keep it bounded within 0 to 15. Write C code that adds one to n. However, if n is 15, we want adding one to wrap back to 0. Write two versions: one version with if-then-else and another without if-then-else.

Assume x y are unsigned variables, and we wish to set y to 1 if x is even, and set y to 0 if x is odd. Write two versions: one version with if-then-else and another without if-then-else.

Assume u1 u2 u3 result are unsigned variables. Write C code to set result to be the median of u1 u2 u3. The median of three numbers is the middle value, not the largest, not the smallest, but the middle one.

 

Quiz 3.4

 

3.5. Loops

                  

Video 3.5.1. While loop statement

int main(void){
uint32_t x = 100; // input
uint32_t y; // result = sqrt(x)
  y = 0; // initial guess
  while(y*y < x){ // repeat until y*y>= x
    y++; // get closer
  }
  while(1){
  }
}

Program 3.5.1. While loop to implement sqrt.

Observation: If you want a faster method search "Newton's method for square root"

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 3.5.1 and presented in Program 3.5.2. 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 3.5.1. Flowchart of a while structure. Execute Body() over and over if bit 3 of Port A is high.

Program 3.5.2 begins with a test of Port A bit 3. If the condition in the while statement is true, the statements between the braces are executed. If the condition in the while statement is false then the while loop is escaped. The unconditional branch (B loop) after the body cause the condition to be evaluated again. In this way, the body is executed repeatedly until Port A bit 3 is low.

      LDR  R4, =GPIOA_DIN31_0

      MOVS R5,#8

loop: LDR  R0,[R4]  // R0 = Port A

      ANDS R0,R0,R5 // test bit 3

      BEQ  next     // if so, quit

      BL   Body     // body of the loop

      B    loop   

next

 

while(GPIOA->DIN31_0&0x08){

  Body();

}

Program 3.5.2. A while loop structure.

Interactive Tool 3.5.1

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 True False a>0 End a = a / 10; Start Output Start Output End Output a
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 MSPM0 LaunchPad has a positive logic switch connected to PA18. The function LaunchPad_Init will initialize this input pin (see details in Chapter 2). The goal in Program 3.5.3 is to count (touchCount) the number of times the switch 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;
  LaunchPad_Init();
 
touchCount = count = 0;
  while(1){

    while((GPIOA->DIN31_0&(1<<18))==0){
      count--; // decrement while PA18 is low
    }

   
while((GPIOA->DIN31_0&(1<<18))==(1<<18)){
      count++; // increment while PA18 is high
    }
   
touchCount++;
  }
}

Program 3.5.3. Simple program illustrating the C while control structure.

:  The pin PA18 will be low if the switch on the LaunchPad is not pressed. Assume the operator never pushes the switch (PA18 is always low). What will be the behavior of Program 3.5.1?

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 PB1 is an output and PA5 is an input, with negative logic switch. Program 3.5.4 will toggle the output PB1 as long as input PA5 is low (switch is pressed).

      LDR  R4,=GPIOA_DIN31_0 

      LDR  R5,=GPIOB_DOUT31_0 

      MOVS R6,#2 

      MOVS R7,#0x20 

loop: LDR  R0,[R5]  // read all of PortB

      EORS R0,R6    // toggle bit1

      STR  R0,[R5]  // toggle PB1

      LDR  R2,[R4]  // Input Port A

      ANDS R2,R2,R7 // bit 5 set?

      BEQ  loop     // spin while low  

next:

// toggle PB1 while PA5 low

do{

  GPIOB->DOUT31_0 ^= 0x02;

}
while((GPIOA->DIN31_0&0x20)==0);

Program 3.5.4. 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 3.5.4?


The for control structure has three parts and a body.

     for(part1;part2;part3){body;}


part1 involves initialization, which is performed only once before entering the loop;part2 involves a condition-check, which is performed to see if the body ought to be executed (true) or not (false); part3 involves an update that is peformed at the end of each iteration; The assembly version in Program 3.5.5 places the loop counter in the Register R4. According to AAPCS, we assume the subroutine Process preserves the value in R4.

 

      MOVS R4,#0    // R4 = 0

loop: CMP  R4,#10   // index >= 10?

      BHS  done  // if so, skip to done

      BL   Process  // process function

      ADDS R4,R4,#1 // R4 = R4 + 1

      B    loop 

done:

uint32_t i;

for(i=0; i<10; i++){

  Process();

}

Program 3.5.5. A simple for-loop.

If we assume the body will execute at least once, we can fast assembly code, as shown in Program 3.5.6, by counting down. Counting down is one instruction faster than counting up.

      MOVS R4,#10   // R4 = 10

loop: BL   Process  // body

      SUBS R4,R4,#1 // R4=R4-1

      BNE  loop 

      MOVS R4,#0     // R4 = 0

loop: BL   Process   // body

      ADDS R4,R4,#1  // R4 = R4+1

      CMP  R4,#10    // done?

      BLO  loop      // if not,repeat

Program 3.5.6. Optimized do-while.

Interactive Tool 3.5.2

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 True False i = 0 Start Output Start Output End Output i i<a End i = i + 1
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 3.5.7, 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 3.5.7. Simple program illustrating the C for-loop control structure.

How many times will the function Calc_Area be executed in Program 3.5.7?

Video 3.5.2. if-then conditional and while loop written for TM4C123 and developed in Keil

  

Is the for-loop similar to the while-loop or the do-while-loop? Explain why.

How do you decide whether to use a while-loop or a do-while-loop?

Assume a b c are unsigned variables. Write C code to implement c=a*b; without using the multiply operator. In other words, use addition and a loop.

Assume a b c are nonzero unsigned variables. Write C code to set c to the greatest common divisor, gcd, of a b. In other words, find the largest c such that c divides evenly into a and divides evenly into b. For example the gcd of 36 and 24 is 12.

 

Quiz 3.5

 

3.6. Functions

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 implement the SQRT operation is shown in Program 3.6.1. We can see that the declaration (prototype) 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. 

                  

Video 3.6.1. Square Root Function

uint32_t SquareRoot(uint32_t n); // prototype
int main(void){
uint32_t x = 100; // input
uint32_t y;
  y = SquareRoot(x); // invocation of function
  while(1){
  }
}
uint32_t SquareRoot(uint32_t n){ // definition
uint32_t z;
  z = 0;
  while(z*z < n){
    z++;
  }
return z;
}

Program 3.6.1. While loop to implement sqrt.

A top-down approach is to first declare a function with a prototype, use the function, and lastly define the function as illustrated in Programs 3.6.1 and 3.6.2.

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 3.6.2. A main program that calls a function. In this case the declaration (prototype) occurs first.

A bottom-up approach is to first define a function, and then use the function as illustrated in Program 3.6.3. In the bottom up approach, the definition both declares its structure and defines what it does. In this case, we do not need a prototype.

// Calculates area
// Input:  side of a room (uint32_t) in meters
// Output: area of the room (uint32_t) in square meters
uint32_t Calc_Area(uint32_t s) {
  uint32_t 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 3.6.3. A main program that calls a function. In this case the definition occurs before its use.

 

Video 3.6.2. Functions in C, written for Cortex M4, TM4C123 in Keil

 

Interactive Tool 3.6.1

The sum function (aka subroutine) in Program 3.6.3 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
Register Value
R0 0x00000000
R1 0x00000000
R2 0x00000000
R3 0x00000000
R4 0x00000000
R5 0x00000000
R6 0x00000000
R7 0x00000000
R8 0x00000000
R9 0x00000000
R10 0x00000000
R11 0x00000000
R12 0x00000000
R13(SP) 0x00000000
R14(LR) 0x00000000
R15(PC) 0x00000668
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 3.6.3. A function with two inputs and one output, written for Cortex M4, TM4C123 in Keil.

 

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?

: Write a C function to find the maximum of two signed 32-bit integers. Then, write a second function that finds the maximum of three signed 32-bit integers that uses the first function.

 

Quiz 3.6

 

 

3.7. Arrays

Video 3.7.1. Assembly language access to arrays

Video 3.7.2. Find the maximum value in an array

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.

Just like any variable, arrays must be declared before they can be accessed, see Figure 3.7.1. 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}};

 

Arrays in Memory

Figure 3.7.1. The Data array has 5 16-bit elements, and the scores array has 20 32-bit elements.

 

: Let Data be the base address of the 16-bit array in Figure 3.7.1, and let i be the index into the array. What address is the contents of Data[i]?

: Let scores be the base address of the 32-bit array in Figure 3.7.1, and let j be the index into the array. What address is the contents of scores[j]?

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(int 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 3.7.1. Assume student grades are stored in an array. Write a function that calculates and returns class average.

Solution: Assuming the grades vary from 0 to 100, the data could be stored in int8_t, int16_t or int32_t 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. However, we will provide a mechanism to handle variable-sized arrays by passing in the size.

                  

Video 3.7.3. Find the average of an array

We begin with pseudo code, which will become the comments of our code.

 

    Initialize sum
    Iterate index from 0 to size-1
      Get a value from the array at index
      Add the value to sum
    If size is 0 return result of 0
    Return result of sum/size

The first parameter is a pointer to the array and the second is the number of elements. array and the size. 

int32_t Average(int32_t group[],int32_t size){

int32_t sum;

  sum = 0;

  for(int i=0; i<size; i++){

    sum += group[i];  // add up all values

  }

  if(size == 0) return 0;

  return (sum/size);

}

Program 3.7.1. Function to calculate average.

 

The array parameter is actually a pointer to the array. So to use this new function we call

  Ave1 = Average(Group1, 6);

 

 

: Assume you have an array similar in structure as Example 3.7.1 above. Write a C function that finds the maximum value in the array. The prototype is int32_t Max(int32_t group[], int32_t size); Return 0 if the array is empty.

 

 

Example 3.7.2. 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 3.7.2. Each element is 32 bits. In assembly, we define a word constant using .long, making sure in exists in ROM.

In C, the syntax for accessing all array types is independent of precision. See Program 3.7.2. 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 3.7.2. A word array with 10 elements. Addresses illustrate the array is stored in ROM as 4 bytes each.

 

 

// assembly version
     .text
     .align 2

Powers .long  1, 10, 100, 1000, 10000

       .long  100000, 1000000, 10000000

       .long  100000000, 1000000000

// Input: R0=x    Output: R0=10^x

power: LSLS 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 3.7.2. 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


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";

 

Hello    Name

: Let Name be the base address of the character array defined above, and let i be the index into the array. What address is the contents of Name[i]? What is the value at Name[1]?

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 3.7.1. The Hello string has 13 characters followed by a null (0) termination. The null is called a sentinel and it marks the end of the variable-length array of characters.

 

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 3.7.1. Escape sequences.

 

Note, that in C, ASCII strings are stored with null-termination. So, the C compiler automatically adds the zero at the end. In assembly, a null-terminated string can be defined using the .string pseudo-op.

: What is a sentinel?

: Why do strings have a sentinel?

: Write a function that counts the number of characters in a string (not including the null.)

 

Quiz 3.7

 

3.8. Pointers

A pointer is simply an address, see Figure 3.8.1. There are three steps to using pointers: 1) defining or allocating the pointer; 2) initializing the pointer to point to an object; 3) dereferencing the pointer to read data from the object or write data into the object.

Figure 3.8.1. Pointers are addresses pointing to objects. The objects may be data, functions, or other pointers.


At the assembly level, we implement pointers using indexed addressing mode. For example, a register contains an address, and the instruction reads or writes memory specified by that address. Basically, we place the address into a register, then use indexed addressing mode to access the data. In this case, the register holds the pointer. Figure 3.8.2 illustrates three examples that utilize pointers. In this figure, Pt SP GetPt PutPt are pointers, where the arrows show to where they point, and the shaded boxes represent data.

Figure 3.8.2. Examples of data structures that utilize pointers.


Accessing 16-bit data structures with indexed addressing is slightly different in assembly versus in C. For example, if we create an array of the first ten prime numbers stored as 16-bit integers, we could allocate the structure in ROM using the .short pseudo-op. E.g.,

Prime: .short 1,2,3,5,7,11,13,17,19,23


The equivalent ROM-based definition in C would be

uint16_t const Prime[10]={1,2,3,5,7,11,13,17,19,23};


By convention, we define Prime[0] as the first element, Prime[1] as the second element, etc. The address of the first element can be written as &Prime[0] or just Prime. In C, if we want the 5th element, we use the expression Prime[4] to fetch the 7 out of the structure. In assembly, however, we are responsible for knowing each element is two bytes and the 5th element is actually at bytes number 8 and 9. In general, the nth element of a 16-bit array is at bytes 2n-2 and 2n-1. E.g., to read the 5th element into Register R0 we need to perform

    LDR  R1,=Prime  // pointer to the structure
    LDRH R0,[R1,#8] // read 16-bit unsigned Prime[4]


Either way, manipulating addresses in assembly always involves the physical byte-address regardless of the precision of the data. In this next example we will use the pointer Pt to access data in the array Prime. The first step to using pointers is to define it:

uint16_t const *Pt;


In this case, the const does not indicate the pointer is fixed. Rather, the pointer refers to constant 16-bit data in ROM. Furthermore, since pointers are addresses, the size of every pointer is 4 bytes. In this example, Pt is a 4-byte variable in RAM that points to 16-bit data in ROM. The second step to using pointers is to initialize the pointer at run time:

Figure 3.8.3. An array of the first 10 primes


    Pt = Prime; // Pt points to Prime

or

    Pt = &Prime[0]; // Pt points to Prime


Similarly in assembly, we can define the pointer in RAM as

Pt: .space 4 // pointer to Prime


and initialize it as

    LDR R1,=Prime // pointer to the Prime structure
    LDR R0,=Pt    // pointer to Pt
    STR R1,[R0]   // Pt is a pointer to Prime[0]


: If pt is a pointer to a 16-bit object, why did we allocate 4 bytes for it?

You should not use .long to define/allocate RAM-based variables in microcontrollers, because RAM has no initial value when power is applied to the microcontroller. One must explicitly initialize variables using assembly code like the above three lines. In both assembly and C, we must initialize pointers before they are used. The third step to use pointers is to dereference it. To read the data pointed to by Pt, we use the * symbol:

    uint16_t data;
    data = *Pt; // data contains the value


Similarly in assembly, assuming the variable data is in R2,

    LDR  R0,=Pt  // R0 is pointer to Pt
    LDR  R1,[R0] // R1 is value of Pt
    LDRH R2,[R1] // R2 is the value


Now, to increment the pointer to the next element in C, use the expression Pt++. In C, Pt++, which is the same thing as Pt=Pt+1; actually adds two to the pointer because it points to halfwords. However, in assembly we have to explicitly add 2 to the pointer. E.g.,

    LDR  R0,=Pt  // pointer to Pt
    LDR  R1,[R0] // R1 is value of Pt, assuming it points to 16-bit data
    ADDS R1,#2
    STR  R1,[R0] // update Pt


Observation: We normally add/sub one to the pointer when accessing an 8-bit array, add/sub two when accessing a 16-bit array, and add/sub four when accessing a 32-bit array.

: In C, uint64_t *p; creates a pointer to 64-bit unsigned integer. How many bytes are needed to allocate p?

: In C, uint64_t *p; creates a pointer to 64-bit unsigned integer. In assembly, how would you increment p? E.g., implement p++;

 

Example 3.8.1. Write software to output an ASCII string an output device.

Solution: Because the length of the string may be too long to place all the ASCII characters into the registers at the same time, call by reference parameter passing will be used. With call by reference, a pointer to the string will be passed. The function OutString, shown in Program 5.8, will output the string data to the display. We will assume the function OutChar is given to us, which outputs a single ASCII character. In C, we process one character (*pt gives us the current character) of the string in each iteration of the while loop, which ends when the null value is reached. Each iteration advances the pointer by incrementing it (pt++). In the assembly version, R4 is used as a pointer to the string; one is added to the pointer each time through the loop because each element in the string is one byte. Since this function calls a subfunction it must save its original return address (LR). The POP PC operation will perform the function return.

 

// assembly version
// Input: R0 points to string

OutString:

      PUSH {R4, LR}

      MOVS R4, R0                     

loop: LDRB R0, [R4]

      ADDS R4, #1  //next

      CMP  R0, #0  // done?                    

      BEQ  done    // 0 termination                      

      BL   OutChar // print character

      B    loop 

done: POP  {R4, PC} 
    

// C version
// displays a string

void OutString(char *pt){

  while(*pt){

    OutChar(*pt); // output

    pt++;         // next

  }

}

Program 3.8.1. A variable length string contains ASCII data.

 

Observation: Most C compilers have standard libraries. If you include “string.h” you will have access to many convenient string operations.

 

When dealing with strings we must remember that they are arrays of characters with null termination. In C, we can pass a string as a parameter, but doing so creates a constant string and implements call by reference. Assuming Hello is as defined above, these three invocations are identical:

     OutString(Hello);

     OutString(&Hello[0]);

     OutString("Hello world\n\r");

 

 Previously we dealt with constant strings. With string variables, we do not know the length at compile time, so we must allocate space for the largest possible size the string could be. E.g., if we know the string size could vary from 0 to 19 characters, we would allocate 20 bytes.

char String1[20];

char String2[20];

 

In C, we cannot assign one string to another. I.e., these are illegal

    String1 = "Hello";   //********illegal************

    String2 = String1;   //********illegal************

 

We can make this operation occur by calling a function called strcpy, which copies one string to another. This function takes two pointers. We must however make sure the destination string has enough space to hold the string being copied.

       strcpy(String1,"Hello");  // copies "Hello" into String1

       strcpy(String2,String1);  // copies String1 into String2

 

Program 3.8.2 shows two implementations of this string copy function. R0 and R1 are pointers, and R2 contains the data as it is being copied. In this case, destPt++; is implemented as an “add 1” because the data is one byte each (char). In other non-string situations, the increment pointer would be “add 2” for halfword data (short) and would be “add 4” for word data(long). Again, the C compiler does this automatically, but when writing in assembly one has to do this explicitly.

// assembly version
// Input: R0=&destPt    R1=&sourcePt

strcpy: LDRB R2,[R1]  // source data

        STRB R2,[R0]  // copy

        CMP  R2,#0    // termination?

        BEQ  done

        ADDS R1,#1    // next

        ADDS R0,#1

        B    strcpy

done:   BX   LR
// C version                

// copy string from sourcePt to destPt

void strcpy(char *destPt, char *sourcePt){

  while(*sourcePt){

    *destPt = *sourcePt;  // copy

    destPt++;             // next

    sourcePt++;

  }

  *destPt = *sourcePt;   // termination

}

// another version

void strcpy(char *destPt, char *sourcePt){

char data;

  do{

   data = *destPt++ = *sourcePt++;

  } while(data);

}

Program 3.8.2. Simple string copy functions.

: Write a function that counts the number of characters in a string using pointers.

 

Quiz 3.8

 

3.9. Debugging Dump

Video 3.9.1. Testing and Debugging - Intrusiveness

Many programming environments use printf output to debug software. It allows one to see information as the program runs. printf is not appropriate on an embedded system for two reasons. First, there may be no output channel to which to send the printf output. More importantly, most embedded systems will run in a time-critical fashion. Outputing using printf typically requires milliseconds to complete. The presence of the printf will significantly affect the normal operation of the system. We classify this as highly intrusive. Debugging that has a small but nonzero effect is called minimally intrusive. If it has no effect, it is called nonintrusive. Connecting a logic analyzer or scope to the actual input/output pins, which are the essential to the system operation, is nonintrusive. If we add an output pin and toggle it, just for debugging, it will be minimally intrusive.

The debugging dump is minimally intrusive and can replace printf to observe strategic information while the software is running.

                  

Video 3.9.2. Debugging Dump used to test SquareRoot

uint32_t InBuffer[12];
uint32_t OutBuffer[12];
uint32_t Count=0;
void Dump(uint32_t in, uint32_t out){
  if(Count >= 12) return;
  InBuffer[Count] = in;
  OutBuffer[Count] = out;
  Count++;
}

Program 3.9.1. Debugging dump records strategic information.

: What does nonintrusive debugging mean? Give an example.

: What does minimually intrusive debugging mean? Give an example.

: What does highly intrusive debugging mean? Give an example.

Go to Chapter 4: Finite State Machines

  This material was created to teach ECE319K at the University of Texas at Austin

Reprinted with approval from Introduction to Embedded Systems Using the MSPM0+, ISBN: 979-8852536594

  Creative Commons License
Embedded Systems - Shape the World by Jonathan Valvano and Ramesh Yerraballi is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
Based on a work at http://users.ece.utexas.edu/~valvano/mspm0/

.