0% found this document useful (0 votes)
4 views12 pages

Module 3

The document outlines the syllabus for BCS402 - Microcontroller, focusing on C compilers and optimization techniques, including data types, looping structures, register allocation, function calls, and portability issues. It emphasizes the importance of using appropriate data types and structures to enhance performance and avoid inefficiencies in ARM architecture. Additionally, it discusses potential pitfalls when porting C code to ARM, such as differences in data type handling and alignment requirements.

Uploaded by

Piyu Patt
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views12 pages

Module 3

The document outlines the syllabus for BCS402 - Microcontroller, focusing on C compilers and optimization techniques, including data types, looping structures, register allocation, function calls, and portability issues. It emphasizes the importance of using appropriate data types and structures to enhance performance and avoid inefficiencies in ARM architecture. Additionally, it discusses potential pitfalls when porting C code to ARM, such as differences in data type handling and alignment requirements.

Uploaded by

Piyu Patt
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

1 BCS402 - Microcontroller

MODULE - 3
Syllabus:
C Compilers and Optimization: Basic C Data Types, C Looping Structures,
Register Allocation, Function Calls, Pointer Aliasing, Portability Issues.

Basic C Data Types


ARM C compilers define char to be an unsigned 8-bit value, rather than a signed 8-bit value.
Compilers armcc and gcc use the datatype mappings are shown in below table.

Local variable types: ARMv4-based processors can efficiently load and store 8, 16, and 32-bit
data. But most ARM data processing operations are 32-bit only. For this reason, a 32-bit data type,
int or long, used for local variables wherever possible. Avoid using char and short as local variable
types, even if for an 8- or 16-bit value. The one exception is when you want wrap- around to
occur.

Example 1: The following code checksums a data packet containing 64 words. It shows why to
avoid using char for local variables.
int checksum_v1(int *data) {
char i;
int sum = 0;
for (i = 0; i < 64; i++) {
sum += data[i];
}
return sum; }
At first sight it looks as though declaring i as a char is efficient. All ARM registers are 32-bit and
all stack entries are at least 32-bit. To implement the i++ exactly, the compiler must account for
the case when i = 255. Any attempt to increment 255 should produce the answer 0.
Example 2: The data packet contains 16-bit values for a 16-bit checksum.
short checksum_v3(short *data) {
unsigned int i;
short sum = 0;
for (i = 0; i < 64; i++) {
sum = (short)(sum + data[i]);
}
2 BCS402 - Microcontroller

return sum; }
With armcc this code will produce a warning for enabling implicit narrowing cast warnings using
the compiler switch -W+ n. The expression sum + data[i] is an integer and so can only be assigned
to a short using an (implicit or explicit) narrowing cast.
To avoid unnecessary casts it uses int type local variables. It increments the pointer data instead of
using an index offset data[i].
short checksum_v4(short *data) {
unsigned int i;
int sum=0;
for (i=0; i<64; i++) {
sum += *(data++); // The *(data++) operation translates to a single ARM instruction that loads the data and increments the data
pointer.

}
return (short)sum;
}
Function argument types: For armcc in ADS, function arguments are passed narrow and values
returned narrow. The caller casts argument values and the callee casts return values. The compiler
uses the ANSI prototype of the function to determine the datatypes of the function arguments.
If code uses addition, subtraction, and multiplication, then there is no performance difference
between signed and unsigned operations. However, there is a difference when it comes to division.
Consider the following short example that averages two integers:
int average_v1(int a, int b)
{
return (a+b)/2;
}
The compiler adds one to the sum before shifting by right if the sum is negative. In other words it
replaces x/2 by the statement: (x<0) ? ((x+1) >> 1): (x >> 1)
For the efficient use of C types the following points to be deliberated.
• For local variables held in registers, don’t use a char or short type unless 8-bit or 16-bit
modular arithmetic is necessary. Use the signed or unsigned int types instead. Unsigned
types are faster for divisions operation.
• For array entries and global variables held in main memory, use the type with the smallest
size possible to hold the required data. This saves memory footprint. The ARMv4
architecture is efficient at loading and storing all data widths provided you traverse arrays
by incrementing the array pointer.
• Avoid using offsets from the base of the array with short type arrays, as LDRH does not
3 BCS402 - Microcontroller

