Вы находитесь на странице: 1из 126

Programming PIC microcontrollers in C:

Module 1: First program.


LOGIN
(change page)

First of all visit this page to setup hardware and software for the course.

This module examines the structure of a C program and shows you how to control
the input and output ports of the microcontroller.

First a look at the microcontroller...

A brief review of the microcontroller

The course microcontroller contains all the parts needed to make a fully functional
system including Flash memory, RAM and other internal peripherals.

The 16F88 has six internal peripherals including timers, a UART and analogue
comparators making it useful in many different projects.

16F88 features

Flash memory

This is one of the most important parts of the microcontroller storing the compiled C
program (programmed into the microcontroller using the hex file generated by the
compiler).

The best part about Flash memory is that you can keep on re-programming it at
least thousands of times and for many newer devices hundreds of thousands of times
(16F88 - 100,000 re-programming cycles).

Flash memory is non-volatile meaning that it keeps its contents even when the
power is turned off.

One extra benefit is ICSP (In Circuit Serial Programming) interface which allows you
to re-program the device while it is still in your circuit.

RAM memory

Ram is the volatile storage are which means the contents are lost after the power is
removed. Unlike Flash memory you can easily change the values contained in
it while the program is running.

You use RAM to store values within your program e.g. to count the number of events
on a pin or to store readings from a temperature sensor etc.

Internal units

1
The 16F88 has six internal peripheral which makes it extremely powerful:

• Three timers
• ADC
• Analogue comparator
• CCP (PWM)
• SSP (I2C, SPI)
• USART

This course uses two of the available peripherals namely the PWM (Pulse Width
Modulator) and the USART (for communicating via RS232 to the PC). As the course
progresses circuits using these peripherals are introduced.

Videos
Before you start using the compiler here is a short video demo showing you how to:

• Load a project.
• Edit the source code.
• Compile the source code into a hex file.
• Add project files to your own projects.

The following videos are in shockwave flash format (to install a free viewer visit this
site www.adobe.com).

Using the MikroC compiler 1.9M


Link disabled (part of paid course)

The next video shows you a programming sequence using ICPROG including initial
setup:

Using ICPROG programming


1.1M
software

You can download the reference manual here:

PIC Micro C Reference manual 1.1M


Link disabled (part of paid course)
(right click and "save target as" to save to your computer)

To install a free pdf viewer for reading the manual visit www.adobe.com.

C Compiler and ICPROG


If you haven't downloaded and installed the compiler you should do it now - just re-
visit the C course introduction page and follow the instructions there.

You should also install ICPROG and setup the circuit shown on that page.

All set ?

2
Right lets begin...

PORTS
For microcontrollers the most important aspect in using them is how you
communicate from a program running inside the chip with the outside world and the
answer is that you use I/O ports.

PIC Microcontrollers have several 8 bit ports depending on the number of pins
available and they are usually labeled starting at A and these are the input and
output ports.

So the first port is labeled PORTA and you can use this name in a C program to refer
to that port. If you look at the microcontroller diagram below you'll see pins on the
chip labeled RA0 to RA7 these represent all the bits for PORTA. You can review bit
and byte representations here and it's also in the free reference guide that
accompanies this course.

Note: When outputting a logic 1 the output voltage is 5V (i.e. the supply voltage)
and when outputting logic zero the voltage is 0V. The same applies to inputs.

Notice that I said input and output ports. This because a port can be either an input
port or an output port and not only that each pin within the port can be an input
or an output!

This makes a port extremely flexible as you could assign one pin to read a button
while another (in the same port) outputs a control e.g. turning on an LED or a fan
etc.

Another important aspect of this that you can also change a port pin to either input
or output on the fly i.e. while the program is running - but this is best left for
advanced use e.g. communicating with a Dallas 1 wire device or using I2C.

The C compiler uses the same port labels that you find in the 16F88 data sheet so for
the 16F88 there are two 8 bit ports labeled PORTA and PORTB.

In the diagram below each port bit is assigned to a fixed pin on the device (PORTA
bits are labeled RA0 to RA7 and PORTB bits are labeled RB0 to RB7). The other
labels are the alternate pin functions for internal peripherals (it shows a reduced
description - there are a few more labels!).

3
Note: Since internal peripherals are multiplexed with some pins you have to decide
what the pins should do i.e. you may want to use the internal UART.

Its RX and TX input and output are connected to pins 8 and 11 and multiplexed with
RB2 and RB5 (PORTB bits 2 and 5).

If you want to use the PORTS just as normal I/O then you turn off the UART (the
default state).

For the moment we'll just assume the simplest configuration i.e. not using internal
peripherals.

Download
Here's the first C program when you have reviewed the page download the file and
program your microcontroller.

Note: While you are reading this module you can open the source code in a separate
window (just go to the end of the code display below where you will find a link
labeled 'Open source code').

Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the this module if you are unsure how to do
this.

Download ZIP file program 1 : Download here.

Note: the download has the hex files and compiler project control file as well as the
source code.

4
//////////////////////////////////////////////////////////
//
// File: prog01-16F88-port-flash.c
//
// Author: J F Main.
//
// Description:
//
// Program 1 of C programming course
//
// Compiler : mikroC, mikroElektronika C compiler
// for Microchip PIC microcontrollers
// Version: 6.2.0.0
//
// Note Testing:
//
// Tested on 16F88
//
// Requirements:
//
// Clock : 4MHz (Internal)
//
// Target : 16F88
//
// Version:
// 1.00 - Initial release.
//
// Copyright : Copyright © John Main 2006
// http://www.best-microcontroller-projects.com
// Free for non commercial use as long as
// this entire copyright notice is included
// in source code and any other documentation.
//
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// Start here
//
void main(void) {
/* Setup 16F88 */
OSCCON = 0x60; /* b6..4 = 110 = 4MHz */
ANSEL = 0; /* all ADC pins to digital I/O */
TRISB = 0; /* set as output */
while(1) { /* infinite loop */
PORTB = 0xff;
delay_ms(200);
PORTB = 0;
delay_ms(200);
}
}

If you click this link it displays a separate window displaying the C program shown
earlier - so you can view the source code while reading the description:
Open source code [opens new window]

5
Program 1.1

Internal and External Controls

You need to know about internal and external controls as these are controls that let
the microcontroller start in the way that you want.

Note: If you want to you can skip this section and come back here later as these
controls have been set for you in the downloaded code and they are not specifically
about learning C. They are needed to start the microcontroller running though.

Here is a little bit of detail on why there are certain register values within the
program (ANSEL and OSCCON).

Internal controls are registers that you can use to control the operation of the
internal peripherals and they are the ones that you can change from within the
program e.g. change the baud rate of the USART on the fly, change port
directions etc.

External controls control the features of the chip that must be setup before the
microcontroller can start i.e. it must know if it is to use an external crystal or the
internal clock before it starts - otherwise it will never start. These control values are
written into a separate part of the microcontroller memory that is not accessible
during the normal operation of the chip. It is accessible by the ICSP port so ICPROG
can set the chip up for you

Internal Controls
First of all the two statements...

OSCCON = 0x60; /* b6..4 = 110 = 4MHz */


ANSEL = 0; /* all ADC pins to digital I/O */

... are used to setup the 16F88. The 16F88 has many internal peripherals and most
of them default to off but the ADC does not so you have to turn off the ADC to use
the pins as normal digital I/O using ANSEL=0.

Note: You can find out about all the 16F88 by studying the datasheet for the 16F88
and you will find the same labels in it i.e. ANSEL and OSCCON.

Here are sections of the 16F88 datasheet showing the above registers:

OSCCON register

6
OSCCON is the oscillator control register and here it sets up the frequency of the
internal oscillator to 4MHz.

You can ignore most of the controls in the OSCCON register as the default mode is
the one you need. So bit 6-4 is the one to look at and controls the Internal RC
frequency. The C code

OSCCON = 0x60;

... translates to a binary value of 01100000 - the way C code is operating is


explained in later modules - i.e. bit 6=1, bit 5=1 and bit4=0 giving an oscillator
frequency of 4MHz.

7
ANSEL register

ANSEL is the analogue select control register and it defaults to on so all analogue
pins are set to receive analogue signals. For the initial project you need them set to
digital I/O i.e. no ADC use.

ANSEL = 0x00;

... translates to a binary value of 00000000 - the way C code is operating is


explained in later modules - Since each bit is zero you can see from the table above
that each pin is now a digital I/O.

If you look at the chip diagram the pins that can be analogue inputs are all labeled
ANn where n is 0 to 6 - these are all on PORTA. These pins are all now digital I/O i.e.
they operate the same as a chip without an ADC.

External controls
One question you might have is how does the compiler set up the microcontroller to
use the internal or external clock?

For this compiler a separate screen is used for each microcontroller and this makes it
easy to see what controls are available for individual microcontrollers. Here's a
screenshot for the 16F88:

8
As you can see there are quite a few controls (and not all are shown) but for this
discussion the important ones are the oscillator controls:

• _EXTRC_CLKOUT
• _EXTRC_IO
• _INTRC_CLKOUT
• _INTRC_IO
• _EXTCLK
• _HS_OSC
• _XT_OSC
• _LP_OSC

These are all the oscillator options and here the internal oscillator is selected and the
clock out pin is set for use as a normal I/O pin. For an external crystal at 4MHz you
would select _HS_OSC and deselect the others.

Other controls that are useful are :

9
BODEN_OFF

Brown out detect off - If this is left on it detects slight dips in the power supply which
don't stop the microcontroller operating but which may have caused data corruption
e.g. if a RAM value changed state because of the power dip.

For non-production and non-critical projects just turn off this control otherwise the
microcontroller will automatically reset on a 'brown out'.

MCLR_OFF

Master Clear control (or Master reset input). This input is present on all PIC devices
and lets you reset the processor using an external input voltage. It's active low so
holding it low for a while will reset the processor.

On older processors e.g. the 16F84 you had to have an external reset circuit but on
this one you have the option of using an internal reset circuit (and then using the pin
as a usable input pin). This saves having to have a dedicated reset circuit - meaning
you don't have to wire up an external circuit - it's all taken care of internally.

WDT_OFF

The Watch Dog Timer is an independent internal timer that you have to keep
resetting *using software) in order for the WDT not to time out. It's a software
protection module and if it times out the processor will be reset.

Note: You can look up the meaning of all the other controls in the 16F88 datasheet.

"Start Learning C Here"


This is the start of the C part of the course where you start learning the structure of
a C program.

The main function


The first line is ...

