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

APSC1299 Manual - Spring 2019

This lab introduces using a PIC microcontroller with an LCD display for serial communication. The objectives are to build and compile a first C project for the PIC18F4525 using multiple source files, learn to use a SparkFun LCD as the standard output, and use printf() to control the LCD. Students will prepare the PIC MCU on a breadboard with power supply, learn to connect an LCD display via a serial connector, and use code files to initialize the USART and display "Hello World" on the LCD.

Uploaded by

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

APSC1299 Manual - Spring 2019

This lab introduces using a PIC microcontroller with an LCD display for serial communication. The objectives are to build and compile a first C project for the PIC18F4525 using multiple source files, learn to use a SparkFun LCD as the standard output, and use printf() to control the LCD. Students will prepare the PIC MCU on a breadboard with power supply, learn to connect an LCD display via a serial connector, and use code files to initialize the USART and display "Hello World" on the LCD.

Uploaded by

jackchen1526
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 170

Applied Science 1299

Lab Manual

&

Lecture Notes

Revised December 2018

1
Table of Contents

Labs
INTRODUCTION ................................................................................................................................................3
REPORT FORMAT .............................................................................................................................................4
LAB 1 SERIAL COMMUNICATION ...............................................................................................................5
LAB 2 DEBUGGING TOOLS .......................................................................................................................... 13
LAB 3 DIGITAL I/O .......................................................................................................................................... 16
LAB 4 TOGGLES & MENUS ........................................................................................................................... 23
LAB 5 OSCILLATOR SPEED & PWM .......................................................................................................... 27
LAB 6 TIMERS .................................................................................................................................................. 33
LAB 7 ANALOG TO DIGITAL CONVERSION WITH A SENSOR........................................................... 37
LAB 8 TRANSDUCERS AND INTERFACING ............................................................................................. 45
LAB 9 INTRODUCTION TO THE LINE FOLLOWING ROBOT ............................................................. 47
LAB 10 FINAL PROJECT ................................................................................................................................ 51
MICROCONTROLLER (MCU) FUNDAMENTALS ........................................................... 53
SOFTWARE ............................................................................................................................... 55
WRITING TO THE PIC MCU USING THE PICKIT 2 OR 3 .............................................. 62
ASCII .......................................................................................................................................... 65
THE SIMULATOR .................................................................................................................... 68
THE OSCILLOSCOPE ............................................................................................................. 75
GRAPHING WITH MS EXCEL .............................................................................................. 83
NOTES I INTRODUCTION ............................................................................................................................. 87
NOTES II C PROGRAMMING ....................................................................................................................... 89
NOTES III NUMBERS ...................................................................................................................................... 96
NOTES IV REVIEW OF ELECTRIC CIRCUITS ....................................................................................... 109
NOTES V DIGITAL INPUT OUTPUT (DIO) .............................................................................................. 123
NOTES VI OSCILLATOR FREQUENCY & DELAYS .............................................................................. 136
NOTES VII PULSE-WIDTH MODULATION (PWM) ............................................................................... 143
NOTES VIII TIMER0 MODULE................................................................................................................... 147
NOTES IX ANALOG TO DIGITAL CONVERSION (ADC) ..................................................................... 154
NOTES X ROBOT ........................................................................................................................................... 163

2
Introduction

The Integrated Circuit (IC) has become pervasive in modern society. Once associated only with
Electronics Engineering, the modern IC has evolved to the point where it has become a standard
tool in all areas of engineering. Every professional engineer needs an understanding of the
capabilities of these devices. In this course you will learn the fundamentals of how a Programmable
Integrated Circuit/Microcontroller Unit (PIC/MCU) works. Working with a high level
programming language, C, you will make a MCU handle a wide variety of tasks including reacting
to external stimuli. The ultimate programming challenge will be to make a robot follow a
challenging and arbitrary course. Along the way you will learn basic electronics, interfacing with
sensors, group task management, professional engineer report writing style, and oral presentation
skills.

3
Report Format
There are no formal reports, such as you are used to from your physics and chemistry courses, for
these labs. Instead you will follow industry standards and keep a diary or journal of your daily
progress. A journal is a rough but thorough record of what you did in the lab – including things
that don’t seem to work out at all! It should indicate what you set out to do and why, what data
you measured, what calculations you made, and what conclusions you drew from the data at that
time. It should be as neat and legible as possible considering the circumstances. There are a few
rules to note:

 Use a black hardcover lab notebook. Extra work, such as a graph, is to be glued or stapled
into the notebook.
 Write in permanent ink.
 Do not erase or whiteout anything. Use a single line through data or even paragraphs to
indicate corrections.
 Do not remove pages from the notebook.
 Initial and date each page.
 Symbols must be defined.
 Measurements require units and some indications of estimated uncertainty.
 Record the time regularly as you work

While we are using a journal format, the notebook must be readable to you and your lab
instructor. That requires some structure. Each section should have a Goal outline and at the end
should have a Summary of your findings. In between you should

 record data neatly and in well-labelled tables,


 indicate what you have tried,
 show calculations,
 include circuits drawings,
 include code – with comments – and staple or glue it to the left hand pages,
 explain the available options in XC8 library functions,
 record error and warning messages encountered. Include their number code, when they
occurred, why they occurred, and how they were fixed.
 tell what you have explored on your own, and
 answer questions.

You will be allowed to bring and use your notebook during class tests. Your notebook will only
be as helpful as you make it.

4
Lab 1 Serial Communication

Lab 1 Serial Communication


Objectives:
(a) To build and compile your first C project for the PIC18F4525.
(b) To learn how to make use of multiple source and header files in a project.
(c) To learn to use a SparkFun-enabled LCD as the standard output for the PIC MCU.
(d) To use the printf() function to control the LCD and display ASCII characters

Part A – Working with an LCD

Equipment:
LCD display with SparkFun serial connector, wirestrippers, and wire.

Files:
HelloWorld.c, osc.c, configureUSART.c, osc.h, configureUSART.h

Preparing the PIC MCU


You will be building your circuits on a breadboard (see Figure 1-1). The breadboard allows quick
assembly and disassembly of circuits. The breadboard has a pattern of small holes in it. The leads
on resistors, LEDs, capacitors, and other electrical devices can be snugly inserted into these holes.
There are two long rows of holes on either side of the length of the breadboard. The holes in each
row are all internally connected to a conducting rail of metal. The power supply makes a
connection to these holes. Thus, the holes in the rails labelled with a ‘+’ sign are at +5

Pin label stuck on PIC MCU common node strip Regulator and
connector board
PICkit 3 Programmer
programmer

Connecting MCU
to VDD and VSS +5 V power rails (beside red line)
& 0 V ground rails (beside blue
line)

DC Adaptor

Figure 1-1: Recommended placement of PIC, battery wires, and PICkit 3 connector

5
Lab 1 Serial Communication

Volts and the rails marked with the ‘–’ sign are grounded (0 Volts). Each short column of five
holes is also connected to its own individual metal strip. See Figure I-2 in Lecture I of the Notes
section of the manual for a schematic.

The red board with the connector for the DC adaptor uses a regulator to reduce the adaptor voltage
to 5 V and to connect the power and ground rails. The PIC MCU needs to be connected to power
and ground on both sides. Refer to the pin out diagram in Appendix A to find the correct pin
numbers for VDD and VSS on each side of the MCU. You have also been given a small pin diagram
that can be stuck on the MCU to identify the pins. Stick it on the MCU making sure that it is the
correct orientation. Create small connecting wires for the connections. Use wire strippers to
remove the plastic covering from the ends of connector wires. The bare ends should be about a
centimetre in length. You may need to practice. Wires should be kept short and neat. Plug the bare
ends firmly into the holes. The wires should be flat to the breadboard. Wire pin VDD to power (+)
and pin VSS to ground (–) on both sides, Figure 1-1. Traditionally, red is used for power
connections and black for ground.

The breadboard has a connector or header for the black PICKit 2 or the red PICkit 3 programmer.
The connector should already be wired to the breadboard. If not, check the wiring diagram in
Appendix C: Writing to the PIC MCU using the PICkit 2 or 3. Note that the programmers should
normally be face up with the logo showing when connected properly. Your chip needs to be getting
power from a battery or other power supply for the device to write to the chip.

Possible Pitfall: The PICkit 2 and PICkit 3 devices are not interchangeable. The
wiring to the PIC is different.

Connecting the LCD


Turn off or disconnect the DC Adaptor, if it is
connected, and connect the liquid crystal display LCD
(LCD) that is enhanced by the SparkFun connector.
The 5V and GRD wires of the connector should be
connected to + and – on the breadboard. The RX to TX pin
(receive) wire, see Figure 1-2, carries the signal and
should be connected to the TX (transmit), pin 25, of Figure 1-2: SparkFun Serial LCD
the MCU. A pin-out diagram of the MCU is given in
Appendix A so that you can find the correct pin, or you can find it on the label you have stuck to
the MCU.

 Connect the DC adaptor and watch the display. Note that the SparkFun Splash screen is
automatically displayed before it goes blank. Also note that the LCD has two lines of sixteen
characters each.

6
Lab 1 Serial Communication

Background on Serial Communication and the SparkFun serLCD


There has always been a need to get two electronic machines to communicate with each other. One
of the earliest methods for serial communication is USART (Universal Synchronous-
Asynchronous Receiver/Transmitter). USART communication is very common. For example,
every PC has a serial port, labelled COM1, available for serial communication. The PIC18F4525
and the SparkFun LCD communicate using this protocol as well.

In serial communication, information is sent back and forth in series of ones and zeroes. The ones
are represented by a high voltage while the zeroes are at low voltage. To communicate, two
machines have to follow the same rules for the communication, such as whether data bits will be
sent in groups of eight or nine, and at what speed. In serial communication, the speed is called the
baud rate. The baud rate determines how long a high or low bit lasts; the higher the baud rate, the
shorter the time. Baud rates of 2400, 9600, 14 400, etc are typical. Modern USART connectors
can go higher, up to 115 200 and more.

The PIC18F4525 has a single USART module, or built-in circuit, for serial communication. Two
pins, 25 or TX, and 26 or RX, respectively, handle the transmission and reception of serial signals.
These pins are multiplexed with Input-Output (IO) ports RC6 and RC7. Serial Communication is
basically timed Digital Input and Output (DIO), i.e. sequences of ‘on’ and ‘off’ signals that each
last a specific amount of time. Hence pin 25, RC6, must be configured as a digital output to send
on/off signals, and pin 26, RC7, as a digital input to read incoming on/off signals. The library xc.h
contains functions for serial communication. The principle function for using serial
communication is called OpenUSART(). It is somewhat difficult to understand and to discuss right
now, so instead you have been provided with a simpler source file, configureUSART.c, and an
associated header file configureUSART.h. Be sure to include these files any time you plan on
using serial communication. The function configureUSART(), in the configureUSART source file,
ensures the proper configuration of the TX and RX pins and initializes the serial port for the most
common format of serial communication used by the LCD or by a PC running a terminal program
like PuTTY. This function is easy to use; it only needs the communication speed or baudrate and
the operating frequency of the MCU. The SparkFun LCD can operate at different speeds but the
default rate is 9600 bits/second or bps. We will use the familiar printf() function, in the stdio
library, to send text from the PIC MCU to the LCD. For printf() to work, it needs to know how
to pass text to the USART module. In the XC8 Compiler, this is done via a helper function putch()
that is also in the configureUSART files.

An LCD is a display controlled by its own IC. The IC controls which pixels on the display are dark
or not. The IC can communicate with other devices (it wouldn’t be much use otherwise) but it does
not use USART. At the time the LCD was developed, the electronics needed for USART were
both expensive and physically much larger. The makers of the LCD, Hitachi, opted for a much
simpler control method which requires many more wiring connections. Since USART–enabled
chips are now small and cheap, SparkFun developed the serLCD or serial LCD. An extra IC takes
USART signals and translates them into signals that the LCD understands. This requires only one
wire (besides power for the IC and LCD). The RX or receive pin of the SerLCD should be
connected to the TX transmit pin on the PIC18F4525. The power and ground pins can be connected
to the breadboard without any buffering or other worries.

7
Lab 1 Serial Communication

Your First Project


In the student folder on the desktop, or preferably on your own USB memory device, create a new
directory to hold your projects similar to “YourNameProjects” for easy identification. In this
directory, create two new subdirectories; call one “Lab1” for this lab’s work and the other
“Common”, which will contain functions that you will reuse in other labs. From the APSC1299
website (get the URL from your lab instructor), copy the files listed in the software section to your
directories. Place osc.c, configureUSART.c, osc.h, and configureUSART.h in the Common
directory. Place HelloWorld.c in your Lab1 directory. See the directory structure below. Any
program files you create this week, or any example files we give you, should go in this same Lab1
directory.

 YourNameProjects
 Common
osc.c, configureUSART.c, osc.h, configureUSART.h
 Lab1
HelloWorld.c
 Lab2
 Lab3

Make sure that the case (upper and lower letters) of the names is exactly the same as listed
above. Rename the files if necessary.

Open the MPLAB program. Use the MPLAB icon on the desktop or find it in the Start|Programs
list. Follow the steps outlined in Appendix B to properly use Lab1 as your project directory and
name your project files. Your output files for this lab should end up in your Lab1 directory. Build
your project. With some luck, you will have had a successful build. If not, ask for assistance.

Your source files are HelloWorld.c, osc.c, and configureUSART.c. Open and inspect the source
files. Note the inclusion of the header files we have given you, osc.h and configureUSART.h,
which contain prototypes for the functions in osc.c and configureUSART.c. Inspect these files as
well. The file HelloWorld.c contains the only main() function in this project, as required. The
main() function has an endless while loop to keep the function running forever on this chip. There
are two types of header files used in the HelloWorld.c file. These are listed at the top of each source
file and are marked for inclusion by the #include directive. Header files in chevrons, <…>, indicate
use of the XC8 compiler’s own libraries of functions. The source files that these headers point to
are already compiled and you do not need to include them in your project source file list because
the compiler looks for them in the default XC8 folder. Header files in double quotes, “…”, are
header files for functions you will write or for functions and macros that we provide in this course.
Include these header files in your project by right clicking on Header Files in the project and
choosing add existing item. Do the same with Source Files. Your project files are in the Lab1
directory, while the functions we need to use are in the Common directory. In Appendix B, you are
given directions on how to make sure that Common is an Included directory. As long as this has
been done, the XC8 compile should be able to find all your files. If you get a message about one
of the header or source files not being found, this is probably the issue.

8
Lab 1 Serial Communication

Similarly, we will be using functions from the XC8 peripheral library. That library needs to be
linked to your project. Again, in Appendix B, there are instruction on how to do this. If you get
error messages that the compiler doesn’t understand a function, this is the probable cause.

Installing your Program on the MCU


Connect the PICkit programmer and the DC adapter to your MCU and breadboard. Follow the
steps listed in Appendix C to install your compiled project onto the PIC18F4525. This is also called
‘writing to the MCU’ or ‘loading a program’ on the MCU. Nothing will appear to happen as there
is no display.

 Modify HelloWorld.c by commenting out the while(1) loop. Build the project and program the
MCU. Observe on the LCD the differences with the previous project.

 Undo the previous changes before continuing, i.e. put the while(1) loop back in.

The Configuration Directives


The configuration statements at the start of the HelloWorld.c file are needed in every project you
will build. Rather than retype the lines for every project, it will be more convenient to place them
in their own header file, call it configuration_bits.h, and include it in each new project’s main file.

 In the MPLAB IDE, create a new file. Cut and paste the lines

// standard configuration statements for this course


#pragma config WDT = OFF
#pragma config OSC = INTIO7 // puts osc/4 on pin 14 to check freq
#pragma config MCLRE = OFF
#pragma config LVP = OFF
#pragma config PBADEN = OFF // PORTB<4:0> are digital IO

to the new file. Save the file as configuration_bits.h in your Common folder. Place an
#include directive to configuration_bits.h in HelloWorld.c. Add configuration_bits.h to your
project header files. Rebuild HelloWorld.c to check that everything was done correctly.

Include configuration_bits.h into the project once only at the top of the file containing main()!

Processor Speed and Delays


The PIC MCU has an internal oscillator or clock that is used to keep all operations synchronized.
Unlike the CPU in a desktop computer, the oscillator in a PIC MCU can work at various speeds.
Low speeds reduce the power used by the MCU. Often, the PIC MCU runs off batteries and is run
at low speeds to prolong battery life. Laptop computers often vary their speed for the same reason.
The files osc.c and osc.h contains a function set_osc_32MHz() to set the processor speed to 32
MHz or fOSC = 32 MHz. In this and all other labs, make sure your MCU is running at 32 MHz, as
the configureUSART() function expects the processor to be at 32 MHz. Any other speed will foul
up communication with the LCD. The minimum time for most processor operations to complete
is four times the clock period, which is TOSC = 1/fOSC. This minimum time is called an instruction

9
Lab 1 Serial Communication

cycle; 1 TCY = 4TOSC = 4/fOSC. So for a PIC running at 32 MHz, TCY = 4/(32 MHz) = 1.25 × 10-7
seconds = 125 ns.

The XC8 Compiler has a set of library functions that will delay the MCU, i.e. cause it to do nothing,
for an exact number of instruction cycles TCY. The functions are void NOP(void) which gives a
1 instruction cycle delay, void _delay(unsigned long cycles) which gives a 1×cycles
instruction cycle delay, and _delay3(unsigned char unit) which gives a 3×unit instruction
cycle delay. Cycles in _delay() cannot be 0 or exceed 179200. If you exceed the maximum, the
compiler will give an error message. Note that the variable unit in _delay3() is of unsigned char
type, and can only be from 0 to 255. If you use a value of unit greater than 256, rollover occurs.
As well, zero will not give you a delay of 0 TCY (since any operations takes at least one instruction
cycle), instead it gives you 3 × 256 = 768 TCY.

There are also two functions that work in fractions of a second instead of TCY. The functions
__delay_us(float x) and __delay_ms(float x) actually count instruction cycles like
_delay(), but do a conversion to get the number of TCY to give the correct delay in microseconds
or milliseconds. These functions need to know the conversion factor, which is just the operating
frequency of the PIC MCU. Simply include the following definition to adjust for your operating
frequency.

# define _XTAL_FREQ 32000000 // operating at 32 MHz

Since the smallest time unit is 1 TCY, the smallest value of x in these functions must convert to a
minimum of 1 TCY. Similarly, the largest value of x cannot give a time interval greater than 179200
TCY. A value of x that is too large will generate an error at compilation

Printing and ASCII


Recall that you can use printf() to print strings, characters, and numbers. The basic format of
this function is

printf(“characters and/or formatting specifiers”[, variable 1, variable 2,…])

The parameter in quotes above is called the formatting string. The formatting string may contain
the basic formatting specifiers, such as %s for strings of characters, %c for single characters, %i for
integers, %u for unsigned integers, %x for integers displayed in hexadecimal, and %f for floating-
point numbers. These formatting specifiers indicate the format of the displayed variables. The […]
indicates that variables are optional; you could just print a string printf(“My message.”). Note
that printf(), like most C functions, does very little error checking. If the number of formatting
specifiers and variables don’t match, you will not receive an error message, and your program will
exhibit strange behaviour.

Computers like the PIC MCU only work with on’s and off’s. We know that we can treat these on’s
and off’s as the 1s and 0s of logic and binary numbers. No such simple relation applies to the
letters of the alphabet and other useful symbols. Instead, computers store symbols like ‘A’, ‘#’,
and ‘d’ as 8-bit (one byte) numbers. These numbers are then associated with the symbols of the
ASCII character set via a look-up table. The ASCII table is given in Appendix D. Functions like

10
Lab 1 Serial Communication

