Introduction To
Embedded C
-Yogesh Sharma
Binary Numbers in C
void func(void);
int count=5;
main() {
write_extern();
}
void write_extern(void)
extern int count;
void write_extern(void) {
printf("count is %i\n", count);
}
static lon int num ;
void interrupt update(void) {
++num ;
}
main() {
long val;
val= num ;
while(val !=num);
val = num ;
int *f(void) {
int i = 10;
return &i; /* No! Returning a local variable is bad! */
}
int *f(void) {
int *i = malloc(sizeof (int));
return i; /* Okay, you control when the memory is released */
}
int *parray = malloc(10 * sizeof (int));
if (parray != NULL){
/* Okay, use it */
}
By using arithmetic on a pointer to the first element of the array, you can walk every element in the array by adding one. Note that by adding one to a pointer, the pointer moves forward by the size of its data type. If char is one byte and int is two bytes
char *pc = &c;
(*pf)(); or pf(); //both are same
Pointers can also point to other pointers:
-Yogesh Sharma
A
Register
A register is simply a
collection of some bits (mostly 8 bits in case of 8bit MCUs). Either each
different
bit in a register has some purpose or the register as a whole holds a value.
Registers serves as connection between a CPU and a Peripheral device (like ADC
or TIMER). By modifying the register the CPU is actually instructing the
PERIPHERAL to do something or it is configuring it in some way. And by reading
a register, the CPU can know the state of peripheral or read associated data.
Fig.: CPU writing to Peripheral Register |
Binary Numbers in C
When you write a=110; in C it means you are setting the
value of variable"a" to "one hundred and
ten" (in decimal). Many time in embedded programming we are
not interested in the value of a variable but the state of each
bits in the variable. Like when you want to set the bits of a register (MYREG)
to a bit pattern like 10010111 (binary). Then you cannot write MYREG=10010111.
Because compiler will interpret 10010111 as decimal. To specify a binary number
in C program you have to prefix it with 0b (zero followed by b). So if you
write
MYREG=0b10010111;
it assigns the bit
pattern 10010111 to the bits of Register MYREG.
HEX Numbers in C
In
same way if you prefix a number by 0x (a zero followed by x) then compiler
interpret it like a HEX number. So
MYREG=0x10;
(10 in HEX is 16 in decimal) MYREG=0xFF;(Set all bits to 11111111 or decimal
255)
Setting a BIT in
Register
Here
our aim is to set (set to logical 1) any given bit (say bit 5) of a given
register (say MYREG). The syntax is
MYREG=MYREG |
0b00100000;
The
above code will SET bit 5 to 1 leaving all other bits unchanged. What the above
code does is that it ORs each Bit of MYREG with each bit of 0b00100000
and store the value back in MYREG. If you know how logical OR works then you
will get it.
In
short you can write the same code as MYREG|=0b00100000;
Now
lets come to practical usage. In practice each bit has got a name according to
its work/function. Say our BIT (the 5th bit) has got name ENABLE, and what it
does is clear by its name,when we set it to 1 it enables the peripheral and
when cleared (0) it disables it. So the right way to set it is.
MYREG|=(1<<ENABLE);
The <<
is called left shift operator. It shifts the bits of LHS variable left
by the amount on its RHS variable. If you write
b=1<<3;
then,
1 whose binary value is 00000001 is shifted 3 places to left which results in
00001000 So if ENABLE is defined as 5 (as enable is 5th bit) then
MYREG|=(1<<ENABLE);
will result in MYREG|=(1<<5);
which again result in
MYREG|=(0b00100000);
Now
a beginner would ask "What's the Advantage ?". And once you know it
you would realize that advantage is immense!
1.
Readability of code: MYREG|=(1<<ENABLE); gives a clue
that we are enabling the peripheral while MYREG|=0b00100000; does not
give any clue what it is doing, we have to go to data sheet and find out which
bit actually ENABLEs the peripheral. While ENABLE=5 is already defined in
header files by the developer of compiler by carefully studying the datasheets
of device.
2. Easier
Portability: Suppose you use this code many times in
your program (and your program is reasonably large and uses other
register also) and you now want the same code to run on some other MCU model.
The new MCU is of similar family but has slightly different bit scheme, say
ENABLE is bit 2 instead of bit 5. Then you have to find all occurrence of
MYREG|=(0b00100000); and change that to MYREG|=(0b00000100); But if you have
used the other method then you simply need to inform the compiler (by its
setting options) that you are going to use the other MCU and compiler will
automatically get the definitions for the new device. And in this definition
ENABLE=2 will already be defined by the compiler developer. So it will be lot
easier.
Clearing a BIT in
Register
For
clearing a bit logical AND(symbol &)
operator is used in place of logical OR (symbol |).
The syntax is as follows
MYREG&=~(1<<ENABLE);
This
will clear (i.e. set to value 0) a given bit (identified by name ENABLE) in a
register called MYREG. This operation will not affect any other bits of
register except ENABLE.
Let us see how it
works with the help of following diagram.
Fig.: "Clearing a BIT" how it works? |
So now you know how you can selectively clear any bit in
any given register. If you want to clear more than one bit at a time you can
write like this
//This will
clear bits ENABLE,FAST_MODE and BUSY, leaving all other bits untouched
MYREG&=(~((1<<ENABLE)|(1<<FAST_MODE)|(1<<BUSY)));
Similarly the syntax
for setting(set to 1) multiple bits at a time is as follows
//This will set bits ENABLE,FAST_MODE and BUSY, leaving all
other bits untouched
MYREG|=((1<<ENABLE)|(1<<FAST_MODE)|(1<<BUSY));
Testing The Status of
a Bit.
Till
now we were modifying the registers either setting or clearing bits. Now we
will learn how can be know that a specific bit is 0 or 1. To Know if a bit is 0
or 1 we AND it with a AND MASK. Suppose if we want to check bit 5 of a register
MYREG then the AND MASK would be 0b00100000. If we AND this value with the
current value of MYREG then result will be non-zero only if the 5th bit in
MYREG is '1' else the result will be '0'.
The syntax would be
like this.
if(MYREG &
(1<<ENABLE))
{
//ENABLE
is '1' in
MYREG
...
}
else
{
//ENABLE
is '0' in
MYREG
}
So now you know the basic operation on bits, they are
widely used in firmware programming and will help you understand other codes on
my web site. And Please don't forget to post your comment regarding any doubts,
or reporting errors in the above article, or simply to tell how you liked the
stuff.
Data Types:
The primary data types required in
Embedded C are:
Data Types
|
|
Size
|
|
Range
|
|
||
|
bits
|
|
bytes
|
|
|
|
|
char or signed char
|
8
|
|
1
|
-128
|
|
to
|
127
|
unsigned char
|
8
|
|
1
|
0.420
|
|
to
|
255
|
int or signed int
|
16
|
|
2
|
-32768
|
|
to
|
32767
|
unsigned int
|
16
|
|
2
|
0
|
|
to
|
65535
|
float
|
32
|
|
4
|
3.4 e(-38)
|
|
to
|
3.4 e(+38)
|
double
|
64
|
|
8
|
1.7 e(-308)
|
|
to
|
1.7 e(+308)
|
User defined type
declaration
C language supports a feature where user can
define an identifier that characterizes an existing data type. This user
defined data type identifier can later be used to declare variables. In short
its purpose is to redefine the name of an existing data type.
Syntax:
typedef
<type> <identifier>; like typedef int number;
Now
we can use number in lieu of int to declare integer variable. For example: “int
x1” or “number x1” both statements declaring an integer variable. We have just
changed the default keyword “int” to declare integer variable to “number”.
Typecasting:
Typecasting
is a way to make a variable of one type, such as an int, act like another type,
such as a char, for one single operation. To typecast something, simply put the
type of variable you want the actual variable to act as inside parentheses in
front of the actual variable. (char)a will make 'a' function as a char.
One use for typecasting for is when you want to use the
ASCII characters. For example, what if you want to create your own chart of all
256 ASCII characters. To do this, you will need to use to typecast to allow you
to print out the integer as its character equivalent.
#include <stdio.h>
int main()
{
for
( int x
= 0; x
< 256; x++
) {
/*
Note the use
of the int
version of x
to output a
number and the
use
* of (char)
to typecast the
x into a
character which outputs
the
* ASCII character
that corresponds to the current
number
*/
printf(
"%d = %c\n",
x, (char)x );
}
getchar();
}
Typecasting
is dangerous because - You might temporarily lose part of the data - such as
truncating a float when typecasting to an int.
One good use for typecasting is - To
allow division of two integers to return a decimal value.
Eg: 3/2=1.5
Following conversions
are possible using typecasting:
➢
int
to float
➢
float to int
➢
char to float
Storage Class:
Storage class provides information about their location
and visibility. The storage class divides the portion of the program within
which the variables are recognized.
auto :
➢
It
is a local variable known only to the function in which it is declared.
➢ Auto
is the default storage class when a variable is declared in a function (i.e.
local variables).
➢
It is realized everytime the function is
called – That is memory is allocated to the variable when it enters the
function and it is deallocated when it leaves the function.
static :
➢
Local variable which exists and retains its
value even after the control is transferred to the calling function.
➢
Static
is the default storage class for global variables.
➢
Static
variables can also be defined within a function.
➢
When defined inside the function the variable
is initialized at run time and not reinitialized when the function is called
again.
➢ Thus inside a function a static
variable retain its value during the various function calls.
➢
Static
variables are inialized to zero automatically.
➢
Following
example shows the use of static variable
void func(void);
static count=10; /* Global variable - static is the default */
main() {
while (count--) {
func();
}
}
void func( void ) {
static i = 5; i++;
printf("i is %d and count is %d\n", i, count);
}
This will produce following result
i is 6 and count is 9
i is 7 and count is 8
i is 8 and count is 7
i is 9 and count is 6
i is 10 and count is 5
i is 11 and count is 4
i is 12 and count is 3
main() {
while (count--) {
func();
}
}
void func( void ) {
static i = 5; i++;
printf("i is %d and count is %d\n", i, count);
}
This will produce following result
i is 6 and count is 9
i is 7 and count is 8
i is 8 and count is 7
i is 9 and count is 6
i is 10 and count is 5
i is 11 and count is 4
i is 12 and count is 3
i is 13 and count is 2
i is 14 and count is 1
i is 15 and count is 0
i is 14 and count is 1
i is 15 and count is 0
extern :
➢
Extern
is like a global variable known to all functions in the file.
➢ Extern
is used to reference a local variable that is visible to all the program files
in that project.
➢
Extern variables are not initialized because
they just point to another variable of the same name present in some other
file.
➢
Extern
is useful when one wants to use a variable or a function from another file.
➢ All
functions in C are extern by default. Hence they are accessible by all the
files in the project.
➢
Following
example illustriates the use of extern variable:
File 1: main.c
main() {
write_extern();
}
File 2: write.c
extern int count;
void write_extern(void) {
printf("count is %i\n", count);
}
Here
extern keyword is being used to declare count in another file. Now
compile these two files as follows
gcc
main.c write.c -o
write
This fill produce write
program which can be executed to produce result.
Count
in 'main.c' will have a value of 5. If main.c changes the value of count -
write.c will see the new value
register :
➢
Register is used to indicate the local
variables which are to be stored in a register instead of RAM.
➢
Hence,
maximum size of
variable = size of register
Constant and Volatile:
constant variable:
const means that something is not modifiable.
Hence
a const variable once assigned a value, that value cannot be changed even if
you want to change it.
volatile variable:
A
volatile variable is the one whose values may be changed at any time by some
external sources.
volatile int
num;
The value of data may be altered by some external factor,
even if it does not appear on the left hand side of the assignment statement.
When we declare a variable as volatile the compiler will examine the value of
the variable each time it is encountered to see if an external factor has
changed the value.
Use of volatile:
-
An object that is a memory-mapped I/O port
-
An object variable that is shared between multiple
concurrent processes
-
An object that is modified by an interrupt service
routine
- An automatic object
declared in a function that calls setjmp and whose value is-changed between the
call
to
setjmp and a corresponding call to longjmp
*With
volatile keyword in the declaration the compiler knows that the value of a
variable must be read from memory every time it is referenced.
➢ The following
program shows a typical example of using a volatile variable: Interrupt service
routines often set variables that are tested in main line code.
One
problem that arises as soon as you use interrupt is that interrupt routines
need to communicate with rest of the code .
A
interrupt may update a variable num that is used in main line of code . An
incorrect implementation of this might be:
static lon int num ;
void interrupt update(void) {
++num ;
}
main() {
long val;
val= num ;
while(val !=num);
val = num ;
return val ;
}
}
Problem and Solution:
•
When compilation system execute while
statement, the optimizer in compiler may notice that it read the value num once
already and that value is still in the register.
• Instead
of re-reading the value from memory, compiler may produce code to use the
(possibly messed up) value in the register defeating purpose of original C
program.
•
Some compiler may optimize entire while
loop assuming that since the value of num was just assigned to val , the two
must be equal and condition in while statement will therefore always be false .
• To
avoid this ,you have to declare num to be volatile that warns compilers that
certain variables may change because of interrupt routines.
static volatile long
int num ;
With
volatile keyword in the declaration the compiler knows that the value of num
must be read from memory every time it is referenced.
Pointers:
Pointers are surprisingly simple, all you
have to do is remember that you're working with memory addresses. For example,
let us use an imaginary block of memory (while at the same time showing off my
beautiful artistic abilities)
-----------------------------------
| 123
|| 124 ||
125 || 126
|| 127 |
-----------------------------------
Now,
when you declare a regular variable, for example: char ch;
The
name 'ch' becomes a synonym for the memory location it is given. If ch is given
the location 123 you can imagine the imaginary memory block to look like this:
----------------------------------
| ch
|| 124 ||
125 || 126
|| 127 |
----------------------------------
Now,
a pointer is a separate variable that refers to another location in memory.
However, when it comes down to everything, a pointer is just another variable.
You declare it using the pointer notation *:
char *pch;
Since pch is just
another variable, our imaginary memory block might look like this:
-----------------------------------------
| ch(123)
|| pch(124)|| 125 || 126
|| 127 |
-----------------------------------------
To give ch a value, all you do is assign that value to
the memory location (cleverly disguised by the name 'ch'):
ch = 'A';
The
process is exactly the same with pointers except instead of arbitrary values,
their value is an actual address in memory. To obtain the address of a variable,
you prefix it with &. So to assign the address of ch to pch you would do
this:
pch =
&ch;
Now
pch's value is the address of ch, exactly like ch's value is 'A'. If you access
pch, you'll get that address:
printf("%p\n", pch);
Now, the great thing about pointers is that since they
are references to the address they contain, you can "dereference"
them by prefixing the * character. This accesses the memory address
contained
by the pointer. The
following printf statements output the exact same thing:
printf("%c\n",
ch); printf("%c\n", *pch);
Why?
Because ch is the memory location that we assigned 'A'. By using a dereference,
*pch is also the memory location that we assigned 'A'.
This
is the basic idea behind the pointer. Any data type in C can have a pointer to
that data type (even functions!). Let's look at a few uses of pointers.
Before
going any further, it's a good idea to mention two special variations of a
pointer. The first is a type of pointer and the second is a special value.
First is the void pointer:
void *pv;
A void pointer is special in that it is the generic pointer, you
can assign any pointer value to void and then cast it back to the original
type.
The reason
this cast is required is because void pointers cannot be dereferenced:
void *pv;
int *pi;
int i
= 10;
pi
|
=
&i;
|
|
pv
|
=
pi;
|
|
printf("%d\n", *pi);
|
/*
Prints 10 */
|
|
printf("%d\n", *(int*)pv);
|
/* Also
prints 10 */
->Typecasting
|
Note that the cast is
made *before* the dereference is performed. This is important, if the cast were
(int*)*pv
You would be dereferencing a void pointer and returning a
pointer to int from that invalid reference. Both are quite wrong.
Next is the null
pointer:
A null pointer is a value that you assign to a pointer
variable when you want it to point to nothing in particular. This value is used
as a safe starting value for pointer variables, and erroneous return values
from functions that return pointers, such as malloc. A null pointer is any
integral value that evaluates to 0 (zero). The two common ways of using null
are the integral constant 0, and a macro defined in several standard headers
called NULL:
#include <stddef.h> /*
For NULL */
int *pi = 0;
char *pc = NULL;
A
null pointer cannot be dereferenced because null is an invalid location in
memory to access. You can only assign null and test for it:
if (pc
!= NULL)
/*
Do something */
if (pc
!= 0)
/*
Do something */
Since both NULL and 0 are both null pointer values, even
if you assign NULL to a pointer variable, you can still test it against 0 to
see if it's a null pointer.
Now that all of that
is out of the way, a few uses of pointers. :)
Pointers as Function
parameters and return types:
Pointers are useful
as function parameters and return types in two ways.
First
is a size issue, imagine that you have a structure variable that is big,
say, 30 bytes. If you pass it to a function the usual way
void f(struct
MYSTRUCT mys);
...
f(var);
A copy of the entire
30 bytes is made. This can be wasteful in both memory and performance.
By passing a pointer to that variable
only the size of its memory address is copied and passed. Using our imaginary
memory setup, instead of 30 bytes, only 2 bytes are copied.
This is faster and
more conservative of memory.
void f(struct
MYSTRUCT *pmys);
...
f(&var);
Notice that the
syntax of the pointer hasn't changed from our original examples.
The
second help with pointers as function parameters and return types is the referencing
feature. By passing mys in the first example, you get a *copy* of the
variable passed to the function. So if you want to make any changes to the
original variable, you have to return the copy with changes made and assign it
to the original:
struct MYSTRUCT
f(struct MYSTRUCT mys);
...
var =
f(var);
By passing a pointer to the function, you are
really copying the memory address of the original variable. Since you're able
to access the memory address of the original variable, you can make *changes*
to it from within another function!
int f(struct
MYSTRUCT *pmys);
...
errorcode =
f(&var);
Return values work
the same way with pointers, you can return a pointer for improved efficiency,
and so that a variable can be changed outside of the function. There is one
problem that you should take into consideration though -
Local
variables are destroyed and their memory reclaimed at the end of the enclosing
block, so returning a pointer to a local variable is not correct:
int *f(void) {
int i = 10;
return &i; /* No! Returning a local variable is bad! */
}
Returning memory that
you control (such as memory returned by malloc and calloc) is okay though:
int *f(void) {
int *i = malloc(sizeof (int));
return i; /* Okay, you control when the memory is released */
}
Dynamic data
structures
Pointers
are used heavily for data structures that can grow and shrink dynamically at
runtime. This dynamic sizing requires the programmer to handle memory through
pointers using malloc/calloc, realloc, and free. Such data structures can be
node based (linked lists, binary trees, etc...) or block based (resizable
arrays).
To
use a pointer to create an array, simply malloc enough memory for it by
multiplying the size of the element type by the number of elements you want:
int *parray = malloc(10 * sizeof (int));
if (parray != NULL){
/* Okay, use it */
}
Note
that malloc, calloc, and realloc all return null pointers if they fail. The
above code is basically the same thing as using static arrays:
int array[10];
The only big differences are that you have control of the size
of parray at runtime, you can grow it or shrink it by using realloc.
The second big difference is that you *must* remember to free
the memory you allocate using malloc, calloc, or realloc.
Iterating through an
array and saved locations
Pointers
are helpful for iterating through arrays and saving things that you want to
come back to. For example, if you have the following string:
char s[]
= "This is
a test";
And you want a
pointer to the beginning of each word, you can simply do this:
char *a = s;
char *b = s + 5;
char *c = s + 8;
char *d
= s + 10;
Now
a points to "This is a test", b points to "is a test", c
points to "a test", and d points to "test". Note that
pointers can have arithmetic performed on them. The name of an array is a
pointer to the first element, so if you add 1 to it you get the second element.
The above could also have been written as
char *a
= &s[0];
char *b =
&s[5];
char *c = &s[8];
char *d = &s[10];
Arrays
and pointers are closely related but are *not* the same. Keep this in mind when
mixing them.
If a beginner in C
wanted to print each element in an array, he would most likely do this:
int array[10]
= {1,2,3,4,5,6,7,8,9,-1}; int i;
for (i = 0; i < 10; i++)
printf("%d\n", array[i]);
or
for (i = 0; array[i] != -1; i++)
printf("%d\n",
array[i]);
This can also be done
using pointers:
int
array[10] = {1,2,3,4,5,6,7,8,9,-1};
int *pi;
for (pi = array; *pi != -1; pi++) printf("%d\n", *pi);
char *pc = &c;
int *pi = &i;
pc++; /* Moves forward 1 byte */
pc++; /* Moves forward 1 byte */
pi++; /* Moves forward 2 bytes */
This
is important to remember because you don't have to manage how much memory is
jumped over, the compiler does all of this for you.
Miscellaneous:
Pointers can be constant, but since a pointer really has
two parts (the pointer itself and the address it points to), one or both or
neither can be constant. You do it like this:
const int
*p; /* The pointed-to value is const */
int * const p; /* The pointer itself is
const */
int *p; /* Both are not const */
const int
* const p;
/* Both are
const */
Pointer and Array:
Pointer to an Array:
Declaration : (int*)
a[5];
Representaion:
a ->
| ||||
100
| ||||
101
| ||||
102
| ||||
103
| ||||
104
|
a is a pointer
pointing to address a[0], that is it is
pointing to address of 1.
void (*pf)(void); /* Pointer to a function that takes no args and returns nothing */
void f(void) {
printf("Hello, world!\n");
}
pf = f; /* Not calling f, must be taking the address */
Array of Pointers:
|
||||
Declaration : int*
a[5];
|
||||
Representaion:
|
||||
a =
|
->
|
|||
100
|
||||
101
|
->
|
Pointers
pointing to
|
||
102
|
->
|
|||
other
memory
|
||||
103
|
->
|
location
|
||
104
|
->
|
|||
a is and array containing pointers
101 is a pointer pointing to address of another memory location.
101 is a pointer pointing to address of another memory location.
Pointer and
Functions:
Pointer to a
function:
There are two uses of a function, call it, or take its
address. If you aren't calling a function then you can assign its address to a
pointer. The declaration for a pointer to a function is complex, just pretend
you're writing a function prototype, then prefix the name with * and surround
both with parentheses:
void (*pf)(void); /* Pointer to a function that takes no args and returns nothing */
Now you can assign a
function with the same type:
void f(void) {
printf("Hello, world!\n");
}
pf = f; /* Not calling f, must be taking the address */
Now that pf points to
f, you can call f *through* pf:
(*pf)(); or pf(); //both are same
Pointers can also point to other pointers:
The
standard example of a pointer to a pointer is in the case of memory allocation.
Say you want to pass a pointer to a function and allocate memory to it. You
want to return an error code, so returning the freshly allocated memory isn't
an option:
int allocme(int
*parray)
{
parray
= malloc(10 *
sizeof (int));
if (parray == NULL) return -1;
return
0;
}
...
int *pa;
if (allocme(pa)
!= -1)
/*
All is well,
use pa */
This looks like it should work fine.
By passing a pointer to int you can change the original, right? Right, but
that's not what we want to do. We want to change the *pointer* that points to
int, not the int that it points to. This is a problem because the pointer is a
copy. The solution is the same as when we wanted to make changes to the
original structure variable, pass a pointer to it. :)
int allocme(int
**pparray)
{
*pparray
= malloc(10 *
sizeof (int));
if (*pparray == NULL) return -1;
return
0;
}
...
int *pa;
if (allocme(&pa) != -1)
/*
All is well,
use pa */
Once again note that the syntax for a
double pointer is consistent with a single pointer, just tack on an extra * for
the declaration and dereference. Just make sure that you dereference the outer
pointer first. Pointers to pointers to structures can cause problems because of
this and when you want to access a member you have to surround the first
dereference with parentheses:
struct MYSTRUCT
**ppmys;
...
(*ppmys)->member;
/* Works okay */
*ppmys->member; /* Doesn't work right */