void main(void) {

This is the program start point declared as a function. Functions are used throughout
C programming to define the action of a piece of code and the function name in this
case is main. In general you can define your own names for other functions that you
declare but 'main' is a keyword and the 'main' function must be present and it tells
the compiler where to start the program.

Note: You can create as many functions as you want as long as you use valid
characters for the function name see the reference manual.

10
The keywords words 'void' you'll come back to when you look at function
declarations in more detail.

I talked about the next two statements earlier on:

OSCCON = 0x60; /* b6..4 = 110 = 4MHz */


ANSEL = 0; /* all ADC pins to digital I/O */

You may have skipped this section (Internal controls) as they setup the 16F88 and
are not needed to understand the C program. As we go through using the 16F88
they will be discussed more.

Braces { }
Braces are used to define the contents of a function or statement block – the main
function has a start and end brace that defines its contents.

A statement block is just a group of statements (each ending in a semicolon)


enclosed within braces (see the 'while' keyword below).

In this program there are two enclosing braces one for the main function and one for
the while statement.

Note: Each opening brace '{' must be matched up to its corresponding closing brace
'}'.

Port direction
The statement ...

TRISB = 0;

... sets all the pins in PORTB to outputs.

The word TRISB is defined for you in the compiler setup files but all it is actually
quite simple it is an address in the PIC SFR (Special Function Register) memory.

Note: The TRIS register is probably labeled after the an electronic engineering term
'tristate or three state' in which chips (e.g. 74HC125) have a tristate control pin that
is active low. When low the output is enabled . In fact this is the exact opposite of
the way in which most other microcontrollers work.

We won't do it in this module but to set the ports as inputs you would write:
TRISB = 0xFF;

Infinite loop
The next statement group is started with the keyword while

11
while (1) {

statements...

...which is a loop construct which we'll come back to later on – all you need to know
about it for now is that it defines a continuous loop that keeps executing the
statements enclosed in braces until the power is turned off.

Each statement within the braces is executed one after the other until the matching
end brace after which execution continues again following the opening brace.

Note: There can be other nested braces for other statements as all C constructs use
braces and it's important to place the braces so you can see that they match
together.

You can use any style you like and some people write

while(1)
{

statements...

I just don't like the way it looks.

Note: Most editors have a function to identify the matching brace and this helps
when you have lost one!

Program 1 : action code


So the interesting bit of the code – that actually does something useful is:

PORTB = 0xff;
delay_ms(200);

PORTB = 0;
delay_ms(200);

You can review microcontroller port use in the reference manual.

All the pins of PORTB are set high then it waits for 200 milliseconds (0.2 seconds)
then all the pins of PORTB are set low then it waits 200 milliseconds and then the
whole lot is repeated i.e. All LEDs flash on and off together.

Note: the function delay_ms(200); is a built in function that stops the microcontroller
from doing anything else until the specified number of milliseconds have passed i.e.
It forces the microcontroller to wait for a set time delay.

12
Semicolons
Did you Notice the semicolon at the end of each statement? This is how you separate
individual statements from each other and it is unlike some other languages where
each statement is placed on a separate line to define a complete statement. C uses
this method giving you more flexibility in writing code. For short pieces of code you
don't need to write many lines.

For example using an if-then-else construct (covered later) you could write:

if (a==10) c=20; else c=0;

Or you can put more than one semicolon defined statement on a line
making it easier to read the code. For instance you could also write:

if (a==10)

c=20;

else

c=0;

This is also valid C code using whitespace space for readability. Both ways of writing
the code are valid and you choose which is best to show how your code works. For
instance if you had lots more statements then the second structure would show the
code operation better.

Note: The statement block does not need a semicolon to finish its definition so there
is no semicolon after a closing brace '}'.

Whitespace
In C programming you can use as much whitespace (spaces, tabs,newlines) as you
want to make your code more readable. Which do you like best ?

PORTB = 0xff;
delay_ms(200);

PORTB = 0;
delay_ms(200);
PORTB=0xff;delay_ms(200);PORTB=0;delay_ms(200);

Both of these will be seen by the compiler as correct C you can see which is easier to
read.

13
The circuit

14
Note:

Layout this circuit noting that the diode D7


is the top right diode in a dice display so
allow room to the left and below D7.

Summary Program 1 : What you have learned


• Structure of the 'main' function.
• Braces define the contents of a function or statement block.
• Use of semicolons to define the end of statements.
• Use of whitespace for clarity.
• Sending Output to a microcontroller Port (PORT command).
• Controlling the directions of microcontroller pins (TRIS command).
• Use of a delay_ms function.

Program 1 : Exercises
1. Change the delays in the source code to delay_ms(500).

Recompile and see the effect. This will get you more familiar with programming the
chip.

2. Change the oscillator speed to 8Mz and observe the results (OSCCON).

First change only the source code and re-compile. Secondly change the source code
and the clock speed in the compiler 'Edit project' settings shown above. The reason
you see a difference is that the compiler uses the clock speed setting to calculate the
required delay_ms operation. The other use for the clock speed is in simulation.

Note The compiler can not analyze the source code settings (as you could change the
clock speed in different areas of the code e.g. power saving) so you have to
remember to keep the speed you specify in the compiler the same as the code.

15
Programming PIC microcontrollers in C:
Module 2: Controlling individual port bits.
Here are the links to download second video tutorial and the reference manual:

The MikroC compiler video shows you how to:

• Load a project.
• Edit the source code.
• Compile the source code into a hex file.
• Add project files to your own projects.

Using the MikroC compiler 1.9M

The reference manual contains practical advice as well as C how to write C correctly.

PIC Micro C Reference manual 1.1M

(right click and "save target as" to save to your computer)

To install a free pdf viewer for reading the manual visit www.adobe.com.

Module 2
The previous program is rather like using a hammer to crack a nut since all the port
pins were turned on and off at the same time.

This section shows you how to control individual bits in a port without affecting other
bits in the same port.

First though lets look at a fundamental C concept - operators.

Operators and expressions


An operator is simply a mathematical operation that describes the action you want to
take on a number or variable (for variables - see next module) for example + is an
addition operator.

An expressions is a term used to describe a complete calculation so an expression


has operators and numbers and variables. An example expression is:

3*4 + 5*6

Just writing this down immediately gives you a problem since a processor has no
intelligence and does just what it is told - the question is how does the C compiler
compute the result...

16
The result could be
3*4 = 12 ,
then add 5 =17,
then multiply by 6 = 102.
Or
4+5=9
*3 =27
*6=162
etc.

The way that the expression is worked out is by using something known as operator
precedence and it gives each operator an importance grading relative to each other
operator. This means that the more important operators are worked out first.

In C multiplication and division are more important than addition and subtraction so
the example actually works out as :

3*4 = 12
5*6 = 30
12+30 = 42

Note: The reference manual has a complete list of operators and their precedence.

Sometimes you want to force a expression to evaluate in a specific order regardless


of the compiler ordering and you can do this using parenthesis - any part of the
expression enclosed in thing parenthesis is calculated first so:

3*(4 + 5)*6

... is calculated as

(4+5) = 9
* 3 = 27
* 6 = 162

There are a lot of operators and they are all presented here - most of them do
exactly what you would expect i.e. the arithmetic ones and relational operators are
obvious.

17
Arithmetic Relational Logical Bitwise
Invert
Test for Logical (complement)
* Multiply == ! ~
equality negation each bit is
inverted.
Test for Logical
/ Divide != && & AND
inequality AND
Greater logical
+ Addition > || | OR
than OR
- Subtraction < Less than ~ XOR
Modulus
(remainder Greater
% from >= than >> Shift right
integer equal
division).
Smaller
++ Increment <= than << Shift left
equal
-- Decrement

Note: You can find out more information about these operators in the reference
manual.

One slightly non obvious one is the test for equality i.e. if you want to test if two
variables are the same you can write:

if (a==b) do_something;

This is always confused with the assignment operator

a=b; set a to the value of b.

Just keep that in mind as its a common mistake since the C compiler will not warn
you that this is a mistake.

if (a=b) do_something; // Wrong code

In above statement the expression result is the value of b since b is assigned to a


and the result is returned - the intention is probably this:

if (a==b) do_something; // Correct code

Note: we'll cover if statements and variables later on.

Logical operators
Logical operators are the operators you will use in working out, lets say 'normal'
everyday calculations.

18
For example if you wanted to know if two variables were the same but you also
wanted to know if one of them was greater than 10 then you can use logical
operators. The expression you would use is

(a==b) && (b>10)


Here parenthesis defines the calculation order but it's probably fine without them
(check order of precedence table) but using them ensures the order you want.

The logical operator works with numbers and uses a boolean system where

Logic 1 : is any non zero number.


Logic 0 : is zero.

The difference between a logical operator and a bitwise operator is that as the name
suggests a bitwise operator works on the individual bits within a variable. If you
wrote

(a==b) & (b>10)

... it may or may not work as the compiler will individually 'AND' together each bit
within the results - if a result returned a number (not just 1) as boolean true then it
would fail.

Just remember to use '&&', '||' and '!' for normal coding.

Bitwise operators
As mentioned bitwiise operators work on individual bits of each variable so for
controlling output ports they are ideal.

This is the sort of operation for setting an individual port bit to output a logic 1 (5V)

PORTB = PORTB | (1<<5);

...and it's explained more further on.

The best way to learn about them is to see them in action so work through the
following information and re-program the 16F88.

Download program 2.1


Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the first module if you are unsure how to
do this.

Download ZIP file program 2.1 : Download here.

Note: the download has the hex files and compiler project control file as well as the
source code.

19
//////////////////////////////////////////////////////////
//
// File: prog02.1-16F88-pin-flash.c
//
// Author: J F Main.
//
// Description:
//
// Program 2.1 of C programming course
//
// Compiler : mikroC, mikroElektronika C compiler
// for Microchip PIC microcontrollers
// Version: 6.2.0.0
//
// Note Testing:
//
// Tested on 16F88
//
// Requirements:
//
// Clock : 4MHz (Internal)
//
// Target : 16F88
//
// Version:
// 1.00 - Initial release.
//
// Copyright : Copyright © John Main 2006
// http://www.best-microcontroller-projects.com
// Free for non commercial use as long as
// this entire copyright notice is included
// in source code and any other documentation.
//
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// Start here
//
void main(void) {

/* Setup 16F88 */
OSCCON = 0x60; /* b6..4 = 110 = 4MHz */
ANSEL = 0; /* all ADC pins to digital I/O */
TRISB = 0; /* set as output */
PORTB = 0x00; /* zero out */
while(1) { /* infinite loop */
PORTB = PORTB | (1<<5); /* Set bit five of PORTB */
delay_ms(200);
PORTB = PORTB & ~(1<<5); // Clear bit five of PORTB
delay_ms(200);
}
}

To open the source code in a separate window click here:

Open source code [opens new window]

20
Program 2.1 : Individual bit control
This program is virtually the same as the previous module's code but uses the
following statements to control an individual output port pin:

PORTB = PORTB | (1<<5);

PORTB = PORTB & ~(1<<5);

Note: You can review how these instructions work in reference manual and you
should also review the section on Read-Modify-Write to see how these instructions
may not work (due to the Read-Modify-Write action and the PIC microcontroller
PORT hardware) and how to solve that problem.

The sequence of this statement PORTB = PORTB | (1<<2); is as follows:

Note : The vertical bar is the bitwise OR operator.

1. The value of PORTB is read.


2. The value of (1<<5) is 'ored' with it.
3. PORTB is set to this combined value.

The result is that the instruction sets the sixth bit to the left in the port i.e RB5 high.

Here is the bitwise 'or' action in bits (setting a bit):

PORTB = PORTB | (1<<5); /* Set bit five of PORTB */

PORTB = 0xc1 11000001


(1<<5) 00100000
PORTB | (1<<5);| 11100001

PORTB OUTPUT 11100001

The next statement does a similar job but resets the bit using a bitwise 'and'
operator (&).

Here is the bitwise 'and' action in bits (resetting a bit):

PORTB = PORTB & ~(1<<5); // Clear bit five of PORTB

21
PORTB = 0xc1 11100001
(1<<5) 00100000
Bitwise invert ~(1<<5) 11011111
PORTB & ~(1<<5) 11000001
PORTB OUTPUT 11000001

Different code styles for bitwise operators


You can also write these statements in the following ways:

PORTB |= (1<<2);
PORTB &= ~(1<<2);
PORTB = PORTB | (1<<2);
PORTB = PORTB & ~(1<<2);
PORTB = PORTB | 0x04;
PORTB = PORTB & ~(0x04);
PORTB = PORTB | 4;
PORTB = PORTB & 251

Note: These operators are discussed further in the reference manual.

Each of these pairs of statements produces exactly the same output machine code. C
lets you write code in many different ways and you should choose the style that is
easiest to read for the task you are solving.

For instance in this program bit manipulation is the task so the first method is most
appropriate. If the port was being used to output a number then the last example
would be most appropriate.

Commenting
Did you notice the comments in the code? these are the sections of text enclosed in
/* and */ (see the reference manual on 'comments').

These two are known as block comments and you can use them over several lines
not just the single lines shown here.

To be honest I find them an absolute pain – although they are correct C code you
have to match up the start comment token '/*' to its corresponding ending comment
token '*/'.

There is another single line comment token '//' that comments out anything from
where it is put to the end of the line. This keyword is 'borrowed' from C++ but it is

22
so useful compilers commonly support it in C code. You can see it in use at the end
of the example.

With this keyword '//' you don't have to remember start '/*'and end '*/' comment
keywords and you don't have to match up start and end comment keywords.

DOWNLOAD Program 2.2


Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the first module if you are unsure how to
do this.

Download ZIP file program 2.2 : Download here.

Note: the download has the hex files and compiler project control file as well as the
source code.

Program 2.2 : Reading ports


This program pulls together all the information you have learned so far so it uses the
TRIS registers but also sets individual bits in these registers to control the port
direction as input.

Program 2.2 operation


The program takes the input pin at RA5 and sends it as output to RB5 for display on
the LED.

In the following program only bit 5 of PORTA is set as input while all the bits of
PORTB are outputs. Only RB5 will react when the button is pressed since only RA5
changes when you push the button.

PORTA is continually assigned to PORTB every time the program goes around the
while loop so any changes to the inputs on PORTA are reflected in the outputs at
PORTB.

Just press the button and the LED will be on when the button is pressed and off
when released.

Notice: You are now controlling TRISA as well - this was set to a default value which
is defined in the 16F88 datasheet (which is why you could ignore it before).

23
//////////////////////////////////////////////////////////
//
// File: prog02.2-16F88-port-read.c
//
// Author: J F Main.
//
// Description:
//
// Program 2.2 of C programming course
//
// Compiler : mikroC, mikroElektronika C compiler
// for Microchip PIC microcontrollers
// Version: 6.2.0.0
//
// Note Testing:
//
// Tested on 16F88
//
// Requirements:
//
// Clock : 4MHz (Internal)
//
// Target : 16F88
//
// Version:
// 1.00 - Initial release.
//
// Copyright : Copyright © John Main 2006
// http://www.best-microcontroller-projects.com
// Free for non commercial use as long as
// this entire copyright notice is included
// in source code and any other documentation.
//
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// Start here
//
void main(void) {

/* Setup 16F88 */
OSCCON = 0x60; /* b6..4 = 110 = 4MHz */
ANSEL = 0; /* all ADC pins to digital I/O */
TRISA = (1<<5); // Set only RA5 as input.
TRISB = 0x00; // Set all as output (only RA5 used).
while(1) { /* infinite loop */

PORTB = PORTA; // Copy port A to port B.


}
}

To open the source code in a separate window click here:

Open source code [opens new window]

Circuit Diagram

24
Here is the circuit adding 1 button to what you have so far:

Here RA5 is used as an input and the diode stops contention if you happen to push
the button while programming - the programming voltage wins so the 5V power
supply is isolated from Vpp.

25
Summary : What you have learned
• You can write the same C code in many different ways.
• The best way to control single bits is using the shift left function.
• The best coding style is the one most appropriate for the task.
• Commenting the code helps you remember what the code does.
• Individual TRIS bits control individual pin directions.

• Registers have default values on power up.

Exercises
1. Change the code from

PORTB = PORTB | (1<<5); /* Set bit five of PORTB */

to

PORTB = PORTB | (1<<4); /* Set bit five of PORTB */

to prove that the software is only updating one bit - now bit 5 is never set so there is
no flashing LED - bit 4 is constantly high and bit 5 is constantly low.

2. Change the code to use different style bit operators and check the code works the
same way.

Programming PIC microcontrollers in C:

26
Module 3: Variables, types and
Conditionals.
One of the most important tasks you can ask a microcontroller to do is to make
decisions and this is what distinguishes a microcontroller from discrete logic - its
ability to make decisions on many variables or from input pins or the state of internal
peripherals.

This module flashes the led as the other module did but using conditional
statements. This will show you how to use conditional statements for making
decisions in the program

Using the C language the microcontroller can make decisions using any input data or
expression. An expression can be a mathematical expression which can also include
variables or peripheral register values.

Variables
A variable is simply a storage area within the microcontroller RAM that is given a
name and in which you can store information. You can also update and change the
information stored so it contains contain varying data hence the name: variable.

The fundamental size of the RAM space in the mid range PIC microcontroller is 8 bits,
or a Byte which should mean that you can only store numbers in the range 0 to 255
(unsigned) or -128 to 127 (signed).

If this was true then the microcontroller would not be much use and the way round it
is to use several Bytes at once to store a larger number. In machine code you use
the carry flag to transfer information from the LSB to the MSB. In C all the machine
code is worked out for you and all you need to do is tell the compiler what type of
variable you want to use.

The type of the variable tells the compiler how many bytes you want to use in a
single operation and how it should interpret those bytes.

Variable Types
There are five fundamental types

char, short, int, long, float

The first four are known as integer types since they store a binary value i.e. each bit
of the bytes in use is binary weighted (click here for more information on binary
representation)

In short the type declarations char, int, short and long are integer types storing
whole numbers while float stores fractional numbers. For the integer types you can

27
write signed or unsigned in front of the type name to tell the compiler what number
range you want to use:

The amount of RAM used by each type and number range is shown below:

Type Size Range


signed char 1 byte -128 to 127
short (int) 1 byte -128 to 127 (default is signed)
int 2 bytes -32768 to 32767 (default is signed)
-2147483648 to -2147483647
long 4 bytes
(default is signed)

Type Size Range


unsigned char 1 byte 0 to 255
unsigned short
1 byte 0 to 255
(int)
unsigned int 2 bytes 0 to 65535
unsigned long 4 bytes 0 to 4294967295

Floating pint types can hold large numbers:

±1.17549435082 * 10-38 .. ±6.80564774407 * 1038

Note: For different compilers the number of bytes used by each type may be
different as the C language does not specify this information. So on a different
compiler you may get different number ranges - this can be a problem when porting
code to a different target.

Note: Types are covered more fully in the reference manual.

Choosing variables
The reason that you want to choose a variable instead of just saying:

"OK a long will do because it covers any integer calculation that I can think of."

... is that variables take up your most precious resource i.e. the limited RAM space.

Also each larger representation uses up more processing power since each one is
slightly more difficult to do. The exception is floating point which is extremely
difficult and occupies huge amounts of RAM.

Note: for avoiding floating point use a fixed point representation (see website tips
and techniques).

28
So you should choose the most appropriate variable for the job you want to do i.e.
the minimum size you can get away with!

Variable declaration
This is a fancy way of say "how to tell the compiler what variable I am going to
use". All variables must be declared before they are used - that sounds obvious but
some languages let you declare variables at any time.

Variables are declared at the start of a function statement block i.e. after the starting
brace of the function:

void main(void) {
int a=0;

.... statements;

These are known as local variables as they exist only within the function (so you can
use the same variable name in another function without them interacting with each
other).

The reason that the C compiler likes variables declared first is that it allocates space
for the variables in RAM. In addition some variables are only required for temporary
storage of data (see 'local variables' in the reference manual) and the compiler can
re-use the RAM location again (for another function).

This is one very good reason for using a compiler rather than machine code as the
compiler is optimizing the RAM space and you don't have to examine each machine
code routine to see if you can re-use a variable.

To declare a variable you write the type you want followed by the name you decide
to use and an optional initializer followed by either a semicolon to end the
declaration or a comma to start a new variable:

Using variables
short my_var = 0; // A short variable called 'my_var' initialized to zero.

short myv1=10, myv2=0; // Two variables declared and initialized.

short my_var; // An uninitialized variable.

To use a variable you can either set its value or fetch its value:

To set it write flash = 10; // flash is set to value 10.

To retrieve it write a = flash; // variable a is set to value of flash.

29
Variable assignment
You can assign numbers to variables easily

char my_char;
short my_short;
int my_int;
long my_long;

Different ways of assigning values to variables

Assign a Assign a Assign a Assign an


number. character. hexadecimal. octal.
my_char = 33; my_char = 'A'; my_char = 0x43; my_char = 023;
my_short = 33; my_short = 'A'; my_short = 0x43; my_short = 023;
my_int = 33; my_int = 'A'; my_int = 0x43; my_int = 023;
my_long = 33; my_long = 'A'; my_long = 0x43; my_long = 023;

For more information on hexadecimal binary and decimal number conversions click
here.

There is also a conversion table here.

Note: numbers beginning with zero are defined as octal - I don't use this at all but all
C compilers will let you write it so if you wrote:

my_num = 023;

...thinking that you're going to assign my_num with the value 23 you will actually be
assigning an octal number. So the variable will get 19 decimal instead!

In the above examples all you have to remember is not to assign too large a value
e.g.

my_int = 1231111;

.. will not assign that number to my_int – you will get something different:

The reason is that the compiler automatically casts (see a later course module) the
value into the variable - that is it makes it fit into the available space (here 16 bits).
The hex value of the number is 0x12C907 so the last 16 bits are 0xC907 or 51463
(or -14073 as a signed integer).

Note: You have to be aware of the number range you are using to ensure
that the variables can contain the numbers you need to use.

30
Conditional statements: if else, switch, ?:
There are three ways to create a conditional statement:

• The 'if else' statement.


• The switch statement.
• The query - colon statement ?:.

Note: Other conditional operation is per formed using loop statements which include
a conditional test within the structure e.g. the for loop, the while loop, the do-while
loop - these are covered later.

The if statement

The most obvious of the conditionals is the if statement

if (expression) statement;

The statement' is executed when the expression is true.

If the expression is a logical true value then the 'statements' are executed. A
logically true expression is any expression that results in a n integer number that is
not zero. (logical false is zero). Basically its common sense e.g. these expressions
are logically true

if (1) statement; // statement always executes!


if (1==1) statement; // statement always executes! 1 equal to 1?
if (2>1) statement; // statement always executes! 2 greater than 1?
if (1!=4) statement; // statement always executes! 1 not equal to 4?

Note: see the reference manual for details on expressions, operators, relational
operators, bitwise operators.

To execute more than one statement when the condition is true you can use a
statement block:

if (expression) {
statement1;
statement2;
statement3;
statement4;
... etc.
}

Logical Inversion

So what happens if you want to do something if a complete expression is not true -


the answer is that you use the logical inversion operator ! (or the exclamation mark
symbol). A statement that is executed when the expression is not true would be:

31
if (! expression) statements;

The statement is executed when the expression is not true.

You can think of it as the 'not' operator since saying the word 'not' in place of !
(exclamation) after the expression contents is easier and it describes the operation
i.e. if the expression is not true the execute the statement

if (! expression) statement;

Note: The logical inversion operator works on any integer number (not only boolean)
so it treats any number as a logical one and only the number zero is a logical zero.

The else statement

The else statement provides both parts of the condition so you can execute
statements if the expression is true and execute other statements if the expression is
false e.g.

if (expression) {
statement1; // executed if expression is true.
statement2;
statement3;
statement4;

} else {
statement5; // executed if expression is false.
statement6;
statement7;
statement8;
}

You can also write this without statement blocks:

if (expression) statement1; else statement5

Note: A logical true value is anything that is not zero i.e. any integer.

Note: You can also nest if-else statements and also prioritize execution of multiple if
statements see the reference manual for more details.

The switch statement


The switch statement is another conditional test statement but it lets you test for
multiple conditions and execute statements on the result of these conditions.

It's just an extremely useful and compact way of testing an expression against
multiple integer constants without using tons of if else statements e.g.

32
switch (expression) {
case 1 : statement1; break;
case 2 : statement2; break;
case 9023 : statement3; break;
case 'A' : statement4; break;
case 0xf4 : statement5; break;
default : statement6;
}

For this code :

statement1 executes if the expression is 1,


statement2 executes if the expression is 2,
statement3 executes if the expression is 9023,
statement4 executes if the expression is the character 'A',
statement5 executes if the expression is the hexadecimal number 0xf4
statement6 executes if none of the previous expressions match anything.

Note: the default: keyword is optional - i.e. if you don't need it don't use it.

Note: Always use a break statement after your statement or statements as the
action of the switch statement is to follow on to the next test. 'break' forces the
switch statement to stop e.g. you could write:

switch (expression) {
case 1 :
case 2 :
case 9023 : statement3; break;
case 'A' : statement4; statement4.1;break;
case 0xf4 : statement5; break;
default : statement6;
}

And in this case if the expression matched any value either 1, 2 or 9023 statement3
would be executed.

Note: The use of more than 1 statement in case 'A'.

The conditional expression


The conditional expression is an extremely compact form if the if else statement but
it can also be used inside an expression itself.

If you want to set a variable to x when the test expression is true and y when the
test expression is false you could use an if-else structure as follows:

if (test_expression) var = x; else var = y;

33
... but you can write it using the conditional expression

var = ( test_expression ? x : y );

It looks like an extremely odd code structure but it is very compact. the
test_expression is evaluated (for logical value) when written before the query. If the
result is logical true then the value before the colon is returned and if logical false
the value after the colon is returned.

Here is an example using the flash variable

flash = (flash ? 0 : 1);

...and this does exactly the same as flash = ! flash;

This example does not add any value but you could change the true and false values
to anything so it allows slightly more complex operation:

flash = (flash ? 33+2-202 : b);

Here value when flash is true is a fixed expression while for false the value of the
variable b is returned.

You can also use the conditional expression anywhere that an expression can be
placed. For example you could use it within the expression test of an if statement.

if (flash ? 0 : 1) statement;

or even:
if (flash ? 0 : 1) flash=1; else flash=0; //The same as flash = !flash;

And you can quickly get unreadable code!

It's best to know about the conditional expression so that when you see it its easier
to understand - only use it if it really adds readability to the code.

The circuit
This circuit adds another to RA4 pushbutton to what you have already - this is for
exploring the switch statement.

34
Download program 3.1
Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the first module if you are unsure how to
do this.

Download ZIP file program 3.1 : Download here.

Note: the download has the hex files and compiler project control file as well as the
source code.

35
Program 3.1 if-else
This program does exactly the same as the second program in the course but instead
uses the if else statement to control LED operation.

It uses one variable to store the current state of the LED which is tested using the
expression in the if statement.

Note you could write if (flash) since the result of the flash variable is treated as a
logical value (any value not zero evaluates to true while any value that is zero is
false). This allows an economy of writing the code at the expense of readability.

You could also write the if-else test more compactly:

if (flash==1) { PORTB |= (1<<5); else PORTB &= ~(1<<5);

This is OK if the operation of the code is clear but does not allow comments to detail
the operation - you would need another comment line above the code to detail the
operation.

Notice also the use of the logical inversion operator (!) to change the state of the
variable 'flash'.

//////////////////////////////////////////////////////////
//
// File: prog03.1-16F88-if-else.c
//
// Author: J F Main.
//
// Description:
//
// Program 3.1 of C programming course
//
// Compiler : mikroC, mikroElektronika C compiler
// for Microchip PIC microcontrollers
// Version: 6.2.0.0
//
// Note Testing:
//
// Tested on 16F88
//
// Requirements:
//
// Clock : 4MHz (Internal)
//
// Target : 16F88
//
// Version:
// 1.00 - Initial release.
//
// Copyright : Copyright © John Main 2006
// http://www.best-microcontroller-projects.com
// Free for non commercial use as long as
// this entire copyright notice is included
// in source code and any other documentation.
//
//////////////////////////////////////////////////////////

36
//////////////////////////////////////////////////////////
// Start here
//
void main(void) {
short flash = 0;
/* Setup 16F88 */
OSCCON = 0x60; /* b6..4 = 110 = 4MHz */
ANSEL = 0; /* all ADC pins to digital I/O */
TRISB = 0; // set as output
PORTB = 0xc1; // arbitrary data
while(1) { // infinite loop
if (flash==1) {
PORTB |= (1<<5); // Set bit five of PORTB
} else {
PORTB &= ~(1<<5); // Clear bit five of PORTB
}
delay_ms(200);

flash = !flash;
}
}

To open the source code in a separate window click here:

Open source code [opens new window]

Download program 3.2


Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the first module if you are unsure how to
do this.

Download ZIP file program 3.2 : Download here.

Note: The download has the hex files and compiler project control file as well as the
source code.

Program 3.2 switch statement


The switch statement in the following code takes the input from RA4 and RA5 using
the bitwise 'and' operator (&) and the shift right operator (>>) which you have
already seen in a previous module.

37
In this case you need to get the two inputs RA4 and RA5 and you can mask these
out using the hexadecimal number 0x30 which is a mask for the bit positions 4 and
5. 'Anding' this with the port value gives only the two inputs RA4 and RA5. Shifting
the result to the right by 4 binary positions gives a number in the range 0-3 when
you press the buttons i.e. its a convenient number to recognize in the software.

When the keys are pressed each branch changes the delay time between LED
updates so you can easily see the effect of the switch statement.

So you should press each in turn then both together to see the effect.

Note: the default delay time is 200ms i.e. exactly as it was in the previous code.

//////////////////////////////////////////////////////////
//
// File: prog03.2-16F88-switch.c
//
// Author: J F Main.
//
// Description:
//
// Program 3.2 of C programming course
//
// Compiler : mikroC, mikroElektronika C compiler
// for Microchip PIC microcontrollers
// Version: 6.2.0.0
//
// Note Testing:
//
// Tested on 16F88
//
// Requirements:
//
// Clock : 4MHz (Internal)
//
// Target : 16F88
//
// Version:
// 1.00 - Initial release.
//
// Copyright : Copyright © John Main 2006
// http://www.best-microcontroller-projects.com
// Free for non commercial use as long as
// this entire copyright notice is included
// in source code and any other documentation.
//
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// Start here
//
void main(void) {
short flash = 0;
/* Setup 16F88 */
OSCCON = 0x60; /* b6..4 = 110 = 4MHz */
ANSEL = 0; /* all ADC pins to digital I/O */

TRISA = 0x30; // pins RA4 and RA5 set as input


TRISB = 0; // set as output

38
PORTB = 0xc1; // arbitrary data
while(1) { // infinite loop
if (flash==1) {
PORTB |= (1<<5); // Set bit five of PORTB
} else {
PORTB &= ~(1<<5); // Clear bit five of PORTB
}
switch ( (PORTA & 0x30)>>4 ) {
case 1 : delay_ms(500); break;
case 2 : delay_ms(100); break;
case 3 : delay_ms(40); break;
default: delay_ms(200);
}
flash = !flash;
}
}

To open the source code in a separate window click here:

Open source code [opens new window]

Summary : What you have learned


• Variables store information in RAM.

• You need to choose the smallest variable type for the task.

• Casting is done for you automatically.

• You use the conditional statement to make decisions.

• There are three conditional constructs: if-else, ?: and switch.

• Logical inversion is easy to remember as 'not'.

Exercises
1. Change the code to use the expression query construct.

2. Change the single if statements into two separate if statements to set the led on
and off based on the state of variable 'flash'.

3. Change the single if statement into a switch statement.

4. Re code the last 'input reading' switch statement into multiple if else statements -
to show how much code space 'switch' saves!

39
Programming PIC microcontrollers in C:
Module 4: An LED Dice project using
arrays and while loops.
This module shows you a simple application, but it is a complete project, and the
module also explores while loops and arrays.

The project is a simple application (rolling a dice) but it pulls together all the code
you have learned so far using inputs and generating output to LEDs.

40
The new circuit shown below adds 5 led outputs to PORTB and these are arranged as
a dice. Only 6 LEDs are used and you can just about get away with 6 as shown
below (it cheats a little for showing the number 6).

If you want to add a seventh LED to make it look absolutely right then go ahead but
you'll need to change the software a little.

Random numbers
The problem with processors of any kind is that they are totally predictable and they
guarantee that after they have finished an internal reset that they will initialize all
registers to a known state and start up in exactly the same way every time.

While this is absolutely what you want to happen it's a a problem if you want to
make a random number generator as you need to have unpredictability for creating
a dice. You can use a PRBS generator (Pseudo Random Binary Sequence) which as
its name suggests gives you a random number but the word Pseudo means that it is
always predictable. This is one reason that a button is needed.

As well as starting the dice algorithm it acts as a random number generator since the
microcontroller does not know when the button press will be made.

So the program sits in a loop incrementing a counter until the button press occurs at
which point the counter value is used. This creates a random number since the loop
is so fast there is no way to predict the value when the button is hit.

Debounced inputs
Because mechanical switches bounce when you press them you always need to de-
bounce the inputs. This simply means waiting for a short period until the bouncing
has stopped. There is more information here [opens new window].

Arrays
Arrays are simply blocks of RAM used to store your data and the size of each
element of the array is defined by the type of the data you use.

So if you used a 10 element array of short the RAM used up would be 10 Bytes. If
you used a 10 element array of long then 40 bytes would be needed since each long
variable takes 4 bytes.

The mid range microcontrollers are not that good for arrays since their memory
structure is not contiguous but banked so the maximum array size you will be able to
use is limited by this. The bank size for the 16F88 is 127 bytes but you will never
get an array this big because this RAM area is shared with the register files (or the

41
internal control registers for the device).

If you look at the data sheet there are 96 free registers in each of three banks
except for bank 1 which has 80 3x96+80=368 the specified user RAM for the
device. So the maximum array you will ever be able to use is 96 Bytes long and
even this may not be possible as the compiler will use up some registers to do its
job.

So the advice is : keep to small arrays!

Note: The 18F series devices have contiguous RAM memory so you can use large
arrays with these - well they do use banking but is hidden so you can use the
memory as if it was contiguous.

Array declaration
You declare an array using the square brackets to indicate how many elements you
want the array to have as follows.

int my_array[10]= {0,1,2,3,4,5,6,7,8,9};

One thing that can catch you out is that arrays are referenced starting at zero so
although you declare the array saying how many elements you want (in this case 10
integers) to access the first element you write

a = my_array[0]; // Access the 1st element.

and to access the 10th element you write


a = my_array[9]; // Access the 10th element.

This is also known as an off-by-one error a common error in C programs.

Note: You can also auto initialize an array and you can also use multidimensional
arrays - see the reference manual for details.

Note: The C language does not check if you enter an incorrect array index and is
undefined for numbers larger than the array. This can cause great problems as you
can overwrite areas of memory and these bugs are really difficult to find. There is a
tool called lint that will check code for you but its a lot of effort to use and not worth
it for small programs.

The while loop


Since we have been using the while loop for some time it's about time we examined
it a little more closely.

The while loop is one of three loop constructs (the for loop construct is used in
another module). This construct is the simplest looping construct

while ( expression ) {
statement1;
statement2;

42
statement3;
...
statement n;
}

If the expression is logically true then the statements 1 through 3 and any more
within the braces are executed in turn. After the last statement (statement n) the
while loop tests the expression again and if it is logically true the statements are
executed again.

You have seen the infinite while loop which never exits

while(1) {
statement1;
statement2;
...
}

... but there is one thing I have not talked about yet and that is the break
statement. Although you have seen it in action in the switch construct it is also
useful in while, for and do-while loops.

The break statement


You can insert a break statement and the loop will break or execution will break out
of the loop. Here is an example:

while(1) {

statement1;
statement2;
...
if (a == 324) break;
...
statement n;
}

// execution continues here.

If the variable a becomes 324 then the loop will break and execution continues after
the terminating bracket of the while loop.

Note: You can also break out of a for loop.

If you have nested loops you can exit the innermost loop using the break statement.

while(1) { // outer loop

while(1) { // inner loop


statement1;
statement2;
...

43
if (a == 324) break;
...

statement n;
}
// do something here after the break
// and continue to 1st loop (outer most loop).
}

You have already seen the other place to use a break statement: in the switch
construct.

An interesting use of the while statement


Another interesting use of the while loop is a construct that looks as though it does
nothing but it's actually very useful:

while ( PORTA & (1<<5) == (1<<5) ) ;

You can use this statement as an easy way of waiting for an event. It again tests
the expression and if logically true executes the statements but in this case there are
no statements just the semi-colon. In fact the semi-colon is the statement
terminator and it tells the while loop that there are no more statements.

This construct is useful for waiting for an event and here the event is that bit five of
port B becomes a 0 i.e. the statement is locked going around the loop until the
expression becomes false and the only way that it can happen is if RB5 goes low.

The do while loop


The do while loop is a variation on the while loop and instead of placing the keyword
'while' at the start you place it at the end. At the beginning you place the keyword
'do' Here's an example:

do {
statement1;
statement2;
...
} while( expression);

This construct is useful if you want to execute all the statements in the loop at least
once - you will come across situations where this is necessary.

The circuit

44
45
Download program 4.1
Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the first module if you are unsure how to
do this.

Download ZIP file program 4.1 : Download here.

Note: the download has the hex files and compiler project control file as well as the
source code.

Program 4.1
For this project an array maps the random number, which is limited to the range 1 to
6 to the output LED pattern to give the correct dice pattern.

As you can see when you try out the program the dice does not look very realistic
and as soon as you release the button the new dice value is displayed. Just keep
pressing and releasing the button to see new values.

If the value happens to be the same as the previous value then it looks like nothing
happens and this is the subject of the next module - to simulate the dice rolling and
give a visual indication that the dice value has changed.

Code description
The code has an outer infinite while loop and three other loops; two while loops and
one do-while loop. The two inner while loops do key press and key release detection.

After the 'key press detect loop' the 'counter' variable will have a random value in it
which is kept to the values 0 to 5. One is added to get to the dice values 1 to 6.

This number is then displayed on the dice LEDs.

//////////////////////////////////////////////////////////
//
// File: prog04.1-16F88-dice.c
//
// Author: J F Main.
//
// Description:
//
// Program 4.1 of C programming course
//
// Compiler : mikroC, mikroElektronika C compiler
// for Microchip PIC microcontrollers
// Version: 6.2.0.0
//
// Note Testing:
//
// Tested on 16F88
//
// Requirements:

46
//
// Clock : 4MHz (Internal)
//
// Target : 16F88
//
// Version:
// 1.00 - Initial release.
//
// Copyright : Copyright © John Main 2006
// http://www.best-microcontroller-projects.com
// Free for non commercial use as long as
// this entire copyright notice is included
// in source code and any other documentation.
//
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// Start here
//
void main(void) {
unsigned int counter;
unsigned short dice[7] = {0x00, 0x10, 0x0c, \
0x1c, 0x2d, 0x3d, 0x3f};
unsigned short dice_idx = 0;

/* Setup 16F88 */
OSCCON = 0x60; /* b6..4 = 110 = 4MHz */
ANSEL = 0; /* all ADC pins to digital I/O */
TRISA = 0x30; // pins RA4 and RA5 set as input
TRISB = 0; // set as output
PORTB = 0x00; // arbitrary data

while(1) { // infinite loop


// Wait for keypress
while(1) {
counter++;
if (counter>5) counter=0; // keep to 6 digits
// Debounce by checking the key 10ms later
if ( (PORTA & (1<<5)) == (1<<5) ) {
delay_ms(10);
// Note the break exits the loop
// So it does not matter if
// its inside a nested if.
// If key still pressed must be real
if ( (PORTA & (1<<5)) == (1<<5) ) break;
}

} // while for keypress


// Move to the correct range digits 1 to 6.
dice_idx = counter + 1;
// Display dice value
PORTB = dice[dice_idx];
// Wait for keyrelease

47
while(1) {
// Debounce by checking the key 10ms later
if ( (PORTA & (1<<5)) == 0 ) {
delay_ms(10);
// Note the break exits the loop
// So it does not matter if
// its inside a nested if.
// If key still pressed must be real
if ( (PORTA & (1<<5)) == 0 ) break;
}
} // while for keyrelease
} // infinite while
}

To open the source code in a separate window click here:

Open source code [opens new window]

The physical layout of LEDS is shown below:

So the port bits map to the following positions:

48
Now you can create the array that will map the number onto the output ports of the
microcontroller:

number bits Binary Hex


0 none 0000-0000 0x00
1 RB4 0001-0000 0x10
2 RB3 RB2 0000-1100 0x0c
3 RB2 RB3 RB4 0001-1100 0x1c
4 RB0 RB2 RB3 RB5 0010-1101 0x2d
5 RB0 RB2 RB3 RB4 RB5 0011-1101 0x3d
6 RB0 RB1 RB2 RB3 RB4 RB5 0011-1111 0x3f

Note its easier to work in binary for this task and then translate to hex as C only
allows hex numbers. The minus sign does not mean anything it just separates the
nibbles out. For info on binary to hex conversions see this page.

Now its just a matter of putting the information into an array:

unsigned short dice[7] = {0x00, 0x10, 0x0c, 0x1c, 0x2d, 0x3d, 0x3f};

Whenever you use the array the array index (input number) in the range 0 to 6 is
mapped to the correct output bits for port B. The index zero is kept to make the
software easier to write - you could use an index of 0 to 5 to do the same job and
save memory.

Summary : What you have learned


• Arrays are contiguous blocks of RAM.

• Arrays are limited by the size of the banked RAM in the 16F88.

• How to translate an internal representation to an external display (dice LED).

49
• How to make a complete project with input and output.

Exercises
1. Change the code to use indices 0 to 5 only and note the RAM savings in the
compiler report.

2. Add an extra LED to RB6 and re-code to use 7 LEDS for a better display.

3. Break out of the while loops when the button input at RA4 is pressed.

50
Programming PIC microcontrollers in C:
Module5: Rolling the Dice.
This module is about simulating the roll of the dice so several dice values will be
displayed until the final value is displayed.

Just before the final value the LEDS will be flashed twice indicating that the dice has
stopped.

Before that you need to re-organize the program as it has become too large. To do
this you can use functions to split up the code.

Functions
Functions let you split up a program into manageable chunks instead of putting all
the code into one function.

You can pass information into a function using function arguments which are
declared in the same way as variables e.g. for a function named func1...

long func1( int a, int b) {


statement1;
statement2;
...
}

This function takes two arguments a and b which are both declared as integer types
and it also returns a value which is declared as long type. So the function has two
inputs a and b (integer type) and one output (long type).

When you send data into the function the data must match the type of the
arguments in the function declaration. So if you had part of a program as follows:

int number1 = 10;


int number2 = 20;

func1(number1, number2);

func1 has been given the value of the arguments of number1 and number2.

C is also quite helpful and you can send expressions into the function as well e.g.

func1( 1*5, 99);

Functions returning a value


These examples have not used the returned value and thats OK the value it's just
discarded. Normally you would use the return value as you probably want the

51
function to give you a result. Here is an example of a simple function that multiplies
two numbers.

long multnumbers(int n1, int n2) {


return n1 * n2;
}

The return keywords is used to return data from the function to the caller so if you
had the calling code as follows

long mynum;
mynum = multnumbers( 30, 2);

At the end of this code mynum would contain 60.

Note: The reason that you need to declare the types of all function arguments and
the return type is that the compiler checks that only matching types are used
ensuring that your code is being used as you intend. It's known as type checking
and error checks your code.

Note: All function declarations must appear earlier in the code before you use them
i.e. its the same as variables - compiler has to know about them before you can use
them.

Void functions
Sometimes (quite often) you may need a function that does not take arguments or
does not return anything and you use the keyword 'void' to declare no arguments or
no returned data.

This type of function is valid as perhaps you want to set the port to a specific value
in which case there is no need to return any data e.g.

void set_portb_high(void) {
PORTB = 0xff;
}

In this case there is no input data and no returned data - OK it's an artificial example
because you can just type PORTB = 0xff;. But here's what it could be useful for -
say you want to send data to the port but you don't ever want the port to have a
zero value (for some reason?). This function could do that job with a few mods e.g.

void set_portb(val) {
if (val==0) val = 0xff;
PORTB = val;
}

This function now has added value since wherever it's used it checks the input data
for a specific value.

Note: that there is no data returned so the return type is void.

52
Increment/Decrement
Two special arithmetic operators let you increase or decrease any integer type
variable by one. They are :

increment: ++ (add one)

and

decrement:-- (subtract one)

You can simply use these after a variable to act on the variable e.g.

i = 10;
// i contains 10
i--;
// i contains 9
i++;
// i contains 10

Note: See postfix/prefix in the reference manual for complex use of ++ and --.

For loops
The for loop is the most powerful of the loop structures and lets you execute a block
of statements a number of times. It has the following syntax:

for ( initialize; test; action ) {

statements;

The for loop works using a loop variable which you must declare before using the
loop. The easiest way to understand it is by example:

int i;

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

statements;

Here the loop variable is declared as an integer.

• In the initialization section loop variable 'i' is set to zero.


• In the test section 'i' is tested to see if it is smaller than eight.
• In the action section 'i' is increased by one.

53
If the test returns false then the loop is exited before executing any statements.

So the above example starts off with i equal to 0 and then executes the statements
after testing i<8. It repeats this action until i equals 8 when it jumps out of the loop
without executing the statements (when i reaches 8). So the loop goes through the
values of i from 0 to 7 i.e. the statements are executed 8 times (See the reference
manual for more details).

Break
As with the while and do-while loops you can also use the break statement to exit
from for loop e.g.

int i;

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

if (i==4) break;
statements;

The loop would terminate when i reaches 4.

Continue
Continue is the opposite of break and it lets a loop skip an iteration - it forces
execution of the loop to return to test the 'test expression'.

int i;

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

if (i==4) continue;
statements;

In this case the statements are executed for values of i 0 to 3, 5 to 7. When the
loop index reaches 4 the continue statement forces the test expression followed by
the action expression.

Note: You can also use 'continue' in while or do-while loops.

Static variables
You can use static variables to transfer data between functions in the same file.
Instead of passing information using arguments and return values it is often easier
to use a static variable that has file scope. That is any function in the file will be able
to read or modify the variable.

54
Static variables are declared in the same way as local variables but they are placed
at the start of the file (outside of any function) and they are prefixed with the
keyword 'static'.

In the next program the counter variable and dice array which were local to the main
function have been moved out and changed to a static variables:

// File scope variables


static unsigned int counter;
static const unsigned short dice[7] = {0x00, 0x10, 0x0c, \
0x1c, 0x2d, 0x3d, 0x3f};

Note: It is not a good name to use but it illustrates the concept - you should
label static variables to identify them easily e.g. st_counter.

Note: const assigns the array dice[] as constant i.e. you can not update the array
since you don't want to this is the correct way of writing it. In some compilers the
const keyword can influence where code is place i.e. RAM or ROM.

Wherever the variable name counter is used (in any function that exists in the same
text file) the static variable will be updated, so by when variable is changed in
wait_keypress() the same counter variable can be read in main.

Note: For more information and other uses of the static variables see the reference
manual.

Download program 5.1


Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the first module if you are unsure how to
do this.

Download ZIP file program 5.1 : Download here.

Note: the download has the hex files and compiler project control file as well as the
source code.

Program 5.1
The program that you are now using from the previous module is starting to get a bit
too long so you will now add a function that will perform the dice roll and tidy up the
key detection into separate functions. Here are the declarations:

void roll_dice(void)

This function takes no arguments and returns no output data. All it will do is control
the dice display. It uses a for loop to output 10 rolls of the dice (10 different
displayed values).

55
void wait_keypress(void);
void wait_keyrelease(void);

These functions do exactly as they say.

Note In the listing below how the main function is much easier to read since some of
the code has been moved to supporting functions. You can now see how the whole
program works just by looking at 'main' and you can look at the detailed operation of
each bit by looking at the contents of the functions.

The only really new bit, apart from re-arranging the code into functions is
the roll_dice function.

This displays the numbers 1 to 6. After this the dice display is flashed twice to
indicate that the next displayed dice value is the final one.

//////////////////////////////////////////////////////////
//
// File: prog05.1-16F88-for.c
//
// Author: J F Main.
//
// Description:
//
// Program 5.1 of C programming course
//
// Compiler : mikroC, mikroElektronika C compiler
// for Microchip PIC microcontrollers
// Version: 6.2.0.0
//
// Note Testing:
//
// Tested on 16F88
//
// Requirements:
//
// Clock : 4MHz (Internal)
//
// Target : 16F88
//
// Version:
// 1.00 - Initial release.
//
// Copyright : Copyright © John Main 2006
// http://www.best-microcontroller-projects.com
// Free for non commercial use as long as
// this entire copyright notice is included
// in source code and any other documentation.
//
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// File scope variables
static unsigned int counter;
static const unsigned short dice[7] = {0x00, 0x10, \
0x0c, 0x1c, \
0x2d, 0x3d, \
0x3f};
//////////////////////////////////////////////////////////

56
// Wait for keypress at port input
void wait_keypress(void) {
while(1) {
counter++;
if (counter>5) counter=0; // keep to 6 digits
if ( (PORTA & (1<<5)) == (1<<5) ) {
delay_ms(10); // Debounce
// If key still pressed must be real
if ( (PORTA & (1<<5)) == (1<<5) ) break;
}
}
}
//////////////////////////////////////////////////////////
// Wait for keyrelease at port input
void wait_keyrelease(void) {
while(1) {
if ( (PORTA & (1<<5)) == 0 ) {
delay_ms(10); // Debounce

// If key still pressed must be real


if ( (PORTA & (1<<5)) == 0 ) break;

}
}
}
//////////////////////////////////////////////////////////
void roll_dice(void) {
int i, d;
unsigned short order[7] = {0,3,4,1,2,6,5};

for(i=1; i<7; i++) {


PORTB = dice[ order[i] ]; // output to display.
delay_ms(300);
}
// Flash the dice display.
delay_ms(100);
PORTB = dice[6];
delay_ms(100);
PORTB = dice[0];
delay_ms(100);
PORTB = dice[6];
delay_ms(100);
PORTB = dice[0];
delay_ms(100);
}
//////////////////////////////////////////////////////////
// Start here
//
void main(void) {
unsigned short dice_idx = 0;
/* Setup 16F88 */

57
OSCCON = 0x60; /* b6..4 = 110 = 4MHz */
ANSEL = 0; /* all ADC pins to digital I/O */
TRISA = 0x30; // pins RA4 and RA5 set as input
TRISB = 0; // set as output
PORTB = 0x00; // arbitrary data
roll_dice();
while(1) {
wait_keypress();
// seed the build in random number generator
srand(counter); // counter updated in wait_keypress.
roll_dice();
// Move to the correct range digits 1 to 6.
dice_idx = counter + 1;
// Display dice value
PORTB = dice[dice_idx];

wait_keyrelease();
} // infinite while
}

To open the source code in a separate window click here:

Open source code [opens new window]

Summary : What you have learned


• You can split a program into easier to read function blocks.

• You can send data between functions using a static variable.

• Increment and decrement operations are easy.

• You can 'break' from or 'continue' in a loop.

• Use of the powerful for loop.

Exercises
1. Change the code in the function roll_dice() at the line beginning:

// Flash the dice display

...to use a for loop (and loop variable) to control the index for dice[] - this should let
you compact the code so you only have one PORTB=dice[] and one delay_ms(300).

58
Programming PIC microcontrollers in C:
Module6: Hello World
This is the "Hello World" program that you usually find at the start of most C
courses. It was not at the start of this one because there was no interface to use for
displaying text!

This module is about using an RS232 serial communication interface to send and
retrieve data (ASCII text) to and from the PC.

To do that you need to know how to use string data within C and there are two

59
aspects to this. The first is string representation and the second is the built in string
functions.

Strings
Strings in C are just arrays of characters of type char so you can declare a string in
the same way that you declare a normal variable. The main difference is that you
can also initialize an 'array of type char' using a string initializer. Here's an example:

char my_str[] = "This is my string";

This character array has been auto initialized i.e. the amount of memory reserved for
the string is set by the compiler to be the length of the string plus 1. You could also
write:

char my_str[18] = "This is my string";

... as 18 bytes are needed since the length of the visible string is 17 characters so
you need 17 plus 1; but the first method is easier.

Null string terminator


So what's the 'plus 1' - It's a single character at the end of the string with the value
of zero (not the character zero). Its needed so that you can store any size string in
memory. All the built in routines use this string terminator (or null) sometimes it
can be written as a character : '\0';

Note: When a character array (or string) is automatically initialized the compiler
adds the null string terminator for you.

#Include libraries
This compiler is a bit unusual as it lets you use any built in library routines with out
you needing to reference them. Usually compilers will not let yo proceed unless you
include library information.

In other compilers (e.g. a PC compiler - non microcontroller) you are normally


required to specify all libraries that you intend to use using the 'hash include'
preprocessor directive ( '#include' see the reference manual for more information
preprocessor).

To do this you use the following type of declaration:

#include <stdio.h>

This tells the compiler that you want to use the library 'stdio' and any routines in that
library.

In one way it's a good that you don't need to '#include' the header as all functions
are available so you don't need to go searching for the correct library header. In
another way its bad as if you use a different compiler you won't be familiar with

60
correct compiler usage.

Note: This only applies to built in library functions and when you create your own
functions in other files of your own you 'll need to '#include' .h files (see later course
modules).

So the result of this is that to use the functions defined within the string library you
don't need to write this:

#include <string.h> // Not required for this compiler

For other compilers you probably need the above at the top of the file.

Note: See the reference manual for some useful built in libraries (that are also part
of ANSI C). Also this compiler has a lot of other built in functions (see the compiler
help for details).

string.h
string.h gives you lots of useful string handling functions

String library
memcmp compare memory.
memcpy copy memory.
memmove move memory.
memset set memory to a value.
strcat concatenate strings (append).
strchr locate character within a string.
strcmp compare strings.
strcpy copy strings.
strlen return string length.
append strings no more than n
strncat
characters.
copy strings no more than n
strncpy
characters.
string span check chars from s2
strspn
that are in s1.

The most useful routines to start off with are strlen and strcpy for finding the string
length and copying strings respectively. Check the compiler help for details of how
they work.

Note: Other interesting libraries are stdlib.g math.h (see the reference manual).

Formatting numbers

61
One very common process you'll want to do when using text output is to embed
within the text the ASCII equivalent of a number. Since numbers are stored in
binary for example the two bytes that form an integer are not ASCII characters and
you need a routine to translate the binary into ASCII.

Built in library functions are used to do this and the most common one is sprintf
(String print format) but this function was developed for use on a PC and it can
translate just about any number type you can think of and it also has multiple
formatting options. This means that it takes a lot of resources some of which you
won't need. For this it is not available for the 16F series and smaller devices. Its
mentioned here as it is very common function and it is important to know about it.

The alternatives are much smaller functions dedicated to one translation task. These
will be non-standard functions for each different compiler and in this compiler they
are:

• ByteToStr
• ShortToStr
• WordToStr
• IntToStr
• LongToStr
• FloatToStr

The arguments to each function are of the form:

function( short number, char *output )

... where you change 'short' for the relevant type.

Note: Each function outputs a fixed length string (see compiler help for details).

The second argument is the important one and defines a pointer to type char which
for the moment all you need to know about it is that it is exactly the same as a
string array. Here's an example:

int mynum = 12931;


char my_string[30];

IntToStr( mynum, my_string);

This will place the ASCII equivalent of 12931 (or "12931") into the string labeled
'my_string' and using number of characters defined by the function (see compiler
help on each function) starting at the 1st character location (index zero).

If you wanted to place the string starting not at index zero but somewhere else then
you can write:

IntToStr( mynum, &my_string[20] );

// Here the ampersand is telling the


// compiler to take the address of the array element 20.
// so character output starts at location 20.

62
Using Formatting
Using this information if you had a text string to display e.g a temperature
measurement either an output to an RS232 port or an LCD you could write the
following sort of code.

// Reserve some workspace and initialize


char op[18]="Temperature: "; // 5 spaces at end - to be filled.
unsigned short Temp = 25;// temperature in Degrees C
...
lots of code
...
ByteToStr(Temp, &op[13]); // update output string.

This outputs the converted text string directly into the 'op' string array starting at
string character index 13.

So it would output (to the character string named 'op') :

'Temperature: 25 '

... the '2' starts at location 14 as 13 is filled with a space.


Note: ByteToStr outputs 3 characters so 4 are needed to store the null
terminator. Also if there are less than 3 characters the area is left filled with spaces
to pad out the string so it is always 3 characters long.

The USART
A USART is a Universal Synchronous Asynchronous Receiver Transmitter and there
one of these built into the 16F88. It's just a fancy acronym for a serial transmission
module and it is implements RS232 protocol. For more information on RS232 click
here.

In short it can transmit bytes of data to and receive bytes of data from the PC serial
port (or any other RS232 serial interface).

Here's the circuit you'll need to use the RS232 interface as although the 16F88
implements all the timing for data transmission and reception it does not change the
voltage levels to those needed to meet the RS232 specification.

The MAX232 chip does the level translation (and voltage generation ±12V) for you.

The circuit
This is the same circuit as before but adds in the RS232 interface. The resistor R10
isolates the MAX232 and the 16F88 i.e. if they both drive out at the same time 1
high and 1 low (during development) only small current flows.

Note: If you use a MAX232A you only need 100n (0.1u) Farad capacitors.

63
64
As you can see the RS232 transceiver (MAX232) is attached to the 16F88 via RB2
(receive data) and RB5 (transmit data). These are the fixed pins that the 16F88
uses for its USART module.

The fact that LEDs are attached to the USART lines does not affect the operation of
the USART as these are only drawing a low current but the 2 LEDs will react to data
on Rx and TX. In fact this is useful to see what is happening (although for the first
program the output is continuous so the LED is always on).

Note: The the RS232 9-way connector is labeled female as the connector on the PC
is male but it is also labeled with the pin descriptions applicable to the PC. So for
instance Tx is the transmitted data from the PC. This is often a great source of
confusion as depending on which side of the interface you are looking at the wire is
Transmitting from the sender but also Receiving at the receiver i.e. it can be labeled
as Tx and Rx. To save confusion always label the 9-way with the PC side then you
know to connect your circuit Tx to the PC Rx and your circuit Rx to the PC Tx as
shown in the above circuit. i.e. make the cross-over on your development board.

Hyperterminal
Now start up hyperterminal with the following parameters :

Bits per second 9600


Data bits: 8
Parity: None
Stop bits: 1
Flow control: None

Note: For more information on setting up Hyperterminal click here.

Note: Connect the serial connector on the development board (CN1) to the PC using
a straight through connector. You can not tell a straight through from a cross-over
(null modem) by looking at it so you either have to buy a straight through or test out
your existing cable with a multimeter.

The reason that the 2400 Baud rate is used is that the internal oscillator is not that
accurate (1%) and going faster will not let the PC receive data correctly. If you want
to use a higher baud rate then you'll need to add an external crystal oscillator
circuit.

Note: There is a way of cheating to get a higher baud rate (using the OCSTUNE)
register which can change the frequency of the internal oscillator by ±12%. This is
not recommended as you would have to tune each chip individually and the
frequency may drift - but I have had a 16F88 going at 57600 baud!

Download Program 6.1

65
Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the first module if you are unsure how to
do this.

Download ZIP file program 6.1 : Download here.

Note: the download has the hex files and compiler project control file as well as the
source code.

Program 6.1
This program has two functions 'main' and 'USART_str'. USART_str is a simple string
output routine that takes a character string as input and sequentially outputs each
character until the null string terminator is found.

Transmission routine delay reason

The routine is a little wasteful as it delays the processor after transmission of each
byte. At this baud rate there are 10 bits transmitted and each takes a time of
1/9600Hz = 0.1042ms so 10 takes 1.042ms so a delay of 1.042ms is included after
transmission starts.

The reason for the delay is that the hardware module is capable of working
completely independently from the processor (all it takes is about 40us to load up
and start a transmission) . If the processor reloads the USART fast enough you just
overwrite the contents of the USART before the transmission completes so you have
to wait.

There are more complex ways of controlling the USART so that no time is wasted but
they involve using interrupts and circular buffers and also testing the USART module
to see if a byte is transmitted etc.

A slightly easier method would be to use an interrupt timer to control the USART at
regular intervals and set a flag when it reaches the end of the string.

But the delay method is simpler and more appropriate for an introduction to C
programming and since the processor has lots of processing time left it does not
matter.

It all depends on the task requirements as to which method you choose. Also you
can up the internal frequency to 8Mz or use an external crystal to 20MHz to do more
processing between byte transmissions.

9600 Baud rate for 16F88

The baud rate of 9600 is not accurate at 4MHz unless you change some of the
internal control registers. The built in routines do not use the USART fully so you
need the following instructions as there are many control combinations.

//Post initialize the USART.


TXSTA |= (1<<BRGH); // Set BRGH = 1. See 16F88 datasheet.

66
SPBRG = 25; // See 16F88 datasheet.

These setup the USART for a 0.16% error using a 4MHz clock whereas without them
the error is 6.99% - too high to get data through to the PC.

Output
The program outputs "Hello World" continuously followed by the current value of the
variable 'count'. This uses the techniques discussed previously in this module to
translate internal variables to text form.

Note: The character '\r' is an escape character meaning return to beginning of line
(see the reference manual for more details).

//////////////////////////////////////////////////////////
//
// File: prog06.1-16F88-usart.c
//
// Author: J F Main.
//
// Description:
//
// Program 6.1 of C programming course
//
// Compiler : mikroC, mikroElektronika C compiler
// for Microchip PIC microcontrollers
// Version: 5.0.0.3
//
// Note Testing:
//
// Tested on 16F88
//
// Requirements:
//
// Clock : 4MHz (Internal)
//
// Target : 16F88
//
// Version:
// 1.00 - Initial release.
//
// Copyright : Copyright © John Main 2006
// http://www.best-microcontroller-projects.com
// Free for non commercial use as long as
// this entire copyright notice is included
// in source code and any other documentation.
//
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
void USART_str( char b[]) {
unsigned short i=0;
while (b[i]!=0) {
USART_Write(b[i++]);
delay_us(1043); // 1/(9600 baud) *10 bits = 1.042ms
}
}
//////////////////////////////////////////////////////////
// Start here

67
//
void main(void) {
unsigned int count=0;
char buf[20]="Counter: ";
char buf2[]="Hello World";
/* Setup 16F88 */
OSCCON = 0x60; /* b6..4 = 111 = 4MHz */
ANSEL = 0; /* all ADC pins to digital I/O */
TRISA = 0x30; // pins RA4 and RA5 set as input
TRISB = (1<<2); // set as output, RB2 i/p for RS232 Rx
PORTB = 0x00; // arbitrary data
// Initialise the USART.
USART_Init(2400);
//Post initialize the USART.
TXSTA |= (1<<BRGH); // Set BRGH = 1. See 16F88 datasheet.
SPBRG = 25; // See 16F88 datasheet.
while(1) {
count++;
WordToStr(count, &buf[8]);
USART_str(buf2);
USART_Write(' '); // return character
USART_str(buf);
USART_Write('\r'); // return character
}
}

To open the source code in a separate window click here:

Open source code [opens new window]

Download Program 6.2


Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the first module if you are unsure how to
do this.

Download ZIP file program 6.2 : Download here.

Note: the download has the hex files and compiler project control file as well as the
source code.

Program 6.2
Now we have need to get transmission from the PIC i.e. the other way - the easiest
way to do this is to loopback the PC transmit to the PC receive using the 16F88.

When you type a key in Hyperterminal the 16F88 reads it, stores it and then

68
transmits it back to the PC.

If there is no keypress then the program does the same as before. What you see (at
the PC in Hyperterminal) is the same output as before with a keypress tacked on the
end.

//////////////////////////////////////////////////////////
//
// File: prog06.2-16F88-usart-loopback.c
//
// Author: J F Main.
//
// Description:
//
// Program 6.2 of C programming course
//
// Compiler : mikroC, mikroElektronika C compiler
// for Microchip PIC microcontrollers
// Version: 5.0.0.3
//
// Note Testing:
//
// Tested on 16F88
//
// Requirements:
//
// Clock : 4MHz (Internal)
//
// Target : 16F88
//
// Version:
// 1.00 - Initial release.
//
// Copyright : Copyright © John Main 2006
// http://www.best-microcontroller-projects.com
// Free for non commercial use as long as
// this entire copyright notice is included
// in source code and any other documentation.
//
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
void USART_str( char b[]) {
unsigned short i=0;
while (b[i]!=0) {
USART_Write(b[i++]);
delay_us(1043); // 1/(9600 baud) *10 bits = 1.042ms
}
}
//////////////////////////////////////////////////////////
// Start here
//
void main(void) {
unsigned int count=0;
char buf[20]="Counter: ";
char buf2[]="Hello World";
unsigned short i, chr; // general purpose.
/* Setup 16F88 */
OSCCON = 0x60; /* b6..4 = 111 = 4MHz */
ANSEL = 0; /* all ADC pins to digital I/O */

69
TRISA = 0x30; // pins RA4 and RA5 set as input
TRISB = (1<<2); // set as output, RB2 i/p for RS232 Rx
PORTB = 0x00; // arbitrary data
// Initialise the USART.
USART_Init(9600);
//Post initialize the USART.
TXSTA |= (1<<BRGH); // Set BRGH = 1. See 16F88 datasheet.
SPBRG = 25; // See 16F88 datasheet.
while(1) {
count++;
WordToStr(count, &buf[8]);
USART_str(buf2);
USART_Write(' '); // return character
USART_str(buf);
USART_Write(' '); // return character
if (Usart_Data_Ready()) {
USART_Write(USART_Read());
delay_ms(300);
}

// Start at next line


USART_Write('\r'); // return character

} // while
}

Summary : What you have learned


• Character strings and functions.

• Translation of internal data types into strings (or ASCII text).

• How to set up a 16F88 USART.

• How to communicate with a PC using the RS232 interface.

• Looping back an RS232 interface.

Exercises
1. Change the messages in the programs and re-compile.

2. Use a different variable type and send its output e.g. long.

70
3. Change the loop back program to display text as you type it.

Programming PIC microcontrollers in C:


Module7: local statics

Sometimes you want a function to remember data between calls but a normal local
variable is only valid for the duration of the function.

When the function is finished (either when it executes a return statement or reaches
the end of the statements) then the variable inside the function is forgotten.

Note: A value returned using the return statement is returned using the stack so the
value is preserved for use. ( I used to worry about 'if the variable is destroyed how
can I get the value out of a function!).

Static variables

71
If you want a function to remember values between calls then you can use a local
static variable which is simply a variable declared in the same way as a normal local
variable but with the word static at the start.

Here's an example

void a_function(void) {
static int remember=0; // with initialization to zero (done once).
int forget=0;

remember++;
forget++;
}

In this case every time the function is called the variable 'remember' is increased by
one and it retains its value between calls whereas the variable 'forget' will only ever
have the value zero (at creation) and then one (when its destroyed) i.e. the memory
freed up for other use.

Note: Initialization of 'remember' (remember=0) is only done the first time that the
function is executed after that it is left for the function to control. You can initialize it
to what ever value you need.

If you called the function three times as follows

a_function();
a_function();
a_function();

... then the variable 'remember' would have the value 3 ... but ... its not much use
as you can not see the variable 'remember' outside the function i.e. the scope of its
visibility is just the function 'a_function'

Scope
Scope is just a way of describing the visibility of variables and this specifies the parts
of the program in which you can, and in which you can not use variables i.e. in which
block of a program can a variable's value be changed or retrieved and in which part
of a program is a variable hidden(the opposite). It sounds odd but its important as
its a way of hiding information from other parts of the program.

Why is scope important

If you do a lot of assembler programming then all your variables are visible to all
other parts of the program (they are global).

As the assembly program grows you need to label each variable for its particular use
and you have to be absolutely certain that no other part of the program uses a
variable for another use.

Since there is no error checking for assembly programs its quite possible that one
part of the program will use a variable or depend on the value of a variable e.g you

72
call a routine and it modifies a loop variable (but your routine was using the loop
variable). This causes a major error in the program and the only way to avoid it is to
a) stick rigidly to assembler coding guidelines b) create lots of new variables (i.e.
unused RAM locations) c) analyze the code until you are sure there are no
interactions.

Note: The bigger the assembler program the more difficult it is to maintain especially
if there is a group of people working on the code (or separate individuals one after
another.

The C language takes care of all variable management for you and enforces scoping
rules so you don't have to think about physical memory usage.

Local scope

A variable that has function scope is only usable within that function

void local_scope(void) {
int a;
for(a=1; a<10; a++) {
do something;
}
}

Here the variable 'a' is a local variable and it has function scope so it is only visible
within the function 'local_scope'. It is created when the function starts and
destroyed when the function ends. It is also only usable within that function. So if
you wrote the following 'main' routine

void main(void) {
int i;
local_scope();
i = a; // THIS IS AN ERROR
}

... this code will fail as the variable 'a' is only visible in 'local_scope'
Note: To get the value out use a return statement or pass by reference arguments
(see reference manual).

These functions all use the variable 'a' but none of them are visible to any of the
others so you can safely use the variable in each function with no effect on the
others.

void func1(void) {
int a;
...
}

void func2(void) {
long a;
...
}

73
void main(void) {
short a;
func1();
func2();
a=5;
}

File scope: static variables


You used file scope static variables to tidy up the dice project to allow sending data
between functions.

As stated before static variables declared outside any function are visible to all
functions within the file. That is you can use the static variable within any function
without declaring it in the functions.

static int st_a;

int func1(void) {
st_a = 3;
}

int func2(void) {
st_a = 1;
}

void main(void) {
st_a = 10;
func1();
func2();
}

After main executes and at the end of func2 in 'main' the variable st_a will have the
value 1. Each function can affect the value of st_a;

The one exception is if you declare the same named variable within a function e.g.

static int st_a;

int func1(void) {
st_a = 3;
}

int func2(void) {
int st_a;
st_a = 1;
}

void main(void) {
st_a = 10;

74
func1();
func2();
}

In this case func2 declares a local version of the variable and this variable has
precedence over the external variable so at the end of main st_a will have the value
3 (since func2 now has no effect on that variable).

Global variables
The next step up from the file scope static variable is the global variable . These are
exactly the same as variables used in an assembler program and they have exactly
the same problems.

Because of this you should only use globals if absolutely necessary (if at all) and
there are always ways around using them e.g use a function to access the value of a
local static instead of declaring a global.

You declare them the same as file static variables but omit the word static.

int gl_a; // global

int func1(void) {
gl_a = 3;
}

int func2(void) {
gl_a = 1;
}

void main(void) {
gl_a = 10;
func1();
func2();
}

Again at the end gl_a will be 1.

Global variables are visible from any file within the project.

Note: See extern in the reference manual for accessing global variables declared in
another file.

Static functions
There is one other use of the static keyword and that is to hide functions from the
global scope. Normally all functions declared in all files in the project have global
scope - that is you can call any function from any other function within the project.

If you add the keyword static to the front of a function then that function becomes
visible only within the file so it is hidden from all others.

75
This lets you create functions in files that you may not want other parts of the
program to use i.e. they will be helper functions that perform a task but they only
make sense for use within one file.

Its a good idea to hide functions in this way (when appropriate) since when you
review the code (a year later maybe) you will know that the the static function is
only used within one file - so you can understand the code operation better. A static
function declaration:

static int func1(void) {


st_a = 3;
}

Compact operators
There is a shorthand form of the operator which was not shown earlier as it adds too
much information to understand but this method saves making errors and uses less
typing. The following two statements are the same:

dice_throw[dice_idx-1] = dice_throw[dice_idx-1] + 1;

dice_throw[dice_idx-1] +=1;

As you can see the first method requires much more typing and there is the chance
of introducing a typing error e.g.

dice_throw[dice_idx-1] = dice_throw[dice_idx+1] + 1;

The error in the above statement is not that easy to see and writing the short form is
much easier.

You can do this with all assignment operators:

Compact Equivalent
x += 2; x = x+2;
x /= 30; x = x/30;
x *=12; x = x*12;
x -=33; x = x-33;
x ~= 0x40; x = x ~ 0x40;
x |= 0x29; x = x | 0x29;
x &= 0x12; x = x & 0x12;
x ^= 0x55; x = x ^ 0x55;
x <<= 2; x = x << 2;
x >>=3; x = x >> 3;

76
Download Program 7.1
Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the first module if you are unsure how to
do this.

Download ZIP file program 7.1 : Download here.

Note: the download has the hex files and compiler project control file as well as the
source code.

Program 7.1
This program uses the random number generator from the dice project and gathers
dice throw from each keypress. The throws are stored in memory in an array and
output to the the RS232 port.

Just to show the static local variable working a static local variable is updated and
output as well (within the keyrelease function).

Note: If the words 'Starting' are displayed as the power is cycled but the static
variable does not initialize to zero then the board is being powered through control
signals from the programmer. If you measure the volts at the 16F88 +5V i/p with
the power off there may be 1.2V (approx.) there which is enough to power the
internal RAM so the RAM value is kept. To solve it remove the programming
connector and cycle the power supply.

//////////////////////////////////////////////////////////
//
// File: prog07.1-16F88-local-static.c
//
// Author: J F Main.
//
// Description:
//
// Program 7.1 of C programming course
//
// Compiler : mikroC, mikroElektronika C compiler
// for Microchip PIC microcontrollers
// Version: 6.2.0.0
//
// Note Testing:
//
// Tested on 16F88
//
// Requirements:
//
// Clock : 4MHz (Internal)
//
// Target : 16F88
//
// Version:
// 1.00 - Initial release.
//
// Copyright : Copyright © John Main 2006

77
// http://www.best-microcontroller-projects.com
// Free for non commercial use as long as
// this entire copyright notice is included
// in source code and any other documentation.
//
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// File scope variables
static unsigned int counter;
//////////////////////////////////////////////////////////
void USART_str( char b[]) {
unsigned short i=0;
while (b[i]!=0) {
USART_Write(b[i++]);
delay_us(1043); // 1/(9600 baud) *10 bits = 1.042ms
}
}
//////////////////////////////////////////////////////////
// Wait for keypress at port input
void wait_keypress(void) {
while(1) {
counter++;
if (counter>5) counter=0; // keep to 6 digits
if ( (PORTA & (1<<5)) == (1<<5) ) {

delay_ms(10); // Debounce
// If key still pressed must be real
if ( (PORTA & (1<<5)) == (1<<5) ) break;
}
}
}
//////////////////////////////////////////////////////////
// Wait for keyrelease at port input
// and update a local static variable
void wait_keyrelease(void) {
static int b=33;
char buf[14];
while(1) {
if ( (PORTA & (1<<5)) == 0 ) {
delay_ms(10); // Debounce

// If key still pressed must be real


if ( (PORTA & (1<<5)) == 0 ) break;

}
}
// show static use
b++;
IntToStr(b,buf);
USART_Str(buf);
}

78
//////////////////////////////////////////////////////////
// Start here
//
void main(void) {
unsigned short dice_idx = 0;
unsigned char dice_throw[6] = {0,0,0,0,0,0};
char buf[20]; // No initialization = ok
unsigned int i; // General purpose.
/* Setup 16F88 */
OSCCON = 0x60; /* b6..4 = 110 = 4MHz */
ANSEL = 0; /* all ADC pins to digital I/O */
TRISA = 0x30; // pins RA4 and RA5 set as input
TRISB = 0; // set as output
PORTB = 0x00; // arbitrary data
// Initialise the USART.
USART_Init(9600);
//Post initialize the USART.
TXSTA |= (1<<BRGH); // Set BRGH = 1. See 16F88 datasheet.
SPBRG = 25; // See 16F88 datasheet.

delay_ms(200);
USART_Str("\rStarting\r");
while(1) {

wait_keypress();
dice_idx = counter;
// Update dice throw array.
dice_throw[dice_idx] +=1;
// Output dice throws
for(i=0;i<6;i++) {
ByteToStr(dice_throw[i],buf);
USART_Str(buf);
USART_Write(' ');
}
// wait_keyrelease updates a local static
// and outputs its value to RS232.
wait_keyrelease();

USART_Write('\r');
} // infinite while
}

To open the source code in a separate window click here:

Open source code [opens new window]

Summary : What you have learned

79
• Local static variables retain their values between function calls.

• A static variable outside any function is visible only within the file.

• A variable declared outside any function is globally visible.

• The scope of variables depends on where they are declared.

• A static function is visible only in the file and not outside the file.

Exercises
1. Add a local static to another routine and output its value to RS232.

2. Add an external static (outside a function) of the same name as the internal static
i.e. 'b'. Update and report this new variable in main to show that these variables are
treated separately by the compiler i.e. they obey scope rules. Note: use the compact
operator += to update the new variable.

80
Programming PIC microcontrollers in C:
Module 8 : Enumerated Types, typedef
and Preprocessing.
In this module you will learn what a state machine is and how to use one. You'll
then re-code the dice project to use a state machine instead.

Before changing the project to use a state machine it's worth learning about
enumerated types as these are ideal for describing state machines.

This module also introduces the pre-processor - a useful part of the C compiler that
executes before the compilation proper.

State Machines
In short a state machine is a useful programming tool which allows you to control
and manage complex operation.

It is simply software that uses a variable to store current state of the system. You
control the variable to change the 'state' so that the program moves into a different
state of operation . It lets you think about the task you want to solve - rather than
the details of how you can code it in software.

One of its advantages is that you can create a picture of the state machine without
writing the code. And this means you can design a system and see how it is going to
work before going into the details of the software.

Here's a picture of the state machine used in the code later in this module.

State machine diagram

81
Note: There are whole software applications dedicated to extremely complex systems
UML (Universal Modeling Language) but one firm I worked in spent 8 Months coding
in UML (Using a software UML modeling tool). The problem was that they were so
separate from the designers that they could not code it into software and had to
restart. The moral is that a state machine is a tool, and like any other tool it can be
misused. Here we'll only look at simple (but useful state machines appropriate for
low level software coding i.e. microcontrollers).

Typedef
In module 3 you learned about the five fundamental types:

char, short, int, long and float

But you can also define these as aliases so you can use any name you want to to
describe the type. You usually do this because you want to make the code more

82
readable. For example if you are using Bytes of data transmitted over an RS232
interface you could use the following type:

unsigned char data;


unsigned char keypress;

The above two variables use the same type but the type does not indicate what its
used for (and its not essential to do this if you comment the code properly). You
could do the following

typedef unsigned char Byte;

... and then use the alias as follows...

Byte data;
unsigned char keypress;

This code now gives more information i.e. you know that data is transferred as bytes
whereas the keypress uses a character - which all makes sense.

In commercial environments, since you can get into trouble when using variables
that change size when changing to a different compiler (see reference manual), you
will see the following sorts of definitions:

typedef unsigned char UINT8;


typedef unsigned int UINT16;

Here the code readability is not as important as showing each new designer what bit
width the type is supposed to have.

Although...

typedef unsigned char U8;

...makes for a lot less typing ;) i.e. wherever you use 'unsigned char' you could just
type U8.

Enumerated types
An enumerated type definition is a way of allowing you to create your own types and
it lets you specify a set of constants grouped so you can use them as a type.

You can give your new type a name; Let's say you want to create a state machine;
you could write:

enum states...// incomplete definition.

Here the keyword enum tells the compiler you are going to define an enumeration
and the name of it is 'states'.

Next you can assign names for each constant in the enumerated type.

83
enum states { Starting, Action1, Action2, Action3, Finished };

Note: Constants are assigned automatically starting at zero so the above


enumerated type will have constants of the following values Starting = 0, Action1 =
1 ... Finished = 4. The compiler takes care of this and you should not really use this
fact as if you change the number of constants then the values will change.

Note: You can assign each constant a specific value e.g enum States {Starting=10,
Action1, Action2, Action3=90, Finished };

Now you can use the enumeration as a type just as you can with any other type
e.g.:

enum states m1;

...this declares a variable named m1 and you can assign values as you would with
any other variable...

m1 = Starting;

Of course you could combine the typedef keyword to turn the enumerated type into
an alias (making the code easier to read (possibly) ) e.g.:

typedef enum states Outer_control;


typedef enum states Inner_control;

The preprocessor
The pre-processor is part of the C ANSI specification so it is also part of the C
language. As its name suggests it processes the source code but does the
processing before any other tasks.

You have already seen one part of the pre-processor and that is the #include
keyword in module 6 where standard libraries were introduced (although you haven't
used it properly yet see later modules course summary).

#include

The #include keyword takes the following file name (in either angle brackets or
quotes) and pastes the contents of the file into the current file (in an internal
representation) i.e. the directive includes the contents of another file.

