0% found this document useful (0 votes)
96 views

Unit 2 Embedded C

This document discusses practices for embedded C development, including use of inline assembly language, device knowledge through #pragma directives, and mechanical knowledge. It covers topics like defining ports using #pragma directives, setting ports, data direction, libraries, data types, and special variable types like bits. Inline assembly allows direct access to hardware without compiler optimization. #pragma directives describe target hardware to the compiler. Variables require specification of size and storage to efficiently utilize limited memory resources on embedded systems.

Uploaded by

Rohit
Copyright
© © All Rights Reserved
Available Formats
Download as PPTX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
96 views

Unit 2 Embedded C

This document discusses practices for embedded C development, including use of inline assembly language, device knowledge through #pragma directives, and mechanical knowledge. It covers topics like defining ports using #pragma directives, setting ports, data direction, libraries, data types, and special variable types like bits. Inline assembly allows direct access to hardware without compiler optimization. #pragma directives describe target hardware to the compiler. Variables require specification of size and storage to efficiently utilize limited memory resources on embedded systems.

Uploaded by

Rohit
Copyright
© © All Rights Reserved
Available Formats
Download as PPTX, PDF, TXT or read online on Scribd
You are on page 1/ 43

C for Embedded Systems

UNIT-2
(Part-1)
Practices that characterize embedded C development:
• in-line assembly language
• device knowledge
• mechanical knowledge.

Inline Assembly
• The labels and variables used in C are available within included
assembly,
• Compiler will not attempt to optimize such code.
• The compiler assumes that the user has a good reason to avoid
the compiler's code generation and optimization.
• The microcontroller's manufacturer should provide assistance in
hand-crafting assembly language programming.
• Required to flip opcodes out of order to accommodate a
pipeline, something the compiler will do transparently.
C functions with inline Assembly language
Device Knowledge

• The preprocessor deal with #pragma directives in your source


code, or it may be the compiler that acts upon these directives.
• The #pragma directive is used most commonly in embedded
development to describe specific resources of your target
hardware, such as available memory, ports, and specialized
instruction sets.
• Even processor clock speed can be specified, if it matters to the
compiler.
• #pragma describes specific architectural qualities of the processor.
• The qualifiers of the #pragma has instruction are dependent upon
the processor family and the compiler.
• Most #pragma has statements will appear in the device header file
Pragma MUL enabled
#pragma port

• #pragma port directives describe the ports available on the target


computer.
• This declaration reserves memory-mapped port locations, so the
compiler does not use them for data memory allocation.
• #pragma port directives indicate read or write access, or both. The
electronics of I/O ports may sometimes forbid writing to them or
even reading from them.
• The compiler can report undesirable accesses to a port if it finds a
restriction in the declaration.
• Besides protecting the port register, the declaration allows to
provide a useful mnemonic name for the port.
• We can use the name associated with the port to read or write its
input or output state.
Defining ports in pragma
#pragma portrw PORTA @ 0x0000 ;#pragma portrw PORTB @ 0x0001;
#pragma portw DDRA @ 0x0004;; #pragma portw DDRB @ 0x0005;

Setting ports using assignment


