Complete Beginner
Michael A. Covington
Artificial Intelligence Center
The University of Georgia
Athens, Georgia 30602-7415
http://www.ai.uga.edu/mc
These days, the field of electronics is divided into haves and havenots people who can program microcontrollers and people who cant.
If youre one of the have-nots, this article is for you.
1
Microcontrollers are one-chip computers designed to control other equipment, and almost all electronic equipment now uses them. The average
American home now contains about 100 computers, almost all of which
are microcontrollers hidden within appliances, clocks, thermostats, and
even automobile engines.
Although some microcontrollers can be programmed in C or BASIC,
you need assembly language to get the best results with the least expensive
micros. The reason is that assembly language lets you specify the exact
instructions that the CPU will follow; you can control exactly how much
time and memory each step of the program will take. On a tiny computer,
this can be important. Whats more, if youre not already an experienced
programmer, you may well find that assembly language is simpler than
BASIC or C. In many ways its more like designing a circuit than writing
software.
The trouble with assembly language is that its different for each kind
of CPU. Theres one assembly language for Pentiums, another for PIC microcontrollers, still another for Motorola 68000s, and so forth. There are
even slight differences from one model of PIC to another. And that leads
to a serious problem each assembly-language manual seems to assume
that you already know the assembly language for some other processor!
So as you look from one manual to another in puzzlement, theres no way
to get started.
Thats the problem this article will address. I wont teach you all of
PIC assembly language, just enough to get you started. For concreteness,
Ill use just one processor, the PIC16F84. To be very precise, Ill use the
Note added 2004: The 10-MHz version is now more common and will work in all the
same circuits.
PIC16F84
1
2
3
4
5
6
7
8
9
A2
A1
A3
A0
A4
O1
MCLR
O2
GND
V+
B0
B7
B1
B6
B2
B5
B3
B4
18
17
16
15
14
13
12
11
10
8 - BIT BUS
CLOCK
OSCILLATOR
O2
SPECIAL
FUNCTION
REGISTERS
PORT A
PORT B
A4 A3 A2 A1 A0
B7 B6 B5 B4 B3 B2 B1 B0
CPU
W REGISTER
O1
FILE
REGISTERS
(RAM)
14 - BIT BUS
PROGRAM
MEMORY
(FLASH EPROM)
PIC inputs are CMOS-compatible; PIC outputs can drive TTL or CMOS
logic chips. Each output pin can source or sink 20 mA as long as only one
pin is doing so at a time. Further information about electrical limits is
given in the PIC16F84 data sheet.
The F84 also has some features we wont be using, including an EEPROM for long-term storage of data, an onboard timer-counter module,
and optional pull-up resistors on port B.
ON/
OFF
1.5V
1.5V
1.5V
ON/
OFF
1N4001
6V OR
FOUR 1.5V
IN SERIES
+4.5V
+5.4V
+4V TO 6V
7805
OR 78L05
+7V
TO 20V
IN
OUT
PIC16F84
10K
4
+5.0V
GND
MCLR
GND
V+
14
0.1
F
+6V
TO 20V
1K
1N4733A
(5.1V)
+5.1V
(SEE
NOTE)
Figure 3: Some ways to power a PIC. The last one is only for a PIC that is
not powering an LED or other high-current load.
Like any CPU, the PIC needs a clock an oscillator to control the speed
of the CPU and step it through its operations. The maximum clock frequency of the PIC16F84-04P is, as already noted, 4 MHz. There is no lower
limit. Low clock frequencies save power and reduce the amount of counting the PIC has to do when timing a slow operation. At 30 kHz, a PIC can
run on 0.1 mA.
Figure 4 shows the most popular clock circuits. The clock signal can be
fed in from an external source, or you can use the PICs on-board oscillator with either a crystal or a resistor and capacitor. Crystals are preferred
for high accuracy; 3.58-MHz crystals, mass-produced for color TV circuits,
work well and are very cheap. The resistor-capacitor oscillator is cheaper
yet, but the frequency is somewhat unpredictable; dont use it if your circuit needs to keep time accurately.
PIC16F84
O1
O2
16
15
EXTERNAL CLOCK
NC
+5V
3.3K (1.5 MHz)
10K (600 kHz)
100K (100 kHz)
PIC16F84
O1
O2
16
15
100
pF
NC
PIC16F84
O1
O2
16
15
2-4
MHz
22
pF
22
pF
; File TURNON.ASM
; Assembly code for PIC16F84 microcontroller
; Turns on an LED connected to B0.
; Uses RC oscillator, about 100 kHz.
; CPU configuration
;
(Its a 16F84, RC oscillator,
;
watchdog timer off, power-up timer on.)
processor 16f84
include
<p16f84.inc>
__config _RC_OSC & _WDT_OFF & _PWRTE_ON
; Program
org
; start at address 0
B00000000
PORTB
; w := binary 00000000
; copy w to port B control reg
B00000001
PORTB
; w := binary 00000001
; copy w to port B itself
goto
end
fin
; program ends here
10
00000010000110
10100000000100
The earliest computers were programmed by technicians writing binary
codes just like this. As you can see, though, binary codes are very hard for
human beings to read or write because theyre completely arbitrary; they
look like gibberish.
Another reason binary codes are hard to write is that many of them
refer to locations in memory. For instance, a go to instruction will have
to say what memory address to jump to. Programming would be much
easier if you could label a location in the program and have the computer
figure out its address.
For both of these reasons, assembly language was invented over forty
years ago. Or, to be more precise, many assembly languages have been invented, one for each type of CPU. What assembly languages have in common is that the instructions are abbreviated by readable codes (mnemonics)
such as GOTO and locations can be represented by programmer-assigned
labels. For example, in assembly language, the binary instructions just
mentioned would be:
fin:
movlw
B00000000
tris
PORTB
movlw
B00000001
movwf
PORTB
goto
fin
In English: Put the bit pattern 00000000 into the W register and copy it to
the tri-state control register for port B, thereby setting up port B for output;
11
then put 00000001 into W and copy it to port B itself; and finally stop the
program by going into an endless loop.
goto
fin
the label is fin: (with a colon), the opcode is goto, and the operand is
fin.
The label, opcode, and operand are separated by spaces. The assembler
doesnt care how many spaces you use; one is enough, but most programmers use additional spaces to make their instructions line up into neat
columns.
If theres no label, there must be at least one blank before the opcode,
or the assembler will think the opcode is a label. Although current PIC
assemblers can often recover from this kind of error, it is an error, and
other assemblers arent so tolerant.
12
+5V
R1
10K
1
2
3
4
5
6
R3
1K
7
8
9
IC1
PIC16F84
A2
A1
A3
A0
A4
O1
MCLR
O2
GND
V+
B0
B7
B1
B6
B2
B5
B3
B4
R2
100K
18
17
16
C2
100
pF
15
14
13
12
C1
0.1
F
11
10
LED
Figure 6: Circuit for simple program that turns on an LED (Fig. 5).
14
15
mpasm turnon.asm
and Figure 7 shows what youll see on the screen.
What MPASM is telling you is that it assembled your .ASM file, generating one warning message (which is unimportant more about this later)
and no error messages. The results consists of a .HEX file containing the
assembled instructions and a .LST file containing a detailed program listing with error messages. If the program contained serious errors, no .HEX
file would be generated and you should study the .LST file to see what
went wrong.
MPASM is the simple way to go. Microchip also gives away a development environment called MPLAB (Figure 8) that contains an assembler
plus a simulator so you can make your PC pretend to be a PIC and actually
see your program run. MPLAB is very useful but its operation is beyond
the scope of this article. See http://www.covingtoninnovations.com/noppp
for some tips.
Now that you have a .HEX file, you have to get it into the PIC. This
is done with a programmer such as Microchips Picstart Plus or the
NOPPP programmer featured in Electronics Now, September 1998, and now
marketed by Ramsey Electronics as PICPRO-1. On your PC, you run whatever software your programmer requires and follow the instructions.
Finally, put the programmed PIC into the circuit (handling it carefully
to prevent static damage) and apply 5 volts. The LED should turn on.
There youve made a PIC do something.
16
17
from endless loops, but its very important to turn it off if youre not using
it, or your program will keep restarting itself at inopportune moments.
The last pseudo instruction is
org 0
which means, The next instruction should go at address 0 in program
memory. That is, youre telling the assembler where to start. Then come
the program instructions, and finally the pseudo instruction
end
which tells the assembler that the program is over. Unlike END in BASIC,
end in assembly language does not tell the program to stop running.
fin:
movlw
B00000000
tris
PORTB
movlw
B00000001
movwf
PORTB
goto
fin
What the program needs to do is set up port B for output, place a 1 into
the lowest bit of port B (causing pin B0 to go high), and stop. Consider
the last of these first. How do you stop a program? Not by making the
processor go dead, because then the output at B0 would disappear and
19
the LED would turn off. Nor by exiting to the operating system, because
there isnt an operating system. Your program has the PIC all to itself.
The way you stop this program is by putting it in an endless loop.
Thats accomplished by the instruction
fin:
goto
fin
20
put. This is done by zeroing the corresponding bits in the TRISB specialfunction register.
B00000000
tris
PORTB
21
; File CHASER.ASM
; Blinks LEDs on outputs (Port B) in a rotating pattern.
; Reverses direction if port A, Bit 0, is high.
processor 16f84
include
<p16f84.inc>
__config _RC_OSC & _WDT_OFF & _PWRTE_ON
; Give names to 2 memory locations (registers)
J
equ
H1F
; J = address hex 1F
K
equ
H1E
; K = address hex 1E
; Program
org
0
; start at address 0
; Set Port B to output and initialize it.
movlw
B00000000
; w := binary 00000000
tris
PORTB
; copy w to port B control reg
movlw
B00000001
; w := binary 00000001
movwf
PORTB
; copy w to port B itself
bcf
STATUS,C
; clear the carry bit
; Main loop. Check Port A, Bit 0, and rotate either left
; or right through the carry register.
mloop:
btfss
PORTA,0
; skip next instruction if bit=1
goto
m1
rlf
PORTB,f
; rotate port B bits to left
goto
m2
m1:
rrf
PORTB,f
; rotate port B bits to right
m2:
; Waste some time by executing nested loops
movlw
D50
; w := 50 decimal
movwf
J
; J := w
jloop: movwf
K
; K := w
kloop: decfsz K,f
; K := K-1, skip next if zero
goto
kloop
decfsz J,f
; J := J-1, skip next if zero
goto
jloop
goto
mloop
; do it all again
end
; program ends here
23
+5V
R1
10K
1
2
3
4
5
6
7
8
9
IC1
PIC16F84
A2
A1
A3
A0
A4
O1
MCLR
O2
GND
V+
B0
B7
B1
B6
B2
B5
B3
B4
18
R2
100K
S1
17
C2 100 pF
16
15
14
C1 0.1 F
13
12
11
10
R3R10
1K
LED1LED10
(RED OR
GREEN)
Figure 10: With program in Fig. 9, LEDs flash in chaser sequence; switch
S1 reverses direction.
24
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Hexadecimal
| Binary
| |
00 00000000
01 00000001
02 00000010
03 00000011
04 00000100
05 00000101
06 00000110
07 00000111
08 00001000
09 00001001
0A 00001010
0B 00001011
0C 00001100
0D 00001101
0E 00001110
0F 00001111
10 00010000
11 00010001
12 00010010
13 00010011
14 00010100
15 00010101
16 00010110
17 00010111
18 00011000
19 00011001
1A 00011010
1B 00011011
1C 00011100
1D 00011101
1E 00011110
1F 00011111
20 00100000
21 00100001
22 00100010
23 00100011
24 00100100
25 00100101
26 00100110
27 00100111
28 00101000
29 00101001
2A 00101010
2B 00101011
2C 00101100
2D 00101101
2E 00101110
2F 00101111
30 00110000
31 00110001
32 00110010
33 00110011
34 00110100
35 00110101
36 00110110
37 00110111
38 00111000
39 00111001
3A 00111010
3B 00111011
3C 00111100
3D 00111101
3E 00111110
3F 00111111
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
Hexadecimal
| Binary
| |
40 01000000
41 01000001
42 01000010
43 01000011
44 01000100
45 01000101
46 01000110
47 01000111
48 01001000
49 01001001
4A 01001010
4B 01001011
4C 01001100
4D 01001101
4E 01001110
4F 01001111
50 01010000
51 01010001
52 01010010
53 01010011
54 01010100
55 01010101
56 01010110
57 01010111
58 01011000
59 01011001
5A 01011010
5B 01011011
5C 01011100
5D 01011101
5E 01011110
5F 01011111
60 01100000
61 01100001
62 01100010
63 01100011
64 01100100
65 01100101
66 01100110
67 01100111
68 01101000
69 01101001
6A 01101010
6B 01101011
6C 01101100
6D 01101101
6E 01101110
6F 01101111
70 01110000
71 01110001
72 01110010
73 01110011
74 01110100
75 01110101
76 01110110
77 01110111
78 01111000
79 01111001
7A 01111010
7B 01111011
7C 01111100
7D 01111101
7E 01111110
7F 01111111
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
Hexadecimal
| Binary
| |
80 10000000
81 10000001
82 10000010
83 10000011
84 10000100
85 10000101
86 10000110
87 10000111
88 10001000
89 10001001
8A 10001010
8B 10001011
8C 10001100
8D 10001101
8E 10001110
8F 10001111
90 10010000
91 10010001
92 10010010
93 10010011
94 10010100
95 10010101
96 10010110
97 10010111
98 10011000
99 10011001
9A 10011010
9B 10011011
9C 10011100
9D 10011101
9E 10011110
9F 10011111
A0 10100000
A1 10100001
A2 10100010
A3 10100011
A4 10100100
A5 10100101
A6 10100110
A7 10100111
A8 10101000
A9 10101001
AA 10101010
AB 10101011
AC 10101100
AD 10101101
AE 10101110
AF 10101111
B0 10110000
B1 10110001
B2 10110010
B3 10110011
B4 10110100
B5 10110101
B6 10110110
B7 10110111
B8 10111000
B9 10111001
BA 10111010
BB 10111011
BC 10111100
BD 10111101
BE 10111110
BF 10111111
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
Hexadecimal
| Binary
| |
C0 11000000
C1 11000001
C2 11000010
C3 11000011
C4 11000100
C5 11000101
C6 11000110
C7 11000111
C8 11001000
C9 11001001
CA 11001010
CB 11001011
CC 11001100
CD 11001101
CE 11001110
CF 11001111
D0 11010000
D1 11010001
D2 11010010
D3 11010011
D4 11010100
D5 11010101
D6 11010110
D7 11010111
D8 11011000
D9 11011001
DA 11011010
DB 11011011
DC 11011100
DD 11011101
DE 11011110
DF 11011111
E0 11100000
E1 11100001
E2 11100010
E3 11100011
E4 11100100
E5 11100101
E6 11100110
E7 11100111
E8 11101000
E9 11101001
EA 11101010
EB 11101011
EC 11101100
ED 11101101
EE 11101110
EF 11101111
F0 11110000
F1 11110001
F2 11110010
F3 11110011
F4 11110100
F5 11110101
F6 11110110
F7 11110111
F8 11111000
F9 11111001
FA 11111010
FB 11111011
FC 11111100
FD 11111101
FE 11111110
FF 11111111
26
STATUS,C
Here STATUS and C are names given by P16F84.INC to the status register
and the carry bit within it. They must be written in all capitals. You could
refer to the carry bit by its address if you wanted to.
The actual rotating is done by one of the instructions
rlf
PORTB,f
rrf
PORTB,f
but the program also has to decide which one to use, depending on the
signal at pin A0, and then introduce a time delay after each rotation.
27
m1
m1
rlf
PORTB,f
goto
m2
rrf
PORTB,f
m1:
28
ADDLW
ADDWF
ADDWF
value
address,F
address,W
ANDLW
ANDWF
ANDWF
value
address,F
address,W
BCF
BSF
address,bitnumber
address,bitnumber
BTFSC
BTFSS
address,bitnumber
address,bitnumber
CALL
label
CLRF
CLRW
address
CLRWDT
COMF
COMF
address,W
address,F
DECF
DECF
address,W
address,F
DECFSZ
DECFSZ
address,W
address,F
GOTO
label
INCF
INCF
address,W
address,F
INCFSZ
INCFSZ
address,W
address,F
IORLW
IORWF
IORWF
value
address,F
address,W
29
MOVLW
MOVF
MOVF
MOVWF
value
address,W
address,F
address
Place value in W
Copy contents of address to W
Copy contents of address to itself (not useless; sets Z flag if zero)
Copy contents of W to address
NOP
Do nothing
OPTION
RETFIE
RETLW
value
RETURN
RLF
RLF
address,F
address,W
RRF
RRF
address,F
address,W
SLEEP
SUBLW
SUBWF
SUBWF
value
address,F
address,W
SWAPF
SWAPF
address,W
address,F
TRIS
TRIS
PORTA
PORTB
XORLW
XORWF
XORWF
value
address,F
address,W
30
m2:
rotates the bits of port A to the left if A0=1 and to the right if A0=0.
If you know another assembly language, you may be wondering how
the PIC gets away without having a byte compare (CMP) instruction. The
answer is that bytes are compared by subtracting them and then checking
whether the result is zero.
3.7 Looping
Obviously, the main program will be an endless loop shift the bits of
Port A, delay a few milliseconds, and then go back and do the whole thing
again. Thats taken care of by the mloop label at the beginning of the main
loop and the goto mloop instruction at the end.
The time delay loop is more complicated because it requires counting.
In BASIC, it would look roughly like this:
FOR J=50 TO 1 STEP -1
FOR K=50 TO 1 STEP -1
REM do nothing
31
NEXT K
NEXT J
That is, waste time by counting 50 50 = 2500 steps.
But were not programming in BASIC. In PIC assembly language, the
time delay loop looks like this:
movlw
D50
movwf
jloop:
movwf
kloop:
decfsz
K,f
goto
kloop
decfsz
J,f
goto
jloop
Heres how it works. First, note that counting down is easier than counting
up because its easier to test whether a byte equals 0 than whether it equals
some other number. So we stuff decimal 50 into variables J and K at the
beginning of the loop. The value 50 is stored in W, which doesnt change
during the whole process; thus it can be copied into K fifty times.
The crucial instruction here is
decfsz
K,f
which means, Decrement (that is, subtract 1) and skip the next instruction
if the result of decrementing was zero. Thus, a decfsz followed by a
goto means Subtract 1 and go back unless the result of the subtraction
was 0. Two decfsz loops, nested, produce the time delay we need. On
32
the last pass through the loop, the value of J or K respectively is 1; then,
when it reaches 0, the program exits the loop.
equ H1F
equ H1E
define J and K to be abbreviations for two hex numbers, 1F and 1E, which
are the addresses of two suitable file registers. These registers were chosen
by looking at the memory map in the PIC16F84 manual; its entirely up to
the programmer to choose a suitable address for each variable.
Unused output lines can be put to good use for debugging. If your
program is complicated, make it send some signals out one or more spare
port pins so that you can use a voltmeter or oscilloscope to tell what part
of the program is running. If nothing else, output a heartbeat bit that
toggles every time the program goes through its main loop.
34