support this.
• Use explicit casts when reading array entries or global variables into local variables, or
writing local variables out to array entries. The casts make it clear that for fast operation
taking a narrow width type stored in memory and expanding it to a wider type in the
registers.
• Switch on implicit narrowing cast warnings in the compiler to detect implicit casts.
• Avoid implicit or explicit narrowing casts in expressions because they usually cost extra
cycles.
• Avoid char and short types for function arguments or return values. Instead use the int type
even if the range of the parameter is smaller. This prevents the compiler performing
unnecessary casts.

C LOOPING STRUCTURES
Below example shows the compiler treats a loop with incrementing count i++.

• An ADD to increment i
• A compare to check if i is less than 64
• A conditional branch to continue the loop if i < 64
4 BCS402 - Microcontroller

SUBS and BNE instructions implement the loop


Loops Using Variable Number of Iterations

In this case a do-while loop gives better performance than a for loop
5 BCS402 - Microcontroller

do-while loop removes the test for N being zero that occurs in a for loop & hence it gives better
performance than a for loop.
Loop unrolling: Repeating the loop body several times, and reducing the number of loop

iterations by the same proportion.

Points to remember while using Looping statement efficiently


• Use loops that count down to zero. Then the compiler does not need to allocate a register to
hold the termination value, and the comparison with zero is free.
• Use unsigned loop counters by default and the continuation condition i!=0 rather than i>0.
This will ensure that the loop overhead is only two instructions.
• Use do-while loops rather than for loops when you know the loop will iterate at least once.
This saves the compiler checking to see if the loop count is zero.
• Unroll important loops to reduce the loop overhead.
• Do not over unroll, if the loop overhead is small as a proportion of the total, then unrolling
will increase code size and hurt the performance of the cache.

REGISTER ALLOCATION
The compiler attempts to allocate a processor register to each local variable use in a C function.
It will try to use the same register for different local variables if the use of the variables does not
overlap. When there are more local variables than available registers, the compiler stores the
excess variables on the processor stack. These variables are called spilled or swapped out variables
since they are written out to memory (in a similar way virtual memory is swapped out to disk).
Spilled variables are slow to access compared to variables allocated to registers.
To implement a function efficiently, minimize the number of spilled variables & ensure that the
most important and frequently accessed variables are stored in registers.
C compiler register usage
Table shows the standard register names and usage when following the ARM-Thumb procedure
call standard (ATPCS), which is used in code generated by C compilers.
6 BCS402 - Microcontroller

Provided the compiler is not using software stack checking or a frame pointer, then the C compiler
can use registers r0 to r12 and r14 to hold variables. It must save the callee values of r4 to r11 and
r14 on the stack if using these registers.

In theory, the C compiler can assign 14 variables to registers without spillage. In practice, some
compilers use a fixed register such as r12 for intermediate scratch working and do not assign
variables to this register. Also, complex expressions require intermediate working registers to
evaluate. Therefore, to ensure good assignment to registers, try to limit the internal loop of
functions to using at most 12 local variables.
If the compiler does need to swap out variables, then it chooses which variables to swap out based
on frequency of use. A variable used inside a loop counts multiple times.
The register keyword in C hints that a compiler should allocate the given variable to a register.
Different compilers treat this keyword in different ways, and different architectures have a
different number of available registers (for example, Thumb and ARM).
7 BCS402 - Microcontroller
Efficient Register Allocation
• Try to limit the number of local variables in the internal loop of functions to 12. The compiler
should be able to allocate these to ARM registers.
• Guide the compiler as to which variables are important by ensuring these variables are used
within the innermost loop.

FUNCTION CALLS
The ARM Procedure Call Standard (APCS) defines how to pass function arguments and return
values in ARM registers. The more recent ARM-Thumb Procedure Call
Standard (ATPCS) covers ARM and Thumb interworking as well.
The first four integer arguments are passed in the first four ARM registers: r0, r1,
r2, and r3. Subsequent integer arguments are placed on the full descending stack,
ascending in memory shown in figure below. Function return integer values are
passed in r0.
This description covers only integer or pointer arguments. Two- word arguments
such as long or double are passed in a pair of consecutive argument registers
and returned in r0, r1. The compiler may pass structures in registers or by reference
according to command line compiler options. Functions with four or fewer
arguments are far more efficient to call than functions with five or more arguments.
For functions with four or fewer arguments, the compiler can pass all the arguments in registers. For
functions with more arguments, both the caller and callee must access the stack for some arguments.