DDRA=0xFF; /* set the direction to output */
PORTA=0xAA; /* set the output pins to 10101010 */
Endianness
• C does not deal directly with endianness, even in multi-byte shift operations.
• In cases in which you will directly manipulate part of a multi-byte value, you must
determine from manufacturer's information whether the high byte (big end) or low
byte (little end) is stored first in memory.
• With the restricted resources of microcontrollers, some quirks appear. The COP8
architecture stores addresses in memory (for indirect operations) as big-endian, and
data as little-endian.
• Addresses pushed on to the stack do not appear in the same endianness as they do
in registers or in RAM.
• Compilers, when building their symbol tables, normally use the lowest (first) memory
location to record the location of an identifier, regardless of the endianness of the
processor.
Data direction
• One useful technique employed by the port library is to define the letters I
and O to the appropriate settings for port control registers that govern
data direction..
• Applications may need to use a port both as input and output (for
instance, driving a bidirectional parallel port through software), and
setting a port's data direction using these macros provides device
independence.
Device-independent data direction settings
#pragma portw DDR @ 0x05;
#include <port.h>
/* port.h contains numerous definitions such as the following:
#define IIIIIIII 0b00000000
#define IIII0000 0b00001111
#define 00000000 0b11111111
where 'O'utput sets DDR bits to one ('1') and 'I'nput sets DDR bits to
zero ('0').
They can be regenerated for the opposite settings.
*/
/* ... later ... */
DDR = 00000000; /* all bits set for output */
DDR_WAIT();
/* ... perform write to port ... */
DDR = IIIIIIII; /* all bits set for input */
DDR_WAIT();
/* ... perform read of port ... */
Libraries
• Libraries are the traditional mechanism for modular compile-time code reuse.
• A library is a code module that has no main() routine.
• The associated header file should declare the variables and functions within the library
as extern.
• The linking process is simpler than that for desktop software development. There is no
need to archive object files, and there is no dynamic linking to worry about.
• It is unacceptable in embedded software for unreferenced functions to be left in the
object file during linking.
• In the Byte Craft compiler, the #pragma library and #pragma end library bounding
statements identify that not all routines within a library need to be linked in. .
• Peering into the code generated for libraries is as important as seeing the code for the
main module.
• The statement #pragma option +l; within a library causes the compiler to add the
source and assembly code from the library into the listing file of the final program.
Listing 5.7 A "Hello World!" for microcontrollers
#include <hc705c8.h>
/* #pragma portrw PORTA @ 0x0A; is declared in header
#pragma portw DDRA @ 0x8A; is declared in header */
#include <port.h>
#define ON 1
#define OFF 0
#define PUSHED 1
void wait(registera); /* wait function prototype, not displayed */
void main(void){
DDRA = IIIIIII0; /* pin 0 to output, pin 1 to input,
rest don't matter */
while (1){
if (PORTA.1 == PUSHED){
wait(1); /* is it a valid push? */
if (PORTA.1 == PUSHED){
PORTA.0 = ON; /* turn on light */
wait(10); /* short delay */
PORTA.0 = OFF; /* turn off light */
}
}
}
} /* end main */
Data types
• Constants or initialized variables will consume a more significant proportion of
ROM, as well as RAM.
• Global variable declarations that contain an initialization will automatically
generate machine code to place a value at the allocated address shortly after
reset.
• In the Byte Craft compiler, one or more global variable initializations will generate
code to zero all variable RAM before assigning the initialization values to the
variables.
• Variables of type register are available, but the scarcity of registers in the typical 8-
bit architecture makes them more volatile than usual.
• When the compiler comes across a variable declaration, it checks that the variable
has not previously been declared and then allocates an appropriately-sized block
of RAM.
• For example, a char variable will by default require a single word (8 bits) of RAM or
data memory.
• Data type modifiers influence the size and treatment of the
memory allocated for variables.
• Storage modifiers affect when memory is allocated and how it is
considered free to be re-used
• Some variables are meant to be allocated once only across several
modules.
• Even previously compiled modules may need to access a common
variable.
• The compilation units — libraries or object files — must identify
these as external symbols using the extern storage class modifier.
• Non-static variables that are of mutually-exclusive scope are likely
to be overlaid.
• Embedded C regards scope in much the same way that standard C
does, but there is an extra effort to use scope to help conserve
memory resources
Special data type and variables
Function Data Types
• A function data type determines the value that a subroutine can
return. For example, a function of type int returns a signed integer
value.
• Without a specific return type, any function returns an int.
• An embedded C compiler provides for this even in the case of
main(), though returning is not anticipated.
• To avoid confusion, you should always declare main() with return
type void
• Parameter data types indicate the values to be passed in to the
function, and the memory to be reserved for storing them.
• A function declared without any parameters (i.e., with empty
parentheses) is deemed to have no parameters, noted as (void).
• The compiler allocates memory differently depending upon the target
part
• If local memory is specifically declared, the compiler will allocate
parameter passing locations out of that space.
The Character Data Type
The C character data type, char, stores character values and is allocated one
byte of memory space.

Integer Data types


