|
Understanding & Programming the
PIC16F84
A beginners' tutorial
by
Jim Brown
BSc(Eng), HDipEdAd, GDE (Wits)
Page 1a
INDEX
To
understand how to program a microcontroller, you need input from many
different sources.
This includes ideas and discussions from different
instructors. Everyone provides a different point of view with different terminology to
describe a feature, especially
something as complex
as programming.
This article by Jim Brown covers many of the features of the PIC16F84
microcontroller and helps you get an overall picture of this amazing device. |
Contents
Introduction
What you need
What you should know
Introducing the PIC16F84
Architecture
Instruction Set
A simple PIC16F84 program
Using MPLAB to debug a program
Pause to reflect
The Instruction Set
Instruction format
The STATUS register
1: Move instructions
MOVF f,d (Move f)
MOVWF f (Move W to f)
MOVLW k (Move literal to W)
2: Clear instructions
CLRF f (Clear f)
CLRW (Clear W)
3: Arithmetic instructions
ADDWF f,d (Add W & f)
SUBWF f,d (Subtract W from f)
ADDLW k (Add literal & W)
SUBLW k (Subtract W from literal)
4: Logical functions
ANDWF f,d (AND W with f)
IORWF f,d (Inclusive OR W with f)
XORWF f,d (Exclusive OR W with f)
ANDLW k (AND literal with W)
IORLW k (Inclusive OR literal with W)
XORLW k (Exclusive OR literal with W)
COMF f,d (Complement f)
5: Decrementing & Incrementing
DEC f,d (Decrement f)
INC f,d (Increment f)
6: Bit setting & clearing
BCF f,b (Bit clear f)
BSF f,b (Bit set f)
7: Program control
GOTO k (Go to address)
CALL k (Call subroutine)
RETURN (Return from subroutine)
RETLW k (Return with literal in W)
RETFIE (Return from Interrupt)
8: Skipping instructions
DECFSZ f,d (Decrement f, skip if 0)
INCFSZ f,d (Increment f, skip if 0)
BTFSC f,b (Bit test f, skip if clear)
BTFSS f,b (Bit test f, skip if set)
9: Rotations & Swap
RRF f,d (Rotate right f thru carry)
RLF f,d (Rotate left f thru carry)
SWAPF f,d (Swap nibbles in f)
10: Sleep & Watchdog Timer
SLEEP (Sleep,)
CLRWDT (Clear watchdog timer,)
11: Miscellaneous
NOP (No operation)
OPTION (Not recommended)
TRIS (Not recommended)
Pause to reflect
Interrupts on the PIC16F84
What's an interrupt?
Types of Interrupt and the INTCON register
Servicing an Interrupt
Timers on the PIC16F84
The basic idea
The TIMER0 module
Using the timer's overflow
Using the EEPROM data memory |
Introduction
When I first started looking into programming the PIC16F84, I was daunted by what I saw.
It seemed so complex. The PIC16F84 data sheet from Microchip is quite a technical
document and the MPASM manual is a complete reference. Neither of these documents served as a tutorial,
which is what I needed. So this
article is a tutorial for programming the PIC16F84, covering both the instruction set and some MPASM
directives. Along the way, it covers the
PIC16F84 itself, in terms of registers, pins and so on.
At the end you will not be a fully-fledged '84 expert. But you will be quite comfortable with the
device.
What you need
First, you need to have some of Microchip's documentation. As a minimum, you'll need the
PIC16F84 data sheet. This contains all the real info you will need on the chip itself.
You should also have the MPASM User Guide. Both of these are available from Microchip's website, at
http://www.microchip.com, or from their annual CD 'Microchip
Technical Library'.
You could work through this article with just the above, but you will get a lot more out of it by
being able to experiment. So,
load a copy of MPLAB (for Windows) onto your PC.
This is their Interactive Development Environment (IDE), and contains an editor, assembler and a
simulator. Here, you can create your source code (editor), produce the executable code (assembler)
and then run it on your PC (simulator).
In the simulator, you can watch the program running, keeping an eye on the registers and even
simulate events like externally caused changes to the I/O pins. You really should get this, and
it's also available as above. Work through its tutorial before you start this
article and it will help you understand the points we will be covering.
The Simulator (Emulator) is fairly good for watching the registers and executing
a program in a step-by-step mode but it will not solve all your problems when a
fault occurs.
The single-step mode does not take into account the effect of an input on a
program and a delay routine must be skipped-over to prevent waiting hours for it to
execute.
That's all you need, but there's lots more you might like. Not least, is a
PIC16F84 chip and a programmer. The Multi Chip Programmer presented by
Talking Electronics is the cheapest kit on the market and comes with some downloadable
software for programming a whole range of chips, of which the PIC16F84 is
included.
Of extreme help, perhaps in the longer run, are Microchip's Application Notes
(ANs). These are available from them, and a handy source is the CD-ROM mentioned above.
These notes cover all sorts of uses of the PICs, with many helpful insights into real-world use.
The ANs are in Adobe's .pdf format: using Adobe Acrobat you can search through the ANs looking
for terms like 'motor' or 'serial' and read the ANs appropriate to your needs.
One of the notes, AN585 on real-time operating systems for the PIC, refers to Real
Time Programming - Neglected topics. I urge you to get your hands on a copy. It is a fascinating
tutorial on the whole subject of interrupts, closed-loop control, and the like.
What you should know
It's not really for me to say what you should know: I don't know who you are or what you do.
But, I guess you'll find it easier going if you
understand (or can find out about) basic computer terminology like bits, bytes & EEPROM
and concepts like binary and hex. Of course, if you're using the simulator on your PC, you
must feel comfortable with your PC and Windows, and have played with the IDE tutorial.
I certainly don't expect that you have any PIC16F84 knowledge - this tutorial is aimed at the absolute
beginner.
The PIC16F84
Architecture
A microcontroller such as the PIC16F84 is, by its nature, a complete computer on a chip.
It has:
1. a processor and
2. registers, as well as
3. program and
4. data memory.
This makes it different from a CPU, (Central Processor Unit) which has only the
processor and registers.
1: The PIC16F84 has an 8-bit processor, and that's all we need to know.
2: The 90 registers are the processor's internal working storage. These have names
such as (STATUS, PORTA etc). Some have special functions and we'll examine them later. PORTA,
for instance, contains the contents of one of the I/O ports, namely port A.
Of the 90 register, there are 68 general purpose registers, and we can consider these as ours
(as opposed to the processor's special ones) and use them as our 'scratch pad'.
3: The PIC16F84 has 1K program memory: that means there are 1024 program
locations. The memory goes from
000h to 3FFh: 3FFhex is 1023 in decimal so there are 1024 locations including
000.
4: The PIC16F84 has 64 bytes data memory, called EEPROM memory, which is used for storage of,
for instance, calibration
values of data collected from the outside world. We'll look at the use of this EEPROM
memory last in this
discussion.
In addition, the device includes four types of interrupt and 13 I/O pins.
5: Interrupts are a means of halting the execution of the program and attending
to an operation as defined by the interrupt. For instance, the press of a button
could interrupt the program's normal flow and perform some special operation. We'll examine the
PIC16F84's interrupt techniques later.
6: The use of the I/O pins is the key to the use of a device like the '84, since any process consists of 3
parts: the input to the process, the process itself, and the output. The
PIC16F84 has 13 pins configured as 2 ports: port A has 5 pins and port B has 8; and we'll
be using them later.
Instruction Set
There are 35 instructions in the PIC16F84 instruction set consist of an opcode and operand/s.
Basically, the opcode specifies what to do and the operand/s specify how or where.
These instructions are split into 3 groups:
byte-oriented, bit-oriented and literal & control. Later on we will
use each
of these operations.
For now, let's look at one of the instructions. Concentrate more on the format, rather than
the function which we'll see closely later. I've chosen the instruction
ADDWF. In this tutorial, I have adopted the Courier font below for all coding examples.
The syntax of this instruction is:
ADDWF f,d
Where ADDWF is the opcode and "f d" are the operands.
The letters ADDWF is called a mnemonic. This is an abbreviation that both the
computer and humans can understand. You will see this same layout in all the instructions.
This is an appropriate time to introduce the working register. This is a special register
(over and above those already mentioned) known as the W
register and it is where the arithmetic and logic unit (ALU) does the maths. Any instruction with
W in the name acts on W. The other registers are known as
files and we use the letter 'F' in the commands to indicate a file is being
accessed. So, we can understand the command ADDWF adds W to file F. But
what is file F?
Each file (other than W) is given a hexadecimal address. For instance, file 0C
is called "0C," file 10 is called "10" and file 2F is called
"2F." The small letter "h" is always added to indicate the file has
a hex value, such as file 10h as it really the sixteenth file in memory.
Some files have names, such as PORTA is 05h, PORTB is 06h and TRISA is 85h. The 'f' in the
instruction above is the actual address of the file. There is no file
called "F" or "f" This is a symbol to represent any of the
files. So, we would code the instruction: "add W to file 3C" as:
ADDWF 3Ch,d
Well, not quite - what's the 'd' for?
It is the destination of the result, and you'll see this in many instructions. Depending on
the requirement, the result of the instruction can be put into the working register
(W) or
file F itself.
If 'd' is 0, the result is in W,
If 'd' is 1, the result is in F.
The instruction can be be either of the following:
ADDWF 3Ch,0 ; adds W to file 3C, result in W
ADDWF 3Ch,1 ; adds W to file 3C, result in 3C
Let's write . . . .
A simple PIC'84 program
This sample program serves a number of purposes. Apart from showing how to use a few
instructions, it also introduces a couple of the assembler concepts and will also
shows some simple simulator techniques.
The program, Simple.asm is presented below; I'll walk you through it line by line.
Program 1: Simple.asm
;simple.asm
;to demonstrate the look of a program
; and introduce some instructions & directives
;***************** setup ***************************
processor 16F84 ;processor type
org 0010h ;origin of program in memory
w equ 0 ;for byte instructions, use w & f
f equ 1 ; instead of 0 & 1, it's much clearer
MyReg_1 equ H'10' ;position MyRegisters in memory
MyReg_2 equ H'15' ; at h'10' & H'15'
;***************** program ******************************
;We are going to load the accum (reg w) with
; values and perform some arithmetic operations in w
; using MyReg_1&2
movlw H'08' ;put value H'08' into w register
movwf MyReg_1 ;move contents of w to MyReg_1
; note - same as movwf 10h, since
; MyReg_1 and 10h are the same thing.
movlw 32h ;put 32h into w reg
addwf MyReg_1,f ;add contents of w to that of MyReg_1
; answer goes into MyReg_1, due to f.
movlw 92h ;put value 92h into w register
movwf MyReg_2 ;move contents of w to MyReg_2
; note - same as movwf 15h, since
; MyReg_2 and 15h are same thing.
movlw 26h ;put 26h into w reg
subwf MyReg_2,w ;subtract w from MyReg_2
; answer goes into w
end Let's examine this sample program. You
can verify the following by finding each directive or instruction in the PIC
Programming section of this e-magazine.
Anything after a ';' is a comment. Every programming book recommends the
creation of comments to explain what's going on.
In the section called 'setup', we meet three assembler directives:
"processor 16F84" tells the assembler which chip the program
had been written for. If this is incorrect,
the code will be assembled for the wrong processor.
"org" tells the assembler where to start putting the code in program memory.
You should put your code beyond 004h to be clear of the address where the
processor goes when an interrupt is detected. "equ" is an equivalence
or "equal to." It simply means that the items on either side of equ mean
the same thing. For instance, the ADDWF instruction expects a 0 or 1 in the 'd' place:
by equating a 'w' with a '0', it means we can use the 'w' in the instruction rather
than '0'. This is easier to remember during coding, and
when reading later. Similarly, by equating
MyReg_1 with 10h, every time we refer to register 10h, it is more easy to refer to
it by its meaningful, more easily remembered, name.
In the part called 'program', we meet a number of PIC16F84 instructions. Check the
full descriptions; we'll be looking at them
later.
MOVLW k causes the value k (eg 08h) to be put into the working register.
MOVWF f copies the contents of W into register f. Note that movwf
is strictly a misnomer, since this is really a copy (W is not cleared), not a
move (in which W would be emptied). Note too, Microchip's convention
of comparing movlw and movwf: in describing the operation, the parentheses
() mean 'the contents of'. Thus k ® (W)
means the value k becomes the contents of W; (W)
® (f) means that the contents of W
become the contents of f.
Lastly, be sure you understand the use of the equ concept with respect to registers. The 'f' in
MOVWF f refers to a file. It
shows that files have hex addresses and we would expect the instruction to read MOVWF
10h for instance. Equating
MyReg_1 with 10h, means we can write MOVWF MyReg_1 with the same result.
ADDWF f,d and SUBWF f,d respectively perform the arithmetic addition &
subtraction on the contents of W and f. Note the use of the equivalencies here too; we can
refer to
MyReg_1 as before, and also replace the allowed values of d (ie, 0 &1) with w & f
respectively.
Hence we can write ADDWF
MyReg_1,f in place of ADDWF 10h,1.
Let's assemble and run this program . . .
Using MPLAB to debug the program . . .
This is a three step process: edit the source code, assemble it, and then run the simulator
to see what happens. If you worked through the MPLAB tutorial, you will already know about
editing and assembling. I'll assume you have and do, and so we can move to the simulation.
With sample .asm successfully assembled, and the editor being the only window open in MPLAB,
single-step through the program (the footstep icon). Not too
helpful - you see each line of the program being highlighted as it is activated, but that's it.
So, let's start to use a few of the simulator's other facilities. I suggest you open the
following other windows in MPLAB. They all serve similar
purposes - namely to see what's happening in the registers - but all implement differently.
I found it useful to have them all open at once, to compare the implementations:
Window > File Register
This window lists the contents of all file registers from 00 to 4F- ie, it does not show
those in the region 80 to 8B. As you step through the program, you will be able to see the
hex contents of registers 10h and 15h change. Note that any register whose contents have
just changed is shown in
red - this goes back to blue on the next step (unless there's another change).
Window > Special Function Registers
Here you can see the SFRs by name, rather than location which is what the previous window shows.
The contents are shown in decimal, hex and binary.
Window > New Watch Window
This is your custom window into the registers, and it allows you to select which registers
you want to monitor. All registers are available to choose, both SFRs and GPRs. When you
choose New Watch Window, you are prompted with an input box and a down arrow. Click the
arrow, and a list of all available registers appears: choose the one you want (eg,
Myreg_1) and then OK. You'll see the contents of the register displayed in the window. To
add other registers you can either click in the watch window's top-left-corner icon, and
then choose Add Watch; or just press the Insert key (while the watch window is selected).
You can save often used combinations as .wat files, and then load them back later with
Window > Load Watch Window.
A word of warning . . . Where did the list of register names to select from, come from?
The SFRs are obvious: the MPLAB knows about them anyway. The others, like
MyReg_1&2, come from the equ directives in your code, which is great. However, in our
example, we have also used w equ 0 and f equ 1 although these are not intended as register
names. However, they still appear in the list of possibilities. So what's the
problem - just don't choose them you say. But don't forget that W is a register: so now there are
two W entries in the list, one was there all along (the working register), and we
created the other with the equ. Using either of them, causes a symbol not found error in
your watch table, meaning that you cannot monitor the working register if you have declared a
W equ in your code. Try it yourself, then comment the offending line out with a ;, re-assemble
and try again. Now you can use the working register in the watch.
With these three windows, step through your program: you've now got some insight into what's
going on. Satisfy yourself that all is working
ok.
Pause to reflect
Where shall we go from here? Well, where are we now? We've looked at the architecture and
instruction set briefly; we've created a simple program and examined the code to get the
feel of it; we've edited & assembled this program; and we've seen some of the simulator's
facilities.
I think we should move on to look more fully at the instruction set.
The Instruction Set
Microchip has full details of the instruction set. They group it into 3 categories which I
don't find particularly useful. These are
Byte - and Bit-oriented and Literal/Control operations. I have taken the
view of grouping them into what we use each command for, such as performing arithmetic.
A windows help file is available on the Internet from Trisys Inc: I found this very useful.
It's a normal windows help file, with the expected hypertext links & searches.
For each instruction, I have explained the operation, and have expanded the explanations
where appropriate. (For instance, I have explained what an Inclusive-OR actually is in IORWF). Perhaps more importantly, I have suggested
exercises to use the instructions as presented.
The exercises suggested are quite
simple, but will let you use each instruction, and also practice using the simulator. Remember
to use things like the watch window to see what's going on as you step your program: keep an eye on
the registers you decide to use, as well as W and STATUS.
Instruction format
Microchip's grouping does keep instructions of similar format together. The instructions look
like this:
byte_command f,d
where f is the file register designation and d is the destination; if d=0, the result goes to
W. If d=1, the result goes to register f
bit_command f,b
where f is the file register, and b is the bit therein; bits are numbered from 0 on the right,
to 7 on the left. In the text, a bit is written as FILE_REG<n>, for instance INTCON<4> means bit
number 4 in the intcon register.
The INTCON register resides at location 0B
other_command k
where k is an 8-bit constant or literal.
The STATUS register
The '84 has the STATUS register at 03h, which contains the arithmetic status of the arithmetic & logic
unit. Many '84 instructions affect certain parts of STATUS. Many instructions affect STATUS<2>,
Z- the Zero flag, which gets set if the result of an operation was zero. Certain operations affect
the carry bits: STATUS<0>,
C - the Carry bit, is set if a carry-out occurs from the left-most bit in the result; STATUS<1>,
DC - the Digit Carry bit, is set if there is a carry between the hex digits (ie, from the right hand
nibble (bit 3) to the left hand nibble (bit 4). Two commands affect STATUS<3>, the Power Down bit PD,
and STATUS<4>, the Time Out bit TO.
1: Move instructions
We have met some of these instructions, which do nothing other than put things in registers.
MOVF f,d (Move f)
(f) ® (dest)
MOVWF f (Move W to f)
(w) ® (f)
MOVLW k (Move literal to W)
k ® (W)
Exercise: Write a program to use these commands to (for instance) put
something into W, move it from there to another register,
put something else into W, and then move the original thing back to W. See moves
.asm.
2: Clear instructions
These 2 commands clear a register.
CLRF f (Clear f)
00h ® (f)
CLRW (Clear W)
00h ® (W)
Exercise: Expand the above program to clear registers at the end. See clears .asm
3: Arithmetic instructions
Performing arithmetic is pretty important. The '84 can only add and subtract.
Arithmetic occurs either between W and an f register:
ADDWF f,d (Add W & f)
(W)+(f) ® (dest)
SUBWF f,d (Subtract W from f)
(f)-(W) ® (dest)
or between W and a literal:
ADDLW k (Add literal & W)
(W)+k ® (W)
SUBLW k (Subtract W from literal)
k-(W) ® (W),
Exercise: Use these and previous commands to load a register from W, and do some adding
and subtracting. Keep a close eye on the carry bit and on the zero bit in the status register.
See arith .asm
Technical aside: You may see that subtraction is performed using the 2's complement method.
This is the normal way that subtraction occurs in binary. I'll explain, then
show an example.
Firstly, express both numbers in binary. Leave the number from which you are taking away, unchanged.
Form the 2's complement of the one which is being taken away thus: change all 0s to 1s and
all 1s to 0s (this is the complement), add 1 to the right hand digit, carrying
to the left as necessary. Now add the result to the unchanged other number.
Discard the carry at the left, if there is one. That's the answer. Let's check . .
We want to subtract 20 from 27 - we should get 7. Proceed as follows:Convert to binary: 27 ® 11011 . . . . x
20 ® 10100 . . . . y
Make 2's complement of y: complement: 01011
add 1: 01100 . . . . z
Add x and z: + 11011 . . . . x
(1) 00111 = 7
The 1 in brackets is the carry, which gets discarded.
4: Logical functions
At this stage, before we examine the logical functions provided by the '84, we'll discuss
logical functions. Consider an electronic device with 3 wires attached. Let's say that 2 wires are inputs, and
output on the 3rd wire depends on the inputs. Further, we'll say that this is a digital device, so that the 3 wires can only have binary values of 1 & 0.
What relationships can exist between the 2 inputs and the output? The input combinations are easy:
00, 01, 10 & 11. Pin 3 can be 1 if and only if the inputs are both 1 (i.e: 11) or
if either or both inputs are 1 (i.e: 01, 10, 11), and other combinations.
The basic relationships are known as AND and OR. AND means both inputs while
OR means either or both. Most explanations of this resort to a TRUTH
TABLE, which is drawn below for the following logical operations: AND,
OR, XOR, NAND, NOR. I'll explain them once you've had a look at the table.
Inputs A B |
A AND B |
A OR B |
A XOR B |
A NAND B |
A NOR B |
|
both |
either,
both |
either,
not both |
not both |
neither,
not both |
00 |
0 |
0 |
0 |
1 |
1 |
01 |
0 |
1 |
1 |
1 |
0 |
10 |
0
|
1 |
1 |
1 |
0 |
11 |
1 |
1 |
0 |
0 |
0 |
The function AND means the result is 1 only when both inputs are 1. The OR
means that either input may be a 1 for output to be 1, but so may both. The function
XOR means exclusive or, and means that either input as 1 will cause a 1 to be output,
and specifically excludes the situation where both inputs are 1. Lastly, the
NAND and NOR are the negations of AND and OR respectively:
compare the columns and you'll see what this means.
By the way, the OR function (as opposed to the XOR function) is
sometimes known as the inclusive or
(IOR). The PIC16F84 uses this term.
The PIC16F84 provides a number of logical operations which act on two, 8-bit
values; these values are compared bit for bit. For
example - without looking at an '84
instruction - consider the ANDing of the numbers H'5F' (equivalent to D'95'
or B'01011111') and H'A3' (which is D'163' or B'10100011'); resulting in H'03'
(which is D'3' or B'00000011').
5F: 01011111
A3: 10100011
and: 00000011
Clearly, only in the rightmost 2 positions is the
AND satisfied. The result is 1, and 0 elsewhere.
Let's move to the instructions as provided by the 'PIC16F84.
Comparison occurs either between W and an f register:
ANDWF f,d (AND W with f)
(W) AND (f) ® (dest)
IORWF f,d (Inclusive OR W with f)
(W) OR (f) ® (dest)
XORWF f,d (Exclusive OR W with f)
(W) XOR (f) ® (dest)
or between W and a literal:
ANDLW k (AND literal with W)
(W) AND k ®
(W)
IORLW k (Inclusive OR literal with W)
(W) OR k ® (W)
XORLW k (Exclusive OR literal with W)
(W) XOR k ® (W)
Lastly, you may have noted that the '84 doesn't provide NAND or NOR
functions. Rather, it provides the means of complementing (negating) a register; this means to NAND
you must first AND and then:
COMF f,d (Complement f)
complement of (f) ® (dest)
Exercise: I suggest two things here. First, using Windows' calculator, verify the results
of the above: this will ensure you understand the concepts. Then, write an assembler program
to verify the '84 instructions, by loading the appropriate registers, performing the
operations and checking the results. See logic .asm.
5: Decrementing & Incrementing
Two simple instructions can decrement or increment the contents of a register, thus:
DEC f,d (Decrement f)
(f)-1 ® (dest)
INC f,d (Increment f)
(f)+1 ® (dest)
Exercise: In a program, perhaps by adding to one of the previous ones in which you do some
arithmetic or some logic, check out that these commands do work. Keep an eye on the Zero flag, which
is set if either command causes the register in question to go to zero: we'll rely on this fact in
two more commands later. See dec_int
.asm
6: Bit setting & clearing
Using the following 2 commands, you can set or clear any bit b in register f. But why? Two
reasons come to mind:
First, as an example, the register in question might be a port controlling some external
equipment. Each bit could be switching a different device: a motor, a light or whatever.
Setting and clearing each bit switches each device on or off.
Second, as we'll see in 8. below, we can use the fact that a bit is set or clear to skip
instructions in our program. Being able to set or clear any bit gives us the ability to
control this process.
BCF f,b (Bit clear f)
0 ® (f<b>)
BSF f,b (Bit set f)
1 ® (f<b>)
We read these two operations as 0 (or 1) becomes the content of bit 'b' in register
'f'.
Exercise: Put these commands into any of the programs above and watch the changes
to individual bits in your watch window. See bits
.asm
7: Program Control
For many reasons, we need to control the flow through our program. Normally, flow proceeds
linearly from the top; often this is not suitable.
Firstly, may we need to loop through certain instructions: we might have a system in which
one process is a continuous one. This might be the control of a bucket conveyor which
continuously adds ingredients to a vat. Here, when a bucketful is added, we take it
from the top and go through the same steps again, adding more on each pass.
Second, there may be part of our program which is useful in many other parts. Rather
than repeating this code in many places, we separate the piece from the rest
of the code; then we call it in as often as we like. The reusable
piece is called a SUB-ROUTINE. The subroutine returns control to the point from which it
was called when it's finished.
Let's look at looping first:
This instruction 'goes to k', which it does this by loading the address of k
into the program counter, PC. In order to use GOTO you should start where you want to go, with a
Label. Then you GOTO Label.
Exercise: Modify one of the programs you have already written. You could put a label
near the top, and a GOTO later on. As you step, you'll see from the highlighted line
your program is looping. Look at the program
counter (PCL) in a watch window and you'll verify this. See GOTO .asm
Now we shall examine subroutines. We need to understand the concept of the stack.
The stack - which has 8 levels in the PIC16F84 - is the place where the address
of the next instruction is placed, when a CALL instruction is met. As each
instruction is executed, it is the job of the Program Counter (PC) to know where
the microcontroller is, at any point in time. The placement of the next address
in the Stack tells the microcontroller where to go, after a subroutine
is finished.
We refer
to loading the stack as pushing and taking a value off later as popping. The stack is
only accessible at the top: it's like a pile of plates in a hopper. The
top of the stack is abbreviated to TOS.
There are 2 instructions associated with any subroutine - one to send the microcontroller
to the sub-routine,
the other to bring it back:
CALL k (Call subroutine)
(PC)+1 ® TOS,
k ® (PC)
The CALL to a subroutine pushes the current PC+1 onto the stack, then changes the PC to
the address k. This results in program flow jumping to the subroutine, but the address
to come back to later is safely held on the stack for later retrieval.
RETURN (Return from subroutine)
TOS ® (PC)
RETURN is the last instruction in the subroutine itself. The return instruction pops the
stack and so the program resumes in the right place. Think about the depth of the stack:
it means that calls can be nested, and flow will be correct as long as each
call is matched
by a return.
RETLW k (Return with literal in W)
k ® (W),
TOS ® (PC)
This returns with k ®
(W) added.
RETFIE (Return from Interrupt)
TOS ® (PC),
1 ® GIE
When an interrupt occurs, the PC is pushed to the stack like with a subroutine call, so
RETFIE pops it. Also, the occurrence of an interrupt disables further
interrupts: one doesn't want interrupts to be interrupted. What happens is,
the interrupt causes the global interrupt enable flag, GIE (INTCON<7>) to be set to 0.
Returning from an interrupt means that further interrupts should be allowed, hence
RETFIE sets GIE back to 1.
8: Skipping Instructions
There are 4 instructions that allow you to skip over the next instruction.
DECFSZ f,d (Decrement f, skip if 0)
(f)-1 ® (dest), skip if result = 0
INCFSZ f,d (Increment f, skip if 0)
(f)+1 ® (dest), skip if result = 0
The above commands are based on the DECF and INCF commands seen earlier. The sz part means
'skip if zero'.
Exercise: Verify these 2 instructions by loading a start value into a register called
'count.' Then use either instruction to change it, looping through this block of code.
Check to see if the instruction following the decfsz or incfsz (probably a GOTO, to cause the
looping) is skipped or not, as appropriate. See Skip .asm.
BTFSC f,b (Bit test f, skip if clear)
skip if (f<b>)=0
BTFSS f,b (Bit test f, skip if set)
skip if (f<b>)=1
Read these instructions as 'bit test f, skip if clear/set".
Exercise: Write a program with a block of code with a loop. Put a BTFSS at the bottom,
followed by a GOTO back to the top. Include some more code. The BTFSS will need to refer to one of the
PIC16F84's I/O ports as its f (H'05' or H'06'
for PortA & B respectively), and you can use any pin 0-4 on PortA, 0-7 on
PortB.
You should have another GOTO right at the bottom, to make an outer loop back to the
top so you can see the program loop to test
what happens when an input pin changes. See Bits .asm
But how do we get the pin to change state? There's an
easy way of doing this in MPLAB's simulator. Go Debug > Simulator stimulus > Asynchronous
stimulus; you'll get a table of Stim0 to Stim12 buttons. Right click on any one, perhaps Stim7,
and click Assign pin. Double click on the pin you've elected to use as the sensor, perhaps it
was RA3. Stim7 then changes to RA3 . Now right click on the button again: you can choose
to pulse, make low, make high or toggle the pin. Here, we would want to toggle the
pin.
Now we're all set to test. Step through your program. Depending on the initial state of the pin, which says is unknown at power up, the program will loop or not. (At
the inner loop, that is; it will always go back to the top from the very bottom, for testing
purposes.) Any time you like, left click on your chosen Stim button, now known as RA3. (If you have the port showing in a watch window, you'll see the pin change state
at the next instruction boundary). In any case, next time the program gets to the
BTFSS step, it should behave differently, since we've toggled the pin and so the
BTFSS should
get a different answer.
9: Rotations & Swap
Three instructions allow you to manipulate the bits within a register. Of these, two
slide the bits to the right or left (through the carry bit), the third switches the register's two
nibbles.
RRF f,d (Rotate right f thru carry)
Each bit in the f register is moved 1 to the right; the one that falls off the right
is circled round to the carry, and the carry moves in from the left.
RLF f,d (Rotate left f thru carry)
Each bit in the f register is moved 1 to the left; the one that falls off the left
moved into the carry, and the carry is circled round to the right.
SWAPF f,d (Swap nibbles in f)
(f<3:0) ® (dest<7:4>), (f<7:4>)
®
(dest<3:0>)
Exercise: Observe the effect of these commands on the contents a register.
See Rot_swap.asm.
10: Sleep & Watchdog Timer
These two commands are related. The WDT is one of the ways of waking up from sleep.
SLEEP (Sleep)
0 ® WDT,
0 ® WDT prescaler,
1 ® TO,
0 ® PD
CLRWDT (Clear watchdog timer)
0 ® WDT,
0 ® WDT prescaler,
1 ® TO,
1 ® PD
11: Miscellaneous
NOP (No operation) This instruction does nothing. It is very
handy for the creation of short delay intervals (1uS) to allow things to settle
down before another operation is executed.
TRIS 05 This instruction controls the direction of each line of the port
called Port A.
It is the Direction Control Register. There are 5 lines in port A,
called RA0, RA1, RA2, RA3 and RA4. Each line can be either input or output.
To make a line input, the corresponding bit in the TRIS register is made
"1." To make a line output, the corresponding bit is made
"0." Thus the value 01 will make RA0 an input and all other lines
output.
The value 23 (0010 0011) will make RA0 and RA1 input and bit5 will not have any
effect as no line corresponds to this bit!)
TRIS 06 This instruction controls the direction of each line in Port B.
There are eight lines in port B. RB0, RB1, RB2, RB3, RB4, RB5, RB6, and RB7. A
"0" in the TRIS register makes the corresponding line in port B and
output. A "1" in the TRIS register makes the line an Input.
This is easy to remember as "0" is similar to 0utput
and "1" is similar to 1nput.
You should not use the following instruction, which is included to provide backward compatibility:
Pause to reflect
We have now met each PIC16F84 instruction, with the necessary background if appropriate, and used them
in simple programs.
Now, we'll move on to explore two important areas of the PIC16F84: interrupts and timers.
These
two areas move the PIC16F84 into the real world.
Interrupts on the '84
What's an interrupt?
Put simply, it's exactly what it says: a means of getting the computer's attention.
Once an interrupt has been taken care of, control must
go back to where it was earlier. Not only that, but the state of the system must be
put back: if any registers were changed while handling the interrupt, they must be
restored. We'll look into this later.
Now we know what an interrupt is in concept, we can look at how the '84 allows for this notion.
Types of Interrupt and the INTCON register
The PIC16F84 has 4 different types of interrupt:
an external interrupt on pin 6, also known as: RB0
a change on any of pins 10-13, aka RB4-7
a timer overflow
an EEPROM write complete.
In order to allow interrupts, and to determine the source thereof, the processor uses
the INTCON register (0Bh). Each bit serves a specific purpose, and interrupts work according
to the value (set or clear) of the various bits.
Firstly, interrupts as a whole may be enabled or disabled by INTCON<7>, the GIE
(Global Interrupt Enable) bit. If this bit is clear, then no interrupts may occur:
this is the power-up value. Also, if an interrupt occurs, then GIE is
cleared to prevent the interrupt being interrupted; returning from the interrupt
with the RETFIE instruction re-enables interrupts.
Second, each of the interrupt types must be enabled with its own bit in INTCON, before it can be used:
external interrupt on pin 6: INTCON<4>, the INTE bit
change on any of pins 10-13: INTCON<3>, the RBIE bit
timer overflow: INTCON<5>, the T0IE bit
EEPROM write complete: INTCON<6>, the EEIE bit.
Third, when interrupts occur, certain bits (known as interrupt flags) are set so that
we may determine the source of the interrupt:
external interrupt on pin 6: INTCON<1>, the INTF bit
change on any of pins 10-13: INTCON<0>, the RBIF bit
timer overflow: INTCON<2>, the T0IF bit.
Why do we need to know the source? Well, depending on the type of interrupt, we will
take different action: checking the appropriate bit tells us which interrupt it is.
Note that the interrupt flags always get set when there's an interrupt, regardless of
the state of the corresponding enable bit.
Servicing an Interrupt
In the PIC16F84, all interrupts are sent to 004h.This point is the interrupt vector and the program is
said to vector to this address. At the vector, we must make sure we provide
the necessary code to take care of the problem.
A crucial point is that we might need to save the status of the machine as it was
before interruption, before we service the interrupt. Clearly, the activities
undertaken during servicing could change such things as the W & STATUS registers;
we would need these restored after handling the interrupt, in order that our other work
may resume. The PIC16F84 only saves the PC to the stack (PC+1, actually), and it's up to us
to ensure we save and restore anything else we need!
There are 3 parts to a program which must handle simple interrupts:
an initialize
section, where in our case we'll enable the interrupts;
a main section, where the bulk
of the time is spent (doing the payroll, or whatever) and
the handler part where the
interrupt is taken care of.
Initialize: set GIE, INTCON<7>, to enable interrupts set INTE, INTCON<4>, to enable pin 6 interrupt
Handler: save the status of the machine; probably W & STATUS check interrupt flags for source; INTF, RBIF, T0IF
branch to the correct 'sub-handler' for the interrupt type (we've only set INTE, though)
do whatever is required for the interrupt restore the status return
Main: do the payroll calculation, for instance.
Exercise: Start slowly with interrupts. This can get quite complex. Write a program to
provide the absolute minimum interrupt ability: enable them, have an interrupt service
routine that does little if anything other than re-enable interrupt and then return, and a main
routine that merely loops through some code waiting for an interrupt. You need
not save the status of the machine, nor check for the type of interrupt at this stage. In
order to see this program working properly in the simulator, I suggest using the interrupt
on change in port B, using the asynchronous stimulus technique discussed before. Use a
toggle on the pin to effect the change: while the program is looping in the main section,
toggle the pin and check that the interrupt occurs when you next step the program. While
the program is then in the ISR, toggle the pin again: if it's all working, the pin change
will not cause an interrupt, because GIE will have been cleared. See Inter1 . asm.
Exercise: Now add to the program, to save the state of the machine at the interrupt. To
test this, make sure you load W (say) in the main part, then change it in the ISR, and
check that it gets restored properly. See inter2.asm
Exercise: Finally, allow for more than 1 kind of interrupt - change on one of
PortB pin 4:7
as well as an interrupt on the RB0/INT pin. Now, you'll need to determine the kind of
interrupt, and handle it accordingly. See Inter3.asm.
Timers on the '84
The basic idea, is that we will want to use our PIC16F84 to time something. Time, that is, in
the way we would use a wall clock: we might switch on our fish-tank lights for instance,
or close the curtains at 18h00 each night. But there's no clock in the '84: no clock in
the wall clock sense anyway.
What there is though, is a simple relationship between the carrying out of instructions
and the speed with which the processor ticks away. An
instruction completes in 1uS when the chip is being clocked at 4MHz.
Let's verify this, and introduce a handy feature of MPLAB at the same time: the Stopwatch.
In MPLAB prepare to run any of the programs you have written. Open the stopwatch
by going Window > Stopwatch. You'll see a simple window, containing 3 important pieces of
information: the number of steps completed in a particular time, and the frequency. Check
that the frequency is set to 4MHz for now, click zero and step your program once.
You should see that you progress to 1 cycle in 1uS. (Unless this was coincidentally
a program branch, which is a 2-cycle instruction.)
So, we have the glimmerings of a wall clock type of timer. We know how much time has
passed if we know how many steps have occurred. Enter Timer0 . . .
The TIMER0 Module
The PIC16F84 timer, TIMER0, works on the basis that each time an instruction cycle occurs, a
certain interval of time has elapsed. When TIMER0 is running, its register (TMR0) is
incremented each cycle; each 1uS if the clock is a 4MHz one. Therefore, the value of
TMR0 represents the 'time' in 1uS steps. But TMR0, like all registers, is an 8-bit one
and can thus only count as high as FF: that means time only goes as far as 255uS,
which is not very long. Cast your mind back to the discussion of
interrupts earlier, where we said that one of the interrupt types was a timer overflow
interrupt: that means that the TMR0 register has gone over the top at FF and back to 00.
That means the interrupt occurs every 256uS of real time. It's up to us to do something
with this interrupt, which probably means incrementing a register of our
own.
Let's walk through the process of simply getting TIMER0 going, using the initialize, handler,
main interrupt technique used earlier.
Initialize: set GIE, INTCON<7>, to enable interrupts set T0IE, INTCON<5>, to enable timer interrupt
clear T0CS, OPTION<5>, to enable timer - see set PSA, OPTIOPN<3>, to keep prescaler out of this.
Handler: increment 'clocked', our overflow counter
Main: have a loop to keep the program going
Exercise: Write a program to implement the above simple timer, bearing in mind we won't be doing
anything with the timer overflows other than clicking them. Check the program in the
simulator, having visible any registers you think you should monitor, also the stopwatch. Before
you start, you'll need to figure out how to access the option register, which is not quite as
simple as accessing say status. I've explained it below. See Time1.asm.
Accessing registers in Bank1: You'll have noticed that some registers like
option are in what they refer to as Bank1, as opposed to Bank 0. Others, like status are in both
Banks. What this means in practical terms, is that these Bank 1 registers are not normally available,
because our default sphere of operations is Bank 0. We use the status register to effect a switch
between banks, with RP0 (STATUS<5>) doing the trick. This simply means
that we must set RP0 to go to Bank1 and clear it to return. For those registers which are in both
banks, you can access them no matter where you are, but remember the register has 2
different addresses: status is known as h'03' and h'83'.
Using the timer's overflow
Now we can probably see where we are going. We have a register which is updating whenever
the timer overflows. That's roughly every 256uS and it will overflow at 256.
We'll need to count these overflows too, into another register (file). Now, we're getting closer to
a second in timing, which is what we're heading for. How high do we need to count
the second file to
reach 1 second? Incrementing every .0256 seconds there are roughly 39 increments in a second
, so this means we can count real-time seconds by incrementing another register,
perhaps SECONDS, every 39 steps. To get to minutes and hours, we can simply overflow
SECONDS into MINUTES at 60 seconds and MINUTES into HOURS every 60 minutes.
Exercise: Create a program to implement the above, at least as far as the SECONDS register.
Using the EEPROM data memory
Appendix: Program listings
The following programs are intended as illustrative of the use of the commands discussed.
They are simple and don't show full programming technique. In each,
I have added a section called simulator. In this section is a suggestion for things you might
like to do during the simulation of your program. For instance, you might need to have a
watch window open showing the W register and PortB, as well as having the stack open too.
Of course, it's up to you how you design and code these programs, and how you experiment
with them in the simulator. Program 2: Moves.asm
;moves.asm to show how MOVF, MOVWF & MOVLW work
;*********************************** simulator ***
;watch window: reg1, w, pcl
;*********************************** setup ***
processor 16F84
reg1 equ h'10'
;*********************************** program ***
start: movlw h'05' ;load w
movwf reg1 ;move (w) to reg1
movlw h'82' ;change w
movf reg1,0 ;restore w
end
Program 3: Clears.asm
;clears.asm to show how clrf & clrw work
;based on moves.asm
;*********************************** simulator
;watch window: reg1, w, pcl
;*********************************** setup
processor 16F84
reg1 equ h'10'
;*********************************** program
start: movlw h'05' ;load w
movwf reg1 ;move (w) to reg1
movlw h'82' ;change w
movf reg1,0 ;restore w
clear: clrf reg1 ;clear reg1
clrw ;clear w
end
Program 4: Arith.asm
;arith.asm to show using ADDWF, SUBWF, ADDLW, SUBLW
;************************************* simulator
;watch window: reg1,reg2,status,w,pcl
;************************************* setup
processor 16F84
reg1 equ h'10'
reg2 equ h'12'
;************************************* program
loads: movlw d'20' ;load w
movwf reg1 ;load reg1
movlw d'80' ;load w anew
movwf reg2 ;load reg2
arith: addlw d'05' ;add d'05' to w
sublw d'100' ;sub w from d'100'
addwf reg1,1 ;add w to reg1, into reg1
subwf reg2,1 ;sub w from reg2, into reg2
end
Program 5: Inter1.asm
;inter1.asm is a simple interrupt handler-
; it does not save the state of the machine
; nor does it determine the kind of interrupt.
;*********************************************simulator
;watch window: intcon, pcl
;stack: have this open too - see the return address come & go
;asynch stimulus: have a toggle on rb4
;********************************************* setup
processor 16F84
movlw h'0'
movwf h'0b' ;clear intcon
goto main ;hop over the isr
;********************************************** isr start
isr: org h'0004' ;interrupts always vector to here
nop ;here we actually do the stuff
nop ; of the interrupt
bcf h'0b',0 ;clear int before going back
retfie ;this re-sets intcon gie
;*********************************************** isr ends
;*********************************************** main start
main org h'0020' ; leave enough room for the isr!
bsf h'0b',7 ; set global int enable
bsf h'0b',3 ; set change on b int enable
payrol nop ; loop here until interrupted,
nop ; doing payroll stuff
goto payrol
nop
nop
end
Program 6: Inter2.asm
;inter2.asm saves the state of the machine
; but does not determine the kind of interrupt.
;
;****************************************************simulator
;watch window: intcon, pcl, portb, w, w_saved
;stack: have this open too - see the return address come & go
;asynch stimulus: have a toggle on rb4
;**************************************************** setup
processor 16F84
w_saved equ h'10' ;a place to keep w
movlw h'0'
movwf h'0b' ;clear intcon
goto main ;hop over the isr at the beginning
;**************************************************** isr start
isr org h'0004' ;interrupts always vector to here
movwf w_saved ;save w as it was in main
movlw h'65' ;do something to change w
nop ;do more isr stuff
;now, restore w: there is
; no "movfw"- 2 swapf's seems to be
; the way to do this....
swapf w_saved,1 ; first, swap w_saved into itself
swapf w_saved,0 ; then, swap it into w
bcf h'0b',0 ;clear int on b before going back
retfie ;this re-sets intcon gie
;***************************************************** isr ends
;****************************************************** main start
main: org h'0020' ; leave room for the isr!
bsf h'0b',7 ; set global int enable
bsf h'0b',3 ; set change on b int enable
payrol nop ; loop here until interrupted,
nop ; doing payroll stuff
movlw h'0f' ; simulate a calc by loading w
goto payrol
nop
nop
end
Program 7: Inter3.asm
;inter3.asm saves the state of the machine
; and determines the kind of interrupt.
;
;**************************************************simulator
;watch window: intcon, pcl, portb, w_saved, w
;stack: have this open too - see the return address come & go
;asynch stimulus: have a toggle on rb4(for rbi) and rb0(int)
;************************************************* setup
processor 16F84
w_saved equ h'10' ;a place to keep w
movlw h'0'
movwf h'0b' ;clear intcon
goto main ;hop over the isr at the beginning
;*************************************************** isr start
isr org h'0004' ;interrupts always vector to here
movwf w_saved ;save w as it was in main
;find out what kind of interrupt
btfsc h'0b',0 ; is it an rbi?
call _rbi ; yes- call the rbi 'sub-handler'
btfsc h'0b',1 ; is it an int?
call _int ; yes- call the int 'sub-handler'
;end up here after sub-handler
;now, restore w
swapf w_saved,1 ; first, swap w_saved into itself
swapf w_saved,0 ; then, swap it into w
bcf h'0b',0 ;clear rbif before going back
bcf h'0b',1 ;clear intf before going back
retfie ; this re-sets intcon gie
_rbi movlw h'65' ;do something to change w
return
_int movlw h'24' ;do something different to w
return
;***************************************************** isr ends
;
;**************************************************** main start
main org h'0020' ; leave room for the isr!
bsf h'0b',7 ; set global int enable
bsf h'0b',3 ; set change on b int enable
bsf h'0b',4 ; set ext int on pin6 enable
; we've got 2 types of int!
payrol nop ; loop here until interrupted,
nop ; doing payroll
movlw h'0f' ;simulate a calc by loading w
goto payrol
nop
nop
end
Program 8: Time0.asm
;time0.asm to see how timer0 works
;************************************ setup
processor 16F84
STATUS EQU H'03'
OPTIO EQU H'81'
tmr0 EQU H'01'
BANK_ EQU H'05'
T0CS EQU H'05'
PSA EQU H'03'
clocked equ h'10'
INTCON equ h'0B'
T0IE equ h'05'
GIE EQU H'07'
goto top
;************************************ isr
isr ORG h'0004'
incf clocked ;clocked is the 'wall clock'
BCF INTCON,2
retfie
;************************************ program
top org h'0010'
clrf tmr0 ;clear the tmr0 register
clrf clocked
mode bsf STATUS,BANK_ ;switch to page 1
bcf OPTIO,T0CS ;go from counter to timer
bsf OPTIO,PSA ;no prescaler yet
bsf INTCON,GIE
bsf INTCON,T0IE
loop nop
nop
goto loop
END
NEXT
|