Example: The following code creates a Queue structure and passes this to the function to reduce the
number of function arguments.
There are other ways of reducing function call overhead if the function is very small and corrupts
few registers (uses few local variables). Put the C function in the same C file as the functions that
will call it. The C compiler then knows the code generated for the callee function and can make
optimizations in the caller function:
• The caller function need not preserve registers that it can see the callee doesn’t corrupt.
Therefore, the caller function need not save all the ATPCS corruptible registers.
• If the callee function is very small, then the compilers can inline the code in the caller function.
This removes the function call overhead completely.
For efficient use of calling a functions
• Try to restrict functions to four arguments. This will make them more efficient to call. Use
structures to group related arguments and pass structure pointers instead of multiple arguments.
• Define small functions in the same source file and before the functions that call them. The
compiler can then optimize the function call or inline the small function.
• Critical functions can be inlined using the inline keyword.

POINTER ALIASING
Two pointers are said to be alias when they point to the same address. To write one pointer, it will
affect the value read from the other pointer. In a function, the compiler often doesn’t know which
pointers can alias and which pointers can’t. The compiler must be very pessimistic and assume that
any write to a pointer may affect the value read from any other pointer, which can significantly
reduce code efficiency.
Example: The below code for function increments, two timer values by a step amount:
void timers_v1(int *timer1, int *timer2, int *step) {
*timer1 += *step;
*timer2 += *step; }
The compiler loads from step twice. Usually, a compiler optimization called common sub expression
elimination would kick in so that *step was only evaluated once, and the value reused for the second
occurrence. But the compiler can’t use this optimization here. The pointers timer1 and step might
alias one another i.e., the compiler cannot be sure that the write to timer1 doesn’t affect the read from
step.
In this case, the second value of *step is different from the first and has the value *timer1. This forces
the compiler to insert an extra load instruction.
The same problem occurs if you use structure accesses rather than direct pointer access. The
following code also compiles inefficiently:
typedef struct {
int step;
}
State;
typedef struct {
int timer1, timer2;
}
Timers;
void timers_v2(State *state, Timers *timers)
{
timers->timer1 += state->step; timers->timer2 += state->step;
}
Avoiding Pointer Aliasing
• Do not rely on the compiler to eliminate common sub expressions involving memory accesses.
Instead, create new local variables to hold the expression. This ensures the expression is
evaluated only once.
• Avoid taking the address of local variables. The variable may be inefficient to access from then
on.

PORTABILITY ISSUES
Here is a summary of the issues you may encounter when porting C code to the ARM.
■ The char type. On the ARM, char is unsigned rather than signed as for many other processors. A
common problem concerns loops that use a char loop counter i and the continuation condition i ≥ 0,
they become infinite loops. In this situation, armcc produces a warning of unsigned comparison with
zero. You should either use a compiler option to make char signed or change loop counters to type
int.

■ The int type. Some older architectures use a 16-bit int, which may cause problems when moving to
ARM’s 32-bit int type although this is rare nowadays. Note that expressions are promoted to an int
type before evaluation. Therefore if i = -0x1000, the expression i == 0xF000 is true on a 16-bit
machine but false on a 32- bit machine.

■ Unaligned data pointers. Some processors support the loading of short and int typed values from
unaligned addresses. A C program may manipulate pointers directly so that they become unaligned,
for example, by casting a char * to an int *. ARM architectures up to ARMv5TE do not support
unaligned pointers. To detect them,
run the program on an ARM with an alignment checking trap. For example, you can configure the
ARM720T to data abort on an unaligned access.

■ Endian assumptions. C code may make assumptions about the endianness of a memory system, for
example, by casting a char * to an int *. If you configure the ARM for the same endianness the code
is expecting, then there is no issue. Otherwise, you must remove endian-dependent code sequences
and replace them by endian-independent ones. See Section 5.9 for more details.