• Integer values can be stored as int, short, or long data types.
• size of int values is usually 16 bits on 8-bit architectures.
• The Byte Craft default int size is switchable between 8and 16 bits.
• Traditional C platforms,the size of an int is more than two bytes.
• most 8-bit microcontrollers — the short data type will typically
occupy a single byte.
• To manipulate values larger than an int, you can use the long data
type.
• Most platforms the long data type reserves twice as much memory
as the int data type.
• On 8-bit microcontrollers, the long data type typically occupies 16
bits.
• It is important to note that long integer values are almost always
stored in a memory block larger than the natural size for the
computer.
• long and short are useful because they are less likely to change
between a target with a natural 8-bit data type and one that delves
into 16-bit values.
• In cases of a switchable int, you can maintain code portability by
using short for those values that require 8 bits, and long for values
which require 16 bits.
• Like the int, the short and long data types uses a sign bit by default
and can therefore contain negative numbers.
Listing 6.2 Bit-sized variable types
bits switch_fixup(void)
bit heat_flag;
bit cool_flag;
bits switches;
heat_flag = PORTB.0;
cool_flag = PORTB.1;
switches = PORTB;
if(switches.5 &&heat_flag)
switches.1 = 0;
return(switches);
}
Real Numbers

• The resources needed to store and manipulate floating point numbers can place
overwhelming demands on an 8-bit computer.
• The fundamental data type for representing real numbers in C is the float type.
• The maximum value for the target computer is defined in a C header file called
values.h as a symbolic constant called MAXFLOAT.
• C compilers generally allocate four bytes for a float variable, which provides
approximately six digits of precision to the right of the decimal.
• You can have greater precision with the double and long double data types.
• Compilers typically allocate eight bytes for a double variable and more for a long
double.
• You can assign an integer value to a floating point data type, but you must
include a decimal and a 0 to the right of the decimal.
myFloatVariable = 2.0;
Pointers

• The implementation of pointer variables is heavily dependent upon the


instruction set of the target processor. The generated code will be simpler if the
processor has an indirect or indexed addressing mode.
• It is important to remember that Harvard architectures have two different
address spaces, and so the interpretation of pointers can change.
Arrays
• When you declare an array, you must declare both an array type and
the number of elements it contains.
• For example, the following declares an array containing eight int
elements.
int myIntArray[8];
• When you declare an array, a single, contiguous block of memory is
reserved to hold it.
• This is why you must specify the array size or assign the contents in
the declaration.
Initializing array
00C0 0008 int myarray[8];
/* uninitialized */
00C8 01 08 02 07 03 06 04 05 int my2array[] = {1,2,4,8,16,32,64,128};
/* initialized below */
0312 01 08 02 07 03 06 04 05 const int myconsts[] ={1,8,2,7,3,6,4,5};
/* no code generated for const array */
/* ... main() code omitted for clarity ... */
Enumerated Types

• Enumerated types are finite sets of named values.


• For any list of enumerated elements, the compiler supplies a range of
integer values beginning with 0 by default.
• Since character constants are stored as integer values, they can be
specified as values in an enumerated list.
• enum DIGITS {one='1', two= '2', three='3'};
will store the appropriate integer values of machine character set (usually
ASCII) for each digit specified in the element list.

Specifying integer values for enumerated elements


enum WDWinSel { Bit7 = 7, Bit6 = 6 };

Specifying a starting value for enumerated elements


enum ORDINALS {first = 1, second, third, fourth, fifth};
When the compiler encounters an element in an enumerated list without an
assigned value, it counts from the last value that was specified.
Structures
Structures support the meaningful grouping of program data. Building
understandable data structures is one key to the effectiveness of a new
program.
The following declaration creates a structured type for an extended time
counter and describes each element within the structure.
The display is defined as having the components hours, minutes,
seconds, and an AM/PM flag.
Later, a variable timetext is declared to be of type struct
Declaring the template of a structure
struct display {
unsigned int hours;
unsigned int minutes;
unsigned int seconds;display.
char AorP;
};
struct display timetext;
Bit fields in structures
struct reg_tag {
int ICIE : 1; /* field ICIE, 1 bit long */
int OCIE : 1; /* field OCIE, 1 bit long */
int notUsed : 3 = 0; /* notUsed is 3 bits and set to 0 */
int IEDG : 1; /* field IEDG 1 bit long */
int OLVL : 1; /* field OLVL 1 bit long */
} TCR;
/* To configure the timer: */
TCR.OLVL = 1; /* TCMP pin goes high on output compare successful */

Compiler dependant storage of bit fields