#include <stdio.h> // standard library path search.


#include "myfile.h" // user project directory path search.

Note: The angle brackets specify looking for standard libraries in the standard library
path (defined by the compiler) whereas the quoted file means search in the current
project directory first (it may look elsewhere as well - depends on the compiler).

#define constants

84
One of the easiest uses of the preprocessor is for defining constants and usually you
define them at the top of the file and use them later in the text. For example you can
write:

#define ALPHA 0x33

If you then use the text 'ALPHA' anywhere in the current file (after the #define line)
then the preprocessor replaces the text with the text following alpha i.e. with
'0x33'. If you had written

if (a==ALPHA) { ....

then before compilation (pre-processing) the preprocessor replaces the text for you:

if (a==0x33) { ...

The real power of this is that the preprocessor replaces all instances of ALPHA
wherever they appear in the file. So you can easily update code by using the
#define constant directive without having to manually change each instance. This
makes the code much easier to maintain if used properly.

Note: If you put the #define constant directive in a header and include that then you
can make global changes in all files (for matching text) just by changing the constant
(see later modules on header files - course summary page).

Note: Its a good idea to use capitals for #define constants as it helps to show you
that something different is happening to the code.

#define macros

A related but slightly different use of the #define directive lets you define macros. A
macro is again a text replacement system (as is the #define constant) but this time
you can use arguments.

In fact they can look like functions (and some standard library functions are defined
as macros) but they do not work like functions (there are no local variables) and no
stack is used up. Macros are replaced inline i.e pasted into the code.

Here's an example that I use in some of my code:

#define setBit(var, bitnum) (var)|=(1<<(bitnum))

...this is a macro that takes two arguments var and bitnum.

If you write the following line in your program

setBit(PORTB,3);

Then the macro is expanded (preprocessed) as follows:

(PORTB)|=(1<<(3));

85
As you can see this makes the code much easier to read.

Note: See the reference manual or Read-Modify-Write problems that may occur with
port access.

Note: always place parenthesis around the arguments - this ensures that the
replaced arguments (when the macro is expanded in the source code) behave
correctly. Note there can still be problems if the arguments are expressions.

Note: See the reference manual for more information about macros.

Conditional compilation

Another set of very commonly used preprocessor directives are the conditional
compilation directives.

#define SOMETHING

Note: The above line defines the constant SOMETHING with null value i.e. with no
value but it does exist - which is why statements1 will be used below.

#ifdef SOMETHING
statements1
#else
statements2
#endif

If the constant definition 'SOMETHING' exists (as it does here) then statements1 are
used in the 'pre-processed' code. If it does not then statements2 are used. This is
usually used to cut out parts of source code depending for example on the target
processor.

#define 16F88

#ifdef 16F88
initialize_16F88()
#elif 16F877
initialize_16F887()
#endif

In this case the function initialize_16F88() will be called.

Note: Sometimes you will need to use these macros and the are powerful but they
do make the code harder to read especially for very complex code e.g. many targets
etc.

Download Program 8.1


Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the first module if you are unsure how to
do this.

86
Download ZIP file program 8.1 : Download here.

Note: the download has the hex files and compiler project control file as well as the
source code.

Program 8.1
Note how the code is changed to use the state machine - using a switch statement to
test and react to the 'state' variable. Also each 'state' corresponds to each of the
state bubbles in the state diagram.

Also note the use of #define for a few of the program constants (declared at the top
of the program).

//////////////////////////////////////////////////////////
//
// File: prog08.1-16F88-enum-pre.c
//
// Author: J F Main.
//
// Description:
//
// Program 8.1 of C programming course
//
// Compiler : mikroC, mikroElektronika C compiler
// for Microchip PIC microcontrollers
// Version: 6.2.0.0
//
// Note Testing:
//
// Tested on 16F88
//
// Requirements:
//
// Clock : 4MHz (Internal)
//
// Target : 16F88
//
// Version:
// 1.00 - Initial release.
//
// Copyright : Copyright © John Main 2006
// http://www.best-microcontroller-projects.com
// Free for non commercial use as long as
// this entire copyright notice is included
// in source code and any other documentation.
//
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// Definitions and Macros
#define RS232_BYTE_delay_ms 1043
#define MAX_COUNT 6
//////////////////////////////////////////////////////////
// Enumerations
enum States {Idle,Check_Press,Check_Release,\
Rolling,Show_face};

87
//////////////////////////////////////////////////////////
// File scope variables
static unsigned int counter;
static enum States state;
//////////////////////////////////////////////////////////
void USART_str( char b[]) {
unsigned short i=0;
while (b[i]!=0) {
USART_Write(b[i++]);
// 1/(9600 baud) *10 bits = 1.042ms
delay_us(RS232_BYTE_delay_ms);
}
}
//////////////////////////////////////////////////////////
// Wait for keypress at port input
void wait_keypress(void) {
while(1) {
counter++;
if (counter>=MAX_COUNT) counter=0; // keep to 6 digits
if ( (PORTA & (1<<5)) == (1<<5) ) {
delay_ms(10); // Debounce

// If key still pressed must be real


if ( (PORTA & (1<<5)) == (1<<5) ) break;
}
}
}

//////////////////////////////////////////////////////////
// Wait for keyrelease at port input
// and update a local static variable
void wait_keyrelease(void) {
char buf[14];

while(1) {
if ( (PORTA & (1<<5)) == 0 ) {
delay_ms(10); // Debounce
// If key still pressed must be real
if ( (PORTA & (1<<5)) == 0 ) break;

}
}
}
//////////////////////////////////////////////////////////
void roll_dice(void) {
int i, d;
unsigned short order[7] = {0,3,4,1,2,6,5};
char buf[10];
// Output to RS232 port
for (i=0;i<7;i++) {
ByteToStr(order[i],buf);
USART_Str(buf);
USART_Write('\r');
delay_ms(100);

88
}
}
//////////////////////////////////////////////////////////
// Start here
//
void main(void) {
unsigned short dice_idx = 0;
unsigned char dice_throw[6] = {0,0,0,0,0,0};
char buf[20]; // No initialization = ok
unsigned int i; // General purpose.
/* Setup 16F88 */
OSCCON = 0x60; /* b6..4 = 110 = 4MHz */
ANSEL = 0; /* all ADC pins to digital I/O */
TRISA = 0x30; // pins RA4 and RA5 set as input
TRISB = 0; // set as output
PORTB = 0x00; // arbitrary data
// Initialise the USART.
USART_Init(9600);
//Post initialize the USART.
TXSTA |= (1<<BRGH); // Set BRGH = 1. See 16F88 datasheet.
SPBRG = 25; // See 16F88 datasheet.

delay_ms(200);
USART_Str("\rStarting\r");
state = Idle; // Initialize to 1st state.

while(1) {
switch (state) {
case Idle:
delay_ms(300); // do something!
state = Check_Press;
break;
case Check_Press:
wait_keypress();
state = Rolling;
break;
case Rolling:
USART_Str("Rolling...\r");
roll_dice();
state = Show_Face;
break;

case Show_Face:
ByteToStr(counter+1,buf);
USART_Str("Dice roll is:");
USART_Str(buf);
USART_Write('\r');
state = Check_Release;
break;
case Check_Release:
wait_keyrelease();
state = Check_Press;
break;

89
}
} // infinite while
}

Summary : What you have learned


• A Graphical state diagram lets you define the code operation.

• The graphical state machine corresponds exactly to the code.

• typedef lets you create rename types for code readability.

• Enumerated types let you create your own types.

• Enumerated types are useful for state machine coding.

• The pre-processor gives you a powerful way to control code.

Exercises
1. Add a new state Done_Rolling. Print a message from this state e.g. "Finished dice
roll".

2. Change the operation of the state machine to continuously output rolling dice
values until the key is released. Hint re-draw the state diagram first.

Programming PIC microcontrollers in C:


Module 9: Pointers and Pass-by-reference
When you learned about functions you learned that C doesn't have procedures as
you find in Pascal. This module shows you the equivalent C 'procedure'.

Pascal provides functions which return one value and procedures that return many
values, whereas C has one programming structure 'the function' which returns only
one value.

Normally a C function takes each argument and re-creates its own copy of each
argument within the function body. This is why you could write a function like this:

int func(int a) {
int b=0;

90
// a=a+1 This action works on the copy
// of the variable inside the function.
a=a+1;
b=a*3;
a=b*56;
return a;
}
So if you used this function

int ma_var=90,new_val;
new_val = func(my_var);

// my_var keeps its original value


// new_val has the value 15288

Whatever variable or value is fed into the function is not affected by the action of the
function.

Pointer arguments allow the functions to modify variables (outside of the function)
see how below. They provide the 'multiple variable return' capability.

First lets look at pointer objects:

Pointers
So what is a pointer...

To answer that its best to review what a variable is

As you learned in module 3 a variable is:

'A variable is simply a storage area within the microcontroller RAM that is given a
name and in which you can store information.'

Each variable is stored in the the microcontroller at a specific address - using a


compiler hides this information as you don't need to know it and it makes
programming far easier i.e. you remember the name of the variable not the
address. For using pointers you need to understand that a variable has an address.

Pointers are C objects that point to addresses of variables they are storage areas
themselves that contain not a value but the address of a variable.

Indirection
Lets say that you want to create a pointer to an integer (we'll see why later). You
can write:

int *ptr;

The indirection operator declares a pointer:

91
The '*' character is called the indirection operator and here it declares a pointer
named ptr.

Note: It's also known as the dereference operator.

You can use any name you want just as you can with normal variables. Working
through the declaration:

The following declares a pointer variable named 'ptr'

*ptr;

...which points to an integer:

int *pt;

Also you can use any variable type.

So thats how you declare a pointer object.

Note: The * symbol is also used for multiplication so you have to use parenthesis in
some expressions to force the action you want (see the reference manual for
precedence of operators).

The address operator


The operator is the ampersand character: &
In normal use expressions are evaluated by taking the value stored at the variable
address and using that in the calculation.

b = 2;
a = b + 8;

Here variable 'a' would end up with the value 10 - exactly as you expect.

But for pointers you need to get the 'address-of' the variable and not its contents.

int *ptr;
b = 2;
ptr = &b;

In this case ptr stores the address-of the variable 'b' and not its value. It could be
anywhere that the compiler decides to store the variable and the actual address is
not important. The fact that the address is obtained rather than the value is the
important fact. Here the pointer variable 'ptr' is pointing to the variable 'b'.

Dereference operator
To 'get' the value of the 'pointed-at' variable you can use the de-reference operator
'*' like this

a = *ptr;

92
Note: This is slightly confusing as the indirection operator and the dereference
operator use the same symbol '*' (just forget that it also means multiply for the
moment!). The important point is that the '*' has two meanings depending on where
it is used :

1. when used in declaring a variable - it creates a pointer object.


2. when used in the code body - it gets the value of the pointed-at variable.

You can use pointer variables in exactly the same way as normal variables when you
use the dereference operator:

int *ptr;
int b;
ptr = &b;
*ptr = *ptr + 1; // increase variable b by one

Pointers and arrays


So far you have not seen anything too special about pointers but the real power of a
pointer is that you can also perform (simple) maths on the pointer itself. This is best
illustrated using an array - recall that arrays are arranged in memory as linear blocks
of RAM.

int i;
int ar[10];
int *p;
p = &ar[0]; // you can also write p = ar; (for the 1st address).

for(i=0;i<10;i++) {
*p = *p + 1;
p++;
}

Here the entire array is updated and the value 1 is added to each element. You could
also write the code only using array access.

for(i=0;i<10;i++) {
ar[i]=ar[i]+1;
}

...but there is a compact form using pointers:

for(i=0;i<10;i++) {
*p++ += 1;
}

... it does exactly the same as the previous versions i.e.it gets the value of the
pointer 'p' adds 1 to it then moves the pointer forwards 1 integer's worth (2 bytes as
an integer uses 2 bytes in this compiler).

One extremely compact example using pointers is finding the length of a string

93
array. Pointer 'p' initially points to the start of the string. The value at 'p' is found
then the pointer moved forwards 1 position. If the value was not zero the i is
increased by one. When the pointer value 'dereferenced' is zero (the end of the
string) the length of the string is in variable i.

while(*p++) i++;

Note: Using pointers instead of arrays is usually faster as for an array index a
calculation is made to find the address whereas the pointer is usually just
incremented by one (which the microcontroller is good at!).

Pointer arguments
Here is how you can return (and use) multiple values from functions. All you do
instead of declaring the arguments as normal variables is that you declare them as
pointers (and then use them as pointers inside the function).

Note: This is known as pass by reference (the reference is the address of the
variable).

Shown below is the function you first saw at the start of this module (on the
left). The equivalent function (using pointers is shown on the right)

Original function Pointer function


int func1(int a) { int func2(int *a) {
int b=0; int b=0;
a=a+1; *a=*a+1;
b=a*3; b=(*a)*3;
a=b*56; *a=b*56;
return a; return *a;
} }

At a first look these two functions appear very similar all thats happened is that you
are using the indirection (argument declaration) and dereferencing (pointer use in
body code) replacing the variable 'a' in the original. But the action of the code on
the right is to work on the variable in the calling routine i.e. outside the function.

Here's what happens in the calling routine.

Original function caller Pointer function caller


routine. routine
void main(void) { void main(void) {
int a,x; int a,x;
a=33; a=33;
x= func1(a); x = func2(&a);
} }
At the end of this routine At the end of this routine
variable 'a' will be 33 and variable 'a' will be 5712

94
variable 'x' will be 5712 variable 'x' will be 5712

The pointer operates on the address of the variable given to it so it changes the
values of the variable in the caller routine.

In fact you don't need to use the function return value at all and you will still be able
to return multiple values. Here's an example function definition which returns two
values.

void calc_xy(int *x, int *y)

Note: I have used the term 'return value' but you can also see that the value of the
variable is also an input into the function when using pass-by-reference. So you can
pass multiple values in and get multiple values out.

The Circuit
The only additions to the circuit are the two pots VR1 and VR2 which should be below
2.5k for fastest ADC reading. These are only needed temporarily to show the ADC
being read.

95
96
Download Program 9.1
Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the first module if you are unsure how to
do this.

Download ZIP file program 9.1 : Download here.

Note: The download has the hex files and compiler project control file as well as the
source code.

Program 9.1
The following program uses an adc read function 'read2ADC' with pointer arguments
to update the variables in 'main' illustrating the use of pointers talked about
earlier. The values from the function are placed in main's local variables adc1 and
adc2.

VT100

The routines also show you a useful little trick - to display text on a serial terminal
screen using VT100 escape code sequences - you can find other escape sequences
on the web.

The VT100 was a standard teletype terminal that was often used at universities
where a central computer allotted processing time to each user so sharing out the
expensive mainframe between many users. The VT100 was really just a dumb
terminal that reacted only to serial RS232 data and most PC terminal programs can
emulate it (since it used quite a simple protocol).

Although its not the best way to display data on a modern PC it is convenient for
microcontroller use as you don't have to make up any software to use it - just set
Hyperterminal correctly and it works.

So using it you can display data at fixed screen positions in Hyperterminal and its
useful to display changing data as it gets a bit hard on the eyes when displaying tons
of ADC readings scrolling past at high speed - much better to keep the data in one
place.

Displaying ADC results

The results of read2ADC are set to positions 2,10 and 3,10 so you can see them
easily on the screen and these are continuously udpated so you can turn the pots to
see different ADC values.

Also displayed is a loop couter value 'counter'.

Pointer operation

97
Before entering the infinite loop the function show_pointer_operation() is executed
and this just works through the pointer examples discussed previously i.e. func1
uses non pointer arguments while func2 uses the pointer arguments - you get the
same values shown above but this time they are running for *real* on *real*
hardware!

//////////////////////////////////////////////////////////
//
// File: prog09.1-16F88-pointer-pass.c
//
// Author: J F Main.
//
// Description:
//
// Program 9.1 of C programming course
//
// Compiler : mikroC, mikroElektronika C compiler
// for Microchip PIC microcontrollers
// Version: 6.2.0.0
//
// Note Testing:
//
// Tested on 16F88
//
// Requirements:
//
// Clock : 4MHz (Internal)
//
// Target : 16F88
//
// Version:
// 1.00 - Initial release.
//
// Copyright : Copyright © John Main 2006
// http://www.best-microcontroller-projects.com
// Free for non commercial use as long as
// this entire copyright notice is included
// in source code and any other documentation.
//
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// Definitions and Macros
#define RS232_BYTE_delay_ms 1043
//////////////////////////////////////////////////////////
// Enumerations
//////////////////////////////////////////////////////////
// File scope variables
//////////////////////////////////////////////////////////
void init(void) {
/* Setup 16F88 */
OSCCON = 0x60; /* b6..4 = 110 = 4MHz */
ANSEL = 0; /* all ADC pins to digital I/O */
// Initialise the USART.
USART_Init(9600);
//Post initialize the USART.
TXSTA |= (1<<BRGH); // Set BRGH = 1. See 16F88 datasheet.
SPBRG = 25; // See 16F88 datasheet.

98
}
//////////////////////////////////////////////////////////
void init_ports(void) {
TRISA = 0x33; // pins RA4, RA5,RA0,RA1 set as input
TRISB = 0; // set as output
PORTB = 0x00; // arbitrary data
}
//////////////////////////////////////////////////////////
// Non-Pointer argument function
int func1(int a) {
int b=0;
a=a+1;
b=a*3;
a=b*56;
return a;
}
//////////////////////////////////////////////////////////
// Pointer argument function
int func2(int *a) {
int b=0;
*a=*a+1;
b=*a*3;
*a=b*56;
return *a;
}
//////////////////////////////////////////////////////////
void USART_str( char b[]) {
unsigned short i=0;
while (b[i]!=0) {
USART_Write(b[i++]);
// 1/(9600 baud) *10 bits = 1.042ms
delay_us(RS232_BYTE_delay_ms);
}
}
//////////////////////////////////////////////////////////
//
// Use pointer arguments to update caller variables.
void read2ADC( unsigned int *a1, unsigned int *a2) {
*a1 = Adc_Read(0);
*a2 = Adc_Read(1);
}
//////////////////////////////////////////////////////////
void show_pointer_operation() {
char buf[20];
int a,x;

// Show variables for non-pointer function (to RS232)


a=33;
x = func1(a);
USART_Str("\rnon-pointer based\ra=");
WordToStr(a,buf);
USART_Str(buf);
USART_Str("\rx=");
WordToStr(x,buf);

99
USART_Str(buf);
USART_Str("\r");
// Show variables for pointer function (to RS232)
a=33;
x = func2(&a);
USART_Str("\rpointer based\ra=");
WordToStr(a,buf);
USART_Str(buf);
USART_Str("\rx=");
WordToStr(x,buf);
USART_Str(buf);
USART_Str("\r");
}
//////////////////////////////////////////////////////////
// Start here
//
void main(void) {
char buf[20]; // No initialization = ok
unsigned int adc1, adc2;
int count=0;
init();
init_ports();
// Init ADC
ANSEL = 0x03; // Activate analogue input AN0
delay_ms(200); // For UART and ADC (100us)

// Clear VT100
USART_Str("\033[2J");

//cursor to row,col <ESC>[{ROW};{COL}H


USART_Str("\033[5;5H");

// show data for


// pointer function and non pointer function
show_pointer_operation();
while(1) { // Infinite loop
read2ADC(&adc1, &adc2);
//cursor to row,col <ESC>[{ROW};{COL}H
USART_Str("\033[2;10H");
USART_Str("ADC1: ");
// Show ADC value
WordToStr(adc1,buf);
USART_Str(buf);

//cursor to row,col <ESC>[{ROW};{COL}H


USART_Str("\033[3;10H");
USART_Str("ADC2: ");
// Show ADC value
WordToStr(adc2,buf);
USART_Str(buf);
//cursor to row,col <ESC>[{ROW};{COL}H
USART_Str("\033[15;1H");
USART_Str("C:");
WordToStr(count++,buf);
USART_Str(buf);

100
delay_ms(400); // less manic screen
}
}

Summary : What you have learned


• You can pass multiple arguments in/out using pointers.

• You can create compact code using pointers.

• Using pointers instead of arrays can sometimes be faster.

• You can easily position text using VT100 escape sequences.

• ADC reading is easy with built in routines.

Exercises
1. Move the VT100 output to different screen positions.

2. Convert the ADC reading to a voltage value before RS232 output.

(Hint: used fixed point maths).

Programming PIC microcontrollers in C:


Module 10: Structures and PWM.
Structures allow you to group a set of related variables into a convenient object.

Since you can also pass structure variables by reference (see previous modules on
pointers) then its easy to manage and control complex sets of data by passing
only one argument.

And when you change the number or type of arguments in the structure you don't
need to update the function arguments to match.

The other use of a structure is for storing individual bits (See later). For example:

short fan_state=0;

.,..and you will set it to one to indicate that the fan is on and zero for off but this

101
uses only one bit of the available 8 within the byte. Structure objects give you a way
of using the other seven bits (or 15 in an unsigned int).

Defining Struct objects


To define a struct object you can use code like this:

typedef struct {

int relay;
int fan;
int lock;
float rpm;

} Controls;

Note: You usually see this definition of a structure object about half way through any
discussion of its 'real' use. The reference manual has the 'other ways of defining
structures (without the typedef etc.) but I find this is the most practical and useful
way as this gives you a ready to use type labeled 'Message'.And you can use this
structure by declaring a variable in the usual way:

Controls my_var;

To access the objects within the structure you use the dot operator as follows:

my_var.relay = 0;
my_var.fan = 1;
my_var.lock = 0;
my_var.rpm = 453.23;

You could also initialize the structure when you declare the variable as follows:

Message my_var = {0,1,0,453.23};

...but you have to know the order of the elements (so if you add one at a later data
e.g. between fan and lock then you have to adjust the initialization - so the first
method is less error prone (and does the same job).

There is a way of using bit fields in a struct but the temptation is to use them for
port access (relying on the position of the bit within the struct - but the C language
does not specify the bit position i.e. its up to the compiler so its best avoided. The
easiest way to manipulate bits is using the bitwise operators described earlier.

Note: DON'T USE struct FOR BIT ASSIGNMENT since the ordering of the bits is not
defined when using struct i.e. the compiler is free to assgn bits to any position.

Saving RAM using struct objects


The structure presented previously is wasteful of RAM since each of the first three
elements is using a complete byte of data to store a single binary value.

102
It would be good if you could use a single bit to store each flag- then you could put
all three flags into a single byte and save 2 bytes from your memory budget.

Fortunately you can do this easily using a special type of struct object:

typedef struct {
unsigned int relay : 1;
unsigned int fan : 1;
unsigned int lock : 1;
} Flags;

Each of the Flags objects occupies one bit (: 1). When you use this structure the
variable will occupy the smallest number of bytes possible.

e.g.
Flags the_flags;
the_flags.fan = 1;

You can also use more than one bit in the structure e.g.

typedef struct {
unsigned int relay : 1;
unsigned int fan : 1;
unsigned int lock : 1;
unsigned int state : 2;
} Flags_2;

Here the object 'state' uses two bits. which you can use as follows

Flags_2 the_flags2;
the_flags2.state = 3;

If you look at the output of the program at the RS232 data then you'll see that the
above structure occupies one byte while the original occupies 10 bytes. This shows
how easy it is to throw away valuable RAM resouce without really realizing it!

Next project data structure


Lets develop a structure to hold the data for the next project. First of all write down
what the project does.

"The project generates two output voltages applied to the DUT (Device Under Test)
and receives analog voltages for measuring the voltage across the DUT and the
current through the DUT. To measure current through the DUT the difference
between the two received volatages is calculated. Knowing the sense resistor value
lets you calculate the current through the DUT."

The output voltages are created using a PWM signal that is filtered to give a dc value
one is generated using the internal PWM module (supply voltage) and one is
generated using software (base voltage). The two inputs are measured using the
internal 10 bit ADC.

103
Note: For the curve tracer for two terminal devices you can ignore the second output
voltage i.e. leave it unconnected.

Here is a suitable structure definition to hold the data described above.

typedef struct {
unsigned int Vhigh;
unsigned int Vlow;
} Measurements;

typedef struct {
unsigned int Vsupply;
unsigned int Vbase;
} Controls;

static Measurements measure;


static Controls control;

Using these structures you can immediately see what the software is going to do and
you know that variables measure' and 'control' are used for i.e. exactly what they
say.

PWM - Pulse Width Modulation


Pulse width modulation is a useful technique and is more often used for RC servo
motor or dc motor control. It simply refers to the width of the output pulse which is
changed (or modulated) within the same time period.

As shown below the period of the signal remains the same - only the pulse width
changes.

Duty cycle

104
The other part of a pulse width system is describing the duty cycle - this is just a
description of the percentage of time that the signal is high relative to the period
that it is low. The above signals are 10%, 50% and 90% duty cycle waveforms

A 100% duty cycle is one where the output is high the whole time and a 0% duty
cycle is one where the output is low the whole time.

The PIC data sheet talks about duty cycle when it refers to the PWM control registers
but the register works at a cruder level than the percentages talked about above.

The actual duty cycle they talk about is not related to the output PWM at all rather it
is the number of machine cycles that the signal is kept high for. You have to do a
bit more work to figure out how the PIC dutycycle relates to the percentage duty
cycle above.

PWM Average
Another way of using PWM is to demodulate or remove the PWM frequency. For this
project this is simply done by filtering out the frequency component using a low pass
filter.

As long as the PWM frequency is far higher than the cutoff point of the filter then the
high frequency will be removed and all you will be left with is a demodulated
signal. In this case it is a d.c. signal.

Note: By choosing the filter and PWM frequnecy correctly you can transmit an
analogue signal e.g. a voice signal and since the PWM signal is more noise tolerant it
can be transmitted over longer distances without loosing quality.

For the curve trancer project a simple capacitor resistor pair filters the PWM output
and this is used as a d.c. voltage generator for the device under test.

Timer 2 Module
Timer 2 is used as the timebase for the PWM signal and this feeds into the PWM
module.

105
Timer 2 is fed by the master clock divided by 4 so running the 16F88 at 8MHz gives
a Timer 2 clock of 2MHz (prescaler set to 1). To set a specific frequency output at
Timer 2 just calculate the number of (2Mz) cycles required to create the desired
frequency.

Note: The prescaler is also used in determining the PWM frequency and PWM
resolution (duty cycle).

For example a 10kHz clock frequency needs:

(1/2E6) / (1/10E3) = 200 counts to Timer 2 roll over.

Note: For exact times use this tool

The actual number needed is 199 as the counter counts to zero.

Note: The PWM module uses the TMR2 output not the postscaled output.

To set this up in the code use the following code:

// Timer 2
PR2=199; // 8MHz clock -> 10kHz PWM frequency
T2CON = (1<<TMR2ON); // no prescale or postscale

T2CON

106
The T2CON register is shown below:

PWM Module

The diagram below shows the essential parts of the internal PWM peripheral. As you
can see Timer 2 is in the center of the module and forms the PWM timebase so you
can not easily use it for anything else. You can use the output from the Timer 2
output or postscaler but the Timer 2 frequency must be set primarily for the PWM
peripheral use.

Note: The PWM module outputs to PORTB on either of two pins (this is controlled by
an external register that you can set in the compiler). You also need to set the
relevant pin to an output using TRISB.

107
Three registers are associated with the PWM itself these are:

CCP1CON: Controls the PWM peripheral, and contains 2 LSB of duty cycle.
CCPR1L: Low value of duty cycle (contains 8 MSB of duty cycle).
CCPR1H: This is used as an internal shadow register (for double buffering).

CCP1CON

The CCP1CON register is shown below:

108
The only important points for the PWM mode of the register are to set the PWM
mode and set the lower 2 bits of the duty cycle.

To set the mode to PWM

CCP1CON = 0xc0; // CCP1CON = 0x0f; does the same.

To set the duty cycle bits 'OR' in the lsb to the correct place using shift operators.

CCPR1L and CCPR1H

Since updates to the PWM registers will cause instantaneous change to the output as
soon as they are written the output ot the PWM could be made unstable producing
random output.

In some cases this could be ok but in most it is undesirable e.g. audio gltiches,
random d.c. output so one of the two registers is used as the request register
(CCPR1L) and the other as the action register (CCPR1H).

You only ever write to the request register (CCPR1L) and the PWM module then
decides to update the action register (CCPR1H) at the end of a PWM period - in this
way no glitches are made at the PWM output. The same action is made for the lower
2 bits in the CCP1CON register see Duty Cycle Control.

Note: You can not write to CCPR1H in the PWM mode as it is read only so writing to
it will have no effect.

109
Duty Cycle Control

The 10 bit duty cycle is set in the registers CCP1RL (8 bits MSB) and bit 5 and bit 4
in CCP1CON (2 bits LSB). As described above they are latched to keep the output
from glitching.

So the maximum value you can use for the duty cycle is 1023 (or 1024 steps 0 -
1023). The data sheet is not too clear on this point but the duty cycle works by
using the Timer 2 prescaler value but using a separate prescaler that operates
using the main oscillator not Fosc/4.

This means that the duty cycle resolution is using a clock 4 times faster than the
Timer 2 clock and allows very fine duty cycle resolution.

The pulse output generated by the PWM module is measured out by counting the
Fosc prescaled clock period - counting up to the duty cycle number of periods. So
at the start of the PWM period the output is set high (if the 'duty cycle is not zero)
and after counting out the number of 'duty cycle clock periods' (10 bit number) the
output is set low.

So the PIC Duty cycle has nothing to do with percentage duty cycle this
means you can easily go wrong with it!

Note: The PWM duty cycle is not the percent of the PWM period but only a count of
the number of periods from the Timer 2 prescaler:

duty cycle number = CCP1RL:CCPCON[5:4] (10 bits in total)

PWM Period =
Timer 2 output period = (1/(Fosc/4)) * (T2 prescaler value) * (PR2+1)
(the actual period generated by Timer 2).

PWM Duty cycle period =


Prescaled Timer 2 input period = (1/Fosc) * (T2 prescaler value).
Note That it is not Fosc/4 in the above equation.

PWM duty cycle period= (duty cycle number) * (Prescaled Timer 2 input)

For example

As an example lets say you want a 50% duty cycle at the 10kHz time worked out
previously

PWM period =
T2 Period = (1/(8e6/4) ) * 1 * (199+1) = 100e-6 (or 10kHz)

PWM Duty cycle period =


Prescaled Timer 2 input period = (1/(8e6)) * (1) = 0.5us
So the resolution of the duty cycle is 0.125us
Since the period of the PWM is 100e-6 there are 100e-6/0.125e-6 resolution steps or

110
800 steps!

So duty cycle number needed for a


50% duty cycle is : (PWM period / 2) / resolution step
= (100e-6/2) / 0.125e-6 = 800.

There is a simplification you can use; the CCPR1L register holds the 8 MSBits of the
PWM duty cycle.

Two points:

• The value in the CCPR1L register is the duty cycle divided by 4.


• The clock fed into Timer 2 is Fosc divided by 4.

So these two elements are equivalent as they both divide by 4 at some point.

If you forget the two lsbs of the duty cycle in CCP1CON then you can use the PWM in
a very simple way although you are loosing PWM resolution by using this method.

You can think of CCPR1L and PR2 as equivalent due to the division by 4.

You can easily set duty cycle (when only using CCPR1L) by writing into CCPR1L a
value that is a fraction of PR2.

For example if PR2 is 199 then

• to set a 50% duty cycle write 100 into CCPR1L.


• to set a 10% duty cycle write 20 into CCPR1L.
• to set a 90% duty cycle write 180 into CCPR1L.

You can think of this as the duty cycle having the same resolution as the Timer 2
clock.

Note For this example there are not 1024 resolution steps even though the duty
cycle register can accept that big a number (To increase the number of duty cycle
resolution steps increase the Timer 2 period - this is the same as decreasing the
Timer 2 frequency.

The "Real duty cycle is" (duty cycle number/resolution steps) *100

Knowing this you can now control the PWM duty cycle from 0 to 100% by just varing
the duty cycle number from 0 to 199.

The circuit
The first part of the circuit uses an LM342 quad opamp and for today we'll use the
two of the opamps. The first accepts the averaged PWM signal and boosts it up to a
useful level. The second opamp is used as a buffer and senses the high voltage

111
reading which is read by the ADC.

Note: To supply the LM324 use a 15V d.c. power supply as the signal generated by
the PWM is from 0-5V and the output is multiplied by three 0-15V (The LM324 does
not go completely to the top voltage supply so it will reach 13.5 approx.).

The R-C filters have a center frequency of 1/(2*PI*R*C) = 146Hz and the PWM
frequency is 10kHz so the d.c. output is very smooth since the PWM frequency is so
far away from the center frequency.

Download Program 10.1


Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the first module if you are unsure how to
do this.

Download ZIP file program 10.1 : Download here.

Note: The download has the hex files and compiler project control file as well as the
source code.

Program 10.1
The PWM module is set up using the registers described earlier and when the top
button is pressed the PWM duty cycle increases - pressing the other button causes
the duty cycle to decrease. You can see the effect as when it dimms and brightens
the LED attached to RB3.

Note: Also monitor (using a volt meter) the voltages at the input and output of the
opamp - and note that the opamp will only go to 13.5V maximum. The output of the

112
opamp should be 3 times the input.

The functions

show_struct_bit_field()

and

show_bit_field()

... do exactly as they say - they generate RS232 data that shows the bit field
operation and structure operation in action on the hardware.

Note also that when the PWM changes due to a button press the value of the ADC
reading read back is sent to the RS232 port at a fixed position on the screen -
remember to enable VT100 emulation in Hyperterminal.

Note: The ADC reading will appear to stick at the high end of the range - but it is
just showing the effect of the LM324 which can not drive a voltage to within 1.5V of
its power supply - so this is not an error.

A simple loopback operation is used to display any key hit at the PC terminal.

//////////////////////////////////////////////////////////
//
// File: prog10.1-16F88-struct-bit-field.c
//
// Author: J F Main.
//
// Description:
//
// Program 10.1 of C programming course
//
// Compiler : mikroC, mikroElektronika C compiler
// for Microchip PIC microcontrollers
// Version: 6.2.0.0
//
// Note Testing:
//
// Tested on 16F88
//
// Requirements:
//
// Clock : 8MHz (Internal)
//
// Target : 16F88
//
// Version:
// 1.00 - Initial release.
//
// Copyright : Copyright © John Main 2006
// http://www.best-microcontroller-projects.com
// Free for non commercial use as long as
// this entire copyright notice is included
// in source code and any other documentation.
//
//////////////////////////////////////////////////////////

113
//////////////////////////////////////////////////////////
// Definitions and Macros
#define RS232_BYTE_delay_ms 1043
//////////////////////////////////////////////////////////
// Enumerations
//////////////////////////////////////////////////////////
// Typedefs
typedef struct {
unsigned int Vhigh;
unsigned int Vlow;
} Measurements;
typedef struct {
unsigned int Vsupply;
unsigned int Vbase;
} Controls;
//////////////////////////////////////////////////////////
// File scope variables
static Measurements measure; // get readings from h/w.
static Controls control; // set controls to h/w.
static unsigned short j=1,oj=0;// pwm test

//////////////////////////////////////////////////////////
void init(void) {
/* Setup 16F88 */
OSCCON = 0x70; /* b6..4 ; 110 = 4MHz111 = 8MHz */
ANSEL = 0x03; /* ADC to digital except RA0,RA1*/

// Initialise the USART.


USART_Init(9600);

//Post initialize the USART.


TXSTA |= (1<<BRGH); // Set BRGH=1. See 16F88 datasheet.
// For USART Settings see 16F88 datasheet.
SPBRG = 51; // 8MHz; SPBRG = 25; //4MHz
// Timer 2
PR2=199; // 4 MHz clock -> 5kHz PWM frequency
T2CON = (1<<TMR2ON);
// Initialize Control PWM
CCPR1L = 30; // Initial Duty
CCP1CON = 0x0f; // PWM mode set and 5,4 duty = 0
}
//////////////////////////////////////////////////////////
void init_ports(void) {
TRISA = 0x33; // pins RA0,RA1,RA4,RA5 set as input

TRISB = 0; // set as output


PORTB = 0x00; // arbitrary data
}
//////////////////////////////////////////////////////////
void USART_str( char b[]) {
unsigned short i=0;
while (b[i]!=0) {
USART_Write(b[i++]);
// 1/(9600 baud) *10 bits = 1.042ms
delay_us(RS232_BYTE_delay_ms);

114
}
}
//////////////////////////////////////////////////////////
void show_bit_field(void) {
char buf[10];
typedef struct { // This is only here to compare memory
int relay; // used with sizof operator.
int fan;
int lock;
float rpm;
} Controls;
typedef struct {
unsigned int relay : 1;
unsigned int fan : 1;
unsigned int lock : 1;
unsigned int state : 2;
} Flags_2;
Controls control;
Flags_2 the_flags2;
the_flags2.state = 3;
control.rpm = 42;

// Show value of the state bit


USART_Str("(3) state = ");
WordToStr(the_flags2.state,buf);
USART_Str(buf);
USART_Write('\r');

the_flags2.state = 1;
USART_Str("(1) state = ");
WordToStr(the_flags2.state,buf);
USART_Str(buf);
USART_Write('\r');

USART_Str("var size :");


WordToStr(sizeof(the_flags2),buf);
USART_Str(buf);
USART_Write('\r');
USART_Str("comp size :");
WordToStr(sizeof(control),buf);
USART_Str(buf);
USART_Write('\r');
}
//////////////////////////////////////////////////////////
void show_struct_bit_field(void) {
char buf[10];

// Clear VT100
USART_Str("\033[2J");
//cursor to row,col <ESC>[{ROW};{COL}H
USART_Str("\033[0;0H");
WordToStr(control.Vsupply,buf);
USART_str(buf);
USART_Write('\r');

115
WordToStr(control.Vbase,buf);
USART_str(buf);
USART_Write('\r');
WordToStr(measure.Vhigh,buf);
USART_str(buf);
USART_Write('\r');
WordToStr(measure.Vlow,buf);
USART_str(buf);
USART_Write('\r');
USART_Write('\r');
show_bit_field();
}
//////////////////////////////////////////////////////////
// returns 1 if button hit
// otherwise returns 0
short update_hardware_pwm() {
if (Button(&PORTA, 5,1,1)) // button on RA5 pressed
j++ ; // increment j
if (Button(&PORTA, 4,1,1)) // button on RA4 pressed
j-- ; // decrement j
if (oj != j) { // If change in duty cycle requested,
// Note CCP1CON bits 5,4 (PWM LSB not set )
// so only using 8MSB of PWM.
CCPR1L = j;
oj = j; // remember old j
return 1;
}
return 0;
}

//////////////////////////////////////////////////////////
void show_adc_value(void) {
char buf[10];
unsigned int num;
//cursor to row,col <ESC>[{ROW};{COL}H
USART_Str("\033[15;20H");
num = Adc_Read(0);
WordToStr(num,buf);
USART_Str(buf);
}
//////////////////////////////////////////////////////////
// Start here
//
void main(void) {
unsigned char chr;
init_ports();
init();
delay_ms(200);
control.Vsupply = 100;
control.Vbase = 200;
measure.Vhigh = 1000;

116
measure.Vlow = 900;
show_struct_bit_field();
while (1) { // Infinite loop
if (update_hardware_pwm()) show_adc_value();
// loopback
if (Usart_Data_Ready()) {
chr = Usart_Read();
if (chr=='\r') {
Usart_Str("\rPIC:>");
} else {
Usart_Write(chr);
}
}
delay_ms(2); // Slow down
}
}

To open the source code in a separate window click here:

Open source code [opens new window]

Summary : What you have learned


• Use of structure variables.

• How to save memory using bit fields in a structure variable.

• How to use the internal PWM peripheral.

• How to create a d.c. signal without using a DAC.

Exercises
1. Change the hardware so that only the filtered signal is fed back to the ADC input -
you should then see the full range of ADC values - since this avoids the LM324
headroom voltage limit.

2. Display some of the other variables within the structure.

3. Change the PWM to generate a triangle wave view on an oscilloscope(or PC sound


card scope). What is the maximum frequency you can acheive? Hint to go higher
increase the R-C cut off frequency - to remove noise increase the PWM
frequency. Note it's all a trade off between resolution / max frequency output /
allowed noise at output.

117
Programming PIC microcontrollers in C:
Module 11: Multiple file structure.

This module looks at how to reduce the complexity of a block of code by splitting it
into smaller sections.

Having all the code in one file is OK for small pieces of code but as the code size
increases it becomes more and more difficult to maintain. The solution is to use
more than one file to store the code.

First a bit of detail on casting - or how you can control the types of variable that the
compiler is to use i.e. force the compiler to do what you need it to do:

Casting
Casting refers to the way you have to write code to change one variable type into
another. Since C uses strong type checking its important to use the same type of
variable throughout your code. Sometimes however you will want to change it into
another type e.g. from a long integer into an int.

Note: The C compiler will do some casting for you automatically e.g.

118
int a;
long b;
a = b;

Here the long integer is cast into an integer type automatically (its up to you to
make sure that only the lower 16 bits of the long are the correct bits - the top
16(MSB) get chopped off!).

You can tell the compiler exactly what you want as well

int a;
long b;
a = (int) b;

Here you are telling the compiler to case 'b' as an integer before it is used - this can
be useful if the compiler mis-behaves. Some compilers are written specially for the
target processor and have bugs in - this is one way to overcome some of the bugs.

Casting variables between types is also useful when you know that in reality the
types are the same thing and you want to tell the compiler that they are the same i.e.
you don't want compiler errors all the time when they are not real errors. Here's an
example using typedefs from a previous module:

typedef unsigned char Byte;

Byte data;
unsigned char keypress;

char chr;

... If the Byte of data came from the serial port you could cast it into a character
type by using the following code:

chr = (char) data;

This tells the compiler its ok to treat data and chr as the same type and not to give
out any errors or warnings.

Examining data using pointers


Sometimes you want to examine data as a series of bytes or process the contents of
a variable as a set of bytes. You can do this using casting. Say you have a long
integer for which you need to examine each byte.

int i
long a_long;
unsigned char *p;
p = (unsigned char *)&a_long;

The last line above is read as follows:

The take the address of variable 'a_long' &a_long

119
Perform a cast operation on it ()&a_long
The cast is a pointer (*)&a_long
The cast is a pointer to an unsigned char (unsigned char *)&a_long
Assign the result to p p = (unsigned char *)&a_long

Note: the cast operator here is casting the address of a_long as a pointer to an
unsigned char i.e. the same as p.

This lets p point to a_long and the compiler won't complain and you can use it as
follows:

for(i=0; i<sizeof(long); i++) {


data = *p++;
}

Each time around the loop data is assigned one byte of the storage area of the long
integer. You can then process 'data' in the loop as you need.

Note the sizeof function is a standard function operator that returns the number of
bytes in an object - you can use in on structure types as well.

Files
Up to this point you have used a single file to contain all functions since this makes it
easy to learn the language but now the file is getting a bit larger and finding a
specific function is a little more difficult.

This section describes how you can split the single file into smaller separate files.

To do this you have to follow a convention and that is:

For each source file in the project - you need a header file.

Note: The mikroc tutorial video shows you how to add files to a project.

Why do you need a header file?


A header file acts as the glue that links all your source code files together and the
header file tells the compiler what to expect i.e. what functions are going to be
defined in the corresponding source file.

The other use for a header file is to define macros or constants.

How to use a header file


To use a header file you include it into a source file and you do this by using the pre-
processor #include directive:

120
#include "USART.h"

When the compiler gets to the above statement in your C source code it 'pre-
processes the line' and loads the contents of the file "USART_library.h" at that point.
Anything written into USART_library is loaded at that point.

The header file is just a normal text file - the same as a C source file but by
convention it is only used to contain some or all of the following:

• macros,
• constant definitions,
• other header include statements,
• types and variables
• prototypes,

Note: It does not have to have all of these.

Prototypes
Prototypes are definitions of functions that are not complete - they are missing the
actual code that does the job. All they really do is define the types of the arguments
and the name of the function that uses those arguments.

Here's a prototype defined in a header file:

float function_one ( short a, float b);

The prototype definition ends after the parenthesis with a semi-colon.

And here is the function defined in a source file:

float function_one ( short a, float b) {


return a * b;
}

Why do you need a Prototype?


Prototypes are used by the compiler to check that the function you use is using the
correct argument type for each argument without it having to go and find out the
information from the other source files.

This saves time since since the compiler does not have to scan all the code before it
can start compilation so it saves you time. You probably won't notice for the short
programs that you use here but when you use a larger device it becomes an issue.

Note: In large programs compile times can be up to 1 hour!

Obviously the function prototype and the 'real' function definition must agree and the
compiler will complain if they don't.

121
The Reason for using many small source code files
The most important reason for using many small source code files is that it makes
the whole program easier to read as each file is focused on a defined part of the task.

Another reason for using multiple files is in a professional environment time is money
and splitting the program into smaller files allows the compiler to selectively compile
each file depending on if that file was edited.

So a typical compile process will compile each and every file the first time producing
intermediate output files (relocatable object files). When you make an edit in only
one file and then re-compile, only that file is compiled again - for all the others the
intermediate files are used.

This is also an efficient compile method since it is inefficient to compile files that are
unlikely to change e.g. a USART source file - once it works it won't need changing.

Partitioning the project


For this project it is useful to partition it into the following groups.

USART
THEORY

All the rest of the code will be in the main source file. So you can partition the
project as follows:

Header C Source code


usart.h usart.c
theory.h theory.c
- main.c

Note: 'main.c' has no header file as nothing but the compiler calls main.

Here is the structure of the program as a diagram:

122
In this case main.c uses the header file from each block including them at the
beginning of main.c as follows:

//////////////////////////////////////////////////////////
#include "usart.h"
#include "theory.h"

....
.... rest of program main.c
....

After these include statements all the functions defined in each source code file
usart.c, theory.c and adc.c (and in the coressponding header dile) are now available
for use in main.c.

Note: Other compilers require you to use headers for the built in functions so
normally you will see the following include statement #include "stdio.h" this includes
the functions for the standard i/o (not avaiable to microcontrollers as it allows file
input/output). MikroC is a bit unusual in that you don't need to use include
statements for built in functions (and this may not be the same in other compilers).

The circuit
Outs PWM from internal module on RB3.
Senses Vhigh on ADC0 (RA0).

123
Senses Vlow in ADC1 (RA1).

Download Program 11.1


Download the zip file containing the hex file and after reviewing the page program it
into the 16F88. See the video tutorials in the first module if you are unsure how to
do this.

Download ZIP file program 11.1 : Download here.

124
Note: The download has the hex files and compiler project control file as well as the
source code.

Program 11.1
The command interface is a simple one letter interface which you can test out using
hyperterminal. These are the commands in funciton command_action:

Letter to Meaning of Description


send to letter
RS232
interface
When a program communicates with serial
interface the interface may be active but the
hardware may not be powered up. This
command just gets a response from the
h hello hardware to check that it is present. The
expected response from the hardware is
"CT\r". This is used in the curve tracer program
in the 'Test hardware' button and shows status

This is the fine PWM control and sets a 10 bit


number into the PWM registers. It us used by
typing a number after 'f' e.g.
f fine
f100\r

where \r is the carriage return in hyperterminal.

Just typing 'g' alone will return two adc readings


from the hardware e.g.
g get
45, 29

For test /
development
only
Show the internal variables i.e. the current
values of:

s show control.Vsupply
control.Vbase

At the moment only Vsupply is used.

Supply coarse variable to PWM used during


v variable development.

b base Supply base value Vbase - not used yet.

Open hyperterminal and press 't' (for test) this will show the cast actions by
executing the following funcions:

show_cast();
examine_long();

Shut down hyperterminal and install tcl (tool command language - a very powerfull
language which is free) from http://www.tcl.tk/.

125
After this open the curver_tracer program curve_tracer.tcl (by double clicking it in
explorer) then Hit the button 'Connect' followed by 'Test hardware' followed by start
to get an I-V trace for the Device Under Test. Try using a diode and then a resitor.

To open the source code in a separate window click here:

Open main code [opens new window]


Open theory.c [opens new window]
Open theory.h [opens new window]
Open usart.c [opens new window]
Open usart.h [opens new window]

Summary : What you have learned


• How to make the compiler do what you want (casting).

• How to partition a project into multiple units.

• How to use prototypes.

• Using RS232.

• Using ADC.

• Using the PWM.

Exercises : Advanced
Add the second port to test a three terminal device - use the interrupt driven method
used in the servo controller project. And create a 2kHz software driven PWM. Use
the same filter as this project and assign the PWM as in this diagram RB0:

The curve tracer program already has controls in to to show multiple traces
controlling the second (software) PWM.

126

Вам также может понравиться