■ Function prototyping. The armcc compiler passes arguments narrow, that is, reduced to the range
of the argument type. If functions are not prototyped correctly, then the function may return the
wrong answer. Other compilers that pass arguments wide may give the correct answer even if the
function prototype is incorrect. Always use ANSI prototypes.

■ Use of bit-fields. The layout of bits within a bit-field is implementation and endian dependent. If C
code assumes that bits are laid out in a certain order, then the code is not portable.

■ Use of enumerations. Although enum is portable, different compilers allocate different numbers of
bytes to an enum. The gcc compiler will always allocate four bytes to an enum type. The armcc
compiler will only allocate one byte if the enum takes only eight-bit values. Therefore, you can’t
cross-link code and libraries between different compilers if you use enums in an API structure.

■ Inline assembly. Using inline assembly in C code reduces portability between architectures. You
should separate any inline assembly into small inlined functions that can easily be replaced. It is also
useful to supply reference, plain C implementations of these functions that can be used on other
architectures, where this is possible.

■ The volatile keyword. Use the volatile keyword on the type definitions of ARM memory-mapped
peripheral locations. This keyword prevents the compiler from optimizing away the memory access.
It also ensures that the compiler generates a data access

of the correct type. For example, if you define a memory location as a volatile short type, then the
compiler will access it using 16-bit load and store instructions LDRSH and STRH Here is a summary
of the issues you may encounter when porting C code to the ARM.

■ The char type. On the ARM, char is unsigned rather than signed as for many other processors. A
common problem concerns loops that use a char loop counter i and the continuation condition i ≥ 0,
they become infinite loops. In this situation, armcc produces a warning of unsigned comparison with
zero. You should either use a compiler option to make char signed or change loop counters to type
int.

■ The int type. Some older architectures use a 16-bit int, which may cause problems when moving to
ARM’s 32-bit int type although this is rare nowadays. Note that expressions are promoted to an int
type before evaluation. Therefore, if i = -0x1000, the expression i == 0xF000 is true on a 16-bit
machine but false on a 32- bit machine.
■ Unaligned data pointers. Some processors support the loading of short and int typed values from
unaligned addresses. A C program may manipulate pointers directly so that they become unaligned,
for example, by casting a char * to an int *. ARM architectures up to ARMv5TE do not support
unaligned pointers. To detect them, run the program on an ARM with an alignment checking trap.
For example, you can configure the ARM720T to data abort on an unaligned access.

■ Endian assumptions. C code may make assumptions about the endianness of a memory system, for
example, by casting a char * to an int *. If you configure the ARM for the same endianness the code
is expecting, then there is no issue. Otherwise, you must remove endian-dependent code sequences
and replace them by endian-independent ones. See Section 5.9 for more details.

■ Function prototyping. The armcc compiler passes arguments narrow, that is, reduced to the range
of the argument type. If functions are not prototyped correctly, then the function may return the
wrong answer. Other compilers that pass arguments wide may give the correct answer even if the
function prototype is incorrect. Always use ANSI prototypes.

■ Use of bit-fields. The layout of bits within a bit-field is implementation and endian dependent. If C
code assumes that bits are laid out in a certain order, then the code is not portable.

■ Use of enumerations. Although enum is portable, different compilers allocate different numbers of
bytes to an enum. The gcc compiler will always allocate four bytes to an enum type. The armcc
compiler will only allocate one byte if the enum takes only eight-bit values. Therefore you can’t
cross-link code and libraries between different compilers if you use enums in an API structure.

■ Inline assembly. Using inline assembly in C code reduces portability between architectures. You
should separate any inline assembly into small inlined functions that can easily be replaced. It is also
useful to supply reference, plain C implementations of these functions that can be used on other
architectures, where this is possible.

■ The volatile keyword. Use the volatile keyword on the type definitions of ARM memory-mapped
peripheral locations. This keyword prevents the compiler from optimizing away the memory access.
It also ensures that the compiler generates a data access of the correct type. For example, if you
define a memory location as a volatile short type, then the compiler will access it using 16-bit load
and store instructions LDRSH
and STRH.

You might also like