struct {
unsigned int shortElement : 1; /* 1 bit in size */
unsigned int longElement : 7; /* 7 bits in size */
} myBitField; /* could be 1 byte, worst case 2 */
Unions
• The union type interprets data stored in a single block of memory
based on one of several associated data types.
• One common use of the union type in embedded systems is to
create a scratch pad variable that can hold different types of data.
• This saves memory by reusing one 16-bit block in every function
that requires a temporary variable.
• The following example shows a declaration to create such a
variable.
Using a union to create a scratch pad
struct lohi_tag{
short lowByte;
short hiByte;
};
union tagName {
int asInt;
char asChar;
short asShort:
long asLong;
int near * asNPtr;
int far * asFPtr;
struct hilo_tag asWord;
} scratchPad
Using a union to access data as different types

struct asByte {
int TMR1H; /* high byte */
int TMR1L; /* low byte */
}
union TIMER1_tag {
long TMR1_word; /* access as 16 bit register */
struct asByte halves;
} TMR1;
/* ... */
seed = TMR1.halves.TMR1L;

Since the compiler uses a single block of memory for the entire union, it
allocates a block large enough for the largest element in the union.
The compiler will align the first bits of each element in the lowest address
in the memory block.
If you assign a 16-bit value to scratchPad and then read it as an 8-bit value,
the compiler will return the first 8 bits of the data stored.
typedef
• The typedef keyword defines a new variable type in terms of existing types.
The compiler cares most about the size of the new type, to determine the
amount of RAM or ROM to reserve.

Defining new types with typedef


typedef int new_int;
new_int result; /* represents same range of values
in a different context. */
typedef struct {
char * name;
int start;
int min_temp;
int max_temp;
} time_record;
time_record targets[] {
{ "Night", 0, 20, 25},
{ "Day", 5*3600, 20, 25},
{ "Evening", 18*3600, 20, 25},
}
Data Type Modifiers
• Data type modifiers alter the range of allowable values
Value Constancy Modifiers: (const and volatile)
• For example, if your code makes use of π, the constant PI, then you should
place an approximation of the value in a constant variable.
const float PI = 3.1415926;
• When your program is compiled, the compiler allocates ROM space for
your PI variable and will not allow the value to be changed in your code.
• Volatile variables are variables whose values may change outside of the
immediately executing software.
• For example, a variable that is ''stored" at the location of a port data
register will change as the port value changes.
• Using the volatile keyword informs the compiler that it can not depend
upon the value of a variable and should not perform any optimizations
based on assigned values.
Data Type Modifiers
• Data type modifiers alter the range of allowable values
Value Constancy Modifiers: (const and volatile)
• For example, if your code makes use of π, the constant PI, then you should
place an approximation of the value in a constant variable.
const float PI = 3.1415926;
• When your program is compiled, the compiler allocates ROM space for
your PI variable and will not allow the value to be changed in your code.
• Volatile variables are variables whose values may change outside of the
immediately executing software.
• For example, a variable that is ''stored" at the location of a port data
register will change as the port value changes.
• Using the volatile keyword informs the compiler that it can not depend
upon the value of a variable and should not perform any optimizations
based on assigned values.
Allowable Values Modifiers:
signed and unsigned

• The sign value of an integer data type is assigned with the


signed and unsigned keywords.
• The signed keyword forces the compiler to use the high bit of
an integer variable as a sign bit.
• The sign bit is set with the value 1, then the rest of the variable
is interpreted as a negative value.
• Bydefault, short, int, and long data types are signed.
• The char data type is unsigned by default.
• To create a signed char variable, you must use a declaration
such as signed char mySignedChar
• If you use the signed or unsigned keywords by themselves, the
compiler assumes that you are declaring an integer value
Size Modifiers
short and long

• The short and long modifiers instruct the compiler how


much space to allocate for an int variable.
• The short keyword modifies an int to be of the same
size as a char variable (usually 8 bits).
short int myShortInt;
• If you use the short keyword alone, the compiler
assumes the variable is a short int type.
short myShortInt;
• The long keyword modifies an int to be twice as long as
a normal int variable.
long int myLonglnt;
Pointer Size Modifiers:
near and far

• The near keyword creates a pointer that points to objects in the