printf() tell displays to use the symbol at the corresponding table number. Since a char is byte-
sized, only 28 characters are available, some of which are nonprinting characters like TAB or
CARRIAGE RETURN (CR). Double quotes in C programming indicate a string of text symbols
that a computer should store as a string of bytes. Single quotes indicate a single character to be
stored as a single byte. For example, printf(“%c”,’A’) will print the capital letter A on the
display, but so will printf(“%c”,65), since decimal 65 is the number corresponding to the
symbol A in the ASCII table. An oddity that arises because of this is ASCII math. The statement
printf(“%c”,’#’+’A’) prints the character ‘d’. This happens because ‘#’ is represented by the
number 35, and ‘A’by the number 65. Since 35 + 65 = 100, the symbol ‘d’, ASCII character 100,
is printed.

Any number shown in a display is actually the string of characters for that number. For example
printf(“%i”,125) actually parses the number 125 into three symbols ‘1’, ‘2’, and ‘5’ that it asks
the display to show. Similarly, printf(“%x”,125) displays the symbols for 125 in base 16, i.e.
‘7d’.

Special Commands
The SerLCD IC interprets the ASCII character decimal 254, or hexadecimal 0xFE, as a command
character. Anytime you print this character to the SerLCD, the translator IC will interpret the next
one character, and only that one character, as a command. Some of the non-printing ASCII
characters 0 to 31 are also used as commands. The SparkFun datasheet uses notation like
<control>m, where m may be various symbols, to refer to these non-printing characters. Table
D-2 in Appendix D lets you translate this notation into the equivalent decimal or hexadecimal
value. Some common useful commands are listed in the table in Section 3.3 of the SparkFun
SerLCD datasheet. For example, the small snippet of code below, in Figure 1-3, shows how to
clear the display and move to the start of the display, turn the underline cursor on, move to the
fifth position on the bottom line of the display, and move to the 8th position of the top line. Note
that the character columns are numbered 0 to 15 on each line.

printf(“%c%c”,0xFE,0x01); // Clear and home display


printf(“%c%c”,254,14); // Underline on – can use decimal if
// you wish or both together
printf(“%c%c”,0xFE,128+64+4);// 128 is the command to move to a
// specific position and 64 is first
// column of the bottom line and 4
// indicates 4 more, i.e. the 5th
// column.
printf(“%c%c”,0xFE,128+7); // top line 8th column

Figure 1-3: LCD command examples.


 Make a copy of HelloWorld.c and rename the copy ascii.c. Create a new project that contains
ascii.c and files required to use the LCD. Modify the while(1) loop in ascii.c to contain a
clear LCD command followed by a for loop to print all the ASCII characters from 33 to 123.
There should be exactly a 0.20 second delay between characters being displayed. At the end
of the for loop, make the code wait one second. Observe the output on the LCD.

11
Lab 1 Serial Communication

 The LCD doesn’t actually use all the standard ASCII characters. Watch the output and a non-
ASCII character eventually shows up. What is the character and what is its decimal value?

 Modify your previous version of ascii.c such that the ASCII characters from 33 to 123 are each
printed on the bottom line at column 3, but include the decimal and hexadecimal values of the
character, you may use

printf(“%i %x %c”, i, i, i);

where i is the loop variable. Again, there should be exactly a 0.20 second delay between
characters being displayed and a one second wait at the end of the for loop. Observe the output
on the LCD.

 Make a note of the files and the lines of code needed to use the LCD or any other display. You
will need to use this code in later labs, in assignments, and on tests.

 The commands given in Figure 1-3 and the SparkFun SerLCD datasheet are not obvious.
Create a source file and header file with functions that have easy to understand names. For
example, create a function LCD_ClearDisplay() that contains the single instruction
printf(“%c%c”,0xFE,0x01). Do not include set_osc_32MHz() in that function;
set_osc_32MHz() is used even if you don’t use the LCD. See the SparkFun SerLCD datasheet
for all available functions.

 You will frequently need to use these and other functions in later labs, so make a library
consisting of serLCD.c and serLCD.h and place them in your Common directory. In future
labs, your lab instructor will not accept projects using the LCD that do not use your serLCD
library. Make sure your serLCD library functions work! In class, you will be asked to hand in
a copy of your library as an assignment.

12
Lab 2 Debugging Tools

Lab 2 Debugging Tools


Objectives
(a) Displaying a number in binary format.
(b) To examine various features of the simulator including breakpoints, watch windows, and
stopwatch.
(c) To examine simulated serial communication including keyboard input.
(d) To track down programming bugs by following the error and warning messages given by the
compiler.
(e) To track down programming bugs by using the simulator, setting breakpoints, and observing
variables as program execution proceeds from breakpoint to breakpoint.

Prelab:
Read the section of Appendix E on the simulator.

Files:
binaryformat.c, breakpoints.c, Debug.zip

Note:
We expect you to use your standard configuration_bits.h header and your serLCD library from
Lab 1 from now on.

Part A: Printing in Binary Format

Many C compilers have a format specifier %b to display integers of type char, int, and long as
binary numbers. For example, the code snippet shows how it works,

printf(“15 in binary format %b”, 15);

Output:
15 in binary format 0b00001111

Unfortunately, this is not true of the XC8 Compiler. We will sometimes need to display a number
in binary format, so we need to write our own functions. The function printCharAsBinary(unsigned
char number) in the provided file binaryformat.c shows how to do this.

 Create a second function printIntAsBinary(unsigned int number) to work with integers.

Hint: What is the main difference between data type char and data type int? What will your
printIntAsBinary() function need to do differently than printCharAsBinary() did?

 Create a binaryformat library (i.e. source and header files) and place them with your other
Common files.

13
Lab 2 Debugging Tools

Part B: The Simulator

The IDE has a debugging feature called the simulator that allows you to step through lines of
code and observe how variables change. In the simulator, you can also open a display
window to observe printing output. Read Appendix E on how to invoke (a) the simulator and (b)
the UART display window, then make the necessary changes by selecting File|Project
Properties|Simulator from the menu bar or by clicking on the Set Project Configuration icon.

 Run a project from Lab 1 in the simulator and observe the output on the UART display.

Note that any special code for controlling the serLCD is inappropriate for the display window
and might produce garbage symbols. Furthermore, all the output tries to print out on the same
line. You can have the output print to the start of a new line by using newline and carriage return
formatting specifiers in your print statement, or by simply adding the line
printf(“\n\r”);

after each of your printf() function calls.

Breakpoints, Watch, and StopWatch


Create a project from the file breakpoints.c. The file has sections labelled Section A, B, and C.
Add breakpoints to all the lines in these sections by pressing CTRL-F8 at each line, or by right-
clicking on the line number and selecting Breakpoint from the pop-up menu. From the menu bar,
again select File|Project Properties|Simulator, or click on the Set Project Configuration icon and
choose the correct operating speed – 32 MHz. Open a stopwatch window by selecting
Window|Debugging|StopWatch from the menu bar. Read Appendix E on how to use the Stopwatch.
Select the variables to watch. That is, for variables a to f, right click on the variable and select New
Watch or select CTRL+SHIFT+F9. There will be a new Variables window in the output area of
the IDE. Note that when execution of the program jumps to a breakpoint, the statement on that line
has not yet been executed. Any variable value is determined by the lines above the breakpoint.

 Debug the Main Project and use the watch window to view the bit operations of Section A.
Step slowly through each calculation and note the value of variables a, b, and c in binary in the
Watch window. (You can see the values of the variables in decimal, hex, and octal, but binary
operations make the most sense when the values are shown in binary.) When you stop at a
breakpoint, remember that line of code has not yet executed.

 Recall the logic operation truth tables for & (AND), | (OR), and ^ (XOR – exclusive or). The
NOT operator, ~, flips bits so ~1 = 0 and ~0 = 1. Are the results for the value c consistent with
these operations?

& 0 1 | 0 1 ^ 0 1
0 0 0 0 0 1 0 0 1
1 0 1 1 1 1 1 1 0
Figure 2-1: Truth tables.

14
Lab 2 Debugging Tools

 We can use the Stopwatch window to observe the number of instruction cycles (TCY) between
two successive breakpoints. Use the Stopwatch window to record the number of instruction
cycles for the addition of two numbers, first char, and then int. Which takes longer and why?

 Use the Stopwatch window to time each delay function and the NOP() function. Does the
number of instruction cycles match the function call? Why or why not?

Part C: Error and Warning Messages

Most errors are caught by the IDE and a message is given in the output window, which often points
out on which line the error occurred. For that reason, make sure to turn on the display of line
numbers for each C file. Some of the messages are very cryptic and you need to refer to the manual
to figure out what the message means. Use the listed error and warning message explanations in
Appendix C of the MPLAB XC8 Compiler User’s Guide to debug the following projects.

There are a number of project folders in the file Debug.zip. Unzip the directory and place it on the
same level as your Common, Lab 1, and Lab 2 folders.

 YourNameProjects
 Common
 Lab1
 Lab2
 Debug

 Load each DebugN.X from inside the IDE using File|Open Project. Choose the appropriate
programmer, PICkit2 or PICkit 3. An explanation of what each program is supposed to do is
given at the start of the .c file of the same name. If there are function source and header files
in the folder, they must be added from the Common folder. Build and run the file and observe
the message produced in the output window. If there is an error or warning message, what do
you need to do to fix the projects? Do not significantly change the code.

Misbehaving Programs
Some projects compile without error messages but the program does not behave as desired. An
explanation of what each program is supposed to do is given at the start of the .c file of the same
name.

 For each Debug project, use the simulator and watch how important variables change from
breakpoint to breakpoint. Check, using Stopwatch, that the timing is exactly as described at
the start of the source file. Find and fix each problem where the behavior does not match the
description. Do not significantly change the code. You are not supposed to rewrite the code.
Make only small changes to get the program to operate correctly. Note which changes are
needed and why.

15
Lab 3 DIO

Lab 3 Digital I/O


Objectives
(a) To wire series and parallel circuits on a breadboard.
(b) To program a pin as a digital output or as a digital input.

Equipment, Files, and Data Sheets


Item Quantity Files
Breadboard 1 Blink_RD2.c
PIC 18F4525 1 Control_RD2.c
10 kOhm resistor 2
200 Ohm resistor 4 Data Sheets
LED 2 PIC 18F4525
Push-button switch 2 LED
PICkit-2 or PICkit-3 1
Wire stripper 1
Needle-nose pliers 1
DMM 1

Part A: Light up a Light Emitting Diode (LED)

Each short column of five holes, called the ‘internal strip’ or ‘rung’ or ‘tie down’, on the
breadboard is connected to its own individual metal strip. See Figure I-2 in Lecture I of the Notes
for a schematic. Figure 3-1 shows a typical circuit of a resistor and LED in series with the power
supply, and how to use the breadboard blocks to make the connection. In the lower part of Figure
3-1, conventional current flows from the +5V strip, through the connector wire, to the first
common block. The resistor makes a path from the first common block to the next. Finally, the
LED provides a path from the second common block to ground to complete the circuit.

+5 V 200  LED
(power
supply)

+5 V +5 V
connector
wire

Figure 3-1: Schematic to light an LED

16
Lab 3 DIO

Only one end of a resistor, or LED, or other circuit element, can be connected to a particular
internal strip. The other must be connected to a different internal strip or to the bus (power/ground)
rails. A circuit element with both ends connected to the same internal strip has been short circuited.
No current flows through it. The internal strips act as nodes or junctions that different circuit
elements can share.

Building simple circuits with series and parallel elements on a breadboard is a basic skill to be
learned. The ends of each circuit element (e.g. a resistor or LED) are connected to two different
strips or nodes. Connector wires can be used to create series and parallel arrangements. Connector
wires should only be as long as necessary. This keeps the circuit from being cluttered and hard to
trace. To gain some familiarity with building circuits and light emitting diodes (LEDs), we will
build the circuits given in Table 3-1 below.

 Arrange two 200- resistors and one 10-k resistor across


the central groove of the breadboard as shown in Figure 3-2.
Add two LEDs as well, with one leg of each in ground.
However, make sure one LED is in the reverse orientation of
the other. You will need to use short lengths of wire to
complete various circuits. It is fine to leave any extra
resistors and LED unconnected.

 Build the circuits listed in Table 3-1 below one at a time.


Each time, make sure that the forward biased LED lights up
and note the comparative brightness. Use the DMM (Digital
Multimeter) to measure the voltage drop across each resistor
and separately across the LED. Each path of your circuits
goes from +5 V to ground, so the voltage drop across each
circuit element along the path must add up to 5 Volts.
Describe the brightness of the LED in each case. Put your Figure 3-2: Resistor circuits
measurements in a table and leave room for some columns
of calculations (see next paragraph).

 The current through a resistor can be determined from Ohm’s Law I = V/R. Note that LEDs
do not obey Ohm’s Law, as they do not have a fixed resistance (as you will see from your
measurements.) To determine the current through the LED, first determine the current through
the resistors in the circuit and then use Conservation of Charge. Calculate the power P =
ILEDVLED used by the LED. Add columns for LED current and power to your table above.
Table 3-1: Resistor & LED combinations
One 200- resistor and LED in series
Two 200- resistors in series followed by an LED in series
Two 200- resistors in parallel followed by an LED in series
One 10-K resistor followed by an LED in series
One 200- resistor and LED in series but the LED is reverse biased. Note
voltmeters can give faulty readings across reverse-biased diodes since
they act like they have extremely large resistance.

17
Lab 3 DIO

 We have stated that forward-biased LEDs can be treated as a 2 Volt drop. Is that always
reasonable? We have also said that the resistance of the LED circuit controls the brightness of
the LED. Is that a correct statement?

Possible Pitfall: Check the polarity of your LED (use the information provided in
the appendix). If it does not light, then you may have it reversed.

Grief Saver: Until you are experienced, remember to check the pattern of the
connecting bars inside the breadboard. See Figure I-2 in Lecture I of the Notes
section of the manual for a schematic.

Warning: Turn off the power to the board when making circuit changes. That way,
you avoid shocks and damaging the equipment.

Warning: If any element gets hot and or smoke rises, disconnect the power! You
probably shorted something.

Remember to make note of all your observations in your notebook; also include notes when things
do not work.

Part B: Programming a Pin as a Digital Output

You will now use your PIC to control an LED, making it turn on and off. You do this in your
program by declaring a particular pin to be a digital output. That is, make the pin act as a simple
power supply, and turn the output pin on or off. Most pins on the P18F4525 can be used for DIO
– Digital Input and Output – operations. We will discuss Digital Input in another lab. The pins are
grouped in groups of eight pins known as ports. The ports are called A, B, C, and D. The first pin
of Port A is called RA0. The second pin of Port B is RB1. The last pin of Port D is RD7, and so
on. A glance at the pin-out diagram in Appendix A will show which pin belongs to which port. The
input or output status of each port is controlled by special function registers (SFR), which are a
byte of special memory of the PIC MCU called TRISA, TRISB, TRISC, and TRISD. C variables
with the same names, TRISA, TRISB, TRISC, and TRISD, are defined in xc.h to access these
ports. Each variable is an unsigned char, 8 bits long, and each bit controls a particular pin. The
input and output status of each pin of Port D can be set by the command

TRISD = 0b11110000; // Read from right to left


// bit 0 = 0 -> RD0 as an output
// bit 1 = 0 -> RD1 as an output
// bit 2 = 0 -> RD2 as an output
// bit 3 = 0 -> RD3 as an output
// bit 4 = 1 -> RD4 as an input
// bit 5 = 1 -> RD5 as an input
// bit 6 = 1 -> RD6 as an input
// bit 7 = 1 -> RD7 as an input

If we wish to set or clear individual bits by themselves, we can. A bit-field variable has been
defined in xc.h for each port and its eight pins for this purpose. The name of each bit-field is in

18
Lab 3 DIO

the form TRISXbits where X is A, B, C, or D. Each bit in the bitfield is named in the form RXn
where X is A, B, C, or D and n runs from zero to seven. The member operator (a period ".") is
used to make an expression that combines the bit-field variable name and the member name. For
example

TRISDbits.RD2 = 0; // = 0 configure RD2 as an output


// = 1 configure RD2 as an input

When ports are configured as outputs – that is, when the pins can be set to high or low voltage – a
second set of SFRs – PORTA, PORTB, PORTC, and PORTD – are used to set (turn on) or clear
(turn off) the output signal, as shown in the next code fragment.

TRISD = 0b11110000; // see above RD0 to RD3 are outputs


PORTD = 0b01010101 // bit 0 = 1 -> RD0 high
// bit 1 = 0 -> RD1 low
// bit 2 = 1 -> RD2 high
// bit 3 = 0 -> RD3 low
// bit 4 = 1 -> no effect! RD4 is an input
// bit 5 = 0 -> no effect! RD5 is an input
// bit 6 = 1 -> no effect! RD6 is an input
// bit 7 = 0 -> no effect! RD7 is an input

Again, a bitfield for each port and its eight pins has been defined in xc.h so that each pin may be
set individually. The names of the bitfields are PORTAbits, PORTBbits, PORTCbits, and
PORTDbits. Each bit in the bitfield is addressed by the expression PORTXbits.RXn where X is
A, B, C, or D and n runs from zero to seven. For example

PORTDbits.RD2 = 0; // Note: works only if pin is output


// = 1 set or turn on RD2
// = 0 clear or turn off RD2

When using a pin as an output, always


ensure that the current load placed on
the PIC does not exceed its tolerances.
The data sheet indicates that maximum PIC18F4525
current that the PIC can tolerate is about
25 mA. Consider the resistor and LED 21
circuit in Figure 3-3. From Kirchhoff’s RD2
Loop Rule, Vsource = VR + VLED. Since R
we know Vsource and VLED, and VR = IR 1 LED
from Ohm’s Law, the current to the
LED is given by I = (Vsource – VLED)/R
where Vsource is the output voltage and R VSS
is the resistance in series with the LED.
Figure 3-3: Schematic to control LED with PIC
 Calculate the minimum resistance R to ensure that the PIC is not damaged when lighting the
LED. Wire a large enough resistor and LED to pin 21, RD2, of the MCU as in Figure 3-3.

19
Lab 3 DIO

Possible Pitfall: The location of the PIC pins on the circuit schematic does not
correlate to the physical location of the pins. Use the pin-out diagram provided
(or check the data sheet) for the physical location of the pins.

Grief Saver: leaving your wires longer than needed can lead to a rat’s nest
forming on your breadboard. Once you are sure that the circuit is working
properly, trim your wires relatively flush to the breadboard. This makes your
circuit easier to debug and it is less likely for a wire to be accidentally pulled out.

 You have been provided with C source code (Blink_RD2.c) that will cause an LED connected
to RD2 to blink on and off. Make a project with this code and program the PIC MCU. Observe
the behaviour of the LED.

 Open the simulator and open a Watch window. Add the TRISD and PORTD SFRs to the Watch
window. It is best to display them in binary. Add breakpoints to the source file so that you can
observe how the values of the SFRs change as code executes.

Part C: Switching Control

Now you will add user control to your circuit by means of a pushbutton switch connected to the
PIC. As long as the pushbutton is pressed, we want the PIC to light up an attached LED. The PIC
pin that the pushbutton is attached to must be configured as a digital input and the LED pin must
be configured as an output. We will use RD1 as the input from the switch and RD2 as the output.
Refer to the PIC pin-out schematic in Appendix A to determine the pin numbers (record this kind
of information in your notebook). The appropriate configuration code is

TRISDbits.RD2 = 0; // configure RD2 as an output to power LED


TRISDbits.RD1 = 1; // configure RD1 as an input for switch

Digital hardware within the PIC will change the value of PORTDbits.RD2 in response to the input
voltage level. The value of PORTDbits.RD1 will thus automatically become 0 if the pin is pulled
low (VSS, ground or ‘zero’ volts) and automatically becomes 1 when the pin is pulled high (VDD,
5V). You need to add code to the while(1) loop to check the value of RD1 and respond in the
desired fashion:

if( 1 == PORTDbits.RD1 ) // button is pressed


PORTDbits.RD2 = 1; // turn on the LED pin

if( 0 == PORTDbits.RD1 ) // button is not pressed


PORTDbits.RD2 = 0; // turn off the LED pin

Alternatively, you can streamline your code by using the variable value as a Boolean directly:

if( PORTDbits.RD1 ) // button is pressed


PORTDbits.RD2 = 1; // turn on the LED
else // button is not pressed, !PORTDbits.RD1
PORTDbits.RD2 = 0; // turn off the LED

20
Lab 3 DIO

Or, even more compactly, you can use:

PORTDbits.RD2 = PORTDbits.RD1; // Change RD2 based on status of RD1

A small file Control_RD2.c is provided, but wiring the actual pushbutton to the PIC is more
complicated than the code. There may be 4 pins on the switch you use. When the switch is
depressed (closed) all 4 pins are electrically connected. When the switch is open, two pairs of pins
each remain connected. This is illustrated in Figure 3-4. Use a multimeter to investigate your
switch before attempting to build the circuit shown in Figure 3-5.

Switch open Switch closed

2 left 2 right 2 left 2 right


pins pins pins pins

Figure 3-4: Pushbutton illustration and pin schematic.


We are not wiring this switch in the same way as a flashlight or house lighting switch. In those
switches, they complete a circuit to allow a large current to flow through a lightbulb. Here, we are
using the switch to pull a point of the circuit (the PIC pin) either up to high when pressed or to
keep it low otherwise. This is called a logic switch and very little current will flow into the input
pin.

Trace your finger along the schematic VDD


shown in Figure 3-5, imagining the
switch open to determine what the PIC Button
pin potential will be. Then do a tracing switch
with the switch closed. B1 200  25
20 TX LCD
RD1
Note that the 200- resistor has PIC18F4525
virtually no effect in the circuit when 200 
pin 20 is configured as an input; RD0
RD2
however, should the pin inadvertently 21
10 k
be configured as an output, the 200- LED
resistor will ensure that the pin is not
connected directly to 5 volts by the VSS
VSS
switch. It is simply too easy to
download the wrong program into the Figure 3-5: Schematic to control PIC with a switch
PIC and it is quite possible another
program would have pin 20 configured
as an output and set low. In such a case, it is quite possible to damage the PIC with excessive
current.

21
Lab 3 DIO

Wire your switch to the PIC according to the schematic shown in Figure 3-5 and connect your
LCD if it is not already connected. Use the DMM to check that the voltage at the input is indeed
zero when the button is not pressed and +5 V when it is pressed.

 Build and test a project based on Control_RD2.c.

Part D: In-Circuit Debugging

In Lab 2, we used the Simulator to investigate how internal variables changed from one part of the
program to the next. Since it was a simulation, the program was running virtually on your PC, not
on the MCU. So while the Simulator is great for debugging and tracing the behaviour of a program,
it cannot tell us how the program will respond to an external stimulus like a button press.

The PICkit 2 and 3 have a feature, In-Circuit-Debugging (ICD), that runs, halts and single-steps
the program while the PIC microcontroller is actually embedded in the application. When halted
at a breakpoint, the file registers can be examined and modified. For instance, this lets you see if
your button press actually has the intended consequence. You can install up to 3 breakpoints for
the PIC18F4525. The in-circuit debug implementation on PIC microcontrollers will halt execution
on the instruction after the breakpoint instruction. This means the breakpoint instruction will have
executed when the debugger halts. This is referred to as "breakpoint skidding". You may need to
add one or more NOP() functions (technically they are macros but they are written to look like
regular functions) after your desired breakpoints to stop near where you actually want to stop.

 Add a breakpoint inside the while(1) loop. Again, you may need several NOP() functions. Add
a watch window that contains the PORTD SFR displayed in binary. It can be expanded to show
the individual pins, RD0 to RD7, by clicking on the + sign beside PORTD. Select Debug Main
Project and build and load the program. Observe the PORTD SFR as you step through the
project with and without your finger on the button.

 Double click on the binary value of PORTD with your finger off the button. Change bit 2
(RD2) from 0 to 1 and observe the results.

Part E: Controlling the LCD

Rewriting a string to the LCD every time the while(1) loop executes is inefficient. It wastes time,
and, if done with clear display commands, results in a faded display.

 Modify your Control_RD2.c program to work with the LCD. When the switch is depressed,
the LED should light up and the LCD should display “Button #1 – ON”. When the switch is
not pressed, the LED should be off and the LCD should display “Button #1 – OFF”. If your
display is not the same darkness in both cases, explain why. To reiterate, the LCD display
should only update when necessary.

22
Lab 4 Toggles & Menus

Lab 4 Toggles & Menus


Objectives
(a) To create a software toggle switch.
(b) To use two buttons with an LCD menu.

Files:
buttons.h, buttons.c, softToggle.c, alterBlinkRate.c

Part A: A Pushbutton as a Soft-Toggle Switch

The switch you have installed in the previous lab was a pushbutton switch. When depressed, the
contacts connect and the pins are all connected. Once released, the contacts open and no connection
is made. You are likely familiar with pushbutton mechanical toggle switches, where a single button
push turns the switch on (closes) and it latches on until you press the switch again and it releases
(opens). This is called a toggle switch because each action ‘toggles’ the state between open and
closed.

To make the software respond to a pushbutton switch toggle, i.e. a software-toggle or soft-toggle,
you must have a variable that will change from 0 to 1 when the switch is first pressed, and then
flip values for each new press of the button. A new button press is found by detecting a rising
voltage at the input pin. A function to do this, called monitor_switch1_for_edges(), has been
written for you and placed in buttons.c. Examine the logic in the incomplete code sample,
softToggle.c, below:

#include “buttons.h"

int main(void)
{
unsigned char has_switch1_changed = 0;
TRISDbits.RD1 = 1; // set RD1 as input (switch 1)
while(1)
{
has_switch1_changed = monitor_switch1_for_edges(PORTDbits.RD1);
// most of the time has_switch1_changed is 0
if ( has_switch1_changed == 1 ) // rising edge
{
//code here
}
if ( has_switch1_changed == 2 ) // falling edge;
{
//code here
}

}
}

23
Lab 4 Toggles & Menus

Note that you only have to insert code at the appropriate place to have a button or any other digital
input do useful actions.

 Modify the above “action” function in the program so that a single press of the button will turn
on an LED while the next press of the button will turn the LED off. This is most easily
accomplished by checking if the LED pin is already on. If it is on, turn it off, else turn it off.
Also add in all the missing #include files to run at 32 MHz. Have the program update the LCD
as well. Make sure the LCD does not refresh unless it has to.

 Examine buttons.h and buttons.c. Modify them so you can have a second switch (switch2).
You only need duplicate the existing functions and change the labels on them from 1 to 2.

Part B: Double Trouble: Two Buttons and Two LEDs

Wire a second switch and second LED in the same manner as the first switch and LED. Your
schematic should look similar to Figure 4-1. You may choose to use different pins for your
convenience, but you should avoid Port A and B pins for now (see the very end of Notes V).
Each toggle separately controls its own LED.

VDD VDD

Button Button
switch switch PIC18F4525
B1 B2 200  25
TX LCD
RD0
200 
200  RD3
RD1 200 
RD0
RD2 LED
10 k 10 k
LED

VS VS VSS VSS
S S

Figure 4-1: Schematic to control PIC with two switches

 Build a two toggle project and make sure it works.


Grief Saver: We can sometimes overwrite previous versions of working source
code when modifying old code to ‘create’ a new program. It is good practice to
save incremental versions of your code. Give them predictable names
(myhappycode_A then myhappycode_B, etc.) Be sure to comment in the header
of each file what the code does, and how it differs from previous versions. These
comments are ideally done as a history table.

24
Lab 4 Toggles & Menus

Part C: Blinking LEDs

To blink an LED, you turn the output high for a given length of time using a delay function, then
turn it off for another length of time. The total time is the period of the blink. The ratio of the ‘on’
time to the period is called the duty cycle and is often given as a percent. The blink will repeat if
you are inside the while loop and make no other changes, as in the code snippet below.

while(1){
PORTXbits.RXn = 1; // pin on
// some delay here
PORTXbits.RXn = 0; // pin off
// some delay here
}

We want to be able to choose different blink rates by pushing a button. You will need to monitor
the button each time the while(1) loop repeats and increment a counter that controls the blink rate.
We can use a switch statement to move between the different blink rates, as shown in
alterBlinkRate.c. Note that if the total time to blink an LED is long, you don’t get to the
monitor_switch1_for_changes() function very often and the button will seem unresponsive.

#include "buttons.h"

int main(void)
{
unsigned char blinkrate = 0; // 4 rates, 0 to 3
unsigned char has_switch1_changed = 0;
TRISDbits.RD1 = 1; // set RD1 as input (switch 1)

while(1){
has_switch1_changed = monitor_switch1_for_edges(PORTDbits.RD1);
//happens every cycle of while loop

if (has_switch1_changed == 1)
{
//anything that should happen occasionally should be in here
blinkrate++; //increment counter
if (blinkrate > 3) blinkrate = 0; //cycle back to 0
}

//happens every cycle of while loop


switch(blinkrate) // switch between blink rates
{
case 0: // code here for pin on, delay, pin off, delay
break;
case 1: // code here for pin on, delay, pin off, delay
break;
case 2: // code here for pin on, delay, pin off, delay
break;
case 3: // code here for pin on, delay, pin off, delay
break;
}
}
}

25
Lab 4 Toggles & Menus

 Modify alterBlinkRate.c to work properly and to have four rates of blinking; off, slow,
moderate, and full on. For slow blinking, use a period of 0.240 s and a duty cycle of 33%, and
for moderate, use a period of 0.080 s and a duty cycle of 66%. The LCD should display OFF,
SLOW, MODERATE, and ON. It should only be refreshed when necessary, i.e. on the button
press. Have the second toggle change the behaviour of the LEDs according to Table 4-1.

Table 4-1: Two buttons making incremental changes


LED behaviour
Cycle up the rate of blinking of both LEDs with each depression of B1
Cycle down the rate of blinking of both LEDs with each depression of B2

Part D: Alternating LEDs

With two buttons you can also have the option of using the near-simultaneous pressing of both
buttons to perform a third action. One way to achieve this is the following:

if (has_switch1_changed == 1 && has_switch2_changed == 1)


{
// put action here for simultaneous button presses
}

 Add logic to your program so that pressing both buttons at the same time will flip the polarity
of the output pin signals, i.e. so LED 2 will be off when LED 1 is on for the same amount of
time, and vice versa. Pressing both buttons again should return to synchronous blinking.

 Have the LCD display the word Synchronous or Alternating on the bottom line, as appropriate.
The LCD should only change when necessary.

Note: We have now built a modestly complicated program in a series of small steps.
Small steps are much easier to write and to understand and to debug. Keep this in mind
when you work on later projects. Always break your development process down into a
series of small steps. You will become less frustrated trying to fix code and be more
efficient and productive. Lab time during the group project can be used more efficiently
by splitting large jobs up into smaller ones, with each partner taking their share.

26
Lab 5 Oscillator Speed & PWM

Lab 5 Oscillator Speed & PWM


Objectives:
(a) To use an oscilloscope to determine the processor speed of the PIC184525 chip.
(b) To use an oscilloscope to time the LED on and off delay periods from last lab.
(c) To use the PWM module and associated library functions to control LED brightness.
(d) To use the PWM module and associated library functions to generate sound.

Equipment:
Oscilloscope, 2 sets of probes, two-way bi-colour LED, your flashing LED program from last
week’s lab, speaker

Prelab:
 Read the appendix in the manual on the Oscilloscope.
 Read Section 2.7 in the PIC18F4525 datasheet on oscillator switching and the Oscillator
Control Register (OSCCON). Read Section 2.6.4 in the PIC18F4525 datasheet on using PLL.
Also examine the Oscillator Tuning Register (OSCTUN).
 Read section 15.4 on PWM in the PIC18F4525 datasheet.

Files:
Example code: osc.h, osc.c, pwm.c

Part A: Processor Speed

Timing is extremely important in most embedded systems. Operations, particularly


communications, have to happen at particular times or time intervals. The rate at which program
instructions execute depends on the processor speed or clock speed of the chip. A programmer
needs to know the processor speed of the PIC being used. The range of clock speeds is given in a
datasheet, but the chip manufacturer makes the direct determination of processor speed possible
through the use of a dedicated pin, pin14 CLK0, on this chip. This pin outputs a square wave which
is just a series of on-off pulses (see Figure 5-1). The frequency of this square wave output is one-
fourth of the actual processor speed, FOSC/4. That is, if you measure the frequency to be 1 MHz,
the processor runs at 4 MHz.

0
T t

Figure 5-1: A square wave signal.

27
Lab 5 Oscillator Speed & PWM

Pin 14 only has the CLK0 functionality if it is configured that way in your program. You must
have included the following directive:

#pragma config OSC = INTIO7 // puts osc/4 on pin 14 to check freq

Once configured this way, you cannot program pin 14 to be a DIO pin or anything else.

Check your configuration_bits.h file to make sure that this line is present.

 Set your oscilloscope to its default settings as outlined in Appendix F. The probes have small
alligator clips to facilitate connecting the oscilloscope to the breadboard. Adjust the time
control until you can see several complete square waves.

 Use the cursors to measure the voltage, the frequency, and the period T of the pulse or square
wave.

 Also measure the ‘on’ cycle of the square wave and compute the duty cycle. Duty cycles are
the ratio of the ‘on’ period to the complete period and are often expressed as a percentage.

Part B: Timing Your LED Output

Last lab, you created a program to blink LEDs at different rates. The signal you sent to the LED
was a square wave. You are going to use the oscilloscope to measure that square wave. Since
oscilloscopes work best with fast signals, change last week’s project so that the period for the
blinking is a thousand times faster. For example, if the blinking period was 0.240 s, change it to
0.240 ms.

 Connect the pin for LED1 to CH1 of the oscilloscope and likewise have the pin for LED2 read
by CH2. Separate the two signals vertically so that you can see them clearly. Measure the
voltage, frequency, period, and duty cycle of each square wave output.

Grief-saver: If your duty cycles on SLOW and MODERATE no longer look like 33% and
66%, use the Stopwatch to determine what is altering your code’s timing at these speeds

Part C: Altering the Clock Speed of the Processor

Unlike the CPU in a desktop computer, the PIC MCU can work at various speeds. Low speeds
reduce the power used by the MCU. Often, the PIC MCU runs off batteries and is run at low speeds
to prolong battery life. Laptop computers often vary their speed for the same reason. The speed of
the chip is controlled by two registers, register 2.2 OSCCON, the Oscillator Control Register, and
register 2.1 OSCTUNE, the Oscillator Tuning Register.

28
Lab 5 Oscillator Speed & PWM

Registers are special reserved portions of the PIC MCU’s memory whose settings determine how
the particular feature will operate. Registers only have 8 setting bits, which can be either on (1) or
off (0), and thus are only 8 bits wide. All the bits of the OSCCON and OSCTUN registers can be
read or written (i.e. changed by you) but some registers have bits that are read-only. The notation
in the datasheet for each individual bit is registername<bitnumber> so OSCCON<6> refers to the
6th bit of the Oscillator Control Register and OSCCON<6:4> refers to bits 6, 5, and 4. For extra
convenience, the individual bits of a register have names. For instance, OSCCON<6> is called
IRCF2 in the datasheet under Register 2-2 on page 30. In the XC8 compiler, you set (turn on) the
value of a register bit by the code registername+bits.bitname = 1. For example, to set
OSCCON<6>, type OSCCONbits.IRCF2 = 1. If you want to clear (turn off) the register bit, you
type OSCCONbits.IRCF2 = 0. The header file xc.h tells the processor how to interpret your
command to set the proper area of memory to the proper state.

The header file osc.h contains a prototype for the function set_osc_32MHz() to set the processor
speed to 8 MHz by setting the appropriate bits of the OSCCON register. The function itself is in
the osc.c source file. The function also sets PLLEN, bit 6 of the OSCTUNE register, which further
increases the oscillator speed by a factor of four. As a result, the PIC MCU will now operate at 32
MHz. Note that the PLLEN only increases the frequency if the oscillator speed is 4 MHz or 8
MHz. At lower processor speeds, it has no effect.

Since osc.h is not a XC8 header file, you must tell the compiler to use it. In your code you need a
#include directive. Use quotes rather than brackets, i.e. “osc.h” rather than <osc.h>. Chevron-
brackets indicate that the header file is in the XC8 compiler directory. Also, in the project window,
you must add the file to header files.

 Add functions to make the processor operate at 8 Mhz, 1 Mhz, and 500 kHz. They should be
named consistently with set_osc_32MHz().

 Run the processor at these different speeds and use the oscilloscope to confirm that the chip
frequency changes.

Note: You must make sure that you are communicating with the LCD at the correct
speed for each clock frequency. The supplied configureUSART() function should
make this easy to do.

Note: You should run your programs at the highest speed of the processor for the
rest of the course.

Part D: The PWM Module

An LED or a DC motor only has two states: on – if the applied voltage is great enough, and off –
if it is not. You could control the brightness or motor speed by putting a variable resistor in series
with the LED or Motor. When the resistance is low, the current is high and the LED is bright and
the motor spins fast. When the resistance is high, the current is low and the LED is dim and the
motor spins slowly. This is not a very efficient approach because energy is wasted in heating the

29
Lab 5 Oscillator Speed & PWM

resistor. An alternate, much more efficient, approach is to vary the LED brightness or the motor
speed using a square wave. Provided the period of the square wave is short enough, your eye
cannot register the LED going on and off. You see a brightness level controlled by how much of
the period the light is on. If the duty cycle is large, the LED is bright, and if the duty cycle is low,
the LED is dim. Similarly with a DC motor, the duty cycle lets you vary the speed of the motor.
During the duty cycle, the motor spins up to top speed, and after the duty cycle, slows down. For
short pulse periods, the average speed of the motor is controlled by the duty cycle. If the duty cycle
is large, the motor spins quickly and if the duty cycle is low, the motor rotates slowly. Controlling
a DC device in this manner is called Pulse Width Modulation (PWM), since an on-off square wave
is a pulse and you are modulating or controlling the width of the on portion of the pulse.

PWM is a common chore of embedded systems. As a result, many MCU PICs have built-in
modules for just that purpose. The PIC18F4525 has two PWM modules. One advantage of
dedicated PWM modules is that you do not need to continuously monitor PWM, so you can do
other processing chores at the same time.

PWM is controlled by the CCP1CON, CCP2CON, and T2CON control registers and the PR2,
CCPR1L, and CCPR2L data registers. The time base of the period and duty cycle is set by the
Timer2 Module. The properties of the Timer2 module are controlled by T2CON. The PWM signal
controlled by CCP1CON originates at the CCP1 pin, pin 17. PWM is discussed in Section 15.4 of
the datasheet. The steps needed to activate PWM are listed in Section 15.4.4.

Clearly setting all the required registers could be confusing and error prone. To assist in using
PWM, the XC8 Compiler has a set of three functions; ClosePWM1(), OpenPWM1(unsigned char
period), and SetDCPWM1(unsigned int dutycycle). OpenPWM1(unsigned char period)
takes period, a number from 0 to 255, and sets the actual period TPWM by the following formula:

TPWM = [period + 1] × TCY × (TMR2 Prescale Value).

SetDCPWM1(unsigned int dutycycle)sets the dutycycle period TDC by the formula:

TDC = [dutycycle] × ¼ TCY × (TMR2 Prescale Value).

If dutycycle is a number greater than 1023, you will get unintended results! Also, if TDC ≥ TPWM,
the signal is always on. ClosePWM1() turns off the PWM signal. The prescale value of Timer2
used in the above formulas can be set using the OpenTimer2(unsigned char config) function
from the timers library. The only defined names of consequence for PWM in this function are
T2_PS_1_1, T2_PS_1_4, and T2_PS_1_16 for the prescaler values of 1, 4, and 16. Only one
prescaler value can be used at a time.

The example program pwm.c shows the steps to get an 18% duty cycle and a PWM period of 0.4
ms on a chip with a processor speed of 32 MHz.

Note that the second PWM module uses CCP2 (Pin 16). Both PWM modules operate at the same
period, but the duty cycle for each can have independent values. Also, each on cycle will start at
the same instant even though they can last for different amounts of time.

30
Lab 5 Oscillator Speed & PWM

 Compile and load pwm.c. Connect the two-way LED between CCP1 and RB0 (not forgetting
to use an appropriate buffer resistor). Make note of the colour you see and the relative
brightness of the LED.

 Now switch the two-way LED to connect CCP1 and RB1 (again with an appropriate buffer
resistor). Again make note of the colour you see and the relative brightness of the LED

We can understand the behaviour of the two cases by examining Figure 5-2. On the left hand side,
the IO pin is low and conventional current will flow from CCP1 to the IO pin when CCP1 is high,
that is during the duty cycle. This will give you one colour of the bi-colour LED. On the right hand
side, the IO pin is high and conventional current will flow from the IO pin to CCP1 when CCP1
is low (i.e. not during the duty cycle). This will give you the other colour of the bi-colour LED.

high
V
IO Pin low Same
0 voltage,
Same Current
Current so no
voltage, flows high
flows high current
so no to low
to low flows
current
V flows
CCP1
0

When the IO Pin is low When the IO Pin is high


Figure 5-2: PWM – CCP1 to an IO pin

 Create a program to run an LED connected between CCP1 and RB0 progressing through duty
cycles of 0%, 33% of TPWM = 0.2 ms, 33% of TPWM = 0.3 ms, 67%, of TPWM = 0.2 ms, 67% of
TPWM = 0.3 ms, and 100% and back again by pressing a button (use your button code from Lab
4). It should start in the off position and cycle through the percentages with each tap. For now
either colour, red or green, is okay.

 Check your output with the oscilloscope.

 At the two different values of TPWM for the same duty cycle, 33% or 67%, do you see any
difference in the brightness of the LED?

 Replace the LED with a speaker and a 200  resistor in parallel. At the two different values of
TPWM for the same duty cycle, 33% or 67%, do you hear any difference in the sound?

31
Lab 5 Oscillator Speed & PWM

Part E: Installing a Reverse Gear

If you have a bi-colour LED connected between CCPx and RXn (which can be any digital input
pin), you could control the colour of the LED with software by turning the output pin RXn on or
off. Of course, as can be seen from Figure 5-2, the brightness of the two colours would not be the
same as the on and off times are reversed. With a low IO pin, the on time or duty cycle of the LED
is the same as the duty cycle of the PWM signal, LED duty cycle = PWM duty cycle. But when the
IO pin is high, the on and off times switch and LED duty cycle = PWM period – PWM duty cycle.
If you wanted the bi-colour LED to be the same brightness for each colour or current direction, i.e.
have the same LED duty cycle of on and off times, you will need to change the duty cycle of the
PWM signal when you change the polarity of RXn.

Changing the direction of current through a DC motor changes the direction the motor rotates.
Connecting a DC motor between CCPx and RXn lets you have a software-controllable reverse
gear. Since most DC motors require larger currents than our MCU can provide, we will use the bi-
colour LED as a suitable and cheap stand in.

 Connect your LED circuit as shown in Figure 5-2.

 Modify your program so a second button switches the polarity of RB0 and hence the LED
colour with each press. Make sure each color goes through the same sequence (off, 33%, 67%,
full). This would be like having seven speeds on a motor: three forward speeds, three reverse
speeds, and stop.
PIC MCU

R
CCP1
2Way LED
RB0

Figure 5-2: Forward-Reverse connection.

32
Lab 6 Timers

Lab 6 Timers
Objectives:
(a) To use the built-in module Timer0 and associated library functions to measure time intervals
while allowing other operations to proceed.
(b) To use the timer module to produce a third colour from a bi-colour LED.
(c) To use the timer module to time an external square wave signal.
(d) To use the timer module as a counter of rising or falling edges of an external signal.

Equipment:
Photogate, photogate socket with three leads, two-way bi-colour LED

Prelab:
 Read Chapter 11 in the PIC18F4525 datasheet on the Timer0 Module.

Files:
Example code: using_timers.c

Part A: Using the Timer0 Module to Create Non-blocking Delays

The delays we have used so far are called blocking delays; your code cannot change anything until
the delay is over. Since timing is such a crucial part of embedded systems, it is only natural that
the PIC MCU has a better way of measuring time. The PIC18F4525 actually has four timing
modules but we will only look at Timer0. As you have read in Chapter 11 of the datasheet, the
features of the TMR0 module are determined by the Timer0 Control or T0CON register.

The timer module acts a lot like a 60-minute cooking timer. You set the timer for 15 minutes and
go away and do whatever you want rather than watching the stove. When the 15 minutes are up,
an alarm goes off reminding you to come back and check your cooking. Of course the Timer0
module counts instruction cycles rather than seconds or minutes. Another difference from the
cooking timer is that the timer0 module actually counts up rather than down. If the cooking timer
acted like the timer0 module, it would count from 15 up to 60, rather than down to zero, before the
alarm went off. Timer0 also doesn’t sound an alarm but sets a flag, which is a one-bit piece of
memory called the Timer0 Interrupt Flag, TMR0IF. This flag is set when the counter timer rolls
over. You need to clear the flag, i.e. shut off the alarm, before the timer module can be used as an
alarm again.

The T0CON register, like other registers, is a special reserved portion of the PIC MCU’s memory
whose settings determine how this particular module will operate. The TMR0 module can act as a
counter or as a timer but we are only interested in its timing mode.

Possible Pitfall: The timer0 module increments up, not down.

33
Lab 6 Timers

The XC8 Compiler comes with library functions that simplify the use of the T0CON register. The
four functions for the timer0 module – CloseTimer0(), OpenTimer0(), WriteTimer0(), and
ReadTimer0() – are briefly discussed in Section 2.5 of the MPLAB XC8 Peripheral Libraries
datasheet. WriteTimer0(unsigned int timer) sets the timer counter to the value specified by
timer. Conversely, ReadTimer0(void) returns an unsigned integer that contains the current value
of timer counter. CloseTimer0(void) disables the timer0 module and clears TMR0IF, the timer
flag. The final function OpenTimer0(unsigned char config) configures the bits of the
TMR0CON register and is more complicated than the other three. The variable config is a bitmask
of defined names (quantities defined using the #define compiler directive) from timers.h,
TIMER_INT_OFF, TIMER_INT_ON, T0_16BIT, T0_8BIT, T0_SOURCE_INT, T0_SOURCE_EXT,
T0_EDGE_RISE, T0_EDGE_FALL, T0_PS_1_1, T0_PS_1_2, T0_PS_1_4, T0_PS_1_8,
T0_PS_1_16, T0_PS_1_32, T0_PS_1_64, T0_PS_1_128, T0_PS_1_256. The numeric values
of these names are such that you need only AND(&) the names together and the function will set
the T0CON register correctly for you. For example, if we wish to use the module as a timer using
an internal source, we use T0_SOURCE_INT. You can also use an external clock by applying a
square-wave signal of known period to pin6, T0CKI, and using the configuration entry setting
T0_SOURCE_EXT. Note that if the external square wave signal is not periodic, you have a counter
instead of a timer. The settings T0_EDGE_RISE and T0_EDGE_FALL determine if counting starts on
rising or falling edges of the square wave. For a 16-bit timer we will use T0_16BIT. The defined
names T0_PS_1_n, where n is 1, 2, 4, 8, 16, 32, 128, and 256, are prescalars that tell the timer
counter to increment when that number of clock cycles, nTCY, has passed. Your main decision will
be which prescalar to use. Thus, to get all these selections and to start incrementing the timer
counter every 32 TCY, for example, we use

OpenTimer0(TIMER_INT_OFF & T0_SOURCE_INT & T0_16BIT & T0_PS_1_32);

Note that although this means you do not have to work on the T0CON register directly, you must
understand the register settings and the reasonably-clear defined names to get the proper settings.
For example, the names TIMER_INT_OFF and TIMER_INT_ON are mutually exclusive and can’t be
used together – something cannot be on and off at the same time. The names T0_EDGE_RISE and
T0_EDGE_FALL only have an effect if T0_SOURCE_INT is not present, that is when using the Timer0
in counter mode.

An example program, using_timers.c, demonstrates how to use the timer module as a simple alarm
clock delay in two different ways. It uses the Timer0 module to time the on-cycle and off-cycle of
the pulse used to light an LED connected between RB0 and RB1. During both parts of the pulse,
the program monitors a button on RD1 to switch the polarity (on-off state) of the RB0 and RB1
pins. The on-cycle uses the interrupt approach described above. The off-cycle takes a different
approach; it starts the timer at 0 and uses a while loop to poll or check the status of timer counter
with the ReadTimer0() function.

 Carefully read the using_timers.c program and make sure you understand its operation.
Compile the program and load it on your PIC MCU and use the two-way LED, not a standard
LED. Recall that every time you use an LED, it needs a buffer resistor in series with it.

34
Lab 6 Timers

 The using_timers.c program is not using the timer0 module efficiently. How would you make
it more efficient?

 For each prescaler value in T0CON, determine the maximum interval you can time at 32 MHZ.
Put your results in a table.

Part B: Bi-colour to Tri-colour LED

With a bi-colour LED, you actually can produce a third colour. Red-on for a short time followed
by green-on for a short time will be perceived as yellow or orange depending on the ratio of the
length of each colour’s duration, as long as the durations are short. The PWM module of the
previous lab doesn’t allow you to create this mixed colour (why?) so we will use timers instead.

Note, nothing similar would be available for a DC motor. Indeed changing forward to reverse so
quickly would possibly damage the motor.

 Modify using_timers.c and use a bi-colour LED to produce a period signal that lasts 0.100 ms
that is 35% red and 65% green. Pressing a button should switch the signal to 65% red and 35%
green. Pressing the button again should revert back and so on.

Part C: Timing a Signal on an Input Pin

Besides timing an output signal, you can use a timer to measure an input signal. For example, the
photogate timer you use in the physics labs produces a square-wave (on/off) signal. When the
photogate is blocked, the signal is low; when unblocked, it is high. The signal would look like the
diagram below, Figure 6-1. Assume that the photogate signal is connected to pin X (you can pick
any convenient pin). A timer that determines the “blocked photogate” time is said to be in “gate
mode”. Photogates have three connections; +5V power (red), 0V ground (black), and a signal
output (yellow or blue). The signal wire would be connected to pin X, of course.

V
Photogate blocked

t
V
Figure 6-1: Timing a square wave signal

The logic of a program to measure the on cycle would be as follows:


1. do nothing while the signal is high.
2. the instant the signal goes low (i.e. a falling edge), start the timer
3. monitor the timer and signal simultaneously. Stop if the timer overflows or the signal goes
high (a rising edge).
4. if the timer overflowed give an error message, or

35
Lab 6 Timers

5. if the timer did not overflow, calculate and display the time in milliseconds
6. go back to monitor the input signal – step 1

If we don’t have a photogate available, we can use a button and measure how long it is pressed.
Note that the signal for a button is flipped compared to the photogate signal in Figure 6-1. If you
use a button, the logic of the program is the same as the six steps above but with falling edge
replaced by rising edge and vice versa.

 Connect the photogate (or button if a photogate is not available) to the breadboard. Write a
complete main.c source file to act as a gate timer (or button-on timer) and display the result on
the LCD.

Time-saver: Remember that buttons.c, from Lab 4, has functions that allow you to run code
when either a rising or falling voltage edge is detected

Although the precision of your timing can be quite small, down to a few TCY, the accuracy of your
timer is limited by the accuracy of your PIC’s internal clock, which is only several percent. It is
possible to use an external oscillator, which can be far more accurate, but that is beyond the scope
of this lab.

Part D: Timers as Counters

Timers measure time by counting edges, rising or falling, of square waves with a well-known
period. The time measurement is simply the number of edges times the period. The square waves
can come from an internal or an external clock source. If the source is a square wave that is not
periodic, a timer acts as a counter to inform you of the number of edges that have occurred at the
pin associated with that timer. For Timer0 that pin, number 6, is designated T0CKI. To use Timer0
as a counter, simply include T0_SOURCE_EXT and either of T0_EDGE_FALL (to count falling edges)
or T0_EDGE_RISE (to count rising edges) when configuring OpenTimer0(). Each edge at pin 6
increments the Timer0 counter, and the function ReadTimer0() reports the current count. Pin 6
(T0CKI/RA4) should be configured as a digital input for Timer0 to correctly read signal edges.
Note that when you use Timer0 as a counter, the only prescaler that is usually appropriate is
T0_PS_1_1.

Counters are useful in a number of situations. Imagine, for instance, you wanted to know the
number of people entering a doorway through a turnstile. It is easy to design a circuit to generate
a square wave at each turn of the turnstile. In turn, the PIC MCU does the counting.

 Connect a button to pin 6. Modify your program to count button presses at pin 6. Use the LCD
to show the current count.

 What happens if you use a different prescaler, say T0_PS_1_2?

36
Lab 7 ADC

Lab 7 Analog to Digital Conversion with a Sensor


Objectives
(a) to learn ADC
(b) to learn the characteristics of a voltage reference
(c) set up a PIC to perform ADC and display the result on an LCD
(d) to convert the digital value and display in meaningful units
(e) to learn how reflective infrared light sensors (RLS) operate
(f) to perform an ADC on a RLS and display the result on an LCD

In this lab you will configure the PIC to perform ADC and display it on an LCD. First you will
test your new ADC on some known values (a voltage divider). Later you will interface a reflective
sensor with the MCU and investigate its properties.

Equipment
Single RLS and holder, white plastic board and electrical tape, DMM, voltage divider board, LCD

Files:
sampleADC.c

Prelab
Read the RLS datasheet.
Read Appendix G on graphing in MS Excel.

Part A: How ADC Works

Analog to Digital Conversion (ADC or A/D) is a major method of recording a smoothly varying
continuous signal. A MP3 file is an example of analog data, V
music, converted and stored as binary data. The fundamental
concepts involved in converting a continuous voltage signal
Vref
to a discrete binary value are straightforward. Consider a 2- 11
bit ADC for which the only possible values of the conversion ¾Vref
10
are 00, 01, 10, and 11. If the ADC reference voltage is VREF, ½Vref
then these four values correspond to four bins or ranges of 01 ¼Vref
voltages: 0 – ¼VREF, ¼VREF – ½VREF, ½VREF – ¾VREF, and 00 t
¾VREF – VREF. A voltage signal is sorted into just those four 0
bins. From a graph of a voltage signal varying with time, as t0 t1 t2 t3 t4
in Figure 7-1, the ADC values can be read directly. Thus at
t0, the voltage is between 0 and ¼VREF, and the digitized value Figure 7-1: 2-Bit Digitizing.
is 00. At t1, the voltage is between ¼VREF and ½VREF, so the
digitized value is 01. At t3, the voltage exceeds VREF, so the digitized value is 11, the maximum

37
Lab 7 ADC

value it can be. The quality of the digitizing depends on how many bins you can divide your voltage
range up into. The PIC18F4525 has 10-bit ADC, so it can have 210 = 1024 bins. The quality also
depends on the sampling rate, i.e. how often you make the conversion. It should happen more
quickly than the fastest changes in the voltage.
V
The ADC operation takes time to complete and has
two main steps. The instantaneous voltage must first small V
be captured, to provide a constant voltage that then
can be compared against known voltages. Internally,
a fast switch and a capacitor-resistor circuit with a big V
small time constant  are used to capture the current
t
value. The time constant must be small enough that 0
the signal has not varied significantly during that
small t big t
capture time t  5, see Figure 7-2.

The known voltages that the sample voltage will be Figure 7-2: Effect of long capture times.
compared to are generated internally based on a high
and a low reference voltage, VREF+ and VREF–. Usually VREF+ = VDD (+5 V) and VREF– = VSS (0 V).
The known voltages go from VREF– to VREF+ in steps of size (VREF+ – VREF–)/2n where n is the
number of bits in the ADC operation. In our 2-bit example, the step size is (5 – 0)/4 = 1.25 V. For
10-bit ADC, it is 0.00488 V. In the 2-bit example, the comparison voltages are thus 0 V, 1.25 V,
2.50 V, 3.75 V, and 5 V.

To limit the number of comparisons that need to be


VC=½VREF
made, a search algorithm like the Method of Successive
Approximations is used. For the first comparison, the
middle voltage of the comparison voltages, i.e. 2.50 V, No Vin > VC? Yes

denoted VC, is used. The first decision step (diamond


Move S2 Move S2
shape) of the flowchart in Figure 7-3, which outlines the VC=¼VREF VC=¾VREF
logic of a 2-bit ADC module, starts by comparing the
input signal sample voltage, VIN, to VC. The results of No
Vin > VC? Yes No Vin > VC? Yes
this comparison, Yes or No, determine the first digit 1
or 0 of the conversion. The comparison value VC for the
next step is then changed. If the first digit was 1, VC is 00 01 10 11
now the voltage in the middle of range from the Digital values
previous VC (2.50 V) to VREF+, that is VC = 3.75 V. If
the first digit was 0, VC is now the voltage in the middle Figure 7-3: 2-Bit comparisons.
of range from VREF– to the previous VC (2.50 V), that is
VC = 1.25 V. The second and last comparison step determines the second digit of the conversion.
As you can see, there are two decisions for 2-bit ADC. Ten-bit ADC will require 10 decisions.
These comparisons take significant time to complete. So there is a maximum sampling frequency
for ADC.

Once a signal has been digitized, there is corresponding problem of interpreting the digital value
as a voltage. Since each digital value is a bin or range of voltages, it only makes sense to take the
average, middle-of-the-bin value as the reading. Since the actual reading is anywhere in the range,

38
Lab 7 ADC

the precision of the digital measurement is one-half the width of the range. Thus, four digital values
00, 01, 10, and 11 correspond to readings of 0.125VREF ± 0.125VREF, 0.375VREF ± 0.125VREF,
0.625VREF ± 0.125VREF, and 0.875VREF ± 0.125VREF. Since VREF is accurate to only to a few
percent, the resulting digital conversion has the same accuracy. For an n-bit conversion, the general
formula is

1 [𝑉𝑅𝐸𝐹+ − 𝑉𝑅𝐸𝐹− ] 1 [𝑉𝑅𝐸𝐹+ − 𝑉𝑅𝐸𝐹− ]


𝑉 = {(𝐴𝐷𝐶𝑉𝑎𝑙𝑢𝑒 + ) × 𝑛
+ 𝑉𝑅𝐸𝐹− } ± .
2 2 2 2𝑛

Since the PIC18F4525 is more adept at handling integers, rather than floating type numbers, it is
advisable to use VREF in millivolts in the formula above. We do not usually display the precision,
when giving the voltage, since it is a constant.

Part B: Preparing the Breadboard

Since the ADC conversion feature of the MCU gives us a simple digital voltmeter, we are going
to want perform ADC on a circuit with known voltages to ensure everything is working properly.
For its simplicity, we are going to use a voltage divider circuit with either four or five equivalent
resistors (the five-resistor version is shown in Figure 7-4). The circuit is called a voltage divider,
because each of the points, 1 through 4, is a fraction of the potential at 5. Using Ohm’s Law, V=IR,
we can find that V1 = (200  / 1000 )  V5, V2 = (400  / 1000 )  V5, and so on.

V4 V3 V2 V1

V5 0
200  200  200  200  200 

Figure 7-4: Five Resistor Voltage Divider Circuit Schematic.


In turn, the voltage divider is connected to the MCU as shown in Figure 7-5. The voltage divider
circuit is connected between the VDD and VSS. This means that V5 = 5.0 V in Figure 7-4.

 Connect the voltage divider to VDD and VSS on the breadboard as shown in Figure 7-5.
Investigate this circuit using a voltmeter, checking each of the potentials V0 through V5. That
is, clip the black voltmeter lead to the breadboard at VSS and use the red lead as a probe to
measure and record the circuit divider point potentials V1 to V5 with respect to ground.
Remember to record your observations in your notebook (as always). Note: your voltage
divider board may have fewer resistors!

39
Lab 7 ADC

+5V

V4
Voltage PIC18F4525
probe
V3 2
AN0
200 
V2 4 VREF-
5 VREF+
V1

0V

Figure 7-5: PIC showing ADC reference voltage connections.


Your PIC has thirteen pins than can be used for 10-bit ADC of an input analog signal. They are
labelled AN0 to AN12 (refer to the data sheet and pin-out diagram in Appendix A).

Connect a long wire to your selected pin and buffer resistor to use as the ADC input probe (see
Figure 7-5, where AN0 is shown as the probe pin). The ADC pin is internally connected to ground,
so we only need the one lead to measure potentials (voltages) with respect to ground. Each ADC
pin you use will need its own wire and buffer resistor. When the wires are not in use connect them
to ground. Do not disconnect the serial communication pins that you used in the last lab; you will
be displaying the output of your program to the LCD.

Part C: Configuring PIC for ADC – Software

The XC8 Compiler includes library routines OpenADC(), SetChanADC(), ConvertADC(),


BusyADC(), ReadADC(), and CloseADC() that simplify analog to digital conversions.

The main function OpenADC(unsigned char config, unsigned char config2, unsigned
char portconfig) configures the registers (see Section 19 of PIC18F4525 datasheet) that control
ADC. The function takes three parameters (config , config2 , and portconfig) and, as usual,
the parameters are created by bitmasking groups of preset named constants. The settings for the
parameters are discussed in the Programming the PIC18F4525 for ADC section of Notes VIII.
It takes a little time for the ADC module to be fully operational, so it is a good idea to insert a little
time delay after the call to OpenADC().

The other functions are much simpler. Once the configuration is set by OpenADC(), the actual ADC
operation will not start until ConvertADC() is called. ConvertADC() takes no parameters. The
conversion of an analogue signal value to a digital value takes time (though it is blindingly fast to

40
Lab 7 ADC

people) and you have to wait for the process to complete. You can do other things while waiting
as long as you occasionally check to see if the conversion is complete. You check on the status of
the conversion by examining the status of the function via another parameterless function
BusyADC(). It returns a value of 1 if ADC is still going on and 0 if finished. You get the actual
results of the ADC operation by calling the ReadADC() function, which returns an integer that
holds the digital value. ReadADC() takes no parameters.

You can do another ADC operation by calling ConvertADC() again and polling BusyADC() until
this next conversion is done. You can repeat ADC operations as many times as you like.

You can also change pins and channels from one ADC operation to the next by using the
SetChanADC() function as long as you have already indicated that that channel will be used by
selecting the appropriate value of portconfig in OpenADC(). The SetChanADC(unsigned char
channel) function takes any one of the predefined channels, e.g. AD0_CH1, as an input.

When you are finished with your conversions and wish to free up the analog input pins you have
been using, call the CloseADC() function. It takes no parameters.

Figure 7-6 shows an example of the code to start ADC and continuously collect a value from AN0
followed by a value from AN1.

#include <xc.h>
int firstADCvalue, secondADCvalue;

int main (void)


{
// Configure pin AN0 for ADC operation using VDD as
// the reference. The digital value is right justified.
OpenADC( ADC_FOSC_32 & ADC_2_TAD & ADC_RIGHT_JUST,
ADC_CH0 & ADC_VREFPLUS_VDD & ADC_VREFMINUS_VSS, 13);
_delay(50); // Delay for 50TCY to stabilize
while(1)
{
SetChanADC(ADC_CH0); // Next read from pin AN0
ConvertADC(); // Start ADC operation
while( BusyADC() ); // Wait for completion
firstADCvalue = ReadADC(); // Read result
SetChanADC(ADC_CH1); // Next read from pin AN1
ConvertADC(); // Start ADC operation
while( BusyADC() ); // Wait for completion
secondADCvalue = ReadADC(); // Read result
}
return 0;
}

Figure 7-6: Sample two channel ADC code.

 Modify the sample code given in Figure 7-6, along with your LCD programs from previous
labs, to continuously sample the input voltage at the AN0 and AN1 pins. Display the results
with one signal on the top line of the LCD and one signal on the bottom. Display the results

41
Lab 7 ADC

both as the raw ADC value and as the voltage in millivolts. Measure and record the circuit
divider voltages V1 to V5 using each pin probe (i.e. twice!).

 Use the Stopwatch window to time an ADC measurement.

Part D: Using External Reference Voltages

You can use an external upper reference voltage (that is, a value other than the voltage supplied to
VDD on the PIC) by replacing ADC_VREFPLUS_VDD with ADC_VREFPLUS_EXT and connecting pin 5
(VREF+) to whatever voltage you wish to use instead. Similarly, you can use an external lower
reference voltage (that is, a value other than VSS, which is normally connected to ground) by
replacing ADC_VREFMINUS_VSS with ADC_VREFMINUS_EXT and connecting pin 4 (VREF-) to
whatever voltage you wish to use instead. You can have either or both reference voltages as
external. The simplest source of external voltages that you can use are the voltages of the voltage
divider board.

There are several points to be careful about. The upper reference voltage must be greater than the
lower reference voltage. Also, the minimum allowable difference in these two voltages is about
three volts according to the PIC18F4525 Data Sheet.

 Modify your previous program to work with two external reference voltages. You will need to
modify the portion of your code that converts the ADC value to voltage in millivolts. Use the
voltmeter readings values in your formula.

 Use V4 (on the five-resistor voltage divider) as the upper reference and ground as the lower
reference. Measure and record the circuit divider voltages V1 to V5 using a pin probe.

 Use VDD as the upper reference and (on the five-resistor) V1 as the lower reference. Measure
and record the circuit divider voltages V1 to V5 using a pin probe.

Part E: Investigation of Reflective Light Sensors

Line-following robots use a reflective infrared light sensor (RLS). It consists of both an infrared
(IR) LED and a photo-sensor with an IR filter. Review the RLS datasheet provided for specifics.
The robot RLS board contains five of these sensors. The photosensor voltage changes in response
to the amount of IR light reflected from a surface. The RLS is in a holder so that you can slide it
across a surface at constant height.

The behaviour of the RLS is quite simple. The bottom of the sensor has two pieces, as shown in
Figure 7-8, an IR LED (transparent) and an IR sensor (black). The sensor portion of the RLS is a
transistor. With no IR light shining on it, the sensor has a large resistance, on the order of 1 M.
With bright IR light shining on it, the light excites electrons and a current flows. If you connect an
ohmmeter across the transistor, the resistance will now appear to be only a few hundred ohms

42
Lab 7 ADC

because of the extra current. The right-hand side of the circuit shown in Figure 7-8 thus works like
a simple voltage divider consisting of the 10 k and IR sensor. When the sensor is not illuminated
VDD

IR LED
270  10K

QRD1114 AN0
RLS

IR Sensor
Vss
Figure 7-8: Reflective sensor
Figure 7-8: Connecting a RLS to the Breadboard. and electrical tape.
by IR light, the resistance of the sensor is so high that the voltage measured by the probe should
be near VDD in value. When the sensor is well illuminated, the resistance is low compared to the
10 k resistor and the voltage measured by the probe should be near VSS in value.

To check that the sensor portion is working correctly, attach an ohmmeter to the sensor side using
the sensor and VSS leads. The RLS should not be connected to the breadboard! There are several
LED flashlights that have had their visible light LEDs replaced with IR LEDs. Confirm that the
sensor is responding correctly.

Connect the wires from the holder to your breadboard with the appropriate buffer resistors, as
shown in Figure 7-8. The resistors may already be attached. You can check that the IR LED is
working if you have a camera cellphone. Most digital cameras will respond to IR. With the RLS
connected to power, check your cellphone camera screen to see if you see a white light. It should
go out if you cut the power to the breadboard.

 Investigate the change in voltage as you move the RLS back and forth over white paper
containing a line of black electrician’s tape, as shown in Figure 7-8. Point the sensor towards
an outside window and towards the overhead lights. Remember to record your observations in
your notebook.

 At the end of this lab, you will be given a grayscale chart. The number on each square is a
measure of the grayness of the square, where white is 0 and black is 255. Record the ADC
value for each square and plot the results in MS Excel. See Appendix G for details on graphing.
Fit a trend line to the data. Is the response linear?

 What is the grayscale number of the two squares labelled A and B? Which are you more sure
of? Why (explain using your plot from above)?

 Place the RLS and holder so that the RLS is about 1 cm from the tape. Align the RLS so that
the IR LED and photodetector are lengthwise parallel to the tape (see Figure 7-9). Carefully
slide the RLS perpendicularly across the tape. Measure position and ADC value as you go.

43
Lab 7 ADC

Again, carefully slide the RLS back across the tape. Note that the LED and photodetector have
changed places relative to the tape. Plot both sets of data on the same MS Excel chart. Is there
a threshold value of the readings that clearly indicates the sensor is over the edges of the tape?

Tape
RLS

Figure 7-9: Testing the threshold.

44
Lab 8 Transducers

Lab 8 Transducers and Interfacing


Objectives
(a) To interface a transducer or sensor signal with the ADC function of the PIC18F4525 MCU.
(b) To plot the signal strength for a set of known environmental conditions (such as temperature
or pH).
(c) To determine the equation of a fitting equation or function that relates the signal strength to
the external data value (e.g., thermocouple voltage to external temperature in °C).
(d) To use the fitting function to display the value of the external condition in the correct units
(e.g. temperature in °C) on the LCD.
(e) To estimate the accuracy of the fitting function.
(f) To present the results to the lab section in a 5-minute presentation.

Prelab
Read Appendix G on finding fitting functions for data.
Review voltage divider circuits.

Equipment
DMM
Reference resistors of various sizes
Transducer (a particular transducer and datasheet will be assigned to each group)
Amplifier if necessary

Background
A transducer will in general produce a voltage output that changes with variations in the quantity
it will measure or have a resistance that changes with those variations. In the case of the voltage
output, the signal is often small and will need to be amplified to a suitable size. Once amplified,
the signal can then be measured by the ADC function of the PIC MCU. If, on the other hand, it is
a resistance that is changing, we need to use a voltage divider circuit. That is, the transducer
resistance must be in series with a known resistance with a known voltage over the two resistances,
see Figure 8-1. The voltage signal VT over the transducer resistance RT can then be related to the
external variable. We can use the ADC function to then determine VT. Note that the known resistor
Rref must be chosen carefully so that the measured voltage covers a wide enough range of values.

Collect a suitable number of data points, 6 to 10, for a set of reference values of the external
Rref VT
Vref

RT

Figure 8-1: A transducer with varying resistance.

parameter. For example, if you were using a thermocouple, you could use ice water and boiling
water for two data points. You might then have to use a glass thermometer to get readings at
intermediate temperatures. Be sure to get a wide range of data.

45
Lab 8 Transducers

Use MS Excel to plot the data. Determine the best fitting function – linear, quadratic, logarithmic,
or exponential – to the data. Determine the equation of the trend line. From the fit of the data,
estimate the accuracy of the fitting functions.

Write a program to use your fitting function with the transducer signal and to display the result on
the LCD display. One line should display the signal voltage, and one line the value of the external
condition.

Prepare a short presentation of your results on a sheet of poster paper. Include a short description
of how the transducer works, your graph and equation, and how you estimate the accuracy of your
sensor. Be prepared to have others test your sensor.

46
Lab 9 Robot

Lab 9 Introduction to the Line Following Robot


Objectives
(a) To learn the basic operation of the robot including programming procedures.
(b) To also learn to control the motion of the robot.

Equipment List:
Sumo-bot, stopwatch.

Files:
sumovore.c, sumovore.h, interrupts.c, interrupts.h, main.c, motor_control.c,
motor_control.h

Pre-lab:
(a) Visit websites http://www.vancouverroboticsclub.org/ and
http://www.robogames.net/rules/line-following.php. Learn more about the line follower robot
competition and rules.
(b) Read the program files listed above to get some idea of how to write the robot programs.

Part A: Re-grouping: forming new groups for the robot project

Choose your new group for next 4 weeks of the robot project. The standard group will have 3
students.

Part B: Using the PICkit 2 to program the robot and sensor control

There are three systems to the robot. First, there are the reflective infrared light sensors (RLS) used
to detect the line. The second system is the set of five indicator LEDs on the top of the robot. The
third system is the two electric motors that independently control the two wheels. The library files
sumovore.h and sumovore.c contain the basic functions to run these three systems. The files
main.c, motor_control.h, and motor_control.c show simple logic for following a line. The
files interrupts.c and interrupts.h contain advanced functions that we will not cover in this
course. They help prevent erratic behaviour when voltage drops or spikes. An overview of these
files is provided in Notes X Robot.

 Create and build a project with the file main.c and the library files it uses.

 Make sure at this stage that both power switches on your robot are in the “off” position (see
Figure 9-1) and connect your robot brain board to the PICkit 2 microcontroller programmer
using the given connector (see Figure 9-2). Turn on the power for the robot brain board (switch

47
Lab 9 Robot

#1) but keep the power supply switch for the motors (switch #2) “off”. You will see five small
LEDs mounted on the top of the brain board light up. Load your project onto the robot, turn off
the switch #1, and disconnect the PICkit 2 microcontroller programmer.

Connector
off off

Switch #2: Power Switch #1: Power switch


switch for motors. for brain board

Figure 9-1: Power switch locations for brain board (switch #1) and motors (switch #2).

Robot brain
Connector board

Programmer

Figure 9-2: PICkit 2 microcontroller programmer, connecting bracket, and brain board.

The program you compiled and downloaded will activate the robot’s sensor unit board but the
motors will not turn since the power to them is off. Turn on switch #1 and then place your robot
on the white paper with black tape attached. Move the robot over the tape and see how those 5
LEDs mounted on the brain board respond. Is there any position where sensor unit is over the black
tape yet none of the LEDs is on?

Consider the following line of the file sumovore.c, in the function initialization()

48
Lab 9 Robot

threshold = THRESHOLD_DEFAULT; // value compared to adc result

The macro alias THRESHOLD_DEFAULT is defined to be 512 in sumovore.h. This defines a


particular constant for the sensor voltage value threshold that the program uses to determine the
target area “blackness”. That is, the value the program uses to determine if the RLS is over the
electrical tape. A value of 1023 means complete black, while 0 means complete white.

You do not need to use the default value of the threshold. For your convenience, the following line
is in main.c. It lets you choose any value.

// threshold = {an unsigned int value};

Simply uncomment the line and add a integer from 0 to 1023.

 Modify the threshold in main.c to see the changes in sensor response. What is a good value for
the threshold?

Part C: Motor control function and robot dancing.

The function motor_control() uses the input from the sensors to travel. Forward motion, right
and left turns, and stopping are controlled by separate functions. These functions, in turn, all make
use of a single function set_motor_speed(the_motor, motor_speed, speed_modifier) to
control the speed and direction of each wheel. See Table 9-1 for the values of the function
parameters. The speed_modifier parameter is often zero but is included to allow you to
compensate for one wheel being slower than the other.

Table 9-1: set_motor_speed() parameter options


Parameter Choices
the_motor left right
motor_speed stop rev_slow rev_medium rev_fast
slow medium fast
speed_modifier an int

 Test main.c on an oval track, a short straight line track, and then on various more complicated
tracks to see how it performs.

 It is possible that your robot may malfunction at some point, so it would be good to have a test
program. Change motor_control() to run both wheels forward at fast speed for several
seconds, then medium for several seconds, and so on through the list of motor speeds –
including stop! – all the way to reverse fast. Run the program with the wheels up in the air so
that you may observe them. For added information about how the speed of one wheel compares
to the other, stick a piece of tape on each wheel. If the pieces of tape stay aligned the wheels
are turning at the same rate. Comment on any differences you see.

49
Lab 9 Robot

 Modify motor_control() to always travel forward, ignoring sensor input, and time your
robot’s progress over a measured distance to determine its speed. Note if the robot’s actual
path deviates from straight.

 Modify motor_control() to always travel backwards, ignoring sensor input, and time your
robot’s over a measured distance to determine its speed. Note if the robot’s actual path deviates
from straight.

 A series of “robot dances” are defined in Figure 9-1. Modify the project three times to perform
all three dances.

Possible Literal Pitfall: When programming the robot, make sure that the
brainboard power is on but the motor power is off. A newly awakened robot may
make a dash for freedom and end up plunging off a table to its demise!

Spin around one wheel Spin at one place Spin in larger circle

Figure 9-1: Trajectories of robot dancing for Part C.

50
Lab 10 Project

Lab 10 Final Project


Project Objectives
(a) To program the robot to navigate a complex path quickly and efficiently. Project team members
will not get to see the tracks that their program and robot will be tested on until the competition
time. The main competition track will be a technical course with scoring primarily based on
degree of completion. Time of completion will also affect score results on the technical track
for teams that complete the entire course. Teams will get to see and work with a series of
simple test tracks. The competition tracks will incorporate elements of the test tracks.
(b) Students will learn how to manage and organize this 4 week-long final project. This project
requires students to organize project planning (scheduling), task assignment sharing, progress
report writing, as well as make a final public presentation and submit a formal report.

Equipment List
Sumo-bot, sample tracks

Lab session preparation and work


Each week the group must have a meeting prior to the lab session and come up with a short list of
items they will attempt to resolve in that particular lab session. In this list, each group member
must be assigned to a specific task. Please refer to Table 10-1 for a sample format. This list must
be submitted at the beginning of each lab session to the lab instructors. The level of achievement
for the listed items in this table must be evaluated by the other members of group at the end of lab
session and submitted to the instructors for marking. Similar tables must be produced at the end of
each lab sessions for task-sharing during the following week for research and development of the
project.

Table 10-1: Sample format for the task planning, assigning, and sharing.
Date: March 12, 2006 Morning
Tasks Descriptions Estimated Task leader Self-
time evaluation
1 Write algorithm and code for right 0:50 M. Coombes 100%
angle turns.
2 Write algorithm and code for 0:50 T. McMath
perpendicular crossings.
3 Misc. J. DeBenedictis
4 Etc. F. Callaghan
… … … …

We suggest that you start by writing programs to get your robot around the simpler tracks first.
Algorithms, i.e. path-following strategies for solving particular course challenges, must be
provided and explained. Explain what worked and what didn’t work.

51
Lab 10 Project

gitHUB
Code management will extremely important. Each version of your program should be numbered.
For example, Robot V2.23 .c might refer to the 23rd change of version 2 where version 2 might be
the code to handle right angle turns. Backup copies of each version should be kept. You will be
expected to submit all versions of your code. gitHUB is a service we will use to to keep everything
straignt.

52
Appendix A MCU Fundamentals

Microcontroller (MCU) Fundamentals


The microcontroller is simply a computer designed to work with other machines rather than people.
Because of this, they don’t need a monitor, keyboard, mouse, or any other device that requires
human interaction.

Most MCUs have a CPU, RAM, ROM, and some EEPROM (Electrically Erasable Programmable
Read Only Memory). Inexpensive controllers that are designed to be programmed once (for a
particular application) may omit the EEPROM, because there may be no need to ever change the
program once the project goes “into production”.

The MCU you will use, the PIC18F4525, has 256kb of EEPROM, which allows you to erase and
reprogram it about a million times, so you do not need to worry about “wearing out” the EEPROM
anyway. This EEPROM can hold your program and any data storage required in your application.
Technical details are available in the datasheet.

You will create the circuits you will use with the MCU on a “breadboard”. On a PC, using a
program called the MPLAB IDE, you will create and test the programs that will run on the MCU.
The programs will be written using the C language. When finished, you will transfer the program
to the MCU using a USB connection and a device called the PICkit 2 or PICkit 3 programmer.
More details can be found in Appendix B – Software.

The PCIF4525 is a 40 pin device. Some of the pins are used to connect a battery or power supply
to the MCU. The rest of the pins are for inputs and outputs. The pin-out diagram of the P18F4525
is shown in Figure A-1 below. Note the notch on the chip. This orients the chip and indicates the
top of the chip and which pin is number 1. The PIC18F4525 has many features built into the chip
including digital input and output (DIO), timing and square wave production, serial
communication, and analog-to-digital conversion (ADC) that you will learn to use. Depending on

Figure A-1: Pin out schematic of the PIC18F4525.

53
Appendix A MCU Fundamentals

the features of the MCU being used, the pins have different functions and names (this is called
multiplexing).

The behaviour of a program on an MCU is different from other programs you have used. When a
MCU reached the end of a program, it resets and starts all over again – a useful feature in case of
power outages. You need to adapt your code to this behaviour by adding a while loop that runs
forever as in the sample program in Figure A-2 below.

/* Simple PIC18F4525 program */


/* */
/* It does nothing. */
/* */

#include <xc.h>
/* a header file with information specific to the chip */

#pragma config WDT = OFF


#pragma config OSC = INTIO7 // puts osc/4 on pin 14 to check freq
#pragma config MCLRE = OFF
#pragma config LVP = OFF
#pragma config PBADEN = OFF
/* directives controlling which features are active */

int main(void)
{
/* put instructions that are to run only once here */

while(1) /* this while loop runs forever */


{
/* put instructions that are to run continuously here */
}

}
Figure A-2: Sample MCU program.

54
Appendix B Software

Software
The software we will use to write, compile, and debug C language code and to program the PIC
MCU are free, courtesy of MicrochipTM, the manufacturers of the PIC18F4525 chip that we are
using in this course. The software:

MPLAB XTM IDE (Integrated Debugger Environment), and


MPLABTM XC8 Compiler
PIC18F Legacy Peripheral Libraries(needed for XC8 v1.35 and above)

as well as a wealth of support documentation in PDF format :

MPLABTM XC8 Getting Started Guide,


MPLABTM XC8 C Compiler User Guide,
MPLABTM XC8 Peripheral Libraries,
MPLAB XTM IDE User’s Guide,
MPLAB XTM IDE Quick Start Guide,
MPLABTM PIC18 Configuration Settings Addendum, and
PIC18F4525 Data Sheet

are available from www.microchip.com. Download them for use on your home PC if you wish.
You will find links on the APSC 1299 Moodle home page.

The MPLABX IDE and XC8 Compiler work together as one seamless program from the view of
the user. In the IDE, you build a project. A project consists of a C language code file that you write
and library files supplied by Microchip. A library is a group of functions grouped together for
easier reference and ease of linking. You can edit, debug, or step through your code line by line.
When your project is ready, you instruct the IDE to compile your program to a .HEX file. The Hex
file is your project in a form that the PIC18F4525 chip can understand. A USB device, the PICkit
2 or 3 programmer, is used to write those instructions into the memory of the PIC MCU.

Installing the Programs at Home


The MS Windows, Linux, and Max OS X versions of the programs we use, as well as the support
documentation, are available for download from the Microchip.com site. Search for “MPLAB X”
on the site, then click the “Downloads” tab at the bottom of the page. The programs may be
installed on your home PC if it is operating Windows XP or higher. First, install the MPLABX
IDE using the file MPLABX-v3.10 windows installer.exe (or later version). Second, install the XC8
Compiler using the file xc8-v1.31-win.exe (or later version) also available from the Microchip.com
site by searching for “MPLAB XC Compilers” and then clicking the “Downloads” tab at the
bottom of the page. In the install process for the compiler, you will encounter one or more
configuration windows, see Figure B-1, depending on the version of the compiler you are
installing. Make sure that you check the box to add XC8 to the PATH environment variable – the
second check box of the list in Figure B-1. The IDE and compiler will not work together properly
if you don’t.

55
Appendix B Software

Figure B-1: Configuring the XC8 Compiler.


If you install v1.35 or later of the XC8 compiler, you will also need to install the PIC18 Peripheral
Libraries using the file peripheral-libraries-for-pic18-v2.00rc3-windows-installer.exe. Starting
with v1.35 of the XC8 Compiler, that library is no longer installed automatically This library is
available on the same “Downloads” tab as the compiler is; simply scroll down to “Legacy
Peripheral Libraries” to find it. There is, at the time I write this, also a slight glitch in that the
installer may not find the correct XC8 directory to install the peripheral libraries at the step shown
in Figure B-2. Simple click on the Browse Folders icon and then select the correct (i.e. the latest)
directory as in Figure B-3.

Figure B-2: Installation Directory Problem Figure B-3: Finding the folder

56
Appendix B Software

Creating a Project
A project is a collection of files, some of which are yours and some of which are standard C library
files, that the compiler will turn into a program that will run on the MCU. Typically you will have
several source (.c and .h) files. Only one source file can have a main() function or an error will
occur when you try to compile your project. Since you will be working at home and in the lab, it
is wise to have a USB memory stick and to use it to store your projects or you can use cloud
storage. Be sure to back up your files at home since misplacing a memory stick is a common
occurrence.

Possible Pitfall: You can build your project in the Student Directory on the lab PC but
files in the Student Directory are deleted every night or sooner. Do not leave your
project there. Backup to a USB device.

We suggest having a folder called APSC1299 to contain your projects. Each project should have
its own subdirectory. During the course of the labs, you will be building up a library of useful
functions that can be added to any project. Place these functions, and any functions we give you,
in a subdirectory of the APSC1299 folder called Common. You will need to tell your project to
look in Common for any files it does not find in the project directory.

To start, click on the MPLABX IDE icon on the desktop of your PC. This will bring the main
screen of the program as shown in Figure B-4 below. You can create a new project by either
clicking on File in the Menu bar and choosing New Project or by clicking Create New Project
which is shown circled at the lower left of the diagram.

Menu bar
Icon
bar

Figure B-4: The MPLABX IDE main


window.

57
Appendix B Software

When the New Project window appears, the first step of a seven-step process is to choose the type
of project we will create. It will always be a Microchip Embedded Standalone Project, as shown
in as shown in Figure B-5. In step 2, make sure that the program knows that you are using the
PIC18F4525 chip of the Advanced 8-bit MCUs (PIC18) family, as shown in Figure B-6.

Figure B-5: Choose project type.


Figure B-6: Select PIC18F4525.

Next, in step 3, see Figure B-7, we need to select the tool we will be using. We have two possible
choices here; we can use the Simulator or the PICkit3. If you choose the Simulator, as the name
suggests, you don’t need any hardware. Typically you write and do simple debugging in the
Simulator mode. When it appears debugged, you can then use the PICkit3 (see Figure B-7) to load
the program onto the chip and do further testing.

Figure B-7: Choosing the tool: Simulator or PICkit3

The next window skips to step 6, where we select the XC8 compiler (see Figure B-8). Then, in the
final step, step 7, you pick a name for the project and a location for it (see Figure B-9). The location
should be a folder with a name like APSC1299Projects where you will keep all your different
projects. You might want this work folder to be on a USB Memory Stick if you are working on
the same project at home and at school. Also choose the UTF-8 encoding; this is a universal
character set and should be more portable.

58
Appendix B Software

Figure B-8: Select XC8 compiler.


Figure B-9: Select project name and location.
After Step 7, you are taken to the Project’s X Files pane, which is in the upper left corner of the
MPLABX window. Here, you can add an existing source file, Figure B-10 on the next page, or
you can automatically create an almost empty stub of a source file. If you have been given a sample
.c file with a main() function, copy that file to your project folder. Then, add this file to your
project by right clicking on Source Files in the Project X pane and selecting Add Existing Item...
Choosing New > C Main File instead will create a stub of a C source file that has an int main()
function. Recall that C programs can only have one main() function and that the main() function
is the starting point of any C program. The IDE automatically calls the newly created file
projectnamemain.c. If your project only consisted on one file, you would be finished now. All you
would need to do is add the lines of code you need to the newly created file.

Figure B-10: Create a file or use an existing one.

There are a few more things to do once your project is created. You need to change the project’s
properties. On the Menu Bar, click on File then click Project Properties. This will bring up the
Project Properties Window. On the left-hand side is the Categories frame. In that frame, click on
XC8 linker. This is shown as a box with a 1 in Figure B-11 below. Now, on the right-hand side,

59
Appendix B Software

you have an Option categories dropdown menu. Select Runtime from the dropdown menu, as
marked by the box with the 2 in Figure B-11 below. Scroll to the end and select Link in Peripheral
Library, which is marked by 3 in Figure B-11 below. Without this selection, the project will thow
an error message for all the function we will use from the XC8 peripheral library.

1 3

Figure B-11: Linking in Peripheral Libraries

While still in the Project Properties window for XC8 linker, select Memory model from the Option
categories dropdown menu, which is in a box labelled 4 in Figure B-12 below. For both Size of
Double and Size of Float, noted by the box with the 5 below, select 32 bit instead of 24. This will
ensure any decimal number we use or calculate will have the greatest number of significant digits.

4
5

Figure B-12: Float and double size

Finally, in the Categories frame on the left-hand side of the Project Properties window, click on
XC8 compiler. This selection is highlighted with a number 6 in Figure B-13 below. Then, in the
Option categories dropdown menu, select Preprocessing and messages as noted by a box with the
number 7 in Figure B-13 below. Then, scroll down to Include directories, shown by a box with

60
Appendix B Software

an 8 in Figure B-13 below. This is where we tell the project to find any files that are not in the
project directory. Browse and select the location of your Common folder.

6 8

Figure B-13: Including Common files when compiling

61
Appendix C Writing to the PIC/MCU

Writing to the PIC MCU using the PICkit 2 or 3


You write to the PIC MCU, i.e. install your compiled
program, using the black PICkit 2 or the red PICkit 3
device, usually called a programmer. The device connects
to your PC via a USB port. The PIC MCU has four pins,
MCLR , PGC, PGD, and PGM, that need to be connected
to the programmer.

The correct wiring connections for connecting the


programmer to the PIC MCU are shown in Figure C-2
below. We have already added a small connector to the
breadboard so that you can just slide the programmer onto
the connector pins. Note that the programmers should
normally be face up with the logo showing when connected
properly. Your chip needs to be getting power from a Figure C-1: Programmers.
battery or other power supply for the device to write to the chip.

Pin Description Pin Description


1 = VPP 1 = VPP
2 = VDD (Power to Target) 2 = VDD (Power to Target)
PICkit 2
PICkit 3

1 1
2 3 = VSS (ground) 2 3 = VSS (ground)
3 3
4
4 = PGD 4
4 = PGD
5 5 = PGC 5 5 = PGC
6 6
6 = PGM 6 =Not used

Figure C-2: Programmer connections.

Possible Pitfall: The PICkit 2 and PICkit 3 devices are not interchangeable. The
wiring to the PIC is different.

The MCU can be programmed (your hex file written to the MCU) directly from the MPLABX
IDE. First, connect the PICkit 2 or 3 to your MCU and to a USB slot on the PC. Second, ensure
the power supply is connected to your MCU by plugging your DC adapter into the red regulator
board on your breadboard and turning on the power switch on the regulator board (an LED should
come on). Next, tell the IDE that you are using the PICkit 2 or 3 by choosing Run|Set Project
Configuration|Customize as shown in Figure C-3.

62
Appendix C Writing to the PIC/MCU

Figure C-3: Changing the programming tool in the IDE

This will bring up a window, see Figure C-4, where you may change from using the Simulator to
using the PICkit 2 or 3 and make many other changes necessary.

Figure C-4: Changing to the PICkit 3 programmer.

The icon bar of the IDE will change after you select the programmer. New programming icons
will appear next in the icon bar, Figure C-5.

63
Appendix C Writing to the PIC/MCU

Figure C-5: Programmer toolbar and tool tip

When you click on the Run Main Project icon, the project builds and, if successful, connects to the
PICkit 2 or 3 programmer. A PICkit 2 or 3 tab will appear in the Output pane on the lower right
of the IDE window, see Figure C-6 for a successful write operation.

Hint: If the MCU is not wired correctly, or the power USB cable is disconnected,
or the PICkit 2 or 3 is not connected to the MCU, there will be a message that the
Target device was not found. Check your connections. If the PICkit 2 or 3 is not
connected to a USB port, a message will pop up alerting you of that fact. If these
steps do not work, ask for assistance.

Figure C-6: Successful transfer of hex file to MCU.

64
Appendix D ASCII

ASCII
The char data type in the C language is just a number. However, every possible value of a char
number is associated with a text symbol or special function for printing, such as a carriage return,
as shown in Table D-1 below. The special non-printing characters are shown in bold in the table
below and discussed in more detail in the next section. When characters are printed, e.g. using the
printf() function, the number is replaced by the symbol. Which symbol replaces which number
depends on an arbitrary convention. The particular convention used in the table below is called
ASCII. Other conventions, and modifications of the ASCII standard convention, exist – especially
for different languages. As well, there is the Extended ASCII Character set for decimal values
from 127 to 255. These can be very different from device to device, such as the SmartFun LCD
and a PC.

Table D-1: The ASCII character codes.


Char Dec Oct Hex Bin Char Dec Oct Hex Bin
(nul) 0 0 0x00 0b00000000 (us) 31 37 0x1f 0b00011111
(soh) 1 1 0x01 0b00000001 (sp) 32 40 0x20 0b00100000
(stx) 2 2 0x02 0b00000010 ! 33 41 0x21 0b00100001
(etx) 3 3 0x03 0b00000011 " 34 42 0x22 0b00100010
(eot) 4 4 0x04 0b00000100 # 35 43 0x23 0b00100011
(enq) 5 5 0x05 0b00000101 $ 36 44 0x24 0b00100100
(ack) 6 6 0x06 0b00000110 % 37 45 0x25 0b00100101
(bel) 7 7 0x07 0b00000111 & 38 46 0x26 0b00100110
(bs) 8 10 0x08 0b00001000 ' 39 47 0x27 0b00100111
(ht) 9 11 0x09 0b00001001 ( 40 50 0x28 0b00101000
(nl) 10 12 0x0a 0b00001010 ) 41 51 0x29 0b00101001
(vt) 11 13 0x0b 0b00001011 * 42 52 0x2a 0b00101010
(np) 12 14 0x0c 0b00001100 + 43 53 0x2b 0b00101011
(cr) 13 15 0x0d 0b00001101 , 44 54 0x2c 0b00101100
(so) 14 16 0x0e 0b00001110 - 45 55 0x2d 0b00101101
(si) 15 17 0x0f 0b00001111 . 46 56 0x2e 0b00101110
(dle) 16 20 0x10 0b00010000 / 47 57 0x2f 0b00101111
(dc1) 17 21 0x11 0b00010001 0 48 60 0x30 0b00110000
(dc2) 18 22 0x12 0b00010010 1 49 61 0x31 0b00110001
(dc3) 19 23 0x13 0b00010011 2 50 62 0x32 0b00110010
(dc4) 20 24 0x14 0b00010100 3 51 63 0x33 0b00110011
(nak) 21 25 0x15 0b00010101 4 52 64 0x34 0b00110100
(syn) 22 26 0x16 0b00010110 5 53 65 0x35 0b00110101
(etb) 23 27 0x17 0b00010111 6 54 66 0x36 0b00110110
(can) 24 30 0x18 0b00011000 7 55 67 0x37 0b00110111
(em) 25 31 0x19 0b00011001 8 56 70 0x38 0b00111000
(sub) 26 32 0x1a 0b00011010 9 57 71 0x39 0b00111001
(esc) 27 33 0x1b 0b00011011 : 58 72 0x3a 0b00111010
(fs) 28 34 0x1c 0b00011100 ; 59 73 0x3b 0b00111011
(gs) 29 35 0x1d 0b00011101 < 60 74 0x3c 0b00111100

65
Appendix D ASCII

Char Dec Oct Hex Bin Char Dec Oct Hex Bin
(rs) 30 36 0x1e 0b00011110 = 61 75 0x3d 0b00111101
> 62 76 0x3e 0b00111110 _ 95 137 0x5f 0b01011111
? 63 77 0x3f 0b00111111 ` 96 140 0x60 0b01100000
@ 64 100 0x40 0b01000000 a 97 141 0x61 0b01100001
A 65 101 0x41 0b01000001 b 98 142 0x62 0b01100010
B 66 102 0x42 0b01000010 c 99 143 0x63 0b01100011
C 67 103 0x43 0b01000011 d 100 144 0x64 0b01100100
D 68 104 0x44 0b01000100 e 101 145 0x65 0b01100101
E 69 105 0x45 0b01000101 f 102 146 0x66 0b01100110
F 70 106 0x46 0b01000110 g 103 147 0x67 0b01100111
G 71 107 0x47 0b01000111 h 104 150 0x68 0b01101000
H 72 110 0x48 0b01001000 i 105 151 0x69 0b01101001
I 73 111 0x49 0b01001001 j 106 152 0x6a 0b01101010
J 74 112 0x4a 0b01001010 k 107 153 0x6b 0b01101011
K 75 113 0x4b 0b01001011 l 108 154 0x6c 0b01101100
L 76 114 0x4c 0b01001100 m 109 155 0x6d 0b01101101
M 77 115 0x4d 0b01001101 n 110 156 0x6e 0b01101110
N 78 116 0x4e 0b01001110 o 111 157 0x6f 0b01101111
O 79 117 0x4f 0b01001111 p 112 160 0x70 0b01110000
P 80 120 0x50 0b01010000 q 113 161 0x71 0b01110001
Q 81 121 0x51 0b01010001 r 114 162 0x72 0b01110010
R 82 122 0x52 0b01010010 s 115 163 0x73 0b01110011
S 83 123 0x53 0b01010011 t 116 164 0x74 0b01110100
T 84 124 0x54 0b01010100 u 117 165 0x75 0b01110101
U 85 125 0x55 0b01010101 v 118 166 0x76 0b01110110
V 86 126 0x56 0b01010110 w 119 167 0x77 0b01110111
W 87 127 0x57 0b01010111 x 120 170 0x78 0b01111000
X 88 130 0x58 0b01011000 y 121 171 0x79 0b01111001
Y 89 131 0x59 0b01011001 z 122 172 0x7a 0b01111010
Z 90 132 0x5a 0b01011010 { 123 173 0x7b 0b01111011
[ 91 133 0x5b 0b01011011 | 124 174 0x7c 0b01111100
\ 92 134 0x5c 0b01011100 } 125 175 0x7d 0b01111101
] 93 135 0x5d 0b01011101 ~ 126 176 0x7e 0b01111110
^ 94 136 0x5e 0b01011110 (del) 127 177 0x7f 0b01111111

The SparkFun LCD reserves characters 124 and 254 to use as function identifiers or control
characters. The SparkFun interprets printf(“%c%c”, 124, 13); as a command (discussed
below), not as an order to display characters 124 and 13. That means you cannot get the symbol |
on your LCD.

ASCII Control Characters

Some devices, such as the SparkFun LCD, use the non-printing ASCII characters 0 to 31 as control
characters or commands. The control character, which is number 13 in the ASCII table, is often
denoted <control>M or CTRL-M or ^M in documents with similar notation for the other controls.

66
Appendix D ASCII

Different devices may use the same control character to do different things. It is important to look
up the device documentation to find the meaning of each control character. The SparkFun LCD
documentation indicates that you can select a baud rate of 9600 by first entering special command
character 124 followed by <control>M. Since <control>M means character 13, you would need
the line of code printf(“%c%c”, 124, 13); in your program to make this selection. A list of the
control characters and the common notation is given below.

Table D-2: ASCII Control Characters


Dec Hex Command Dec Hex Comand Dec Hex Comand
0 0x00 CTRL-@ 11 0x0B CTRL-K 22 0x16 CTRL-V
1 0x01 CTRL-A 12 0x0C CTRL-L 23 0x17 CTRL-W
2 0x02 CTRL-B 13 0x0D CTRL-M 24 0x18 CTRL-X
3 0x03 CTRL-C 14 0x0E CTRL-N 25 0x19 CTRL-Y
4 0x04 CTRL-D 15 0x0F CTRL-O 26 0x1A CTRL-Z
5 0x05 CTRL-E 16 0x10 CTRL-P 27 0x1B CTRL-[
6 0x06 CTRL-F 17 0x11 CTRL-Q 28 0x1C CTRL-\
7 0x07 CTRL-G 18 0x12 CTRL-R 29 0x1D CTRL-]
8 0x08 CTRL-H 19 0x13 CTRL-S 30 0x1E CTRL-^
9 0x09 CTRL-I 20 0x14 CTRL-T 31 0x1F CTRL-_
10 0x0A CTRL-J 21 0x15 CTRL-U

67
Appendix E Simulator

The Simulator
When a build does not succeed, the output window in the lower right pane of the IDE will provide
a series of error messages and warnings that will help you track down the problem. The simplest
problems are just typos, such as misspelling a function name or forgetting a closing bracket or
semicolon. These messages will indicate the line number in the code where the problem occurred.
In either case, the error messages and warnings are all numbered and include a short, often cryptic,
explanation of the cause. More information on the error and warning messages is found in
Appendix D of the MPLAB XC8 Compiler User’s Guide and Chapter 7 – Troubleshooting of the
MPLAB XC8 C Compiler Getting Started manual. Although more detail is given there, it too may
be rather cryptic. Double clicking on an error message will bring you to the line of code where the
problem occurred. Note that the real problem might even be a line or two before that line. If the
line involves a function call, it might even be in the source file for that function.

Possible Pitfall: Sometimes the error in your code is a line or two above the line
number specified in the error message.

Possible Pitfall: Do not ignore warning messages even if the build succeeds.
Subtle logic problems indicated by the warning message can make your program
behave in unexpected and undesirable ways.

MPLABX has a number of features, besides messages, to help with debugging. We explore some
of them below.

Line Numbers
By default, the IDE automatically displays line numbers for each blank line or line of code. When
you print your file, however, line numbers are not automatically displayed. To display line number
when printing, select File|Print and then select Print Options in the Print Preview window. In the
Print Options window, check the Line Numbers box (see Figure E-1)

Figure E-1: Printing line numbers.

68
Appendix E Simulator

The Simulator
A program may build successfully but not do what you thought it should because of faulty logic
in the way it was constructed. Such faults are much harder to track down, but the MPLABX IDE
does provide a way to step through your code and watch how variables and registers change. To
invoke the simulator, choose Run|Set Project Configuration|Customize (see Figure C-3 in
Appendix C) and select Simulator as the Hardware Tool in the Project Properties window (see
Figure C-4).

Use Debug|Debug Main Project to start the debugging session. Now Debug, in the menu bar, will
have a number of choices for debugging the execution of your program, see Figure E-2. Similarly,
a new set of equivalent icons, see Figure E-3, will appear on the icon bar. These choices are most
effective when breakpoints and variable watches have been created, as discussed in the next two
sections. Use Debug Main Project from the Debug menu or the icon bar to start the debugging
session.

Finish Continue

Debug Main Reset


Project

Figure E-3: Debugger icons.

Figure E-2: Debugger menu.

Breakpoints
The simulator is often used by setting breakpoints in the code. When run, the simulator will execute
the code but stop at each breakpoint. If you also have a “watch window” open, you can see how
variables and function registers change from breakpoint to breakpoint in the execution of your
code.

You have multiple ways to set a breakpoint. The simplest approach to set a breakpoint in your
code is by left-clicking at the line number in the code pane where you wish to set the breakpoint.
The line number will change to a tiny pink square, indicating the breakpoint, and the line of code
will be highlighted in pink. Clicking twice removes the breakpoint. You can also right-click on the
line number, which brings up a small context menu, or use the shortcut Ctrl-F8, or right-click on
the line of code and pick Toggle Line Breakpoint from the pop-up menu. Sometimes breakpoints

69
Appendix E Simulator

cannot be set exactly where you want them to be. If that happens, insert a NOP(), which is a “do
nothing” or “no operation” function, and set the breakpoint on the line with the NOP().

After you have started the debugging session, the debugging icons will no longer be grayed out
(see Figure E-3 for the active icons). The key icons are the Finish icon, which ends the debugging
session, Continue, which moves you to the next breakpoint, and Reset, which restarts the project
from the beginning.

When the debugging session starts, the program will execute until it hits the first breakpoint. The
line of code will be highlighted in green. There will also be a small green arrow on top of the red
breakpoint square (see Figure E-4). Click on the Continue icon to go to the next breakpoint.

Figure E-4: Setting a breakpoint.

Watching Variables
As you step through your code, you can watch how your variables change from line to line in a
“watch window”. This is an excellent way to spot unexpected behaviour. You open a watch
window by highlighting your variable, or variables, and right-clicking. From the context menu,
select New Watch (see Figure E-5). This opens a small pop-up window asking you to confirm or
change your choice (see Figure E-6). In the Task output pane on the lower right of the IDE window,
there will be a new Variables tab (see Figure E-7). Your watched variables are listed in the tab.
Usually, you are most interested in the value of the variable, but the data type is also listed. The
values may initially be empty. If you did not initialize your variables, you may not see any value,
or you may see 0. It is unwise in C programming to expect that uninitialized variables are
automatically initialized to zero. You can also display the value in various number formats,
including binary and hexadecimal, by right-clicking on the top of the tab (again see Figure E-7).
As you step through the breakpoints, the values will change with the code. Changed values are

70
Appendix E Simulator

noted in red. If a breakpoint is placed on a line in main, and program execution stops on that line,
the local variables will be in scope and will be displayed.

Figure E-6: Confirming selection.


Figure E-5: Setting a Watch.

Figure E-7: Variables tab and value formats.

Similarly, you can examine the behavior of any ‘register’, which is an internal MCU variable, used
in your project. The SFRs (special function registers) used in your project show up in a New Watch
pop-up window (see Figure E-6 again).

Stopwatch
Timing is an important part of MCU operations. In real applications, when things happen – such
as a signal turning on or off – is critical. In the simulator, you can examine how much time is
required for each line, or set of lines, of code to execute. You call up a “stopwatch window” by
selecting the menu item Window|Debugging|Stopwatch, as shown in Figure E-8. A stopwatch
window is shown in Figure E-9. At a breakpoint, you can zero or clear the stopwatch using the last
icon on the left-hand vertical bar (see Figure E-9). Proceeding to the next breakpoint will tell you
the number of clock cycles TCY, and the time between the breakpoints. You can also use the
trashcan icon to clear the tab.

71
Appendix E Simulator

Figure E-9: The stopwatch tab.

Figure E-8: Calling a Stopwatch.

The times will only be correct if the debugger knows what frequency your simulated MCU is
operating at. MCUs have multiple operating frequencies. The default frequency is 1 MHz. You
can check and/or set the frequency being simulated by selecting Run|Set Program
Configuration|Customize, which opens the Project Properties window. Highlight the Simulator in
the Categories box. Then, in the dropbox for Category options, select Oscillator Options (see
Figure E-10).

Figure E-10: Oscillator Options.

72
Appendix E Simulator

UART Display
When you run the simulator, the IDE steps through the code. If you have a printf() function in
the code, it will not print anything, because you do not have a physical display. A simulated display
is available, but it must be enabled. Select Run|Set Program Configuration|Customize, which
opens the Project Properties window. Highlight the Simulator in the Categories box. Then, in the
dropbox for Category options, select Uart 1 IO Options, and check the box to enable UART, and
make sure the output is to a window (see Figure E-11).

Figure E-11: Enabling a UART window.

As soon as the first printf() command is executed, a UART 1 Output tab will open in the lower
right Output pane (see Figure E-12). Your printf() output should appear here. If it doesn’t, it
may be because the UART module has not been properly configured for serial communication.
You have been given a small file called ConfigureUSART to help you do this simply.

Figure E-12: Enabling a UART window.

In-Circuit Debugging
The Simulator can be used to investigate how internal variables change from one part of the
program to the next. Since it is a simulation, the program is running virtually on your PC – not on
the MCU. So while the Simulator is great for debugging and tracing the behaviour of a program,
it cannot tell us how the program will actually respond to an external stimulus like a button press.

The PICkit 2 or 3 has a feature, In-Circuit-Debugging (ICD), that runs, halts, and single-steps
through the program while the PIC microcontroller is actually embedded in the application. When
halted at a breakpoint, the file registers can be examined and modified. For instance, this lets you
see if your button press actually has the intended consequence. You can install up to 3 breakpoints
for the PIC18F4525. The in-circuit debug implementation on PIC microcontrollers will halt
execution on the instruction after the breakpoint instruction. This means the breakpoint instruction

73
Appendix E Simulator

will have executed when the debugger halts. This is referred to as "breakpoint skidding". You may
need to add one or more NOP() functions after your desired breakpoints to stop near where you
actually want to stop.

To use ICD, the PICkit 2 or 3, not the Simulator, must be your hardware tool. Add breakpoints
and variable watches as needed. Then, simply select Debug|Debug Main Project, or click on the
Debug Main Project icon. A window will pop up to ask to change the configuration bits to enable
the MCLR pin (see Figure E-13). Say yes.

Figure E-13: Enable MCLR pin.

As you step through the program from breakpoint to breakpoint, you will able to see the current
values of variables and any SFRs. You will also be able to change these variables and any output
SFRs.

74
Appendix F The Oscilloscope

The Oscilloscope
The oscilloscope is a standard tool in laboratories and industrial settings wherever electronic
equipment needs to be tested. Its inventor, K.F. Braun, a German physicist, shared the 1909 Nobel
Prize with Marconi for their work in perfecting the radio. At its simplest, an oscilloscope is a
voltmeter with a screen showing how a voltage signal changes with time. Most oscilloscopes have
a bewildering number of dials, button, and switches. Fortunately, most can be ignored except for
advanced uses. The controls you need to learn are indicated in the picture of the Tektonix 1000
Series Digital Oscilloscope shown in Figure F-1.
Multipurpose knob Measure Cursor Autoset

Option buttons

Save/Print button

Math button

Reference signal

USB drive port

CH1 and CH2 signals


connect here BNC connectors

Figure F-1: Digital Oscilloscope

Pressing the various menu buttons on the front panel changes the menu displayed on the
oscilloscope screen’s right-hand edge (not shown in Figure A-1). The unlabelled “option” buttons
along the edge of the screen can then be used to step through the options in the menu. The
“multipurpose knob” allows you to go forward and backward through an active menu much
quicker.

Notice that the oscilloscope controls are grouped into sections. The control groups we will
concentrate on are the sections labelled Vertical, Horizontal, and Trigger in Figure F-1.

An important concept in the use of the oscilloscope is the Trigger Condition. An oscilloscope waits
until the voltage either rises above or falls below a certain voltage and then commences to draw
the signal, starting on the left-hand side of the screen. The trigger condition is set by the Level and
TRIG Menu controls on the digital scope in Figure F-1. The Sec/Div dial, in the Horizontal controls
group, controls how large a horizontal chunk, in seconds, of the signal the oscilloscope shows.

75
Appendix F The Oscilloscope

Figure F-2 illustrates how the time setting and the trigger condition control how a periodic
triangular voltage signal would be displayed on the screen. Figure F-2 shows a trigger condition
set to 2 Volts with a rising slope and a timing length T. Once the first trigger condition is met, the
oscilloscope draws a portion of the signal of time length T on the screen. The oscilloscope then
waits a small time delay T until the trigger condition is met again and then it redraws the screen,
and so on. If the timing length T is small enough, you don’t notice the redrawing of the screen and
your oscilloscope appears to have captured a picture of a segment of the voltage signal.

Third trigger
V First trigger Second trigger
condition condition condition
4
2
signal
0
-2
-4
T T T T
T T

Figure F-2: How an oscilloscope displays a periodic voltage.

What would the signal have looked like on the screen if the trigger condition had been 2 Volts
and a falling slope?

If your trigger voltage is above the signal maximum, or below the minimum, you might expect to
see nothing on the screen. However, what happens is that the oscilloscope displays first one trace
or snapshot of the first T seconds of the signal, then the next, and the next, and so on. Since these
traces are not identical, you observe a confusing jumble that appears to move as old traces
disappear while new ones are added.

An important and useful feature of the oscilloscope is that it can display two voltage signals at
once, allowing all sorts of interesting comparisons. The first signal is yellow on the display and is
called CH 1 (Channel 1) and the second CH 2 (Channel 2) is blue. The positioning controls (see
Figure F-1) allow the signals to be moved around and even laid on top of one another. Pressing
the buttons labelled [1] or [2], and using the choices presented by the Options buttons, allows you
to show CH 1 only, or CH 2 only, or both together.

There is a complication with measuring two signals at once. Equipment plugged into wall outlets
and handling large voltages and currents must be grounded. In the event of a malfunction, the

76
Appendix F The Oscilloscope

ground provides a low resistance path for a current to travel. As currents prefer the path of least
resistance, the high-resistance person using the equipment is safer. However, this means that both
channels must have a connection to ground. A typical arrangement might look like Figure F-3. The
black lead from each of CH 1 and CH 2 would need to be connected to ground on the power supply.
When the red lead is connected to the circuit, the channel reads the voltage between that point and
ground. As a result, in this configuration, CH 1 measures V1 + V2 and CH 2 records V2.

There is a neat trick that allows the oscilloscope to display V1 for the circuit in Figure F-3. If you
press the pink MATH button, the option buttons allow you to show a third signal in red, either Ch1
– Ch2, Ch1 + Ch2, or Ch1  Ch2. Choosing Ch1 – Ch2 combines the signals from CH1 and CH2,
so in this case the oscilloscope would display a third trace (in red) of (V1 + V2) + (–V2) = V1.
(Note you can press the Math button again to turn this trace off.). Make sure CH 1 and CH 2 both
have the same scale. The scale of the third trace may be set by the Multipurpose Knob.

Oscilloscope

CH 1 CH 2

R1 R2

V1 V2

Power Supply
Figure F-3: How to connect an oscilloscope to read two voltages at once.

The value of the Horizontal|Scale dial is displayed along the bottom edge of the oscilloscope
screen along with other useful information about the signal. The timing length T of any feature
displayed on the screen is determined from the number of horizontal squares times the setting of
the Horizontal|Scale dial. For example, if the screen is 8 squares wide and the Horizontal|Scale is
set to 0.1 ms, then total time displayed on the screen is (8 squares  0.1 ms) = 0.8 milliseconds.

However, rather than measuring squares off the screen, one can use cursors. Cursors and the
Cursor options menu are described a little later in this appendix. Also, many common
measurements of periodic signals are automatically calculated and can be displayed using the
Measure options to be described later.

77
Appendix F The Oscilloscope

The value of the Vertical|Scale dial is displayed along the bottom edge of the oscilloscope screen,
on the left-hand side. The range of voltage on the screen is likewise determined by the number of
vertical squares times the setting on the Vertical|Scale knob. For example, on a screen with 8
vertical squares and the CH 1 Vertical|Scale dial set to 1 Volt, the maximum range is (8 squares 
1 Volt) = 8 Volts, i.e., the top of the screen is 8 Volts above the bottom of the screen.

There are two Vertical|Scale dials because you can set the scale for the signal on Channel 2
separately from Channel 1. Watch out for this when you make voltage measurements! (Both
signals do have the same time setting, however.)

Each of the channels has a small arrow displayed along the left-hand edge of the scope screen that
marks where the position of 0 Volts, or ground, is. For Channel 1, the arrow looks like this: “1“.
This marker is useful if you have used the vertical Position controls to move the signal around; it
allows you to see where zero volts is, and you can use the vertical Position knob to move the zero
line to a convenient location.

Because there are so many settings on an oscilloscope, it is always best to have your oscilloscope
set to a standard default arrangement. These default settings are outlined in Table F-1. The
oscilloscope we use has a Default Setup button on the second row of horizontal buttons (see Figure
F-1).

Table F-1. Starting settings for an oscilloscope.

Yellow CH1 and Blue CH2 Menus “Trig Menu” Menu


Coupling DC Type Edge
BW Limit Off Source CH1 or CH2
Volts/Div Coarse Slope Rising
Probe 10 Voltage Mode Auto
Invert Off Coupling DC

Note that the oscilloscope has a button in the upper portion of the control panel called Autoset.
Pressing this button will often display your voltage signal looking approximately right. Although
you should ensure you know how to adjust your vertical and horizontal Scale knobs to display
your signal correctly, Autoset can often make that task a bit faster.

When you are examining a single channel, say by pressing [1], the Options buttons let you switch
between AC (for alternating current), GND (for ground), and DC (for direct Current) Coupling.
Coupling refers to how the oscilloscope connects to the circuit. Setting it to GND (for ground)
connects the red lead on the channel to the ground (“grounds out the signal”) no matter where it is
connected in a circuit. Since the oscilloscope and power supply are connected to the same building
electrical system ground, the difference between the two ground connections is therefore zero
voltage and a horizontal line is displayed across the screen. This switch setting is useful is you
have used the vertical positioning controls (See Figure F-1) to move the signal around; it allows
you to find where zero volts is. You can then use the vertical positioning control to move the zero
line to a convenient location.

78
Appendix F The Oscilloscope

Note also that some signals average out to zero over a full period and are said to be pure AC
(alternating current), some signals have a constant or DC (Direct Current) voltage, and other
signals are a mixture of AC and DC. The reason we use the “Coupling: DC” settings for our
oscilloscope—even when we’re looking at an AC signal—is because the AC option causes the
oscilloscope to internally remove any DC portion from a mixed signal. Generally, we don’t want
to ‘hide’ the DC component of our signal like that. Also, the manner in which the DC portion of
the signal is removed introduces a capacitor to your circuit. At low frequencies in particular, this
capacitor can badly distort the shape of your signal.

Connecting the Oscilloscope to a Circuit


You can use either a BNC connector and ordinary leads to connect to your circuit, or you can use
a special oscilloscope probe (see Figure F-4). Leads are convenient when you can plug into a
connector block terminal on a circuit. Probes can grab onto wires and are therefore more
convenient when connecting to circuit boards.

Probe

Ordinary leads
Attenuation
information

– + +
BNC connector

Figure F-4 - Oscilloscope Connectors

You do have to be careful about the properties of your connection, i.e. your leads or probes, and
the settings on your oscilloscope. Probes have circuitry that can attenuate, or lower, voltages while
leads do not. Common rates of attenuation are 10X to 200X. Some probes can switch between one
value of attenuation and another. The attenuation values of a probe are marked on the end of the
probe that connects to the oscilloscope.

Your oscilloscope in its default setting is expecting a probe with an attenuation of 10X and
accordingly multiplies any voltage reading by 10. If the probe setting on the oscilloscope does not

79
Appendix F The Oscilloscope

match the actual attenuation of the probe, the voltage readings on the scope can be either too high
or too low. From the Measure menu, you can see and change the probe setting on your oscilloscope
(see Figure F-1).

When a BNC connector is used, remember that red is the high (+) terminal and black is the (–) or
low terminal. It is recommended that you use leads of the same colour as the terminals to avoid
confusion.

Using the Oscilloscope to Measure Common Features of Periodic Waves


In the upper portion of the scope’s control panel, there is a menu button called Measure. If you
press this button, it will change the menu options displayed on the right-hand edge of the
oscilloscope’s screen. With it, you can measure the horizontal or timing properties of your periodic
signal. These include the frequency, period, how long the signal is above zero, and how long the
signal was below zero. You can also measure the vertical or voltage properties of the periodic
signal. The choices include maximum and minimum voltage, and the peak-to-peak voltage, which
is the difference in voltage between the maximum and minimum voltages. You can also measure
the mean or average of the signal. Finally, you can also measure the RMS or root-mean-square of
the signal. When a constant or varying voltage is applied to a resistor, power is dissipated in the
resistor and it heats up. This is known as Joule heating. By definition, the RMS value of a varying
voltage equals the steady or DC voltage that would produce the same Joule heating.

Using the Oscilloscope’s Cursors to Take Data


Besides the standard measurements above, the oscilloscope allows you to measure any feature you
can see on the display using a set of parallel lines called cursors. You can move the lines
independently on the display, and the oscilloscope will tell you the location and separation of the
lines. To activate the cursors, there is a menu button called Cursor in the upper portion of the
scope’s control panel. If you press this button, it will change the menu options displayed on the
right-hand edge of the oscilloscope’s screen.

Change the options to the following:

CURSOR MENU
Type: Time
Source: CH1

Once you have set these options, you should notice three things:
1) Two vertical lines have appeared on the oscilloscope screen.
2) On the menu displayed along the screen’s right-hand edge, there is now a section that has
1
values for two quantities called “t” and “ ∆𝑡 ”.
3) On the menu, you can now select either Cursor 1 or Cursor 2 by using the buttons.

80
Appendix F The Oscilloscope

Cursor 1 and Cursor 2 refer to the two vertical lines that appeared on your screen. You can select
these lines by using the multi-function buttons beside the screen, and then move them back and
forth across your screen by using the multi-function knob that is located in the upper left-hand
corner of the oscilloscope’s control panel (see Figure F-1).

1
When you move the cursors, the values for “t” and “ ∆𝑡 “ change to reflect the time difference
between the two cursor positions. You can, for example, place your cursors at the beginning and
1
end of a waveform and then read off the period (t) and frequency ( ∆𝑡 ) of your signal.

You can also change the Cursor menu to “Type: Amplitude” to measure the voltage of your signal,
and you can change the Source to CH2 to measure the voltage, period or frequency of a signal on
that channel.

Reference Voltage and Frequency


The digital oscilloscope has two pins on the front just below the LCD screen. These pins output a
square wave, exactly 0 to 5 Volts at 1000 Hz and 50% duty cycle. The reference pins can be used
to self-calibrate the scope, that is to see if the oscilloscope measurements match the reference
values. If they don’t, let your lab instructor know! If you have programmed your MCU to measure
voltage or time edges, these reference pins are a handy check to see if your MCU program is
working correctly.

Saving the Oscilloscope’s Screen Image onto a USB drive:


You may save your oscilloscope’s display, including a lot of the settings and measurement of the
signal, as a file to a USB drive (sometimes called a flash drive, memory stick, or thumb drive).
This is useful if you want to print the display for inclusion in a lab report. To save your scope trace
to a USB drive, do the following:

1) Insert your USB drive into the slot for it on the front of your scope. You will have to wait a
few moments for the scope to recognize your USB drive. (Note that the scope will not be able
to work with USB drives that have too large a memory size.)

2) Press the “Save/Recall” menu button located at the top of the scope’s control panel.

3) Use the multi-function buttons beside the screen to set the following:

SAVE/RECALL MENU
Action: Save All
PRINT Button: Saves Images to File

4) If you wish the image to also include information from the cursor menu, press the Cursor
button again and make sure the screen is displaying everything you want it to.

81
Appendix F The Oscilloscope

5) To save the current screen image to your USB drive, press the SAVE button located in the
upper left-hand corner of the scope’s control panel, just underneath the unlabelled multi-
function knob.

It will take a few moments for the scope to finish saving the image to your USB drive. The image
will be saved to the upper levels of your drive. (There is a way to save to a particular folder on
your drive, but it’s a bit of a hassle to set that up!)

Printing the Oscilloscope’s Screen Image from a USB drive:


Once you’ve finished saving the image, you should be able to transfer your USB drive to the lab
computer and print the image normally.

The default viewer, Windows Picture and Fax Viewer, will allow you to do this, but a nicer
program to use is IrfanView, which is located on the desktop of many of the lab computers.

If you open your image using IrfanView, choose the following options when you print it:

IRFANVIEW PRINT OPTIONS


Print Size: Original Size
Position: Center Image

82
Appendix G Graphing

Graphing with MS Excel


Frequently you will have a set of data that you wish to plot and examine for simple mathematical
relationships. MS Excel lets you do this easily. First, open Excel and enter your data in a column.
Highlight the data and choose the Chart Wizard icon, as shown in Figure G-1 below.

Figure G-1: Starting the Chart Wizard.

In the first window, Figure G-2, of the Chart Wizard, choose


the XY (Scatter) plot with no connecting lines.

Continue to Step 3 of the Chart Wizard where you have


multiple tabs with which to control the look of your plot,
including adding appropriate labels. See Figure G-3 a, b, & c.

Figure G-2: Choosing an xy plot.

Figure G-3 a, b, and c: Controlling the look of the plot.

83
Appendix G Graphing

In Step 4 of the Chart Wizard, Figure G-4, you will be asked where to put the graph, either in the
sheet with the data, or on a separate chart page of the workbook. It is usually a good idea to have
it on its own sheet for editing purposes.

Figure G-4: Putting the plot on a new sheet.


When the chart appears, it will have a grey background. Right-clicking on the background of the
plot brings up a context window. If you select Format Plot Area, the new window lets you change
the background. See Figure G-5.

Figure G-5 a & b. Changing the plot area colour.

MS Excel calls the curve it fits to a set of points a ‘trendline’. To generate a trendline, highlight
the data points by clicking on one of them. Next, right-click to bring up a context window and
choose Add Trendline, as shown in Figure F-6a. The window that appears has two tabs. The first
tab, Figure G-6b, lets you choose the sort of curve that Excel will fit to the data. You have lots of
choice: Linear, Logarithmic, Polynomial (note a quadratic is a polynomial of order 2), Power, and
Exponential. We are not interested in Moving Averages. In the second tab, shown in Figure G-6c,
you can display the equation of the line and the R-squared value. The R-squared value is measure
of how well the data fits the trend line, with 1 being a perfect fit. Try the different types of trendline
until you get the best fit.

84
Appendix G Graphing

Figure G-6 a, b, & c: Fitting a trendline.

Figure G-7 shows the finished plot with the data, a trendline, and an equation.

Figure G-7: Data with trendline and equation.

85
Lecture Notes

86
Lectures - I Introduction

Notes I Introduction
The microcontroller (MCU) is simply a computer designed to work with other machines rather
than people. Because of this, it doesn’t need a monitor, keyboard, mouse, or any other device that
requires human interaction.

Most MCUs have a CPU, RAM, ROM, and some EEPROM (Electrically Erasable Programmable
Read Only Memory). Inexpensive controllers that are designed to be programmed once (for a
particular application) may omit the EEPROM, because there may be no need to ever change the
program once the MCU goes “into production”.

The MCU you will use, the PIC18F4525, has 48kb of flash memory which allows you to erase and
reprogram it at least one hundred thousand times, so you do not need to worry about “wearing out”
the chip. This flash memory can hold your program and any data storage required in your
application. Technical details are available in the datasheet.

You will create the circuits you will use with the MCU on a “breadboard”. On a PC, using a
program called the MPLAB IDE, you will create and test the programs that will run on the MCU.
The programs will be written using the C language. When finished, you will transfer the program
to the MCU using a USB connection and a device called the PICkit 2 or PICkit 3 programmer.

The PIC18F4525 is a 40 pin device. Some of the pins are used to connect a battery or power supply
to the MCU. The rest of the pins are for inputs and outputs. The pin-out diagram of the P18F4525
is shown in Figure I-1 below. Note the notch on the chip. This orients the chip and indicates the
top of the chip and which pin is number 1. The PIC18F4525 has many features built into the chip,
including digital input and output (DIO), timing and square wave production, serial

Figure I-1: Pin out schematic of the PIC18F4525.

87
Lectures - I Introduction

communication, and analog-to-digital conversion (ADC). Depending on the feature of the MCU
being used, the pins have different functions and names (this is called multiplexing).

BreadBoard
A breadboard is a convenient way to assemble circuits where the parts are small, such as
microchips, diodes, and resistors. Pinholes in the front of the breadboard let you securely connect
circuit elements to it. The sets of pinholes are electrically connected to each other by conducting
bars in two different patterns, short horizontal bars and long lengthwise bars, under the plastic
cover. See Figure I-2. This setup allows parallel and series arrangements of elements to be put
together quickly. When using wire connectors, make sure to keep them short and neat!

Pins Connector bars


Figure I-2: Breadboard connectors.

We will start this course with a review of C programming and the particulars of programming an
MCU (microcontroller) unit. We will put a lot of emphasis on understanding how numbers
(integers) are stored in memory.

We will also review basic electric circuits and Kirchhoff’s Rules. We will also cover the simplified
notation used in electronics for circuit diagrams.

Separately, we will learn how to use special modules on the MCU, including timing, DIO, ADC,
and PWM.

The main project for this course, the line following robot, is a simple machine whose brain is the
PIC18F4525 chip. The robot, or more correctly the program you write for the robot, must correctly
interpret sensor input for the surface the robot is on and correctly adjust the wheel motion to follow
a black line. You will be applying your programming skills and your knowledge of the modules
of the MCU to complete the task. In class, we will spend some time discussing algorithm design,
testing, and debugging. We will spend a short amount of time going over proper formal report and
public presentation styles.

88
Lectures - II C Programming

Notes II C Programming
You all have taken an introductory course in C++. C++ is a grandchild of C and, at the introductory
level, C and C++ look very much the same. The version of C you will be using, XC8, is specifically
designed for use with the Microchip PIC18 chips.

What You Should Already Know


We expect that you already know about the basic variable (number) types: char, int, long, float,
and char strings for text. We also expect that you know how to write arithmetic equations such
as c = a + b, d = a*b + c. You should be familiar with the increment and decrement operators,
i++ and j--, and what they mean.

You need to know how to use for and while loops.

You need to know basic decision handling using if…else and switch() statements. This further
means that you need to understand comparisons such as greater than >, equivalent to ==, not !, etc.

You need to be familiar with using some of the standard library functions in C. Some examples
would be printf() in stdio and ln() in math.

XC8 and C
Because the MCU has limited memory, the XC8 is in many ways a stripped down version of
standard C. Not all the functions and libraries in C are available with the XC8 compiler. Even
when those functions do exist, they may not have all the same capabilities as standard C. For
instance, the function printf(), which will print out text and numbers to an output device, needs a
small helper function putch(), which understands how the PIC operates. This helper function is
given to you in the configureUSART.c and configureUSART.h library. Conversely, there are a
number of libraries and functions in XC8, and not in C, which pertain exclusively to working with
microcontroller units.

Structure of Programs and Projects


A project consists of usually one or more source files. A compiler then translates the instructions
into a program, which is a form the end machine can use. In your C++ course, the end product was
an executable file with a name like program.exe for use on a PC. In XC8, the end product is a hex
file with a name like program.hex that must be transferred to the MCU. A USB device (called a
programmer), either the PICkit2 or PICkit3, is used to transfer or write the hex file from the PC
onto the MCU.

The smallest program you could write would consist of a single file containing a single function.
That single function must be called main(). Bigger programs and projects could have multiple
source files and many functions, but there can only be one main() function. Program execution

89
Lectures - II C Programming

starts from inside the main() function. A simple program to add two numbers and display the
result would look like the following, Figure II-1, if we were writing a program for a PC:

#include <stdio.h> // library that contains printf() code


#include “..\Common\configureUSART.h” // library that contains printf()
// helper function putch() code

int main(void) // must have main()!


{
int i = 15, j, k;
j = 22;
k = i+j;
printf(“Sum of two numbers % i and %i is %i”, i , j, k);
return 0; // successful execution
}

Output on display:
Sum of two numbers 15 and 22 is 37

Figure II-1: A simple one-file PC program.


The #include in the first line is a directive to the compiler to look in that library for functions that
have not been defined in the program. The stdio (standard input output) library has code for the
printf() function. When the program is compiled, the stdio code is combined with the program
code. You can have more than one #include directive.

On a PC, the standard output is the display, and the standard input is the keyboard, and this PC
program would output “Sum of two numbers 15 and 22 is 37” in a window on the PC. But
your MCU doesn’t have a keyboard or display, so programming the MCU with this program will
appear to do nothing. We need a display, so we will attach a small two-line, 32 character LCD to
the MCU to be our standard output. Even though now we have a display, this program will not
produce an output. We need to include instructions to tell the MCU which pins are used in the
connection and how to send data, in both format and speed, to the LCD. So even if you are a
programmer with a good knowledge of C, you have a lot more to learn if you want to program
MCUs. You need to know how to connect electronic components together and you need to know
how to control the different features of the MCU.

The first step to programming the MCU is to include a library called xc.h , see Figure II-2, that
contains all the MCU-specific information and definitions and functions we will be using. You
will include this library in every program you write. You will need to know the functions in this
library if you wish to use ADC, PWM, DIO, etc. The second step is to know which functions to
call to access the feature you want. To get you started and to simplify communication with the
LCD, we are providing you with a small library we wrote called configureUSART.h. It contains
the function configureUSART() to let you set up communication with the LCD display using
stdio.h and printf(). As mentioned earlier, the MCU outputs data differently than a program
compiled to run on a PC would. As a result, to keep the printf() function in XC8 looking and
behaving as it does in most C compilers meant for PC, a small helper program putch() needs to
be included. That function is in this library configureUSART.h as well.

90
Lectures - II C Programming

#include <xc.h> // library for functions and defintions


// for the MCU
#include <stdio.h> // library that contains printf() code
#include “..\Common\configureUSART.h” // library that contains printf()
// helper function putch() code
// and function written to
// simplify LCD communication
int main(void) // must have main()!
{
int i = 15, j, k;
j = 22;
k = i+j;

configureUSART(9600ul, 1); // talk to LCD at 9600 bps, MCU working


// at 1 MHz (default speed)

printf(“Sum of two numbers % i and %i is %i”, i , j, k);


return 0; // successful execution
}

Output on LCD display:


Sum of two numbers 15 and 22 is 37

Figure II-2: A simple one-file PIC program.

You can and will write your own functions. The compiler insists that you prototype the function
before it is used or defined. A PC example with several functions is shown below in Figure II-3.

#include <stdio.h> // library that contains printf() code

int QuadraticEquation(int a, int b, int c, int x); //prototype


void MyPrintFunction(int value); //prototype

int main(void) // must have main()!


{
int i = 2, j = 4, k = -2;
int x = 3, result;

result = QuadraticEquation(i, j, k, x);


MyPrintFunction(result);

return 0; // successful execution


}

// Evaluate a quadratic equation //


int QuadraticEquation(int a, int b, int c, int x)
{
int value;
value = a*x*x + b*x + c;
return value;
}

// Print only positive results //


void MyPrintFunction(int value)
{

91
Lectures - II C Programming

if (value >= 0 )
printf(“Result is %i”, value);
else
printf(“Result is negative”);
}

Output on display:
Result is 28
Figure II-3: A program with user-defined functions.
In C, prototypes must always be defined at the top of the file after directives (e.g. #include and
#define statements). A function can be of datatype void, char, int, or float, meaning that it
returns nothing, an integer number of size char, a integer number of size int, or a decimal number
of size float. The functions may take no variables (void), or multiple variables of different
datatypes. Note that although a prototype of the form

unsigned char IsLeapYear(unsigned int); //determines if year is a leap year

is legitimate C, it is not useful. It is much better programming style to include the variable with a
meaningful name

unsigned char IsLeapYear(unsigned int year); // No comment needed

For easier editing and reusability, the source code can be broken up into separate files. Typically,
a family of functions is put into the same file. Another file, called a header, will contain the
prototypes of those functions. Other files can call those functions as long as the header is included
using the #include directive. The previous program could be split as follows:

#include “myfunctions.h” // for the functions called below

int main(void) // must have main()!


{
int i = 2, j = 4, k = -2;
int x = 3, result;

result = QuadraticEquation(i, j, k, x);


MyPrintFunction(result);

return 0; // successful execution

Figure II-4: File main.c.

int QuadraticEquation(int a, int b, int c, int x); //prototype


void MyPrintFunction(int value); //prototype

Figure II-5: File myfunctions.h.

#include <stdio.h> // contains printf()


#include “myfunctions.h” //prototypes and printf()

92
Lectures - II C Programming

// Evaluate a quadratic equation //


int QuadraticEquation(int a, int b, int c, int x)
{
int value;
value = a*x*x + b*x + c;
return value;
}

// Print only positive results //


void MyPrintFunction(int value)
{
if (value >= 0 )
printf(“Result is %i”, value);
else
printf(“Result is negative”);
}

Figure II-6: File myfunctions.c.


Note that stdio.h is in chevron brackets while myfunction.h is in double quotes. The chevron
brackets tell the compiler that this file is one of the XC8 compiler’s own library of functions. The
double quotes tell the compiler the directory path to the file (e.g.
C:/Myfiles/Samples/myfunctions.h). Since no path is specified, the compiler searches for the file
in the same directory as main().

Well-named functions are a great way to write programs that you or anyone else can understand.
For example, see Figure II-7.

#include <xc.h>
#include <stdio.h>

void PrintToLCD(char message[]);


void TurnOnLEDonPin21(void);
float ReadADCVoltageOnPin21(void)

int main(void)
{
float voltage;
PrintToLCD(“Hello World”);
TurnOnLEDonPin21();
voltage = ReadADCVoltageOnPin20();
printf(“Voltage = %f”, voltage);
return 0;
}

//functions to be defined

Figure II-7: Use and name functions to improve readability.

93
Lectures - II C Programming

Libraries
A collection of related functions in a C file and the associated header file is usually called a library.
In group work, large projects are split into smaller tasks with different members of the group
assigned to code different libraries. Usually, although we won’t be doing it, the C file is compiled
to an object file (extension .o) which is not a text file and therefore cannot be read. The header file
is simply a text file, so it is important that comments and instructions to users of the library be
included here. Some brief points:

 Headers must end with a blank line. Leave it out and you can get error messages that point
to the wrong place in the program, usually a line with no problem.

 Headers mostly contain function prototypes but may also have useful #define statements.

 Header functions should not contain #include directives to other libraries.

 The source C file and the header file must have the same name.

 Both the source C file and the header must be added to your project.

 The source C file must #include its own header file before any of the code for the
functions. If the functions call other functions from outside that C file, you must also
#include the appropriate headers for those libraries. For example, if your function in your
library calls printf(), you must have #include <stdio.h> in your source C file.

Programs for the MCU


Each MCU produced by Microchip has a different model number (e.g. PIC18F4525), and each
model varies in the set of modules (things it can do) it possesses. When you write a program for
your MCU you first specify which model MCU you are programming. The library called by the
directive #include <xc.h> knows which pins do what, and which functions are available for that
model MCU. Make sure this directive is in every program you write. A second feature of each
family of MCUs is that certain modules are mutually exclusive. The MCU thus needs to know the
configuration, or set of modules, that may be used by the program. You will not need to concern
yourself with this. We will give you a configuration you will use in all programs for this course.
A third feature of programs for an MCU is that when a program finishes, the MCU is designed to
restart and run the program. This is obviously advantageous if there is a power loss. For this reason,
every MCU program you write will have a while(1) loop that runs forever or until there is a
power outage. A typical program, with your standard configuration settings, looks like Figure II-8

94
Lectures - II C Programming

/* PIC18F4525 program shell. It does nothing. */


#include <xc.h> // information specific to this chip

// standard configuration statements for this course


#pragma config WDT = OFF
#pragma config OSC = INTIO7 // puts osc/4 on pin 14 to check freq
#pragma config MCLRE = OFF
#pragma config LVP = OFF
#pragma config PBADEN = OFF

int main(void)
{
/* put instructions that are to run only once here */

while(1) /* this while loop runs forever */


{
/* put instructions that are to run continuously here */
}

return 0; // not reachable


}

Figure II-8: Sample MCU program.

95
Lectures - III Numbers

Notes III Numbers


A large portion of programming involves simple arithmetic operations such as c = a + b. We are
so used to simple addition and other operations that we sometimes forget how many years it took
for us to master the concepts involved. You are so accomplished at it now that if I say that a is 10
and b is 5, you will quickly compute that c is 15. You won’t be so fast if I ask for the sum of
178786485 and 6211456. Without a calculator, you will probably revert to pen and paper using
the method you learned in grade school – sum the digits, and if the sum is greater than 9, carry the
1 (see Figure III-1).

1 11
178786485
 6211456
184997941

Figure III-1: Elementary addition.

There are similarities between how numbers are added in the above example and how programs
and processors handle the same operation. Like you, the program needs space to write the given
numbers, the answer, and intermediate steps like carrying the one. It doesn’t use scratch paper, of
course, but solid-state memory instead. Unlike you, it doesn’t use decimal, base 10, numbers but
rather binary, base 2, numbers – the zeroes and ones of digital logic.

All RAM “data” is accessed and saved in the PIC MCU in 8 bit bytes. One-bit access and saving
are also possible. To access multiple bytes, the PIC must do sequential read or writes. A C (or
C++) compiler will set aside the correct number of storage locations needed for a particular
variable type when a variable is defined. Common variable keywords are char, int, and long,
which indicate variables that are stored in one, two, and four bytes respectively. Note that although
char is short for character, it is in fact a number type, as shown in the code snippet below.

char a = 10;
char b = 5;
char c;
c = a + b;

A char variable has one byte or eight bits, and each bit can either be a zero or a one. The possible
values of that variable are 00000000 to 11111111. By convention, we break the set of eight bits
into two sets of four bits called nybbles for easier reading, e.g. 0000 0000 and 1111 1111. With
eight bits, there are exactly 28 = 256 possible numbers. By the same logic, an int can hold exactly
216 = 65,536 numbers while a long can hold 232 = 4,294,967,296 numbers. There is a further
categorization of these integer types into unsigned and signed. An unsigned variable type
contains only positive integers and zero, while the signed variable type can hold negative numbers
as well. When you leave out unsigned or signed in the variable declaration, the default is signed.
So in the code snippet above, the variables a, b, and c could hold negative numbers if we wished.

96
Lectures - III Numbers

With unsigned variables, there is a simple relationship between the memory space and the number
it represents or holds. For example, in a char, 0000 0000 = 0, 0000 0001 = 1, 0000 0010 = 2, and
so on up to 11111111 = 255. The relationship between memory and signed numbers is more
complicated and will be discussed later.

Overflow
One point to keep in mind is that the memory space for a variable is fixed. Consider the code
snippet below,

unsigned char a = 100;


unsigned char b = 200;
unsigned char c;
c = a + b;

In an ideal world, we would have the result that c = 300. But 300 is bigger than 255. In binary, 300
= 1 0010 1100 but this is nine bits long. There is no room for the 9th bit to be stored in the allotted
memory space, so it disappears and we are left with c = 0010 1100 = 44. Memory space is like a
loop or ring going from 0000 0000 to 1111 1111 (in the case of a char variable). When you add 1
to 1111 1111, you are back to 0000 0000. This happens on mechanical counters, like your car
odometer, as well. This rollover or overflow is a common source of programming bugs. You need
to make sure that there is sufficient memory space for the calculations your program will do.

We don’t need to convert 300 to binary to see what number a computer will treat it as. Simply note
that 300 = 1 × 256 + 44. This is equivalent to subtracting 256 (28) as often as necessary until the
remainder is less than 256. What number would unsigned char a = 1200 be?

Q. Consider the following unsigned int variables. What numbers are they equivalent to?

unsigned int a = 80000;


unsigned int b = 1000000;

Storing Negative Numbers


Since you can’t store the negative sign in the memory space (it can only hold ones and zeroes),
storing negative numbers is a bit of a challenge. One logical and efficient solution starts by taking
0000 0000 as zero and as the centre of the ring of storage space. The first number above zero in
the ring would be 0000 0001, and we say this is decimal 1. Similarly, the first number below zero
in the ring would be 1111 1111 and we call that –1. Continuing in the same fashion, we stop when
the two numbers would overlap. The series is illustrated in the table below, Table III-1. One
advantage of this approach is that negative numbers are easy to identify because the 8th bit (or bit
7) of a negative number is 1.

97
Lectures - III Numbers

10000000

10000001

11111110

11111111

00000000

00000001

00000010

01111110

01111111
… …

–128 –127 … –2 –1 0 1 2 … 126 127


0x80 0x81 0xfe 0xff 0x00 0x01 0x02 0x7e 0x7f
Table III-1. Storing negative and positive numbers in memory.

For a char variable, signed values run from –128 to 127. For an int, signed values run
from -32,768 to 32,767, and for a long, from –2,147,483,648 to 2,147,483,647.

Rollover will also happen with signed variables. For example, signed char a = 128 is equivalent
to –128 and signed char b = -129 is equivalent to 127. In general, if you wish to know the
actual value of unsigned char number, either subtract or add 256 (28) until the remainder is in the
range –128 to 127. For instance, signed char c = 200 is equivalent to –56 since 200 –256 = -56,
and signed char d = -250 is equivalent to 6 since –250 + 256 = 6.

Q. Consider the following signed int variables. What numbers are they equivalent to?

unsigned int a = -80000;


unsigned int b = 120000;

Printing Numbers in Different Formats


We are most used to writing numbers in decimal notation, e.g. 125. There are other formats with
different bases. In base-2 or binary, 125 would be written 0b01111101. The ‘0b’ prefix is used to
indicate that we are using binary notation. Note that leading zeroes can be dropped, 2 =
0b00000010 = 0b10. Another common format is hexadecimal or base-16 format. In base 16, the
letters a, b, c, d, e, and f are used for the numbers 10 to 15 respectively. In hexadecimal, 125 =
0x7d. Note that 125 = 7*16 + d = 112 + 13. Again, a prefix, ‘0x’ identifies hexadecimal format.

In your code, you can write any integer number in any of these formats, like the code snippet
below, where a = b = c. Each number is stored exactly the same in memory.

unsigned char a = 125;


unsigned char b = 0b01111101;
unsigned char c = 0x7d;

The printf() function will display a number of size char or int, one or two bytes, in decimal,
octal, or hexadecimal formats using the formatting specifiers %u for unsigned integer, %i or %d for
signed decimal, %o for octal, and %x for hexadecimal such as

printf(“Various formats: a = %x b = %i c = %u c = %o.”, a, b, c, c);


Output
Various formats: a = 7d b = -5 c = 125 c = 175.

98
Lectures - III Numbers

Note that the hexadecimal and octal output may look like decimal output. You may wish to do the
following to make the base clear.

printf(“Various formats: a = 0x%x b = %i c = %u c = o%o.”, a, b, c, c);

Output
Various formats: a = 0x7d b = -5 c = 125 c = o175.

Unfortunately, XC8 (unlike some C Compilers) does not support a binary %b format specifier.

Please note that the printf() function allows a great deal of flexibility in what you can print, but
the downside is that there is limited error checking and if you do not match the formatting to
variable type, the results may not make sense, as demonstrated in the code snippet below.

char a = 0b11111111; // 255 = -128


unsigned char b = 0b11111111; // 255
printf(“Unsigned a = %u - Signed a = %i\n”, a, a);
printf(“Unsigned b = %u - Signed b = %i\n”, b, b);

Output
Unsigned a = 65355 - Signed a = -1
Unsigned b = 255 - Signed b = 255

For numbers of size long, 4 bytes, use the formatting specifiers %lu for unsigned decimal, %ld for
signed decimal, and %lx for hexadecimal, rather than %u, %d, or %x. As mentioned, %u, and %x work
with two bytes, not the four of a long number. The printf() function does not check to see if the
variable agrees in size with the formatting specifier, with interesting results. As shown in the code
snippet below, the printf(“%u”, longnumber) only prints the lower two bytes. This is equivalent
to the remainder after dividing the number by 216 = 65536. You can use %u twice, as in
printf(“lower: %u upper: %u”, longnumber), but then you get the remainder and number of
times that 65536 goes into the value. You can use the hex specifiers twice to get the right answer
(although in the wrong order), but you need to keep leading zeroes in the lower byte, 4 for hex (see
the code example for details).

unsigned long k = 723458; // = 0b10110000101000000010 = 0xb0a02;


// the problem with using a too small formatting specifier
printf(“k (base 65536) = %u\n”,k); //%u only reads lower two bytes of k
// hence it rounds k in base 65536
printf(“k = %u + %u*65536 \n”,k); //reading both lower and upper bytes
// remainder + k/65536
printf(“k = %04x %x\n”,k); //reading both lower and upper bytes

// correctly specifying the size


printf("k = %lu = %lx\n", k, k);

Output
k (base 65536) = 2562
k = 2562 + 11*65536
k = 0a02 b

k = 723458 = b0a02

99
Lectures - III Numbers

Q. What would printf(“%x%x %b%b”, k, k) display? What are the decimal equivalent
values of these numbers as displayed?

Decimal or Floating Point Numbers


Decimal numbers such as 0.0032 and 2.63 × 104 require a lot more processing to store in memory
that integers. Arithmetic operations involving decimal number are also more complicated than
those involving only integers. Decimal number have type float.

If you use a literal number (e.g. 50000) rather than a variable with the printf() function, you will
also run into problems because C assumes that any literal number without a decimal in it is an
unsigned int. The cast operator is one way of explicitly making the correct amount of memory
space available, although it is typically used with variables. The simplest method to indicate that
the literal is unsigned is by adding a u at the end of the number. Similarly, adding a terminal l or
ul indicates that the number is signed long or unsigned long.

printf("%i\n",50000); // Must be very careful with numbers bigger than


32767
// Compiler expects numbers to be signed integers
// which go from -32768 to 32767.
// Doesn't know how to handle this number. It treats
// it as a char - size one byte. But 50000 is
two
// bytes big. In binary 50000 = 11000011 0101000.
// Compiler ignores LSB (second byte) and assumes
// number is 11000011 or 195.
// Not all C compilers do this.

printf("%i\n",(unsigned int)50000); // the cast operation lets the compiler


// understand the size but converts it to a signed
// integer since use %i

printf("%u ",(unsigned int)50000);

printf("%i ", 50000u); //terminal u indicates unsigned int but %i converts


//value to signed int
printf("%u ", 50000u); //matching types

Output
195
-15536
50000
-15536
50000

You have your choice of printing a decimal number in ordinary or scientific notation using %f or
%e.

100
Lectures - III Numbers

printf("%f\n",12300.5250000);
printf("%12.5e\n",12300.5250000); // Sci notation to 5 decimal places.

Output
123000.525
1.23001e+4

Arithmetic and Memory


Simple arithmetic can be the source of an amazing number of programming mistakes. This is
largely because we forget the limitations of the memory space assigned to numbers. For example,
consider the code snippet below.

int a = 8;
int b = 52;
int c = 100;
int d;

d = a/b*c;
printf("%i",d);

Output
0

We are usually surprised at the result because we know that 8/52 × 100 = 15.38, which when
rounded to an integer should give 15. However, the first operation, 8  52, is between integers and
must go in a space for an integer. The value 0.1538 is thus rounded to zero before being multiplied
by 100, yielding 0. You can make room for the calculation by using a cast operation, but you need
to use care in doing so, as shown below

int a = 8;
int b = 52;
int c = 100;
int d;

d = (float)(a/b)*c; // cast mis-applied


printf("%i\n",d);
d = (float)a/b*c; // correct cast
printf("%i",d);

Output
0
15

The first cast operation of a/b to float occurs after the division and is thus too late. The variable
a must be promoted to float first. A float divided by an int is of size float. A float multiplied
by an int is also a float. The final step is the rounding to an int to get the value of d.

Q. What will the following code produce on output? How would you change the code to
produce more sensible results?
unsigned char a = 70;

101
Lectures - III Numbers

unsigned char b = 100;


int c = 1000;
int d;

d = a*b – c;
printf(“d = %i\n”,d);

Text and Memory - ASCII


The one byte variable char gets its name from the word character, that is, a text letter or symbol.
Just as it required a bit of ingenuity to encode for negative numbers with just zeroes and ones, the
same holds for text. The solution was ASCII, a convention for pairing the numbers 0 to 126 with
text characters. That still leaves the numbers 127 to 255. These can also be paired with symbols
for letters in other Latin script languages like é and Ü. However different machines may have
different sets of the upper ASCII characters. A table of the lower ASCII characters is given in
Appendix D.

You can assign the value to a char variable either numerically or symbolically, as shown in the
code snippet below. The formatting specifier %c is used in the printf() function to direct the
processor to use the ASCII symbol for that number. Note that $ is number 36 in the ASCII table,
and H is 72.

char a, b, c;
int d = 65*256 + 66.
a = 36;
b = ‘$’;
c = a + b;
printf("a = %c b = %c c = %c\n",a,b,c);

printf(“d = %c”, d);

Output
a = $ b = $ c = H
d = B

Note that %c will convert an integer to a byte by ignoring the upper half. The letter B is number 66
in ASCII.

Q. Translate the following integers using the ASCII table. 65 80 83 67 49 50 57 57

Bitwise Operators
C has a set of operators that derive from Boolean logic. They are and, or, exclusive or, and not (&,
|, ^, and ~). Note that the number 1 is synonymous with true and 0 with false. The truth tables for
the first three operators are

& 0 1 | 0 1 ^ 0 1
0 0 0 0 0 1 0 0 1
1 0 1 1 1 1 1 1 0

102
Lectures - III Numbers

Table III-2: Truth tables.


The not operator works on individual bits so ~1 = 0 and ~0 = 1. The code snippet below reproduces
these results.

char a = 0b0011, b = 0b0101, c;

c = a & b;
printf("a = %u & b = %u => c = %u\n\r",a,b,c);
c = a | b;
printf("a = %u | b = %u => c = %u\n\r",a,b,c);
c = a ^ b;
printf("a = %u ^ b = %u => c = %u\n\r",a,b,c);

a = ~a;
b = -b;
printf(“not a = %u and not b = %u\n\r”, a,b);

Output
a = 3 & b = 5 => c = 1
a = 3 | b = 5 => c = 7
a = 3 ^ b = 5 => c = 6
not a = 252 and not b = 251

Two other operators that work at the bit level are the left shift << and right shift >> operators. The
usage is clearest on integers in binary format. If c = 0b00101 then c = c << 2 yields c = 0b0010100.
Everything moved left two places, and the new positions are filled with zeroes. Similarly, if c =
0b00101, then c = c >> 2 moves everything right two places. This yields c = 0b00001. Shift
operators are equivalent to c = c / 2n (right shift n places) and c = c * 2n (left shift n places). The
shift operations are much faster than the equivalent expressions for division or multiplication.

Be careful using shift operators on large signed numbers though.

int a = -32000, b = 32000, c;

c = a << 2;
printf("a = %i, c = %i\n",a,c);

c = b >> 2;
printf("b = %i, c = %i",b,c);

Output
a = -32000, c = 8384
b = 32000, c = -3072

Q. What is unsigned int a = (1234 & 724) << 2?

103
Lectures - III Numbers

Bit Masks
Many of the functions, like DIO and ADC, of the MCU are controlled by a single bit of special
bytes of computer memory called Special Functions Registers (SFRs). The bit often turns a
particular aspect on (1) or off (0). Since the smallest quantity C can handle is the byte, bit masks
are a way to read the current setting of a particular bit or to set a particular bit.

If we have unsigned char variable named sfr, and two more variables mask_bit5 = 0b0010
0000 and mask_bit6 = 0b0100 0000, then we can set bit 5 (make it have value 1) of sfr with
the code

sfr = sfr | mask_bit5

or more compactly

sfr |= mask_bit5.

We can even set two bits at once with

sfr = sfr | mask_bit5 | mask_bit6

If we wish to clear bit 5 (make it have value 0) of sfr, we use the code

sfr = sfr & ~mask_bit5

or more compactly

sfr &= ~mask_bit5

We can also clear multiple bits at once

sfr = sfr & ~mask_bit5 & ~mask_bit6

We can check if bit 5 is set using

(sfr & mask_bit5) == mask_bit5

That is equivalent to the shorter


sfr & mask_bit5

Many of the functions in the XC8 libraries are written to take advantage of bit masks, although in
a different fashion, by using a literal (i.e. a number rather than a variable) bit mask. For example,
the 8 bits of the config parameter of OpenUSART(unsigned char config, unsigned int
spbrg) function for serial communication, on page 1202-1204 of the
MPLAB_XC8_Peripheral_Libraries.pdf, controls a variety of options. An extract of the PDF for
the function is shown in Figure III-2 on the next page.

104
Lectures - III Numbers

void OpenUSART( unsigned char config, unsigned int spbrg)

Input Parameters Description


config Interrupt on Transmission:
* USART_TX_INT_ON
* USART_TX_INT_OFF
* USART_TX_INT_MASK

Interrupt on Receipt:
* USART_RX_INT_ON
* USART_RX_INT_OFF
* USART_RX_INT_MASK

usart Mode:
* USART_ASYNCH_MODE
* USART_SYNCH_MODE
* USART_MODE_MASK

Transmission Width:
* USART_EIGHT_BIT
* USART_NINE_BIT
* USART_BIT_MASK

Slave/Master Select (Applicable to Synchronous mode only):


* USART_SYNC_SLAVE
* USART_SYNC_MASTER
* USART_SYNC_MASK

Reception mode:
* USART_SINGLE_RX
* USART_CONT_RX
* USART_CONT_RX_MASK

Baud rate:
* USART_BRGH_HIGH
* USART_BRGH_LOW
* USART_BRGH_MASK

Address Detect Enable:


* USART_ADDEN_ON
* USART_ADDEN_OFF
* USART_ADDEN_MASK
spbrg This is the value that is written to the baud rate generator
register which determines the baud rate at which the usart
operates. The formulas for baud rate are:
Asynchronous mode, high speed:
Fosc / (16 * (spbrg + 1))

Asynchronous mode, low speed:


Fosc / (64 * (spbrg + 1))

Synchronous mode:
Fosc / (4 * (spbrg + 1))

Where Fosc is the oscillator frequency

Figure III-2: Example of a bit mask.

105
Lectures - III Numbers

To make the programming more intuitive, a set of macro identifiers that are literal numbers are
defined in the xc.h file

#define USART_TX_INT_ON 0b11111111 // Transmit interrupt on


#define USART_TX_INT_OFF 0b01111111 // Transmit interrupt off
#define USART_RX_INT_ON 0b11111111 // Receive interrupt on
#define USART_RX_INT_OFF 0b10111111 // Receive interrupt off
#define USART_BRGH_HIGH 0b11111111 // High baud rate
#define USART_BRGH_LOW 0b11101111 // Low baud rate
#define USART_CONT_RX 0b11111111 // Continuous reception
#define USART_SINGLE_RX 0b11110111 // Single reception
#define USART_SYNC_MASTER 0b11111111 // Synchronous master mode
#define USART_SYNC_SLAVE 0b11111011 // Synchronous slave mode
#define USART_NINE_BIT 0b11111111 // 9-bit data
#define USART_EIGHT_BIT 0b11111101 // 8-bit data
#define USART_SYNCH_MODE 0b11111111 // Synchronous mode
#define USART_ASYNCH_MODE 0b11111110 // Asynchronous mode

Examining the identifiers above, we see that bit 7 set the transmit interrupt on or off. Similarly,
every other bit is controlled by a pair of identifiers. Rather than remember which bit does what,
the programmer ANDs the appropriate choices together. The bit patterns that are all 1s will have
no effect when ANDed with the others but should be included for clarity.

Q. Consider the literal bit mask in the code example at the bottom of Figure III-2. Using the
macro definitions, what is the 8 bit result?

Structures and Bitfields


In C, there are a number of basic variable types such as int and char. C also allows the
programmer to define new variable types and declare variables of the new type. A new variable
type can be created by grouping basic types together. This grouping is called a structure. For
example, we could define a structure for a Student ID that combines a last name (a string variable)
with a student number (a long integer).
typedef struct studentID {
char studentname[20];
long studentnumber;
}

Then, you can create variables of this type


studentID APSCStudents[70];

You can now access the fields of the variable (studentname and studentnumber) using the
member of operator, which is a period “.”.

strcpy(APSCStudents[1].studentname, “Smith”);
APSCStudents[1].studentnumber = 10123456;

One can also create a structure that is a named groups of bits (such a structure called a bitfield), or
create a structure that is just named individual bits (this is also a structure called a bitfield).

106
Lectures - III Numbers

Microchip has done so for the Special Functions Registers (SFRs), and these variables are already
defined in p18f4525.h to make working with the SFRs much simpler.

Unions
Unions allow you to save space by stuffing more than one variable in the same memory. Of course,
it must make sense to do. Since we often deal with a set of bits, unions can let us deal with them
as a set or individually. Consider the following structure and union

struct setLED
{
unsigned Zero:1;
unsigned One:1;
unsigned Two:1;
unsigned Three:1;
unsigned Four:1;
unsigned Five:1;
unsigned Six:1;
unsigned Seven:1;

};

union sensor_union
{
unsigned char Byte;
struct setLEDs bits;
};

The structure could be used with a set of eight indicator LEDs. The zeroth bit, controlling the first
LED, is labelled Zero and the seventh bit is labelled Seven. The union lets us work with the bits
individually or as a group of size char.

A statement

union sensor_union IndicatorLEDs.Byte = 0; //create and initialize union

creates the space for the information and initializes the values to zero. We can set the values we
want each LED to have using

indicatorLEDs.bits.Zero = 1;
indicatorLEDs.bits.One = 0;
indicatorLEDs.bits.Two = 0;
indicatorLEDs.bits.Three = 1;
indicatorLEDs.bits.Four = 1;
indicatorLEDs.bits.Five = 0;
indicatorLEDs.bits.Six = 1;
indicatorLEDs.bits.Seven = 1;

Then

printf(“%b”, indicatorLEDs.Byte);

107
Lectures - III Numbers

will output 0b11011001. We could reverse this, set all the bits at once using

indicatorLEDs.Byte = 0b00000001;

and now

printf(“%” indicatorLEDs.bits.Zero);

will output 1.

108
Lectures - IV Electric Circuits

Notes IV Review of Electric Circuits

Current
Electrons are negatively charged particles. The movement of electrons constitutes the flow of
electric current. The movement of a negative charge in one direction can be thought of as the
movement of a positive charge in the opposite direction. The direction of the positive charge is the
direction of current flow. You can think of an electric charge as water and of an electric current as
water flowing in a river or a pipe. The water flows in a certain direction – from the higher ground
to the lower ground. Electric current flows from high potential (voltage) to low. The measure of
current is ampere (A). An instrument called ammeter is used to measure the current that passes
through it.

Battery and Voltage


You can think of an electric battery as a generator of a positive charge. Once an electric load is
connected to the battery, the current flows from its positive terminal (+) to its negative terminal (-)
through the connecting wires and the load. The batteries come with different voltage ratings
expressed in Volts (V). For instance, a car battery is a 12-volt battery. The higher the voltage, the
more current will flow from the battery when you connect an electric load to it. You can think of
a battery as a pump, which pumps the water from the lower ground (usually referenced as zero) to
a higher ground. You can think of a battery’s voltage as the height of the elevated water level. The
battery is often called a voltage source. Voltages are measured by an instrument called voltmeter.
To measure the voltage, the voltmeter must be connected between two points where there is a
certain voltage difference (like between the + and the – terminals of the battery). The reference
point used to measure voltages is called ground. For example, one of the terminals of the car
battery is connected to the car’s body and is considered an electric ground. Battery and associated
symbols are shown in Figure IV-1.

+5 V
+

Battery Supply Ground

Figure IV-1: Battery, power supply, and ground symbols.

Resistors
V

The basic element of electrical and electronic circuits is the


resistor. The resistor obeys Ohm’s Law
I
R
V = IR [ IV-1 ]
Figure IV-2: Ohm's Law.

109
Lectures - IV Electric Circuits

where V is the voltage drop across the resistor, I is the current through the resistor, and R is the
resistance of the resistor (see Figure IV-2). Resistors dissipate energy as heat (this is known as
Joule Heating) according to the formula

P = IV = I2R = V2/R. [ IV-2 ]

Resistors are rated by how much heat they can dissipate without melting, e.g. ¼W.

Colour Code
Resistors come in a wide variety of standard resistances. The value of the resistance is give by the
colour bands on the resistor. One band isolated from the other three gives the tolerance or accuracy
of the resistance value. The other three bands encode the value of the resistance. The colour code
is given in the diagram Figure IV-3 below.
Colour Value Multiplier
Black 0 100
Brown 1 101
Red 2 102
Value (×10) Orange 3 103
Value (×1) Yellow 4 104
Green 5 105
Multiplier Blue 6 106
Violet 7 107
Tolerance Gray 8 108
White 9 109

Schematic Symbol Colour Tolerance


Gold 5%
Silver 10%
None 20%

Figure IV-3: Symbol and colour code for resistors.

As an example, a resistor with red, green, and blue bands in sequence separated from a gold band
would have a resistance of R = (2 × 10 + 5 × 1) × 106  ± 5% = 25 × 106  ± 5%.

Q. What is the resistance of a resistor with yellow, violet, brown and silver bands?

Series/Parallel Connections
Resistors can be connected by conducting wires (or paths) in series, or in parallel, as shown in
Figure IV-4 (a) and (b). In Figure IV-4(b), we use the symbols designated for battery and resistors.

110
Lectures - IV Electric Circuits

I1 R1 I1
+ +
 –
R2   –
Rseries

(a)
I2 I2
+
 –
R1 R2   +
Rparallel

(b)
Figure IV-4: (a) Series and (b) parallel connections.

The equivalent resistors will draw the same current I from the battery. Their value can be calculated
as:

Rseries = R1 + R2 [IV-3 ]
1
R parallel  R1 || R2  . [ IV-4 ]
1 1

R1 R2

Note that series resistors always carry the same current as one another.

Shorting
One consequence of Eqn [IV-4] is that
the equivalent resistance of two R1 a R1 a
resistors in parallel is always less than
either of the two resistors. Since
parallel resistors always have the
same voltage drop, V1 = V2. Ohm’s  R2 
Law, applied to this result, means I1R1
= I2R2. So the smaller resistor carries
the bigger current. These results reach
their extreme conclusion when a b b
resistor is shorted. This means a wire
of negligible resistance is connected Figure IV-5: A short circuit.
across a resistor, as shown in Figure
IV-5 below. Having no resistance, all the current flows through the wire and no current flows
through R2. The equivalent resistance of R2 and the wire is zero, so they can be replaced by the
wire itself as shown in Figure IV-5. Shorting resistors unintentionally can cause big problems. If
R1 is a small resistor, the current produced by the battery could be dangerously high, the power

111
Lectures - IV Electric Circuits

rating of the resistor could be exceeded, and the Joule heating could melt the resistor or even start
a fire.

BreadBoard
A breadboard is a convenient way to assemble
circuits where the parts are small, such as
microchips, diodes, and resistors. Pinholes in
the front of the breadboard let you securely
connect circuit elements to it. The sets of
pinholes are connected by conducting bars in
two different patterns, short horizontal bars and
long lengthwise bars, under the plastic cover,
see Figure IV-6. This setup allows parallel and
series arrangements of elements to be put
together quickly. When using wire connectors,
make sure to keep them short and neat!
Pins Connector bars
Figure IV-6: Breadboard connectors.
Electronics Style Circuit Diagrams
In electronics, batteries and other power supplies are seldom shown. A small circle indicates where
one end of the power supply goes. The other end is assumed to be connect to ground (symbol
). There may be more than one ground symbol in a diagram. This indicates that a wire
connecting the two points has not been drawn. The diagram below, Figure IV-7, illustrates how a
standard circuit could be drawn in this style. In general, we will use the electronics style from now
on.

+12
R1 R2
R1
R2

12 V R3
R3

Figure IV-7: Standard and electronics style circuit diagrams.

112
Lectures - IV Electric Circuits

Kirchhoff’s Rules
More complicated circuits are usually analyzed using Kirchhoff’s Rules. Consider the circuit in
Figure IV-8 below. Points b and e, where three or more wires meet, are known as nodes or
junctions. A branch is a path from one node to another. In Figure IV-8 there are three branches,

15 
a b c

12 V 20  I3
10 V

I1 I2

f e d
5

Figure IV-8: A complicated circuit.

be, bcde, and bafe. These branches are said to be parallel, since the start and end nodes are the
same. Each branch carries a single current. Elements in the same branch are said to be in series, so
the 5  and 15  resistors in branch bcde are in series. At a node, Kirchhoff’s Current Rule is
obeyed,

I in   I out , [ IV-5 ]

the sum of the currents entering a node equals the sum of the currents leaving a node. For the
nodes, we have

I1 + I2 + I3 = 0 (point b)

and

0 = I1 + I2 + I3 (point e).

As in the example above, the equations do not have to be independent.

Kirchhoff’s Current Rule is simply conservation of charge – electrons are neither being created
not destroyed in the circuit.

A loop is a closed path from a point back to itself. Where you start is arbitrary. Several loops in
the circuit in Figure IV-8 are fabef and dcbed. Around a loop, Kirchhoff’s Loop Rule:

V
loop , i
i  0, [ IV-6 ]

113
Lectures - IV Electric Circuits

is obeyed where Vi is the voltage drop across each circuit element in the loop. The orientation of
the voltmeter must be kept the same as you go around the elements of the loop. For loop fabef we
get

(12 V) – (10 )I1 + (20 )I3 = 0 (loop fabef)

and for loop dcbed we find

(10 V) – (15 )I2 + (20 )I3 – (5 )I2 = 0. (loop dcbed)

Note that battery voltages are positive when we travel from the negative to the positive side and
vice versa. The voltage drop across a resistor is negative if we travel around the resistor in the
direction of the current through it, and positive if the current is in the opposite direction.

Kirchhoff’s Loop Rule is simply conservation of energy since voltage is energy per charge.

Solution of the equations is straightforward, if tedious, as long as you have as many independent
equations as you have unknown currents.

Applying Kirchhoff’s Laws to circuits written in electronics style is somewhat simpler. As before,
at the nodes, apply circuits current laws. Note that even if a branch has no resistors in it, you still
assign a current, as between points e and f in Figure IV-9.

a b 15  c
+12 V +10 V
I1 I2
20  I3
I4
e d
I5 5

f
Figure IV-9: A complicated circuit in electronics style.

Here the node or current equations are

I1 + I2 + I3 = 0 (point b)

and

0 = I3 + I4 + I5 (point e).

114
Lectures - IV Electric Circuits

In electronic style diagrams, you don’t appear to have loops, but you do know the voltage drop
between each battery point and each ground point.

Here, we can go from a battery to ground four different ways and we get the equations:

(12 V) – (10  )I1 + (20  )I3 = 0 (a  f)

(12 V) – (10  )I1 + (20  )I3 – (5  )I4 = 0 (a  d)

(10 V) – (15  )I2 + (20  )I3 = 0 (a  f)

and

(10 V) – (15  )I2 + (20  )I3 – (5  )I4 = 0 (a  d).

Again you have more equations than unknowns.

Voltage Divider
Shown in Figure IV-10 is a simple circuit called voltage divider.

V0


R1 R2

Figure IV-10: Voltage divider.

The current flowing through both series resistors is given by I = /(R1 + R2). This current, following
Ohm’s law, will cause a voltage drop across the resistor R2. This voltage drop with respect to
ground is given as:
R2 
V0  I  R2   [ IV-7 ]
R1  R2 1
R1
R2
By selecting a proper ratio of R1/R2, we can obtain any voltage V0 between zero and .

Figure IV-10 is an electronics style drawing. Here we use the symbol “ground” to indicate common
connection (or common node). Any voltage indicated without additional reference information is
taken with respect to the ground.

115
Lectures - IV Electric Circuits

In circuits we can talk about the voltage or potential drop across one or more elements of a circuit.
This is what is measured by a voltmeter connected in parallel with the circuit elements. We can
also talk about the voltage or potential of a point. The voltage at a point is simply the voltage of
the point with respect to ground, that is, what a voltmeter would read if connected to that point and
to ground. See Figure IV-11.

Voltage or potential
drop across a resistor

Potential at a point or
voltage wrt ground

Figure IV-11: Voltage terminology

Potentiometer V0
A voltage divider can be conveniently built using a variable Sliding
resistor called a potentiometer (or pot for short). A electrode
R1 R2
potentiometer has the sliding electrode (a wiper), the
position of which can be manually adjusted to a desired 
location resulting in a variable resistance. A voltage divider
using a potentiometer is shown symbolically in Figure R
IV-12. Figure IV-12: Potentiometer

116
Lectures - IV Electric Circuits

The total resistance of the potentiometer is R = R1 +


R2. A potentiometer usually has the form of a round sliding
drum with a central rotating shaft that controls the electrode
position of the wiper, as shown in Figure IV-13. The
10 K potentiometer we use in the lab is adjusted
by a small screwdriver. You cannot tell just by
looking at the potentiometer the resistance, R1, R2,
or R, between any two leads. We recommend using
an ohmmeter to check. Schematic symbol

Diodes Figure IV-13: Potentiometer

A diode is an electrical “valve” that conducts current only in one direction – from Anode to
Cathode. A special diode, called a Light Emitting Diode (LED), emits light whenever there is
current flowing through it. An LED is often used as an indicator of the presence (LED is “on”) or
absence (LED is “off”) of a voltage.

Figure IV-14: Diodes and LEDs

A simple circuit with LED is shown in Figure IV-15.

1 k I  5 mA
+5 V

Figure IV-15: LED in action.

117
Lectures - IV Electric Circuits

A small voltage Vforward, of between 0.5 and 2 V, must first be applied to a diode before a current
can pass through it. Diodes are non-ohmic, meaning that they do not obey Ohm’s Law. To apply
Kirchhoff’s Rules to a circuit containing a diode, we use a simple model. If the diode is forward
biased, assume a constant Vforward for the diode. This is a reasonable approximation if the
resistances in the branch are not huge. If the diode is reverse-biased, the voltage drop over the
diode is unknown but we can assume that the current in the branch with the diode will be zero.
The actual current is not zero, just very, very small – on the order of tens of A.

Examples – KR and diode circuits


Consider the circuits below where the VLED = Vforward = 2 V. Find the voltage drop across each
circuit element and the current through each resistor.

+12 V 3 k 2 k +12 V 3 k 2 k
(i) (ii)
Solution
In both cases (i) and (ii), applying Kirchhoff’s Rules gives the same equation

12 V = VR1 + VLED + VR2

For (i), we assume that the LED is forward biased so that VLED = 2 V. The resistors are in the same
branch, so they must carry the same current. Resistors do obey Ohm’s Law, so we have

12 V = I(3000 ) + 2 V + I(2000 ) .

So I = 2 mA and VR1 = 6 V and VR2 = 4 V. The LED was forward biased since I is positive.

For (ii) the LED is reverse biased so that VLED is unknown. However the current in the branch is
zero, so by Ohm’s Law the voltage drops over the resistors are zero,

12 V = 0 + VLED + 0 .

So the voltage drop over the reverse biased LED is 12 Volts.

For (ii), suppose we were not sure that the LED was reverse-biased. How do we check? Well we
assume it is forward biased with a 2 volts drop from right to left and that a current I also flows
right to left. The equation is then

12 V = –I(3000 ) – 2 V – I(2000 ) .

118
Lectures - IV Electric Circuits

Solving for I, we find I = –14/5000 A. Since the current is negative, left to right, and it cannot flow
that way through a diode, the current is actually zero and the LED is reverse biased.

For circuit (iii) below, determine if the LEDs light up. +8 V


What is the current through each resistor? If an LED (iii)
does not light up, what is the voltage drop across that 100 
LED? Take VLED = 2 V. +5 V

20 
50 

Solution
LEDs light up if they are forward biased. We don’t know if the LEDs are forward biased, so we
will assume that they are and that the currents flow in the correct direction for forward bias.

+8 V

100  I1
+5 V

20  A
50 
I3 I2
B

Our equations for the circuit are:

I1 = I2 + I3
8 V = (100 )I1 + (50 )I3 + VLEDB
5 V = –VLEDA – (20 )I2 + (50 )I3 + VLEDB

Solving the system of equations using VLEDA= VLEDB = 2V, and not showing the details, we get I1
= 17/800 A, I2 = –9/160 A, and I3 = 62/800 A. Since the direction of I2 is impossible, the first LED
A is actually reverse biased, and VLEDA is unknown and must be found. As well, we can assume I2
= 0. Our equations become:

I1 = 0 + I3
8 V = (100 )I1 + (50 )I3 + (2 V)
5 V = –VLEDA – (20 )(0) + (50 )I3 + (2 V)

And we find I1 = I3 = 6/150 A, and VLEDA = –1 V.

119
Lectures - IV Electric Circuits

Capacitors
A capacitor, in its simplest form, is just a pair of parallel metal plates. When equal and opposite
charges +q and –q are placed on the plates, there is a voltage difference that depends on the
capacitance of the particular configuration

V = q/C. [ IV-8 ]

The symbol for a capacitor is shown below in Figure IV-16.

As you put charge on a capacitor, it gets harder and harder and takes longer and longer to add more
charge. The diagram below shows a capacitor-charging circuit and a plot of the voltage across the
capacitor plates as a function of time.

I +q –q
C

Schematic symbol

Figure IV-16: Capacitors and symbol.

The equation for a capacitor being charged from zero is given by the formula
 
t


VC (t )  V 1  e RC . [ IV-9 ]

 

120
Lectures - IV Electric Circuits

The quantity RC is called the time constant  of the circuit. When  is small, the capacitor charges
quickly; when it is large, the capacitor charges slowly. After 5, the capacitor is over 99% charged.
A convenient way of thinking of a capacitor is that it acts as a switch. An uncharged capacitor is
VC
VC
R C
V

VC

Figure IV-17: Charging a capacitor.


like a closed switch with current at its maximum. A charged capacitor is like an open switch, no
current can flow.

When disconnected from a battery, a capacitor can hold its charge for minutes or even hours before
it leaks away. Providing a path between the plates discharges the capacitor much more quickly.
The discharging circuit and the VC-t graph are shown in Figure IV-18 below.
VC
VC
R C
V1

VC V2

t1 t2 t

Figure IV-18: Discharging a capacitor.


The equation for a capacitor being charged from zero is given by the formula

t

VC (t )  VC e RC
. [ IV-10 ]

When the time constant  is small, the capacitor discharges quickly, and when it is large, the
capacitor discharges slowly. After 5, the capacitor is over 99% discharged. The discharge of a
capacitor can be used for timing if you can measure voltage much faster than . Consider the two
voltages V2 and V1, shown on the graph in Figure IV-18. Using Eqn [IV -10], the time difference
between the voltages is
V 
t 2  t1  RC ln  1  . [ IV-11 ]
 V2 

121
Lectures - IV Electric Circuits

Since we are dealing with DC circuits, we may want to know initial currents and voltage drops in
a circuit so that the PIC is safely buffered, as well as the steady state or t →  case values. We can
readily apply Kirchhoff’s Rules to the resistor-capacitor circuits for these two limiting cases. For
the t = 0 case, note that the voltage drop across the capacitor is known. It will be zero, as given in
equation IV-12. This is equivalent to treating the capacitor as a short. For the t =  case, the voltage
drop across the capacitor is unknown but constant. However, a fully charged capacitor stops
current from flowing in the branch where it resides. The capacitor acts like an open, so the voltage
drop across any resistor in the same branch will be zero.

Capacitors also come in handy when you want to measure a varying voltage. An ordinary DC
voltmeter reading the signal would fluctuate. A “sample and hold” circuit can be built using a very
fast switch, a series resistor, and capacitor whose time constant  = RC is much smaller than the
time variation of the voltage signal. The switch is closed long enough for the capacitor to charge
nearly completely, then opened. Since a voltmeter has a huge resistance, connecting it across the
charged capacitor creates a circuit with a large time constant, thus allowing for a more leisurely
measurement.

Transducers
Transducers are electronic devices whose physical properties, usually voltage output or resistance,
change with variations in its surroundings such as temperature, pressure, ambient light, and the
like. For example, you may have learned that the resistance of a resistor depends on its temperature.
However this effect is weak when the temperature changes are only a few degrees and it would be
impractical to use a resistor as a thermometer. A thermocouple, on the other hand, is very sensitive
to temperature and is often used as a high temperature thermometer. A thermocouple utilizes the
Seebeck Effect in which two dissimilar metals in contact with one another develop a potential
difference. This potential difference is strongly dependent on temperature. In general, the
relationship between the external effect one wishes to measure and the physics property of the
transducer is nonlinear or, at best, only approximately linear over some range. As well, if the
voltage output is small, the result may have to be amplified.

122
Lectures - V DIO

Notes V Digital Input Output (DIO)

Most of the pins on the MCU can handle DIO.

Digital means having discrete, as opposed to continuous or analog, values. Here there are two
discrete states, 0 or 1, which can have a number of meanings: OFF or ON, FALSE or TRUE, LOW
or HIGH voltage. For a pin, digital output means that the pin acts as a power supply that can be
set to a high voltage (+5 V) or to a low voltage (0 V or ground). Both these values are approximate,
but no other values are possible.

Digital input means that the pin acts as a voltmeter that can detect whether the pin is connected to
a high voltage (+5 V) or to a low voltage (0 V or ground). If you connect the pin to 4 V, it will
probably read that voltage as high. If you connect the pin to 1 V, it will probably read that voltage
as low. If you connect the pin to 2.5 V, it probably won’t be able to decide if the input is high or
low and it may randomly oscillate between reading high and low. Note the “probably”. The
datasheet specifies which ranges of voltages will be read as high and which as low.

The pins on the MCU that have DIO functionality are grouped into sets of eight, called ports. On
the PIC18F4525, we have 4 ports: PORTA, PORTB, PORTC, and PORTD. The PORTA pins are
labelled RA0 to RA7 on the pin-out diagram, Figure V-1 below. For example, RA0 is pin 2.

Figure V-1: Pin-out diagram.

Similarly, the other port pins are labelled RBn, RCn, and RDn, where n is 0 to 7.

We configure the pins for DIO using variables defined in a header file specific to each PIC. We
can configure a port as a group or as individual pins. Figure V-2 shows the code for an
entire port, while Figure V-3 shows the code for working with an individual pin.

123
Lectures - V DIO

RD7 RD0

TRISD = 0b00011000; // configures PORTD for input and output

outputs inputs

Figure V-2: C code for configuring an entire port.

TRISDbits.RD3 = 1; // set RD3 as input


TRISDbits.RD1 = 0; // set RD1 as output
Figure V-3: C code for configuring individual pins.

TRIS is short for tri-state. The pins have three states

output input
High Low High impedance
+5 V 0V (meaning large resistance)
Note: Voltmeters need a large
resistance to work properly

TRISA, TRISB, etc., are the names of special 8-bit wide areas of RAM on the MCU that hold the
DIO configuration values. TRISA, etc., are called Special Function Registers (SFR). There are
SFRs for functions other than DIO as well. All the SFRs have structures defined in the header file
xc.h so that each bit can be addressed individually. For example, TRISAbits.RA3 is the 4th bit
(remember we start from 0, so 3 is the 4th bit) of that 8-bit area. The structures have the same name
as the SFRs.

In output mode, we can set whether the pin is high or low using another SFR. Each port has its
own SFR. The SFRs are PORTA to PORTD. Again, the SFR PORTx refers to a specific eight bits
of RAM memory of the MCU.

Again we can set all the pins of a port to any combination of high or low all at once, or we can do
any individual pin. See Figure V-4 and Figure V-5 for example code.

TRISD = 0b00011000; // configures PORTD for input and output


PORTD = 0b11101000; // set various pins high or low

set high set low


Does nothing – pins are
inputs not outputs!
Figure V-4: C code for configuring and setting an entire port.

124
Lectures - V DIO

TRISDbits.RD3 = 0; // set RD3 as output


TRISDbits.RD1 = 0; // set RD1 as output

PORTDbits.RD3 = 1; // set RD3 high


PORTDbits.RD1 = 0; // set RD1 low
Figure V-5: C code for configuring and setting individual pins.

Let’s use this last bit of code to write a program that would actually set these pins when loaded
onto the PIC.

// testDIO.c configure and set RA3 (pin 5) and RA1 (pin 3)


#include <xc.h>

#pragma config WDT = OFF


#pragma config OSC = INTIO7 // puts osc/4 on pin 14 to check freq
#pragma config MCLRE = OFF
#pragma config LVP = OFF
#pragma config PBADEN = OFF

int main(void)
{
TRISAbits.RA3 = 0; // set RA3 as output
TRISAbits.RA1 = 0; // set RA1 as output

PORTAbits.RA3 = 1; // set RA3 high


PORTAbits.RA1 = 0; // set RA1 low

while(1); // run forever


}
// end program

Q. The diagram below represents your MCU programmed with the previous code. The vertical
lines represent the power and ground lines of the breadboard. Four voltmeters are connected to
the breadboard and pins on the MCU. Note that when a red (dashed) lead (V on the DMM) is
connected to a higher voltage point and a black (solid) lead (COM on the DMM) is connected
to a lower voltage point, the voltmeter will register a positive value. Reverse the connections
and the value will be negative. What is the reading on each voltmeter?

125
Lectures - V DIO

VDD
+5 V
V1
MCU

V2
RA1

RA3 V3
V4
VSS
0V
V1 = __________________ V2 = _________________

V3 = __________________ V4 = _________________

Q. How would you change the code example to change the sign of the voltage readings?

Digital Output and Controls


Digital output can be used to control devices attached to the
MCU. This can be as simple as turning on a light when MCU
certain conditions are met to operating more complex
R
devices, such as a liquid crystal display (LCD).
RA1 I

The key to control is using the pins to provide power to


circuits. Figure V-6 shows the simplest circuit for each pin
– a single resistor. RA3
I
R
The diagram shows the diagram of the conventional current
(i.e. running from high to low).
Figure V-6: Powering a circuit
The current is determined from Ohm’s law, I = V/R, where using a pin.
V is the voltage difference across the resistor.

WARNING! The pins and MCU have a maximum current they can provide (source) or that they
can accept (sink). Exceeding these limits can alter the operation of the MCU or burn it out.
According to Section 26.0 of the PIC18F4525 datasheet, the maximum current sourced or sunk
by any pin is 25 mA. The total current that can be sourced or sunk by all the pins operating at
the same time is 200 mA.

126
Lectures - V DIO

The pin can supply 5 V and a maximum current of 25 mA. From this, we can determine the smallest
equivalent resistance that any circuit attached to a pin can have, R = 5 V / 25 mA = 200 . Any
resistor of 200  or bigger should be safe to use with the MCU.

Since we cannot see currents, the circuit in Figure V-6 is not very impressive. To see something
happen, we usually add LEDs to the circuit, as shown below in Figure V-7. LEDs are convenient
to use because they require less than 5 volts to operate. LEDs have a very small resistance of
around 10 , so we must use them in series with a resistor to keep the pin currents within
acceptable limits. This also protects the LEDs themselves. LEDs will burn out if the current is too
great. Connecting an LED across 5 V without an added resistor is likely to burn it out. LEDs are
diodes, and diodes only carry current in one direction. Therefore, how you connect the LED to a
pin is important. Here are two LED circuits that will light up an LED using the previous code.

MCU

R
RA1

RA3
R

Figure V-7: Controlling LEDs.

Digital Input and Sensors


Digital input can be used to let the MCU sense the outside world. This can be as simple as
determining whether a button is pressed or as complex as interpreting a digital signal from another
device, such as a printer. The key is that the signal be encoded as high and low voltages.

An entire port of pins, for example PORTD, can be monitored as long as the pins have already
been configured as inputs (digital voltmeters) using the TRISD SFR. The PORTD SFR
continuously monitors the pins and holds their current state. As an example, in Figure V-8 below,
certain pins are high, some low, and one left unconnected.

127
Lectures - V DIO

VDD

RD0
RD1
RD2
RD3
MCU
RD4
RD5
RD6
RD7

VSS

Figure V-8: PORTD configured for input (reading voltages).

The code we would use to examine all these pins at once would be

// readPORTD.c configure and read PORTD pins


#include <xc.h>

#pragma config WDT = OFF


#pragma config OSC = INTIO7 // puts osc/4 on pin 14 to check freq
#pragma config MCLRE = OFF
#pragma config LVP = OFF
#pragma config PBADEN = OFF

int main(void)
{
unsigned char portvalue;

TRISD = 0b11111111; // set RD0 to RD7 as inputs

portvalue = PORTD; // reads current digital voltages at PORTD and records


// values in portvalue which can hold 8 bits
while(1); // run forever
}
// end program

If this program was loaded into the MCU and run with the above circuit setup, the SFR PORTD
and portvalue would hold

_1_ _?_ _1_ _0_ _0_ _0_ _1_ _1_


RD7 RD6 RD5 RD4 RD3 RD2 RD1 Rd0

RD6 is unconnected (it is said to be “floating”) and could randomly hold 0 or 1.

ReadPORTD.c is a useless program, as we have not created a way for the MCU to tell us the status
of the input pins.

128
Lectures - V DIO

A better program follows. It monitors two input pins, and depending on their status, lights up two
LEDs attached to two output pins (see circuit schematic Figure V-9 after code).

// LEDinputsensor.c configure RA3 and RA2 to light LEDS based on RA1 and RA0
#include <xc.h>

#pragma config WDT = OFF


#pragma config OSC = INTIO7 // puts osc/4 on pin 14 to check freq
#pragma config MCLRE = OFF
#pragma config LVP = OFF
#pragma config PBADEN = OFF

int main(void)
{
TRISAbits.RA3 = 0; // set RA3 as output
TRISAbits.RA2 = 0; // set RA2 as output

TRISDbits.RD1 = 1; // set RA1 as input


TRISDbits.RD0 = 1; // set RA0 as input

while(1) // monitor and read pins and light LEDs forever


{
PORTAbits.RA3 = PORTDbits.RD1;
// whatever the value of input at RD1 use that value to turn RA3 on/off
PORTAbits.RA2 = PORTDbits.RD0;
// whatever the value of input at RD0 use that value to turn RA2 on/off
}
}
// end program

VDD
MCU

R
RA3
RD1

RA2
RD0 R

VSS

Figure V-9: Inputs and outputs.

In the circuit in Figure V-9 above, the LED at RA3 would be lit and the LED at RA2 would be
off. The nice thing about this circuit is that you can change the connections at RD1 and RD0 as
you wish.
Q. How would you change LEDinputsensor.c so that if RD1 is high, RA3 would be set low
(LED off) and the same for RD0 and RA2?

129
Lectures - V DIO

Q. If you do not change LEDinputsensor.c above, how could you change the LED circuits at
RA3 and RA2 so that if RD1 is high then RA3 is set low (LED off) and the same for RD0 and
RA2?

Buttons and DIO


Changing the voltage at an input between low and high is easily accomplished by a simple circuit
of two resistors and a button as shown in Figure V-10.

VDD

MCU

button 200  RA2


RD0 R
A B (large R)
10 k

Figure V-10: A button input.

We need to spend a little time understanding how the button works. When the button is up, RD0
is connected to ground through the two resistors, so the voltage at the pin is 0.

The button is just a conducting piece of metal, so when it is pressed down, point A is at the same
potential as VDD, +5 V. What will be the voltage at RD0, point B, in this case? We can figure this
out if we remember that the input pin has a large resistance, on the order of millions of Ohms. We
can model the button circuit and the MCU as shown in Figure V-11 below.

VDD MCU

200  Rinput

A B
10 k

Figure V-11: Input resistance.

Point A is at 5 Volts when the button is pressed, and there are two parallel paths to ground. Because
both paths are high resistance, very little current is flowing. That’s good because it would just be
wasting power. The branch to the input pin is a series circuit of the 200- resistor and Rinput. If we
recall the voltage divider circuit, the voltage at point B is easy to find

130
Lectures - V DIO

VB = VA* Rinput / (200  + Rinput).

Since Rinput >> 200 , VB = VA to a very good approximation.

The voltage drop across the 200 ohm resistor would be negligible. The only purpose of the 200
ohm resistor in this circuit is to limit current if the wrong program is downloaded into the PIC and
the I/O pin is configured as an output.

Now consider the program below to work with the circuit in Figure V-10.

// buttonLED.c configure RA2 to light an LED based on RA0


#include <xc.h>

#pragma config WDT = OFF


#pragma config OSC = INTIO7 // puts osc/4 on pin 14 to check freq
#pragma config MCLRE = OFF
#pragma config LVP = OFF
#pragma config PBADEN = OFF

int main(void)
{
TRISAbits.RA2 = 0; // set RA2 as output

TRISDbits.RD0 = 1; // set RD0 as input

while(1) // monitor and read pins and light LED forever


{
PORTAbits.RA2 = PORTDbits.RD0;
// whatever the value of input at RD0 use that value to turn RA2 on/off
}
}
// end program

button pressed – RA0 high


V

input signal t

RA0 high – LED turned on


V

t
output signal

Figure V-12: Button input and LED output.

131
Lectures - V DIO

With buttonLED.c, if the button is pressed on and off, the LED flashes on and off in unison.

Note that the button and resistor circuit is a logic switch. Modern digital logic devices require a
discrete voltage level to work reliably. In the case of the PIC, the input will be taken as a high if
the input voltage is above 2 volts DC, and the input will be taken as a low if the input is below 0.8
volts DC. If the voltage is between 0.8 and 2.0 volts DC, we can’t predict what the input will be.
This circuit set the input pin voltage high or low but with very little current flow. This is in contrast
to a wall light switch in a house circuit, which allows a large current to flow through a lightbulb
filament, a resistor, to make it glow (see Figure V-13 below).

Switch

Figure V-13: A mechanical switch.

Q. Why don’t we want to use a mechanical switch in series with the MCU as shown in the diagram
below?

MCU
R
RA2
RD0 R
VDD Switch (large R)

You can also use the


button and resistor circuit button pressed – RA0 high
V
as a toggle by modifying
how the program reacts to
the change of state, either
low to high (a rise) or high
to low (a fall), at the input. input signal t
With a toggle, when you
press and release the
button for the first time, toggled on – LED turned on
the LED should stay on. V
When you press and the
button the next time, the
LED should remain off.
This behaviour is
t
illustrated below. output signal

Figure V-14: Toggle on rise logic.

132
Lectures - V DIO

To create a toggle in software, you must look for the times that the input signal goes from low to
high (known as a rising edge). When that occurs, you note if it is a rising edge or falling edge.
Source code to do this is in buttons.c:

// monitor a digital input pin PORTXbits.RXn for changes


// We usually connect a button to this pin but any square wave input will do.

#include "buttons.h"

unsigned char last_switch1_value = 0; // last button value, initialized to low


// global variable

// returns 0 if no edge has happened


// returns 1 at a rising edge
// returns 2 at a falling edge
//
//
unsigned char monitor_switch1_for_edges(unsigned char digitalinputpin)
{

unsigned char has_switch1_changed = 0; // 0 = no change; 1 = rising edge;


// 2 = falling edge

if (last_switch1_value == 0 && digitalinputpin)


{ // rising edge detected if digitalinputpin is 1 (on)
last_switch1_value = 1; // save current switch value (on)
has_switch1_changed = 1; // found a rising edge
}
if (last_switch1_edge == 1 && !digitalinputpin)
{ // falling edge detected if digitalinputpin is 0 (off)
last_switch1_value = 0; // save current switch value (off)
has_switch1_changed = 2; // found a falling edge
}

return has_switch1_changed;
}

Note the elements of this program. There is the monitor_switch1_for_edges() function, which
sits inside the while(1) loop waiting to detect a rising or falling voltage signal or edge. It does
nothing until the edge is detected. A key part of the function is the global variable
last_switch1_edge. It holds the last reading from the input pin. That value is compared to the
current value. If the previous value is 0 and the current value is 1, we have found a rising edge. If
the values had been 1 and 0, we found a falling edge. If you need a second button, simply duplicate
the code, replacing the number 1 by 2 in the function labels.

This function is then put to work in a program like

// SoftToggle.c configure RA2 to light an LED based on RA0 rising edges


#include <xc.h>
#include “..\Common\button.h”
#include “..\Common\configuration_bits.h”

int main(void)
{

133
Lectures - V DIO

unsigned char has_switch1_changed = 0;


TRISAbits.RA2 = 0; // set RA2 as output
TRISAbits.RA0 = 1; // set RA0 as input (switch 1)
PORTAbits.RA2 = 0; // initialize LED off
while(1)
{
has_switch1_changed = monitor_switch1_for_edges(PORTAbits.RA0);
if ( has_switch1_changed == 1 ) //rising edge
{
// insert code here
}
if ( has_switch1_changed == 2 ) //falling edge
{
// insert code here
}
}
return 0;
}

// end program

Insert your code at the indicated spots to make the program do what you wish it to do.

Q. How would you modify the code to change on a fall (see Figure V-15)?

input signal t

t
output signal

Figure V-15: Toggle on fall.

Special Configuration Notes

DIO pins 33 to 37, RB0 to RB4, are only available if the following configuration directive is
included in your programs.

#pragma config PBADEN = OFF // PORTB<4:0> are digital IO

134
Lectures - V DIO

If PBADEN = ON, the pins are only analog and suitable for ADC, discussed later.

Somewhat similarly, pins RA0, RA1, RA2, RA3, and RA5 (but not RA4) are configured as analog
inputs suitable for ADC, discussed later. They will work fine as digital outputs to light up an LED
but will not work with buttons. Their PORTAbits.RAn (n = 0, 1, 2, 3, and 5) value will always
read zero whether the button is up or down. If you wish to use these pins for button input, you will
need to include two lines of code to override the default setting, as shown in the sample code below

#include <xc.h>

void main(void) {

ADCON1 = 0x0F; // necessary for button input on RAn


CMCON = 0x07; // where n = 0, 1, 2, 3, and 5

TRISAbits.RA5 = 1; // input for button


TRISDbits.RD1 = 0; // output to LED

while(1)
{
PORTDbits.RD1 = PORTAbits.RA5;
}

return;
}

One last point: you cannot use RA6 pin 14 for digital input or output because it is outputting the
PIC frequency (fosc/4) due to the following configuration directive

#pragma config OSC = INTIO7 // outputs osc/4 on pin 14 to check freq

135
Lectures - VI Oscillator Frequency & Delays

Notes VI Oscillator Frequency & Delays


The MCU has an internal clock or oscillator that synchronizes the operation of its various modules.
The default speed of this internal clock is 1 MHz, but we can change that if we wish. The internal
clock output is a square wave, shown in Figure VI-1, at this frequency. The manual indicates the
internal clock frequency and period by fOSC and TOSC, where fOSC = 1/TOSC.

0
TOSC t

Figure VI-1: Internal oscillator square wave.

Every operation – storing a number in memory, adding two numbers, or setting a port pin – requires
time. The minimum amount of time required on this chip, because of the limits of the computer
architecture, is 4TOSC. This minimum time is called an instruction cycle TCY = 4TOSC.

0
TCY t

Figure VI-2: The instruction cycle signal.

The MPLABX has a method of letting us find out how many instruction cycles, and how much
real time, a C statement would take to execute by simulating the running of the program. If you
set “breakpoints” in the code, you can stop execution at those points. With the stopwatch feature
of MPLABX selected, you can measure the time elapsed between two breakpoints.

Figure VI-3 show the breakpoints (the red squares) and stopwatch window for a sample program.
The green arrow indicates the current line that the simulator is at, i.e. line 24. The stopwatch
window indicates that 6TCY, or 6 s at 1 MHz, were required to process the calculations between
this line and the previous breakpoint.

The program has comments showing how many instruction cycles are needed for each line.

136
Lectures - VI Oscillator Frequency & Delays

Figure VI-3: Timing code interval in the simulator.

Note that i = 2 + 3 (line 16) takes less time than i = j + k (line 17) because the CPU has to fetch
the variables j and k from memory. Also, the same operation with 16-bit integers (line 21), rather
than 8-bit char (line 16), takes more computing time since there is more memory to juggle.

See that the use of i++ (line 19) is optimized and thus takes less time than i = i + 1 (line 18).

For loops take a lot of instruction cycles to process.

A breakpoint can only be inserted on a line of code and not at a blank line. If there is no code
available where you want the breakpoint, you can insert a NOP() function and place the breakpoint
on the NOP(). Think of NOP() as a do-nothing function. It takes 1 instruction cycle to use a NOP().

Q. How long is 1 TCY on an MCU operating at 1 MHz? At 32 MHz?

137
Lectures - VI Oscillator Frequency & Delays

Determining the Clock Frequency of the MCU


Pin 14 can be used to measure the fOSC and TOSC. In the right circumstances, you can have the
MCU output a square wave at pin 14, with a period TCY, and measure the signal with an
oscilloscope.

This pin is multiplexed with other features. For instance, it is also RA6. These features are mutually
exclusive – at a given time, you can have one but not the other.

To tell the chip to output a timing signal on pin 14, i.e. to configure the pin, you need the following
directive.

#pragma config OSC = INTIO7 // puts osc/4 on RA6 pin 14 to check freq

Without this statement you would not have a signal on pin 14 and you could use the pin for DIO
(with the right code).

Changing the MCU Clock Frequency


The MCU can operate at different speeds. The OSCCON (oscillator control) SFR controls this
feature (see Register 2-2 of the datasheet, reproduced in Figure VI-4). You only need worry about
selected bits of these SFRs. We will supply source files, osc.h and osc.c, that let you operate the
MCU at 32 MHz.

#include <xc.h>
#include “..\Common\osc.h”

void set_osc_32MHz(void)
{
int i;

OSCCONbits.IRCF2 = 1; // Set the OSCILLATOR Control Register to 8 MHz


OSCCONbits.IRCF1 = 1;
OSCCONbits.IRCF0 = 1;
OSCTUNEbits.PLLEN = 1; // Enable PLL, boosts speed by 4x to 32 MHz

for(i=0;i<500;i++); // delay to allow clock PLL to lock (stabilize)


}

Q. Modify the code to tell the MCU to operate at 2 Mhz. Repeat for 16 MHz.

138
Lectures - VI Oscillator Frequency & Delays

Figure VI-4: OSCCON SFR.

Delays
The XC8 compiler has a library of delay functions that wait a precise number of instruction cycles
TCY. The functions in the library, as listed in the header, are:

void NOP(void) // exactly 1 TCY


void _delay(unsigned long cycles) // cycles cannot exceed 179200
void _delay3(unsigned char unit) // delay = 3 * cycles * TCY
void __delay_us(float x) // delay = x microseconds
void __delay_ms(float x) // delay = x miliseconds

Cycles in _delay() cannot be 0 or exceed 179200. If you exceed the maximum, the compiler will
give an error message. Note that unit in _delay3() is of unsigned char type, and can only be
from 0 to 255. If you use a value of unit greater than 256, rollover occurs. As well, zero will not
give you a delay of 0 TCY (since any operations takes at least one instruction cycle), instead it give
you 3 × 256 = 768 TCY.

139
Lectures - VI Oscillator Frequency & Delays

The functions __delay_us() and __delay_ms() actually count instruction cycles, like _delay(),
but do a conversion to get the number of TCY to give the correct delay in microseconds or
milliseconds. These functions need to know the conversion factor, which is just the operating
frequency of the PIC MCU. Simply include the following definition, adjusting for your operating
frequency.

# define _XTAL_FREQ 1000000 // operating at 1 MHz

Since the smallest time unit is 1 TCY, the smallest value of x in these functions must convert to a
minimum of 1 TCY. Similarly, the largest value of x cannot give a time interval greater than 179200
TCY. A value of x that is too large will generate an error at compilation.

DIO and Square Wave Output


It is easy to output a square wave from a DIO pin. The steps are simple:

 Turn pin on
 Wait a while
 Turn pin off
 Wait a while
 Repeat

// Square wave output on RA3, pin 5 //


#include <xc.h>
#include “..\Common\configuration_bits.h”
#include “..\Common\osc.h”

# define _XTAL_FREQ 32000000 // operating at 32 MHz

int main(void)
{
Set_OSC_32MHz();
TRISAbits.RA3 = 0; // set RA3 as output
while(1) // anything in here repeats forever
{
PORTAbits.RA3 = 1; // set RA3 high
__delay_ms(10); // wait 10 ms
PORTAbits.RA3 = 0; // clear RA3 (low)
__delay_ms(30); (30); // wait 30 ms
}
}

We could measure this signal using an oscilloscope connected to RA3 (pin 5) and ground, or we
could also use the stopwatch feature to measure the on/off times in the simulator.

140
Lectures - VI Oscillator Frequency & Delays

We define the duty cycle of a square wave as the ratio of the ‘on’ time to the period of the square
wave times 100% (duty cycle = Ton/T  100 %).

Ton
V

0
T t

Figure VI-5: The duty cycle.

Q. What is the duty cycle for the above code?

Q. How would the output square wave differ if you connect the oscilloscope between RA3 and
VDD? What would the duty cycle be?

Q. On a 32 MHz MCU, what delays would you need for a 0.250 ms period and 32% duty cycle?

Timing Diagrams
When the signal at one or more pins changes with time, a way to understand the circuit behaviour
is to draw the Voltage versus Time graph of the signals. For example, consider an LED and series
buffer resistor connected between pins RA3 and RA5 on the MCU. When RA3 is high and RA5
is low, the LED will shine. The code controlling the pins is

//Square wave output on RA3 and RA5 //


#include <xc.h>
#include “..\Common\configuration_bits.h”
#include “..\Common\osc.h”

int main(void)
{
Set_OSC_32MHz();
TRISAbits.RA3 = 0; // set RA3 as output
TRISAbits.RA5 = 0; // set RA5 as output
PORTAbits.RA3 = 0; // initialize to low
PORTAbits.RA5 = 0; // initialize to low
while(1) // anything in here repeats forever
{
PORTAbits.RA3 = 1; // set RA3 high
_delay(10000); // wait 10,000 TCY
PORTAbits.RA5 = 1; // set RA5 high
_delay(10000); // wait 10,000 TCY
PORTAbits.RA3 = 0; // clear RA3 (low)
_delay(20000); // wait 20,000 TCY
PORTAbits.RA5 = 0; // clear RA5 (low)
_delay(20000); // wait 20,000 TCY
}
}

141
Lectures - VI Oscillator Frequency & Delays

Determine the period and duty cycle of the LED using a timing diagram.

Initial delay
Unknown
5V
RA3
0
5V
RA5
0
t (KTCY)
On
LED
Off 0 20 40 60 80 100 120 140

Figure VI-6: Timing problem solution via a timing diagram.

Reading Figure VI-6, the period is clearly 60 KTCY and the duty cycle is 10/60.

142
Lectures - VII PWM

Notes VII Pulse-Width Modulation (PWM)


Using a square wave signal and varying either the period or duty cycle, see Figure VII-1, is a
common way to control DC devices such as motors, LEDs, and speakers. This sort of control signal
is called Pulse Width Modulation (PWM). The PIC18F4525 has two separate modules that control
two different pins, 17 and 16, labelled CCP1 and CCP2 in the pinout diagram. You can “set and
forget” a signal through each pin. This allows you to do others things at the same time the signals
are being generated. The modules must operate at same period but can have different duty cycles.
An LED will light up when the voltage is on and be dark when the voltage is low. If a square wave

Ton
V

0
T t

Figure VII-1: Square wave signal.

with a long period drives the LED, you see the LED blink on and off. At smaller periods, for
frequencies more than about 20 Hz, the blinking is too fast for your eyes to separate and the light
level appears constant, although dimmer than its full brightness, since it is partly on and partly off.
Thus, a small duty cycle means a dim light and a large duty cycle means a bright light. PWM is
thus the equivalent of a brightness control for an LED. Similarly for a motor, PWM is a way of
controlling motor speed. Motors have a certain amount of inertia. When the motor is turned on, it
takes some time to reach full speed, and when turned off, it takes some times to slow to a stop. For
a short period signal, a motor will never reach full speed during the on cycle and never completely
slow during the off cycle. For both the case of the LED and the motor, you would choose an
appropriately small period and change only the duty cycle. Speakers are the reverse. Our ears are
quite sensitive to frequency. When we change the period, the frequency is different, and we hear
a different tone. The duty cycle is usually kept constant at 50%, since the duty cycle hardly affects
what we hear.

DC motors are more complicated than LEDs, in that they can operate both forward and in reverse,
normally draw a lot of current, and can drive a large current backward into the signal source when
it switches on and off (back emf). Consequently, to protect the MCU, a separate chip called a driver
is usually put between the MCU and a motor. The driver can supply much bigger currents than the
MCU and is designed to withstand much greater reverse currents.

PWM is an efficient method to control brightness and speed, because energy is not wasted during
the offcycle, and so it is widely used. For comparison’s sake, consider an alternate control circuit
consisting of a variable resistor in series with the LED or motor. When the resistance is low, the
current is high and the LED is bright and the motor spins fast. When the resistance is high, the
current is low and the LED is dim and the motor spins slowly. This is not a very efficient approach
because energy is wasted in heating the resistor.

143
Lectures - VII PWM

PWM is controlled by the CCP1CON, CCP2CON, and T2CON control registers and the PR2,
CCPR1L, and CCPR2L data registers. The time base of the period and duty cycle is set by the
Timer2 Module. The properties of the Timer2 module are controlled by T2CON. The PWM signal
controlled by CCP1CON originates at the CCP1 pin, pin 17. Similarly, the PWM signal controlled
by CCP2CON originates at the CCP2 pin, pin 16. PWM is discussed in Section 15.4 of the
datasheet. The steps needed to activate PWM are listed in Section 15.4.4.

The XC8 Compiler simplifies the use of setting of the PWM SFRs through the use of four
functions. PWM also requires the use of the OpenTimer2() function from the timers library to
help set the period.

The functions are:

OpenPWMx(unsigned char period)


SetDCPWMx(unsigned int dutycycle)
ClosePWMx(void), and
OpenTimer2(TIMER_INT_OFF & T2_POST_1_1 & prescalar)

The x in the function names stand for 1 and 2, where 1 is for the signal from CCP1 (pin 17) and 2
is for CCP2 (pin 16). The OpenTimer2() function should be called before the OpenPWMx()
function, as it helps determine the period. In the OpenTimer2() function, the only argument we
need is which prescaler to use, T2_PS_1_1, T2_PS_1_4, or T2_PS_1_16, that is, every 1, 4, or 16
instruction cycles. Note that when PWM is using Timer2, you cannot also use Timer2 for timing.
The argument in the OpenPWMx() function, period, determines the period according to the formula

TPWM = [(period) + 1]  TCY  prescaler [ VII-1 ]

where prescaler is 1, 4, or 16. Note that period can only have values from 0 to 255. The on
time of the signal is chosen by the SetDCPWMx() function using the equation

Tdutycycle = dutycycle  ¼TCY  prescaler [ VII-2 ]

The dutycycle argument can be any number from 0 to 210 (1024). If dutycycle is zero, the output
signal is always off. Note if Tdutycycle / TPWM is greater than 1, the duty cycle is 100% – always on.

Consider the case where the period is 24 and the prescaler is 4. The PWM period is thus 100
TCY. The only useable values of dutycycle would be between 0 and 400, giving dutycycles of 0
to 100 TCY. Anything above 400 is still full on. The on portion of the signal could last between 0
TCY and 100 TCY. The dutycycle, as a percentage of the period, is given by
dutycycle/4/(period+1)*100%.
The output can be shut off by using the ClosePWMx() function.

Q. At 32 MHz, what is the longest period a PWM output can have? What is the shortest? For
the shortest period, what duty cycles are possible?

144
Lectures - VII PWM

A short example of how to use the functions in a program is given below.

// Attach a speaker and a 200  resistor between pin 17 and ground.


// plays note A in different octaves 4, 5, and 6
#include <xc.h>
#include “../Common/configuration_bits.h”

#define _XTAL_FREQ 1000000 / 1 MHz osc freq

int main(void)
{
OpenTimer2(TIMER_INT_OFF & T2_PS_1_4 & T2_POST_1_1); // never changed

while(1) // anything in here repeats forever


{
// how frequency affects sound
OpenPWM1(141); // period is (141+1)*TCY*4
SetDCPWM1(284); // DC = 284/4 / (141+1) = 50%
__delay_ms(2000); // wait 2 seconds
SetDCPWM1(0); // off
__delay_ms(200); // wait 0.2 second

OpenPWM1(70); // period is (70+1)*TCY*4


SetDCPWM1(142); // DC = 142/4 / (70+1) = 50%
__delay_ms(2000); // wait 2 seconds
SetDCPWM1(0); // off
__delay_ms(200); // wait 0.2 second

OpenPWM1(35); // period is (35+1)*TCY*4


SetDCPWM1(72); // DC = 72/4 / (35+1) = 50%
__delay_ms(2000); // wait 2 seconds
SetDCPWM1(0); // off
__delay_ms(8000); // wait 8 second

// how duty cycle affects sound


OpenPWM1(141); // period is (141+1)*TCY*4
SetDCPWM1(100); // DC = 100/4 / (141+1) = 50%
__delay_ms(2000); // wait 2 second
SetDCPWM1(0); // off
__delay_ms(200); // wait 0.2 second

OpenPWM1(141); // period is (141+1)*TCY*4


SetDCPWM1(284); // DC = 284/4 / (141+1) = 50%
__delay_ms(2000); // wait 2 second
SetDCPWM1(0); // off
__delay_ms(200); // wait 0.2 second

OpenPWM1(141); // period is (141+1)*TCY*4


SetDCPWM1(550); // DC = 550/4 / (141+1) = 50%
__delay_ms(2000); // wait 2 second
SetDCPWM1(0); // off
__delay_ms(8000); // wait 8 second
}
}

145
Lectures - VII PWM

The PWM1 and PWM2 square wave signals are restricted to having the same period but the duty
cycle can be different. Another restriction on the two signals is that the start of their on cycles will
coincide because of the shared Timer2 time base. So if, for example, the PWM1 is started and
there is a delay before the PWM2 signal is started, the PWM2 signal starts at the next rise of the
on cycle of the PWM1 signal, as show in the diagram below.

5V
PWM1
0
5V
PWM2
0
t
PWM2 called

PWM2 starts

Initial delay
Initial delay

here
here

Figure VII-2: The rising edge of the two PWM signals always coincide.

Changing the Default CCP2 Pin

The default pin for CCP2 is pin 16 (RC1) but if you examine the pinout diagram you will see that
pin 36 (RB3) can also be CCP2. To switch from pin 16 to pin 36, use the configuration #pragma
directive

#pragma config CCP2MX = PORTBE // pin 36 is now CCP2

To set CCP2 to pin 16, omit the above directive or use

#pragma config CCP2MX = PORTC // pin 16 is now CCP2 (default)

146
Lectures - VIII Timer0

Notes VIII Timer0 Module

Using either for-loops or delay functions means that nothing else can be done while we are waiting
for the count to be finished. Such delays are called “blocking” delays since we cannot do anything
else during their execution. If we wanted to check a button, no luck. During blocking delays, the
programs are largely useless and not interactive. We can only do one thing at a time. Fortunately,
the PIC18F4525 designers and their predecessors in chip design were a clever bunch. These chips
have separate modules that can run simultaneously. This chip has several (4) different timer
modules. The simplest function of a timer module is like an eggtimer. You set it for a certain
amount of time, you start the timer, and you wander away and do something else. You can either
check the timer periodically or you can listen for an alarm (if you have the fancier model).

A timer module works by checking the internal oscillator signals. It can tell time by counting either
rising edges or falling edges of the clock square wave signal. When you tell the module to start at
a rising edge, it waits for that first edge and then increments a counter for each successive edge. If
the setup is done appropriately, when the desired number of edges is reached, a flag is set, see
Figure 0-1. A flag is just a bit in an SFR. At any time, you can check this flag bit.

A counter, which is also an SFR, keeps a running tally of the number of edges counted. The counter
can be byte-sized, 0 - 255, or two-byte sized, 0 - 65535. You can start (write) the counter at any
value in its range. You can also read the counter at any time. However even 65535 edges, i.e.
65535 TCY, is not a long time on a PIC run at 32 MHz. For that reason, you can have the counter
increment only after multiple edges are counted, such as every 2 successive edges, every 4 edges,
and even to every 256 edges. This multiple is called the prescale value. Each timer module is
slightly different in terms of which multiples or prescalars are allowed. Check the
MPLAB_XC8_Peripheral_Libraries.pdf for details.

TCY
V

t
0
flag sets
Figure VIII-1: Timers start at the next edge (falling or rising as requested).

The counter, being limited in memory space, will roll over at 256 (1 byte counter) or 66536 (2 byte
counter). It is this rolling over that sets the special flag bit to 1. Note: once the flag is set it, remains
set (1) until it is cleared (0) manually. Some timer modules also have a postscaler value. If a
postscaler was 2, the 1 byte counter would not roll over until 256  2 = 512.

The XC8 Compiler has a library of functions for using the available timer modules

147
Lectures - VIII Timer0

OpenTimerN(unsigned char config) //configures the modules


WriteTimerN(unsigned int timer) //sets the counter to certain value
ReadTimerN(void) //reads current counter value
CloseTimerN(void) //closes down and frees up module

The N in the functions stands for 0, 1, 2, or 3, the four timer modules of the PIC18F4525. Different
chips can have different numbers of timer modules. The flags for the four timers are respectively
TMR0IF, TMR1IF, TMR2IF, and TMR3IF. The following discussion will be restricted to Timer0, since
the other timers function similarly. The only significant difference is that not every timer has as
many prescaler values (discussed below) as Timer0, and some have postscaler values.

Of the four functions in the XC8 compiler library, only the OpenTimer0() function requires much
thought. The XC8 Compiler uses a bit mask and special defined names to set the proper value of
the input variable config. Figure 0-2 shows the available choices. You should always use
TIMER_INT_OFF. For timing, we will use the internal clock and thus use T0_SOURCE_INT.

void OpenTimer0( unsigned char config)

Input Parameters Description


config Enable Timer0 Interrupt:
* TIMER_INT_ON
* TIMER_INT_OFF

Timer Width:
* T0_8BIT
* T0_16BIT
Clock Source:
* T0_SOURCE_EXT
* T0_SOURCE_INT

External Clock Trigger (for T0_SOURCE_EXT):


* T0_EDGE_FALL
* T0_EDGE_RISE

Prescale Value:
* T0_PS_1_1
* T0_PS_1_2
* T0_PS_1_4
* T0_PS_1_8
* T0_PS_1_16
* T0_PS_1_32
* T0_PS_1_64
* T0_PS_1_128
* T0_PS_1_256

Figure VIII-2: OpenTimer0 configuration


choices.
The other settings are for choosing an 8- or 16-bit counter, whether to count falling or rising edges
of the clock square wave signal, and which prescale value to use (i.e. which multiple of edges to
count). These, you will set depending on your needs. You decide on the counter based on the
number of instruction cycles you wish to wait. For example, suppose you want a delay of 35,000
clock cycles. If you use the 8-bit counter, you would choose the prescale value that gives
35,000/prescaler_value equal to a number less than 256. In this case, only the prescaler

148
Lectures - VIII Timer0

T0_PS_1_256 would work. With this prescaler, you would be timing every 256th edge of the clock.
This is very coarse. Your desired time interval divided by this prescale value is 35,000 / 256 =
136.719. Counting is done in whole numbers, so you would either count 256*136 = 34816 TCY or
256*137 = 35072 TCY. If being off by only 72 TCY is acceptable, you could make this choice.
Otherwise, you would need the 16 bit counter. In that case, all the prescaler values would work.
Indeed, using T0_PS_1_1 would allow you to time the desired interval to within one TCY.

Q. Which counter and prescaler would you use for a 505,116 TCY interval? What is the minimum
uncertainty you would have in this measurement?

There are two slightly different ways of using a timer. For the example we used of 35000 TCY, we
need the timer to count 35,000 edges. The counter can count at most 65536 edges, so we start the
timer at 65336 – 35000 = 30336. That is, we use the function WriteTimer0(30336) to start
counting the edges. When the counter reaches 65536, after 35000 TCY, the flag is set. TMR0IF now
equals 1 (provided it was cleared or set to zero before we started). We monitor the flag using an if
statement to compare its value to 1, and stop what we are doing when the result is true. We should
reset, or clear, the flag for possible later use. The code snippet below shows how to write this.

// Test your reflexes.


#include <xc.h>
#include “..\Common\osc.h”
#include “..\Common\serLCD.h”
#pragma config WDT = OFF
#pragma config OSC = INTIO7 // puts osc/4 on pin 14 to check freq
#pragma config MCLRE = OFF
#pragma config LVP = OFF
#pragma config PBADEN = OFF

int main(void)
{
unsigned int I;
unsigned char winner;
set_osc_32MHz();
configureUSART(9600);
// wait for splash screen to finish
for(i=0; i++; i = 80) _delay(100000ul);

TRISDbits.RD1 = 1; // set RD1 as button output

OpenTimer0(TIMER_INT_OFF & T0_16BIT & T0_SOURCE_INT & T0_PS_1_128);

while(1) // monitor and read pins and light LED forever


{
while(PORTDbits.RD1 == 1); // wait until button is released
WriteTimer0(30336); // reset
TMR0IF = 0; // reset
winner = 0;
while(!TMR0IF || winner == 1)
{
if PORTDbits.RD1 == 1) // wait until button is pressed
{
// message You are fast!

149
Lectures - VIII Timer0

winner = 1;
} // wait until button is pressed
}
// message – “Loser!”
if ( winner == 0 )
{
// message – “Loser!”
}
}
}
// end program

The other approach requires no calculation. Start the timer at zero, WriteTimer0(0). But now
monitor the counter itself using the ReadTimer0() function in an if statement such as

if( ReadTimer0() == (unsigned int)35000 ) …

We don’t even need to bother resetting the flag, as we never use it. Again, a code snippet is below.

// Test your reflexes.


#include <xc.h>
#include “..\Common\osc.h”
#include “..\Common\serLCD.h”
#pragma config WDT = OFF
#pragma config OSC = INTIO7 // puts osc/4 on pin 14 to check freq
#pragma config MCLRE = OFF
#pragma config LVP = OFF
#pragma config PBADEN = OFF

int main(void)
{
unsigned char winner;
set_osc_32MHz();
configureUSART(9600);
// wait for splash screen to finish
for(i=0; i++; i = 80) _delay(100000ul);

TRISDbits.RD1 = 1; // set RD1 as button output

OpenTimer0(TIMER_INT_OFF & T0_16BIT & T0_SOURCE_INT & T0_PS_1_128);

while(1) // monitor and read pins and light LED forever


{
while(PORTDbits.RD1 == 1); // wait until button is released
WriteTimer0(0); // reset
TMR0IF = 0; // reset
winner = 0;
while((ReadTimer0() == (unsigned int)35000) || winner == 1)
{
if PORTDbits.RD1 == 1) // wait until button is pressed
{
// message You are fast!
winner = 1;
} // wait until button is pressed
}

150
Lectures - VIII Timer0

// message – “Loser!”
if ( winner == 0 )
{
// message – “Loser!”
}
}
}
// end program

Timing Events Using a Timer


Timers can also be used like a stopwatch to time events. For instance, suppose you wish to know
how long it takes to print out the message “ Hello World ” on the LCD. Simply start the timer
before the code, and then read the timer after the code has finished, see the sample below.

#include <xc.h>
#include “..\Common\osc.h”
#include “..\Common\configureUSART.h”
#include “..\Common\serLCD.h”
#pragma config WDT = OFF
#pragma config OSC = INTIO7 // puts osc/4 on pin 14 to check freq
#pragma config MCLRE = OFF
#pragma config LVP = OFF
#pragma config PBADEN = OFF

int main(void)
{
unsigned int time = 0, flag = 0;

set_osc_32MHz();
configureUSART(9600);
// wait for splash screen to finish
for(i=0; i++; i = 80) _delay(100000ul);

OpenTimer0(TIMER_INT_OFF & T0_16BIT & T0_SOURCE_INT & T0_PS_1_1);

WriteTimer0(0); // reset
TMR0IF = 0; // reset
printf( Hello World ); //time single or mutliple lines of code
time = ReadTimer0();
flag = TMR0IF;
printf(«t = %u Flag = %u », time, flag);

while(1);
} // end program

You can also time external events, such as button presses. Suppose, for instance, you wish to time
how long it takes you to press a button ten times. We could modify the button counting program
we have used before.

#include <xc.h>
#include “..\Common\osc.h”

151
Lectures - VIII Timer0

#include “..\Common\configureUSART.h”
#include “..\Common\serLCD.h”
#include “..\Common\buttons.h”
#pragma config WDT = OFF
#pragma config OSC = INTIO7 // puts osc/4 on pin 14 to check freq
#pragma config MCLRE = OFF
#pragma config LVP = OFF
#pragma config PBADEN = OFF

int main(void)
{
unsigned int counter = 0, time = 0, flag = 0;
unsigned char has_switch1_changed = 0;

TRISAbits.RA1 = 1; // button is on RA1

set_osc_32MHz();
configureUSART(9600);
// wait for splash screen to finish
for(i=0; i++; i = 80) _delay(100000ul);

OpenTimer0(TIMER_INT_OFF & T0_16BIT & T0_SOURCE_INT & T0_PS_1_1);

while(1)
{
has_switch1_changed = monitor_switch1_for_edges(PORTAbits.RA1);

if ( has_switch1_changed == 1 ) //rising edge


{
counter++; // increment button counter
if (counter == 1) // start the timer on first press
{
WriteTimer0(0); // reset
TMR0IF = 0; // reset
}
if (counter == 10) // read the timer on 10th press
{
time = ReadTimer0();
flag = TMR0IF;
LCD_ClearDisplay();
printf(t = %u Flag = %u , time, flag);
counter = 0; // reset to time next 10 presses
}
} //end if

} //end while
} // end program

Note that in both examples, we print out the timer flag status as well. Overflow in timers can cause
problems if we aren’t careful.

Q. Write a program to time a button press, a rising edge to a falling edge.

152
Lectures - VIII Timer0

Timers as Counters
Timers measure time by counting edges, rising or falling, of square waves with a well-known
period. The time measurement is simply the number of edges multiplied by the period. The square
waves can come from an internal or an external clock source. If the source is a square wave that is
not periodic, a timer acts as a counter to inform you of the number of edges at have occurred at the
pin associated with that timer. For Timer0, that pin, number 6, is designated T0CKI. To use Timer0
as a counter, simply include T0_SOURCE_EXT and either of T0_EDGE_FALL (to count falling edges)
or T0_EDGE_RISE (to count rising edges) when configuring OpenTimer0(). Each edge at pin 6
increments the Timer0 counter, and the function ReadTimer0() reports the current count. Pin 6
(T0CKI/RA4) should be configured as a digital input for Timer0 to correctly read signal edges.
Note that when you use Timer0 as a counter, the only prescaler that is usually appropriate is
T0_PS_1_1.

Counters are useful in a number of situations. Imagine, for instance, you wanted to know the
number of people entering a doorway through a turnstile. It is easy to design a circuit to generate
a square wave at each turn of the turnstile. In turn, the PIC MCU does the counting.

In the example below, we compare and contrast how to count edges using our button code versus
using Timer0 in external counting mode.

Button code Timer0 external counting code


#include <xc.h> #include <xc.h>
#include "..\Common\buttons.h"
int main(void){
int main(void){
unsigned int counter = 0;
unsigned int counter = 0;
unsigned char has_switch1_changed=0; OpenTimer0(TIMER_INT_OFF &
TRISDbits.RD1 = 1; T0_SOURCE_EXT & T0_16BIT &
T0_PS_1_1);
while(1){
WriteTimer0(0); //clear
has_switch1_changed =
monitor_switch1_for_edges(RD1); while(1) {

if(has_switch1_changed==1){ if(counter < ReadTimer0())


counter++; counter = ReadTimer0();
}
// counter only changes when
} //end while // ReadTimer0 changes
} //end main } //end while
} //end main

Button code lets you use many different pins for your button, while you can only use pin 6, T0CKI,
with Timer0. The button code can miss an edge, a button press, if it is busy doing something else
in the while loop. Timer0 won’t miss an edge, since it is a separate module dedicated to the one
task.

153
Lectures - IX ADC

Notes IX Analog to Digital Conversion (ADC)

The DIO pins work with just two voltage values, low and high, which are typically 0 V and +5 V.
As we saw, we need only one bit of memory to store the voltage value as either 0 (low) or 1 (high).
Analog to Digital Conversion (ADC or A/D) is a method for measuring an arbitrary voltage signal
and storing the value as a binary number. In this chapter, we will explore the simplest method for
ADC called the Method of Successive Approximation, which is qualitatively similar to what
happens with the PIC. There are other methods, but we need not concern ourselves with these. In
the Method of Successive Approximation, two circuit elements are involved. The first is called a
comparator and is shown schematically in Figure VIII-1. As the name suggests, it compares two
voltages and if V1 is greater than or equal to V2 it outputs a 1 (true). If not, it outputs 0 (false).
The second element is a voltage divider with 2n equal resistors connected between reference
V1
?

V2

Figure IX-1: A comparator.

voltages, VREF+ and VREF–. Typically, VREF+ will be set to VDD, which is +5 V, and VREF– will be
set to VSS, which is 0 V. However, other reference values may be used. The 2n resistors provide 2n
voltage values with which to compare any input voltage. ADC is categorized by the number of
comparisons available. The PIC18F4525 has a 10-bit ADC module, so it makes 210 = 1024
comparisons. As a result, any voltage measurement is stored as a 10-bit binary number. As an aid
to understanding the ADC process, a 2-bit example is shown below in Figure VIII-2.

?
VREF+ = VDD = +5 V

R
 V3 = 3/4(VREF+ – VREF–) = 3.75 V
R
V2 = 2/4(VREF+ – VREF–) = 2.50 V
S
Vinput R
V1 = 1/4(VREF+ – VREF–) = 1.25 V
R
VREF– = VSS = 0 V

Figure IX-2: Comparing an input voltage.

154
Lectures - IX ADC

With the 22 = 4 resistors in our voltage divider, we have four V


voltages we can compare the input signal to: 0 V, 1.25 V, VREF+ = VDD = +5 V
2.50 V, and 3.75 V. If our input value was 3.05 V for 11
example, this circuit would find that Vinput lies between 2.50 3/ (V
4 REF+ – VREF–)
V and 3.75 V. With 2-bit ADC, the binary result or ADC 10 Vinput = 3.05 V
value for the input signal can only be 00, 01, 10, or 11. The 2/ (V
4 REF+ – VREF–)
binary value for 3.05 V would be 10, as is shown in Figure 01
VIII-3. Note that any input voltage between 2.50 V and 3.75 1/ (V
4 REF+ – VREF–)
V would be converted to this same ADC value. Thus, this 00
range isn’t very precise. However, our MCU actually is 10 VREF– = VSS = 0 V
bit, not 2 bit, so the precision is much greater.
Figure IX-3: Binary. onversion
The flowchart below, Figure VIII-4, indicates how the
binary value is created. First, you start at the midpoint reference voltage V2 and ask if the input
voltage is higher than that value. If yes, the first bit is 1 and you now do a new comparison with
V3. If no, the first binary digit is 0, and you do a new comparison with V1. In the first case, if the
input voltage is higher than V3, then the last binary digit is 1, else it is 0. In the second case, if the
input voltage is higher than V1, the last binary digit is 1, else it is 0.

Move S
to V2

No (0) Yes (1)


Vin > V2?

Move S Move S
to V1 to V3

No (0) Yes (1) No (0) Yes (1)


Vin > V1? Vin > V3?

00 01 10 11
Digital values

Figure IX-4: Comparison flowchart.

Q. If the input voltage was actually Vinput = 5.25 V for the 2-bit example, what would the
binary equivalent value be?

Q. If the input voltage was actually Vinput = –1.60 V for the 2-bit example, what would the
binary equivalent value be?

Once a signal has been digitized, there is corresponding problem of interpreting the digital value
as a voltage. Since each digital value is a bin or range of voltages, it only makes sense to take the
midpoint of the range or bin as the reading. For the example, the midpoints are 1/8(VREF+ – VREF–)
= 5/8 V, 3/8(VREF+ – VREF–) = 15/8 V, 5/8(VREF+ – VREF–) = 25/8 V, and 7/8(VREF+ – VREF–) = 35/8 V.
Since the actual reading is anywhere in the range, the precision of the digital measurement is one-

155
Lectures - IX ADC

half the width of the range, here being 1/8(VREF+ – VREF–) = 5/8 V. Thus, four digital values 00, 01,
1,0, and 11 correspond to readings of (0.125VREF ± 0.125VREF), (0.375VREF ± 0.125VREF),
(0.625VREF ± 0.125VREF), and (0.875VREF ± 0.125VREF). For an n-bit conversion, the general
formula is

 1  V V   1 VREF   VREF  
V   ADCValue   * REF  n REF   VREF    .
 2 2  2 2n

The accuracy of the resulting digital conversion depends on the accuracy of (VREF+ – VREF–), which
may only be accurate to a few percent.

Since the PIC18F4525 is more adept at handling integers than floating type numbers, it is advisable
to use VREF in millivolts in the formula above.

ADC on Time Dependent Input Signals


Varying signals could play havoc with the comparisons we have described above. Each
comparison takes time. Suppose the input signal is initially 3.05 V but jumps to 4.15 V at the
second comparison. The flowchart will produce a value of 11 instead of 10. Our ADC results will
be untrustworthy. To get around this problem, a sample-and-hold circuit is used, see Figure IX-7.
The signal is connected to a resistor-capacitor circuit via a very fast switch. The resistor and
capacitor combination must have a time constant  = RC such that the input signal is largely
constant over 5 or 10 times that time interval. When ADC is to be performed, the switch is closed
long enough for the capacitor to be fully charged and then reopened. The voltage across the
capacitor should be very close to the value of the input signal at the time ADC is started and will
remain constant if the switch is open. The ADC comparisons are done with respect to the capacitor
voltage, not the input signal voltage. The ADC module must have a resistance R ADC much higher
than R so that ADC >>  and thus VC does not discharge appreciably during the conversion.

R To ADC
Vin(t) Module
VC
C

Figure IX-5: Sample and hold circuit.

Q. According to the datasheet on p. 227, C = 25 pF and R (actually the series sum of the R, the
input resistance, and the switch resistance) is about 4 k. What is ? How long does it take to
fully charge the capacitor (i.e. to more than 99%)?

156
Lectures - IX ADC

A second timing factor is that all the ADC comparisons take time to complete. If TADC is the time
for a single measurement and conversion, then the variation in the input voltage signal must be
much slower than this. Figure VIII-6 shows how the number of ADC bits and size of TADC affect
how well the process captures the input signal. Clearly, the more bits involved and the smaller the
TADC, the better the digitized shape captures the input signal. Note as well that no matter how large
N is, the resulting digitization will be poor if the input signal is outside the range set by VREF+ and
VREF–.

V V

VREF+ VREF+

VREF– VREF–
0 t 0 t

V V

0 VREF+ VREF+
0 t

VREF– VREF–
0 t 0 t

Figure IX-6: How N and TADC affect digitization.

Programming the PIC18F4525 for ADC


The PIC18F4525 MCU has 13 pins that can perform ADC on input signals. These pins are labelled
AN0 to AN12 in the pin diagram below. Furthermore, AN2 and AN3 are multiplexed with VREF+
and VREF– so that external voltage references can be used.

The XC8 Compiler simplifies analog to digital conversion using the following functions:
OpenADC(), SetChanADC(), ConvertADC(), BusyADC(), ReadADC(), and CloseADC(). Note that
there are several versions of OpenADC() listed. Use the version for “All Other Processors”.

The main function OpenADC(unsigned char config, unsigned char config2, unsigned
char portconfig) configures the registers (see Section 19 of PIC18F4525 datasheet) that control
ADC. The function takes three parameters (config , config2 , and portconfig) and, as usual,
the parameters are created by bitmasking groups of preset, named constants. In the first parameter
config, the simplest choice is in A/D result justification, which has just two options,

157
Lectures - IX ADC

ADC_RIGHT_JUST or ADC_LEFT_JUST. Recall that we are dealing with 10-bit ADC, so the result of
an ADC is a binary number of length 10. The result will be presented as an integer, which in binary
has 16 digits. The constants control how the 10 digits are placed in the 16 spaces. For now, and
unless otherwise asked, right-justify the result.

The A/D clock source option also must be chosen. Conversion time for a 10-bit measurement is
given as 11 TAD (1 TAD per bit, plus a 1 TAD overhead) on the datasheet on page 229. You can
select how big you want TAD to be: 2 TOSC, 4 TOSC, 8 TOSC, 16 TOSC, 32 TOSC, or 64 TOSC. In general
you want the smallest time possible so that you can deal with rapidly changing, i.e. high frequency,
input signals if necessary. However TOSC changes with the PIC operating frequency, so TOSC can
be quite small. There is a minimum allowable value for TAD of 0.7 s. In Figure IX-7 below, a
variety of processor frequencies and consequent values of TOSC are shown. Below that, the value
of n and TAD for each A/D clock source value is given. The optimal choice of the parameter for
each processor frequency is shown by the shaded cells. These are the smallest values of TAD greater
than 0.7 s. As you can see, running our MCU at 32 MHz requires choosing ADC_FOSC_32. Note
that a further 2TAD is required between conversion cycles.

Frequency 1 MHz 2 MHz 4 MHz 8 MHz 16 MHz 32 MHz


TOSC (s) 1 0.5 0.25 0.125 0.0625 0.03125

A/D clock source n TAD = n TOSC (s)


ADC_FOSC_2 2 2 1 0.5 0.3 0.13 0.06
ADC_FOSC_4 4 4 2 1 0.5 0.25 0.13
ADC_FOSC_8 8 8 4 2 1 0.50 0.25
ADC_FOSC_16 16 16 8 4 2 1 0.50
ADC_FOSC_32 32 32 16 8 4 2 1
ADC_FOSC_64 64 64 32 16 8 4 2
Figure IX-7: Choosing an appropriate A/D clock source.

The last option, A/D acquisition time select, in the config parameter is the amount of time
the sample-and-hold switch is kept closed to allow the capacitor to charge completely. From the
previous question, we see this is about 0.5 s. The choices are expressed in multiples of TAD: 0, 2,
4, 6, 8, 12, 16, and 20. Operating at 32 MHz and using ADC_FOSC_32, TAD = 1 s, as calculated in
Figure IX-7. Thus, ADC_2_TAD is an acceptable choice.

Q. What is the ADC sampling frequency for the above choices?

Q. What is the maximum frequency of a sine wave signal that we could capture with reasonable
coverage, say 10 points per period?

The second parameter config2 has two constants that we must select. Choose the channel from
the list of constants, based on which pin will be used, e.g. ADC_CHO for pin AN0. The A/D voltage

158
Lectures - IX ADC

configuration constants ADC_VREFPLUS_VDD or ADC_VREFPLUS_EXT determine whether VDD or a


reference voltage on pin AN3 is used for the maximum reference voltage. The A/D voltage
configuration constant ADC_VREFMINUS_VSS uses VSS as the low reference (Note - there is a typo
in the XC8 libraries manual), while ADC_VREFMINUS_EXT allows us to use an external reference
voltage on pin AN2. Thus, if we choose VDD as the high or positive reference, and VSS as the low
or negative reference, a probe placed in the + strip of the breadboard will read 1023 and a probe
in the – strip will read 0.

The final parameter, portconfig, determines which of the 13 possible pins on the PIC18F4825
will be configured as analog inputs. If portconfig is 0, 1, or 2, all 13 pins will be analog inputs.
If portconfig is 3, AN12 is not an analog input. If portconfig is 4, both AN12 and AN11 are
not analog inputs, and so on. Figure IX-8 summarizes which pins will be analog inputs for which
value of portconfig. It takes a little time for the ADC module to be fully operational, so it is a
good idea to insert a little time delay after the call to OpenADC().
portconfig

AN12

AN11

AN10

AN9

AN8

AN7

AN6

AN5

AN4

AN3

AN2

AN1

AN0
0 A A A A A A A A A A A A A
1 A A A A A A A A A A A A A
2 A A A A A A A A A A A A A
3 D A A A A A A A A A A A A
4 D D A A A A A A A A A A A
5 D D D A A A A A A A A A A
6 D D D D A A A A A A A A A
7 D D D D D A A A A A A A A
8 D D D D D D A A A A A A A
9 D D D D D D D A A A A A A
10 D D D D D D D D A A A A A
11 D D D D D D D D D A A A A
12 D D D D D D D D D D A A A
13 D D D D D D D D D D D A A
14 D D D D D D D D D D D D A
15 D D D D D D D D D D D D D
A – Analogue Input D – Digital IO
Figure IX-8: Analogue pin selection and portconfig.
The other functions are much simpler. Once the configuration is set by OpenADC(), the actual ADC
operation will not start until ConvertADC() is called. ConvertADC() takes no parameters. The
conversion of an analog signal value to a digital value takes time, as discussed above (though it is
blindingly fast to people) and you have to wait for the process to complete. You can do other things
while waiting as long as you occasionally check to see if the conversion is complete. You check
on the progress of the conversion by examining the status of another parameterless function,

159
Lectures - IX ADC

BusyADC(). It returns a value of 1 if ADC is still going on and 0 if finished. You get the actual
results of the ADC operation by calling the ReadADC() function, which returns an integer that
holds the digital value. ReadADC() takes no parameters.

You can do another ADC operation by calling ConvertADC() again and polling BusyADC() until
this next conversion is done. You can repeat ADC operations as many times as you like.

You can also change pins and channels from one ADC operation to the next by using the
SetChanADC() function. The SetChanADC(unsigned char channel) function takes any one of
the predefined channels, e.g. AD0_CH1, as an input.

When you are finished with your conversions and wish to free up the analog input pins you have
been using, call the CloseADC() function. It takes no parameters.

Figure VIII-9 shows an example of the code to start ADC and collect a value from AN0 followed
by a value from AN1.

#include <xc.h>
#include “..\Common\osc.h”

void main (void)


{
int firstADCvalue, secondADCvalue;
set_osc_32MHz(); // change the internal oscillator frequency

// Configure pins AN0 and AN1 only for ADC operation using
// VDD and VSS as the references. The digital value is right
// justified. The other values are default settings
OpenADC( ADC_FOSC_32 & ADC_2_TAD & ADC_RIGHT_JUST,
ADC_CH0 & ADC_VREFPLUS_VDD & ADC_VREFMINUS_VSS, 13);
_delay(50); // Delay for 50TCY to stabilize
while(1)
{
SetChanADC(ADC_CH0); // Read from pin AN0
ConvertADC(); // Start ADC operation
while( BusyADC() ); // Wait for completion
firstADCvalue = ReadADC(); // Read result

SetChanADC(ADC_CH1); // Read from pin AN1


ConvertADC(); // Start ADC operation
while( BusyADC() ); // Wait for completion
secondADCvalue = ReadADC(); // Read result
}
}

Figure IX-9: Sample two channel ADC.


code.

160
Lectures - IX ADC

ADC and Transducers


Transducers are electronic devices whose physical properties, usually voltage output or resistance,
changes with variations in its surroundings, such as temperature, pressure, ambient light, and the
like. Often the voltage output is quite small, on the range of millivolts or less, and thus needs to be
amplified for most uses.

A photoresistor is an electronic component whose resistance decreases with increasing incident


light intensity. It can also be referred to as a light-dependent resistor (LDR), photoconductor, or
photocell.

A photoresistor is made of a high-resistance semiconductor. When no light is shining on it, its


resistance is very large, as much as 10 M. If light of high enough frequency shines on the device,
photons absorbed by the semiconductor give bound
electrons enough energy to jump into the conduction VDD
band. The resulting free electron (and its hole partner)
conduct electricity, thereby lowering resistance. Under 10K
bright light, the resistance can drop to about 100 . AN0

ADC measures voltages, not resistance, so we would


LDR
need to put the photoresistor in a voltage divider
circuit, Figure VIII-10. Under no light, LDR is millions
of ohms, so the potential at ANO would be almost VDD. Vss
Under the brightest conditions, the LDR has very small
resistance, so the potential at ANO would be almost Figure IX-10: LDR in a voltage
VSS.
divider circuit.
In the lab, you will be working with an infrared (IR)
reflective sensor (RLS). The RLS is a package containing an LED that emits IR light, and an IR-
sensitive phototransistor to detect and amplify any reflected light that falls on the phototransistor.
In operation, the phototransistor is much
VDD
like a photoresistor, but the physics is
much different. A phototransistor can be
thought of as a photodiode and an 270  10K
amplifier. A photodiode is the inverse of
an LED. An LED emits light when a QRD1114 AN0
current passes through it; a photodiode
generates a current when light falls on it. LED Phototransistor
This current is small, so it needs to be RLS

amplified. Figure VIII-11 shows how the


RLS is connected to an ADC pin on the
MCU. The right-hand side is much like the Vss
LDR in the voltage divider circuit. With
no light, there is no current to amplify and Figure IX-11: Connecting a RLS to the
the phototransistor is effectively an Breadboard.
extremely large resistance. Hence, the
voltage at the ADC pin will be large. Conversely, bright light produces the most current, which is

161
Lectures - IX ADC

then amplified. The resistance of the phototransistor is effectively very small, so the voltage at the
ADC pin will be small.

A plot of the measured voltage versus the physical property, such as light intensity, is often a
nonlinear curve. Manufacturers may provide a formula, P = f(V), so you can measure the physical
parameter P. More often, there is only a general curve, and it is necessary to measure one or two
known values of the parameter to calibrate the curve. For example, if you have a thermocouple or
a thermistor for measuring temperature, then ice water at 0° and boiling water at 100° make
excellent references.

Q. What happens if we swap the location of the phototransistor and LED with the 10 K and 270
 resistors? Will the RLS still work? What will the ADC values be over white paper and over
black electrical tape?

162
Lectures - X Robot

Notes X Robot

In your robot project, you will incorporate the major concepts you have already encountered –
PWM, ADC, and DIO – to program a robot to follow a black line around a track. A schematic of
the robot as seen from above is shown in Figure IX-1. The robot senses its environment though an
array of 5 reflective infrared light sensors (RLS). Each sensor is connected to an analog input pin.
The ADC module can then be used to get a reading from each sensor. If a sensor is over a black
line, the reading will be quite different than when it is over white space. In general, you set a
threshold value for this reading and assume values higher than the threshold indicate that sensor is
currently above a black line. There are also 5 LEDs on the top of the robot chassis. Most programs
for the robot use these LEDs as indicators. The typical use is to have an LED light up when a
corresponding RLS finds a black line. This can be a useful programming and diagnostic tool. For
example, moving the robot over an intersection of black lines and watching the LEDs will help
you figure out the sensor pattern that indicates an approaching intersection. You could also, for
instance, have the LEDs flash on and off when it enters particular portion of the programming
code. The robot has two independent motors to drive its wheels. A PWM signal dictates how fast
each wheel rotates, and a DIO signal controls whether each wheel turns forward or reverse. In your
project, you need to write code that uses the sensor data to control the speed and direction of wheels
to keep the robot on track. The pin connections for the robot are shown in Figure IX-2 on the next
page.

Back

LED4 LED2
LED5 LED3 LED1

Right Left
Motor Motor
PIC18F4525

RLS sensors
left center right
center center
left right

Front

Figure X-1. Robot from above.

163
Lectures - X Robot

Inputs Outputs
Right Motor

Left RLS RC2/CCP1 duty cycle


AN0
RC5 direction
CntLeft RLS RE0 dynamic braking
AN1
Left Motor
Center RLS AN2
RC1/CCP2 duty cycle
CntRight RLS AN3 RC0 direction
RE13 dynamic braking
Right RLS AN5
+5 V
RD0 LED1
+5 V
RD1 LED2
+5 V
RD2 LED3
+5 V
RD3 LED4
+5 V
RD4 LED5

RX TX

PC COM port
if necessary

Figure X-2. Robot pin connections.

Four library files are available to make programming the robot easier. The first two are
interrupts.c and interrupts.h. These files are involved in robot functions that we will not be
covering in this course. These functions help keep your robot code functioning smoothly even if
the batteries are running down or there is a voltage spike. The other two library files, sumovore.c
and sumovore.h, contain the basic functions and macros that you need to run the robot. These
functions and macros are based on chip operations that we have covered, such as DIO and ADC.
Other functions watch for voltage problems that cause unexpected exits from code, and provide
error messages about these problems via the LEDs. You will need to understand most of this code.
The function initialization() configures the pins, readies the USART module for serial port
communication at 19200 bits per seconds, configures the ADC module to use pins AN0 to AN5,
and configures the PWM module, including setting the length of the PWM period, and setting the
PWM duty cycle to zero. In effect, the only code you will need to write will be to tell the robot
what to do when.

164
Lectures - X Robot

Controlling the LEDs

The header file sumovore.h also contains macro aliases that simplify turning the indicator LEDs
on and off. The aliases are very useful for making your code easier to read, by swapping an arcane
term for something easier to understand. For example, in Figure IX-2 above, an LED – called
LED1 – is connected between pin RD0 and +5 Volts. When the output pin is low, current will flow
through the diode in the correct direction, and the diode will light up. Provided that you configured
the pin as an output, which is done for you in the function initialization(), you would light
the LED by using the command

PORTDbits.RD0 = 0; // turn LED1 on

However, the header file defines a function-like macro, setLED1(a), where a can be ON or OFF.
A function-like macro is more complicated than the macro aliases we have used, but still easy to
use. Note we also have macros aliases of ON for the value 1, and OFF as an alias for the value 0.

#define setLED1(a) PORTDbits.RD0=~a


#define ON 0b1
#define OFF 0b0

So to turn LED1 on, you simply type

setLED1(ON); // meaning is now self-evident

On compilation, this is replaced by PORTDbits.RD0=~1 since ON is 1. This in turn is equivalent


to PORTDbits.RD0 = 0, as required. The other macros for the LEDs are named similarly.

Furthermore there is a function-like macro

#define set_all_LEDs(a) PORTD=(((~a)&0b00011111)|(PORTD&0b11100000))

that lets you set or clear all the indicator LEDs at once

set_all_LEDs(0b00010); // LED2 on and the rest off

The logic of the macro keeps the three bits that we don’t use from being changed.

Working with the RLS

The sumovore library also tries to make using the reflective infrared light sensors (RLS) easier.
The ADC module is configured for you in the function initialization(). Using the ADC
module is simplified to a single function, unsigned int adc(unsigned char channel), that
returns the ADC value from 0 to 1023. The channel names ADC_CH0 to ADC_CH5 have been
assigned macro aliases corresponding to the RLS sensor locations

165
Lectures - X Robot

#define RLS_LeftCH0 ADC_CH0 // pin AN0


#define RLS_CntLeftCH1 ADC_CH1 // pin AN1
#define RLS_CenterCH2 ADC_CH2 // pin AN2
#define RLS_CntRightCH3 ADC_CH3 // pin AN3
#define RLS_RightCH4 ADC_CH4 // pin AN5

So, to get the ADC reading from the centre left RLS, we use the code

reading = adc(RLS_CntLeftCH1);

The ADC reading returns a number from 0 to 1023 that rates the blackness of the path under the
sensor. This value is then compared to a threshold value to determine if the sensor is over the black
line or not. The default threshold is 512, and you may need to change this, depending on your
robot’s sensors’ sensitivity. To read the sensors all at once, simply use the provided function
check_sensors().

The information from the ADC readings is a set of five elements consisting of 0s and 1s that you
will use to guide the robot’s motion. That information needs to be stored in a variable so you can
do comparisons. We would like the flexibility to work with either the individual elements or all
the elements as a set. To this end, a structure and a union have been created for you to create a
flexible variable called SeeLine.

struct sensors
{
unsigned Left:1;
unsigned CntLeft:1;
unsigned Center:1;
unsigned CntRight:1;
unsigned Right:1;
unsigned :3;
};

union sensor_union
{
unsigned char B;
struct sensors b;
};

extern union sensor_union SeeLine;

The union is configured so SeeLine has two members, B and b. Member B is of size char and thus
can hold all the information at once. However, only the five least significant bits hold sensor data.
The three most significant bits are not used. The other member of SeeLine, b, is of type sensors,
where sensors is a structure containing five member elements that will hold the individual ADC
comparisons to the threshold. The five member elements of the structure are named Left,
CntLeft, Center, CntRight, and Right and will only have values of 0 or 1. The member elements
of the structure b, itself a member of SeeLine, can be accessed by the member of operator, “.”,
e.g. SeeLine.b.Left.

166
Lectures - X Robot

The function sensor_check() generates the values that are held by variable SeeLine by
comparing the ADC reading to the threshold, as shown in the code below.

SeeLine.b.Left = ( adc(RLS_LeftCH0) > threshold ); // =1 if line below


SeeLine.b.CntLeft = ( adc(RLS_CntLeftCH1) > threshold );
SeeLine.b.Center = ( adc(RLS_CenterCH2) > (threshold) );
SeeLine.b.CntRight = ( adc(RLS_CntRightCH3) > threshold );
SeeLine.b.Right = ( adc(RLS_RightCH4) > (threshold) );

Note that since we are dealing with a union, the above code has also set the value of SeeLine.B.

The value of threshold is set in sumovore.c by the line

threshold = THRESHOLD_DEFAULT;

where THRESHOLD_DEFAULT is defined to be 512 in sumovore.h. You can change this value if
necessary.

Once the RLS are polled, i.e. the data is stored in variable SeeLine, that data is usually used to set
the indicator LEDs on top of the robot. The function-like macros setLEDn(), defined in
sumovore.h, contains code to do this individually

setLED1(SeeLine.b.Left);
setLED2(SeeLine.b.CntLeft);
setLED3(SeeLine.b.Center);
setLED4(SeeLine.b.CntRight);
setLED5(SeeLine.b.Right);

or, as a group, all at once using

set_all_LEDs(SeeLine.B);

For example, if only the left sensor was over a line, SeeLine.B would have the value 0b00001
and set_all_LEDs(SeeLine.B) would turn on LED1. Note if you look at the indicator LEDs
using the orientation shown in Figure IX-1, you can read the binary value of SeeLine.B from the
on/off settings of the LEDs. In the figure, SeeLine.B = 0b00010. Be careful! If you accidently
flip the robot around, as if you turned Figure IX-1 upside down, you reverse the positions of the
LEDs and the value would appear to be 0b01000 instead.

Motor and Wheel Control

We will use PWM to control the two electric motors that each control one wheel. Again, the
function Initialization() configures the PWM module and the pins used in PWM.
Furthermore, the motor control pins also have macro aliases based on their function. If pins RC1
and RC2 are set, the PWM signal is sent to the motors, so these pins turn the right and left motors
on and off. The aliases from the sumovore.h are

167
Lectures - X Robot

#define EnableRmotor PORTCbits.RC2


#define EnableLmotor PORTCbits.RC1

Pins RC0 and RC4 control the spin direction of the motors, on for forward and off for reverse,
e.g.

#define RmotorGoFwd PORTCbits.RC4


#define LmotorGoFwd PORTCbits.RC0

Pins RE0 and RE1 are used for dynamic braking. If you merely cut voltage to an electric motor,
the motor and thus the wheel will continue turning due to inertia. Dynamic braking occurs when
the motor leads are shorted together. This can be achieved by grounding both sides of the motor.
The braking is much closer to instantaneous.
#define RmotorGoFwdCmp PORTEbits.RE0
#define LmotorGoFwdCmp PORTEbits.RE1

These four macros are used in several functions, set_motor_speed() and motors_brake_all(),
contained in sumovore.c, that give you full control over the robot’s motion. You should not need
to access these macros directly. The function motors_brake_all(), as the name suggests, stops
the robot very quickly.

The motor speed and direction is set for each wheel individually by the function
set_motor_speed(enum motor_selection the_motor, enum motor_speed_setting
motor_speed, int speed_modifier). The enum keyword in the first two parameters of the
function is a way of assigning meaningful names or aliases to the members of a numerical list. The
first enumerated list, which determines which motor to activate and which wheel to turn,

enum motor_selection { left, right };

has default values of left as 0 and right as 1.

In the second enumerated list, we have


enum motor_speed_setting { rev_fast, rev_medium, rev_slow, stop, slow,
medium, fast };

but the statement in the function set_motor_speed()

const static int motor_speeds[] = { -800, -600, -400, 0, 400, 600, 800};

sets the value of rev_fast to –800 rather than the default 0, and so on. The rev_ prefix in the list
names is short for reverse. The numerical values actually refer to the PWM duty cycle. The PWM
period is 0.400 ms. For this period, the duty cycle for the wheels – as determined by the dutycycle
length, which can be 0 to 800 – are stop (0 or 0%), slow (400 or 50%), medium (600 or 75%), and
fast (800 or 100%). You should not need more speeds, however the variable speed_modifier lets
you increase the duty cycle length if it is a positive number, or decrease the duty cycle if it is a
negative number.

168
Lectures - X Robot

Most of your battery energy is going into turning the wheels. Under certain circumstances, this
load can drop the voltage across the MCU. This can lead to erratic behavior or program
termination. Extra code, using functions we have not covered in this course, are present to try to
prevent erratic behaviour.

main.c &motor_control.c

The sample files main.c and motor_control.c are provided to show you the bare bones of
making the robot follow a simple curved black line using the functions described above. After
calling the initialization function, it uses an infinite while loop to read the sensors, adjust the
indicators LEDS based on the sensor readings, and then hands off control of the motors to a
function called motor_control(), defined in the library motor_control.c and
motor_control.h. The other functions present are to prevent erratic behaviour due to voltage
spikes. The motor_control() function handles five cases best shown through a set of diagrams,
Figure IX-3 , that indicate what the sensors are reading.

The goal of the program is to keep the centre sensor centered on the black linek as shown in (c) in
Figure IX-3. When such a sensor reading occurs, the program has both wheels move forward at
fast speed. When the Left sensor is activated, as in (a), the program has the left wheel reverse at
fast speed while the right wheel continues forward at fast speed. This makes the robot twist sharply
to the right. If instead the CntLeft sensor is set as in (b) below, less of a correction is needed. The
left wheel is stopped – not reversed – while the right wheel continues forward as before. The robot
rotates right, but not as much as in the previous case. The case for (e) is the mirror image of (a),
so the motion of the wheels is just reversed. The same is true for case (d) with case (b). The code
is shown below

void motor_control(void)
{
// very simple motor control

if ( SeeLine.b.Center ) straight_fwd();
else if (SeeLine.b.Left) spin_left();
else if (SeeLine.b.CntLeft) turn_left();
else if (SeeLine.b.CntRight) turn_right();
else if (SeeLine.b.Right) spin_right();

if (SeeLine.B == 0b00000) motors_brake_all();


}

There is a sixth case, in which the sensors are not reading any line, and dynamic braking is called.

It is important to note that the ADC module completes measurements very rapidly and the motion
of the wheels only lasts as long as it takes to run through the if else statements and rerun the
ADC to get a new sensor status. It is quite possible for the robot to be making hundreds of course
corrections each second.

169
Lectures - X Robot

(a) (b) (c) (d) (e)

Figure X-3: Following a simple curve.

The program is a good starting point but is ill-suited to any more complicated cases. You will need
a much larger and more sophisticated motor control decision tree to get your robot to follow
complicated tracks smoothly and without mistakes.

170

You might also like