bottom section of addressable memory.
• These pointers occupy a single byte of memory, and the memory
locations to which they can point is limited to a bank of 256
locations, often from $0000–$00FF.
int near * myNIntptr;
• The far keyword creates a pointer that can point to any data in
memory:
const char * myString = "Constant String";
char far * myIndex = &myString;
• These pointers take two bytes of memory, which allows them to
hold any legal address location from $0000–$FFFF. far pointers
usually point to objects in user ROM, such as user-defined functions
and constants.
Storage Class Modifiers

• Storage class modifiers control memory allocation for


declared identifiers.
• C supports four storage class modifiers that can be used in
variable declarations: extern, static, register, and auto.
• Only extern is used in function declarations.
• When the compiler reads a program, it must decide how to
allocate storage for each identifier.
• The process used to accomplish this task is called linkage.
• C supports three classes of linkage: external, internal, and
none.
• C uses identifier linkage to sort out multiple references to
the same identifier.
External Linkage

• References to an identifier with external linkage


throughout a program call the same object in
memory.
• There must be a single definition for an identifier
with external linkage or the compiler will give an
error for duplicate symbol definition.
• By default, every function in a program has
external linkage. Also by default, any variable
with global scope has external linkage.
Internal Linkage

• In each compilation unit, all references to an identifier with


internal linkage refer to the same object in memory.
• This means that you can only provide a single definition for
each identifier with internal linkage in each compilation
unit of your program.
• No objects in C have internal linkage by default
• Any identifier with global scope (defined outside any
statement block) and with the static storage class modifier,
has internal linkage
• Any variable identifier with local scope (defined within a
statement block) and with the static storage class modifier,
has internal linkage.
No Linkage
• References to an identifier with no linkage in a
statement block refer to the same object in
memory.
• If you define a variable within a statement block,
you must provide only one such definition.
• Any variable declared within a statement block
has no linkage by default, unless the static or
extern keywords are included in the declaration
Extern modifier
• To declare an external function, use the extern keyword.
extern int Calculate_Sum()
• When the compiler encounters an external function declaration, it
interprets it as a prototype for the function name, type, and
.
parameters
• extern keyword claims that the function definition is in another
compilation unit. The compiler defers resolving this reference to the
linker.
• Global variables have external linkage.
• To create a global variable that can be read or set outside its
compilation unit, you must declare it normally within its source file
and declare it as extern within a header file.
extern int myGlobalInt
Static Modifier

Using the static data modifier to restrict the scope of variables

static int myGlobalInt;


static int staticFunc(void)

• These declarations create global identifiers that are not accessible


by any other compilation unit.
• The static keyword works almost the opposite for local variables.
• It creates a permanent variable local to the block in which it was
declared.
• For example, consider the unusual task of tracking the number of
times a recursive function calls itself (the function's depth).
Using static variables to track function depth
void myRecurseFunc(void) {
static int depthCount=1;
depthCount += 1;
if ( (depthCount < 10) && (!DONE) ) {
myRecurseFunc();
}
}
• myRecurseFunc contains an if statement that stops it from recursing too
deeply.
• The static variable depthCount is used to keep track of the current depth.
• Memory for static variables, however, is only initialized once.
• The static variable depthCount retains its value between function calls.
• Because depthCount is defined inside the myRecurseFunc() statement
block, it is not visible to any code outside the function
Using the register data type modifier
{
register int myCounter = 1;
while (myCounter<10) {
/* ... */
myCounter += 1;
} /* end while */
} /* enclosing block enforces reallocation of myCounter */

Because of the scarcity of registers on 8-bit machines and the desire for
size optimization rather than speed, the register keyword is not very
useful for embedded system programmers.
The example does two things: it places the register declaration and the
while loop close together and inside a statement block.
This minimizes the cost of potentially dedicating a register to a specific
variable.
It also forces the compiler to reallocate storage for myCounter as soon as
the loop is finished:
• Using the auto data modifier

int someFunc(NODEPTR myNodePtr) {


extern NODEPTR TheStructureRoot;
/* global pointer to data structure root */
auto NODEPTR tempNodePtr;
/* temporary pointer for structure manipulation */
/* ... */
}
• In this example, we declare tempNodePtr as an auto
variable to make it clear that, unlike the global
TheStructRoot pointer, tempNodePtr is only a temporary
variable

You might also like