First Program

We've put it off for long enough - it's time to jump in the water!

We are going to use Microchip assembler for this. People are often afraid of assembly language, but for a simple processor like the PIC, it's really not that bad. The programs you'll write will generally be quite small, and provided you comment your code properly, you should find that it's quite straightforward.

The programming cycle

We first need to consider the "programming cycle".

PIC programming cycle (15K)

The first step is to write the source code, and save it as an .ASM file. Next, using MPASM, you assemble the source code. This will create a number of files, the most relevant being the list file (.LST) and, if you're lucky, the hex file (.HEX). If any errors were encountered during the assembly process, there will be no .HEX file, and you'll need to examine the .LST file and correct the errors in your .ASM file.

The next step is to open the .HEX file with your programming software and program the PIC. Then, place the PIC into the test circuit, and see if it works. Invariably, you'll have to go back to the .ASM file and make some more modifications to the source code. This cycle repeats until the PIC does what you require.

You can download an IDE (integrated development environment) from Microchip called MPLAB - this brings together the first few steps of the process. This sounds like a good idea, but earlier versions of MPLAP were somewhat clunky, and I quickly found that it was better to use ConTEXT, which can be configured to act a bit like an IDE. During this section, I'm assuming that you've installed and configured ConTEXT as described on this page.

Having said that, MPLAP is much better these days, and is worth investigating. It incorporates an emulator, which allows you to step through your programme on a line-by-line basis. This is useful at the beginning, but when you start interfacing with real hardware like LCD displays, this becomes less useful.

Programme specification and flowcharts

At the risk of stating the obvious, we must decide what we actually want our programme to do. Expressing this in simple words is the first step.

4 Bit Counter

A program to count from 0 to 15, and output the result in binary on four ports. The counter should increase by 1 (increment) every second. When the counter reaches 15, it should reset and continue counting from 0.

The next step to to convert these words into a flow diagram. If you've done programming before, you were probably advised to use flow diagrams, but if you were like me you probably didn't see the point - after all, high level languages like BASIC are so easy to understand that by the time you've drawn the flowchart, you might as well have written the code!

But, right at the very start, we must accept that we need to use some sort of tool to understand the process. Believe me, you do need flowcharts!

Let's start by drawing up a simple flowchart:

Counter flowchart (5K)

This is a good start, but the next stage is to zoom in and add some detail to the counter section:

Counter flowchart (7K)

With this flowchart, we've introduced the idea of decision making. By following through the diagram, you should be able to see how we start by resetting the counter, and increase it until it gets to the highest value required. Then the counter is reset and the whole cycle repeats.

But there are still some sections missing. We haven't incorporated a means of outputting the counter value. Without this, our PIC will be merrily counting away to itself, but we'd have no idea if it was working or not.

Simple counter (8K)

The exact position of the output block could be in one of two places, but the key thing is to ensure that it is within the main counter loop. If it was placed after the "reset counter" box, but before the arrow coming from the decision box, then we would only ever see zero on the output.

By putting the output box before the "increase counter" box, the counter will actually start from zero. If placed after the increase, then the first number outputted will be "1".

But with this increase in the detail, we need to have a careful think about our algorithm. Following it through from the beginning, we can see that the counter will start at zero, this number will be outputed, then it will be increased by one, and as it doesn't yet equal 15, this "1" will be outputted. Follow this through, and you should see how the counter will happily increase in value, outputting the result as it goes.

But, we have a problem here... Think about it before scrolling down.

...

...

...

What happens when the counter gets to 15? Work around the loop, assuming that the counter has just been increased to 14. This "14" is outputted, and the counter variable is incremented to equal "15". But, as we get to the decision box, the program realises that the counter has reached 15, and it will take the alternative path and reset the counter. We never get to see "15"!

The solution to this is simple: change the fixed number in the decision box to 16. This will be included in the next revision of the diagram, along with the final thing - the delay.

Without it, the counter will count far to quickly for us to see it. So we need to add the delay, but where?

Like the output box, it must be within the "inner" loop. How about making it the first item in the loop? This will work, but what happens the very first time the program runs? The output won't be updated until after the first delay routine. But as a principle, it's safest to assume that we don't know what happens when the PIC is initially powered up, so we ought to take control as soon as we can.

Simple counter (9K)

This flowchart incorporates the correction to the counter and the time delay. Also, I've added a section called "initialise hardware", which is an essential step in any PIC programme. Comparing this flowchart to our initial attempt, you can see that we've "drilled down" and added considerable detail. This simple process is applicable to any programming challenge, no matter how large and complex, and it is well worth getting used to it right at the beginning. These sketches are very simple to make on paper - it's worth saying that the more time you spend away from the computer at first, the quicker your programme will work!

Coding

Finally we've got to a stage where we can begin translating those boxes into lines of code. Knowing when you can make this step comes with experience, but when a lot of the boxes are describable with single lines of code, you know that you're nearly there.

Download the PIC quick-reference sheet, and refer to it as we work through the rest of the page.

Reset Counter

We'll start with "Reset counter". There are two ways to do this:

     movlw    d'0'     ;Place 0 into working register
     movwf    Count    ;Move W into Count
    

The first statement moves a literal number into the working register. A literal is a number that is fixed when the program is assembled, and cannot be changed as the program runs. So this line will always place zero into the working register, and to change this you will need to reassemble and re-program the PIC.

The second line moves the working register into a file register. We discuss this in more detail later, but for now, assume that the Count variable has been previously set up. Note that comments follow the semi-colon.

This is perhaps the logical and obvious method. Remember that the working register is the equivalent to the Accumulator in other processors, and most operations work through it.

This is fine, and will work. But it occupies 2 memory locations, and as PICs have limited amounts of program memory available, it pays to think about code-efficiency right from the beginning. You can achieve this using only one instruction:

     clrf      Count    ;Reset Count
    

This instruction clears a file register. Clearly, in this context, "clear" means "to make zero". So, this rather handy instruction achieves the same as the above two lines in half the space. As an added bonus, it doesn't affect the working register in any way.

Output Count

Next, we need to "Output Count". Assume that all of PORTB has been set up to be outputs. You'll remember from before that PORTB appears in the memory map, and as far as the processor is concerned, it's just RAM. So we can write to PORTB in the same way as we wrote to Count above.

     movfw     Count    ;Move Count into W
     movwf     PORTB    ;Write W to PORTB
    

The first line copies the value stored in Count to the working register. The next line writes it to PORTB. If there are LEDs connected to PORTB, they will light up to show a binary representation of the value of count. Suppose Count was 15, then four LEDs would light as "1111" is 15 in binary. A good understanding of binary and hex number systems is useful for any sort of "machine code" programming.

Wait 1 Second

The next box on our flow diagram is "Wait 1 second". For the purposes of this introduction, we'll use some code that has been written before - we'll examine it later. But it's a good excuse to introduce the concept of subroutines.

A subroutine is a section of code that is deliberately placed outside of the normal program flow. But, your main program can jump to the subroutine whenever it needs to - and at the end of the subroutine, the program flow will return to where the subroutine was called from. Subroutines are extremely useful, and we'll talk in more detail about them later.

So, we just need one line:

     call      Wait1Second    ;Call the 1 second subroutine
    

Think of this as "modular programming" - the subroutine can be copied and pasted from another program and used as required.

Increment Counter

Next is "Increment counter". As is often the case, there is more than one way to do this. You might have spotted the ADDLW instruction on the quick-reference sheet...

     movfw     Count     ;Move Count into W
     addlw     d'1'      ;W = W + 1
     movwf     Count     ;Count = Count + 1
    

This is perfectly reasonable, and is the most logical. But there are variations on the theme:

     movlw     d'1'      ;W = 1
     addwf     Count,f   ;Count = Count + 1
    

This occupies one less programme memory location, and introduces instructions that have a destination in their syntax. Have a look at the quick-ref sheet:

     addwf     FileReg, dest
    

People who are new to PIC assembly often see the "dest" part of the instruction, and assume that "dest" can be any file register they like. Unfortunately, it can only be F or W - in other words, the result of the operation will be placed back into the file register, or left in the working register. This is something to check for if your program doesn't behave as expected (where there's a 50% chance of getting something wrong, you will!). Also, if you omit the destination, the assembler won't generate an error - rather it puts an easily-missed warning in the .LST file. In that instance, it assumes F - which obviously will only work if that's what you wanted in the first place!

There's an even easier way:

     incf      Count,f   ;Count = Count + 1
    

This instruction increments the file register, and note that you have to specify the destination - in this case, we want the result to go back into the file register.

Does Counter = 16?

Hopefully, everything has been reasonably logical so far. But now, we need to decide if the counter has reached 16, and this is where things get confusing!

There are only 4 instructions that deal with making decisions, and they all work by testing individual bits in a file. The trick is to make use of the various status flags that are available in the processor - refer to the STATUS register on page 2 of the quick-reference.

Have a look at the third column of the instruction set sheet, and you'll see that some instructions affect Z, while others don't. The instructions that do are the arithmetic operations.

The easiest way to see if two numbers are equal is to exclusive-OR them. This almost certainly needs some explanation, so let's start with the truth-table for an exclusive-OR gate:

Input A Input B Output
0 0 0
0 1 1
1 0 1
1 1 0

As you can see, this gate produces a logic "1" if the two inputs are different. So how does that translate into a microprocessor instruction? Let's take two numbers and EX-OR them:

  Decimal 128 64 32 16 8 4 2 1
Number A 135 1 0 0 0 0 1 1 1
Number B 170 1 0 1 0 1 0 1 0
Result 45 0 0 1 0 1 1 0 1

The process is to convert the two numbers from decimal to binary, and then EX-OR each of them, bit by bit. Starting with the least-significant bit (right of the table), number A has a "1", and number B has a "0". These two bits are clearly different, so the result is "1". Work left across the rest of the bits, filling in the results, and then convert the binary number back into decimal, giving 45 in this case.

  Decimal 128 64 32 16 8 4 2 1
Number A 170 1 0 1 0 1 0 1 0
Number B 170 1 0 1 0 1 0 1 0
Result 0 0 0 0 0 0 0 0 0

Look at this result - when number A and B are the same, the result is zero. This is an important principle, which we shall exploit next. Consider these lines:

     movlw     d'16'       ;W = 16
     xorwf     Count,w     ;W = 16 XOR Count
    

The first line simply puts 16 into the working register. Remember, this is a constant value, and will be 16 forever - it you want to change it, you will have to change the source code and re-programme the PIC. In many applications, you'll use a variable rather than a literal.

The second line does the XOR, but note what happens to the result - it is left in the working register. It would be a disaster to put it back into the file register because the result from this sum is absolutely meaningless as far as our algorithm is concerned. As mentioned above, if you neglected to type the ",W" after "Count", the assembler would assume a destination of "F", and the program would appear to count in a random sequence!

So, we've effectively "thrown away" the result from the XOR operation. But the key thing is that this line has affected the Z flag. If the two numbers are the same, then the result will be zero. If the result from an operation is zero, then the Z flag will be set. And we can test for this:

     btfss     STATUS,Z    ;Check the Z flag in the STATUS register
                           ;Is it set?
     goto      Loop        ;  No, so Count <> 16 - keep counting
     goto      Reset       ;  Yes, so Count = 16 - reset counter
    

The first line here performs a bit test on a file register, and will skip the next instruction if the bit is set. So if the Z flag is clear, the program just flows onto the next line, and meets the goto Loop instruction. If the Z flag is set, the first goto is skipped, and the program flow is diverted to the goto Reset instruction.

All of this needs a little thinking about, but it will make sense eventually. It might seem complicated because of the number of steps that we have to make, especially as everything else has seemed so straightforward up to now. But that's a consequence of using a processor with a small instruction set - OK, there aren't many instructions to learn - which is good - but you sometimes end up having to use more of them that you'd expect.

It's worth repeating that famous law of engineering - if there is a 50% chance of getting something wrong, you will! This applies here because there is a sister instruction - btfsc - which means bit test on a file, skip if clear. Using this instruction reverses the logic, so you would need to swap the order of the goto statements to make this work.

Nearly finished?

Let's bring together all of the lines that we've got so far:

Reset     clrf      Count        ;Reset Count
Loop      movfw     Count        ;Move Count into W
          movwf     PORTB        ;Write W to PORTB
          call      Wait1Second  ;Call the 1 second subroutine
          incf      Count,f      ;Count = Count + 1
          movlw     d'16'        ;W = 16
          xorwf     Count,w      ;W = 16 XOR Count
          btfss     STATUS,Z     ;Check the Z flag in the STATUS register
                                 ;Is it set?
          goto      Loop         ;  No, so Count <> 16 - keep counting
          goto      Reset        ;  Yes, so Count = 16 - reset counter
    

You see that I've added the labels that correspond to the two goto statements - compare this to the flow diagram and don't continue until you understand it!

Unfortunately, you can't just type all of that into a text editor and expect it to work. The assembler needs much more information before it can create a .HEX file. For example, it doesn't know which PIC processor you wish to use. It won't understand "Count", or "Wait1Second". So how do we turn this into a working program?

Standard template

I recommend starting with a standard template. This is split into a number of sections:

  1. Program name and comments
  2. Assembler directives
  3. Memory definition
  4. Main program
  5. Subroutines

Let's look in detail at each of these:

1. Program name and comments

This really is just comments for your own use, and can take any form you like. My advice is to be as explicit as you can and include as much information as you can think of - assume that when you look back on your code in a few months time, you will have forgotten everything!

2. Assembler directives

This is where we tell the assembler what processor we're using, and set other environment options. Let's look at an example:

     LIST       P=16F84, R=DEC
     __FUSES    _XT_OSC & _WDT_OFF & _CP_OFF & _PWRTE_ON
     include    "P16F84.inc"
    

The LIST statement tells the assembler to switch on output to the .LST file. Next, the processor is defined as a PIC16F84, and the default radix is set to decimal. You might have noticed that we were writing numbers as d'16', for example. Should you want to enter a binary number, you can write b'00001111'. Should you wish to use hexadecimal, you can write h'0F'. This ability to use any number system you wish at any point of the source code is really convenient. The default radix setting tells the assembler how to interpret any numbers that it finds without the letter and quotes.

Next, fuses. This is something that hasn't been mentioned yet, but fuses are a separate section of memory that is outside the normal program memory. These vary from device to device, and the datasheet for the PIC you wish to use will explain more fully, but basically options like oscillator type and code protection are set up here.

Finally, the correct .INC file is included. This line simply tells the assembler to find the appropriate file, and insert it into the .ASM file at this point. These include files are supplied by Microchip as part of the assembler, and simply tell the assembler what numbers it should assign to words like STATUS and PORTB. Take a look at the memory-map on the PIC quick-reference sheet, and you'll see that PORTB lives at memory location 6. This might be in a different location for another PIC, so that's why the separate include file is used.

It's worth saying that you can write your own include files that contain sub-routines and other bits of code. Personally I prefer to copy and paste in the subroutines that are needed, as you can see everything on one screen, but when programs get bigger and more complex, this can be a useful technique.

3. Memory definition

The next thing is to list all the variables that your program wants to use - this pre-warns the the assembler of all the words that it is likely to come across. This can cause some confusion at first, but the key thing to remember is that you just deciding where in memory you are storing your variables. Look again at the memory map, and you'll see that the first free GPR (general purpose register) is 0Ch (12 in decimal).

Ram            EQU    h'0C'
Count          EQU    Ram+0
next_variable  EQU    Ram+1
another_var    EQU    Ram+2
    

There are lots of ways of doing this, but this is how I tend to lay them out. Should I write a program and decide to "port" it to another processor where the general-purpose RAM begins at a different location from 12, then I simply need to change the number on the first line. For example, the first free address on a PIC16F877 is 20h (32 decimal), and when developing my Hi-Fi pre-amp, I remember how painful it was changing from a 16F84 to the 16F877 - every variable needed a different address. It was at this point that I decided on this approach!

4. Main programme

That's the bit that we've already written!

5. Subroutines

As we said earlier, subroutines need to be away from the normal program flow. After the end of the main programme code is the logical place, but it's really up to you.

So with all that in mind, here's what our programme looks like:

;*****************************************************************
; 4Bit.ASM
;
; This is a simple 4 bit counter, writing the result to PORTB...
;
;*****************************************************************

       LIST     P=16F84, R=DEC
       __FUSES  _XT_OSC & _WDT_OFF & _CP_OFF & _PWRTE_ON
       include  "P16F84.inc"

; RAM definitions

Ram      EQU      h'0C'
Count    EQU      Ram+0

; Main program starts here

         ORG      0            ;Reset vector
         call     Init         ;Setup hardware
Reset    clrf     Count        ;Reset Count
Loop     movfw    Count        ;Move Count into W
         movwf    PORTB        ;Write W to PORTB

         call     Wait1Second  ;Call the 1 second subroutine

         incf     Count,f      ;Count = Count + 1

         movlw    d'16'        ;W = 16
         xorwf    Count,w      ;W = 16 XOR Count
         btfss    STATUS,Z     ;Check the Z flag in the STATUS register
                                ;Is it set?
         goto     Loop         ;  No, so Count <> 16 - keep counting
         goto     Reset        ;Yes, so Count = 16 - reset counter
    

Note the "ORG" statement - this is another assembler directive, and tells the assembler to start assembling commands into program memory from memory location zero. When the processor resets, it goes to location 0 and executes the first instruction it finds there.

Next we need to consider the subroutines. There is quite a bit to deal with in the initialisation sub-routine, so lets deal with that first:

;*****Init - set up all ports, make unused ports outputs

Init     clrf     PORTA        ;all of porta low
         clrf     PORTB        ;all of portb low
         bsf      STATUS, RP0  ;change to bank1
         clrf     TRISA        ;all of porta outputs
         clrf     TRISB        ;all of portb outputs
         bcf      STATUS, RP0 ;back to bank0
         return
    

With this simple program, we simply need to configure the two ports. There are a total of 8 bits in PORTB, and each of these bits corresponds to an actual pin on the PIC. Each of these pins can be either an input or an output, depending on your requirements. Remember that we are only using 4 bits of PORTB, so the remaining bits will be unused - also, PORTA is unused in this simple program. It is good practice to configure unused bits as outputs, so that is what we do here.

There is a pair of registers called TRISA and TRISB - "tris" is short for tri-state, referring to a type of logic that is used for bus connections. Microchip refer to these as "data-direction registers". Each bit in the TRIS register maps to the appropriate bit of the port, so bit 0 of TRISB is associated with bit 0 of PORTB - the short-hand way of expressing this is "RB0". Setting a bit in the TRIS will make the appropriate port an input, and clearing the bit will make it an output.

So this subroutine clears all the bits in both TRIS registers using the clrf instruction.

But when we refer to the memory map we notice that TRISA and TRISB is on the right-hand half of the memory map:

  Bank 0 Bank 1  
00h (0) Indirect addr Indirect addr 80h (128)
01h (1) TMR0 OPTION 81h (129)
02h (2) PCL PCL 82h (130)
03h (3) STATUS STATUS 83h (131)
04h (4) FSR FSR 84h (132)
05h (5) PORTA TRISA 85h (133)
06h (6) PORTB TRISB 86h (134)
07h (7)     87h (135)
08h (8) EEDATA EECON1 88h (136)
09h (9) EEADR EECON2 89h (137)
0Ah (10) PCLATH PCLATH 8Ah (138)
0Bh (11) INTCON INTCON 8Bh (139)
0Ch (12) 68 GPR'S
General
purpose
registers)
Mapped to Bank 0 8Ch (140)
7Fh (127)     FFh (255)
       
  - Not implemented  

We need to instruct the processor to move to the right-hand side of the memory map, or in other words, change to Bank 1. You may remember from the discussion on the previous page that this must be done explicitly, and that's because there isn't enough space for the whole 8 bits of the memory address in an instruction. To explain this, consider how the assembler deals with instructions like movwf PORTB and movwf TRISB.

    13           7 6           0
    Opcode Address (06h)
movwf PORTB 0086h 0 0 0 0 0 0 1 0 0 0 0 1 1 0
movwf TRISB 0086h 0 0 0 0 0 0 1 0 0 0 0 1 1 0

It might surprise you to see that the assembler turns these two different instructions into exactly the same machine code - 0086h. This is the number that is actually programmed into program memory, and when the core of the processor meets this, it knows that it should load the contents of memory location 06h into the working register.

While 06h is the correct address for PORTB, 86h is the address of TRISB. Just to be completely explicit, compare the binary representation of these two numbers:

    7 6 5 4 3 2 1 0
PORTB 06h 0 0 0 0 0 1 1 0
TRISB 86h 1 0 0 0 0 1 1 0

As you can see, the difference is bit 7. And as the previous table shows, there isn't space to store bit 7 in the programme memory along with the instruction. To get around this problem, the designers at Microchip decided to store this 7th bit somewhere else - in the STATUS register:

  7 6 5 4 3 2 1 0
STATUS (03h and 83h) IRP RP1 RP0 /TO /PD Z DC C

There are lots of other things in here - you've already met the Z flag, for example. But we're interested in something called RP0, which is our 8th bit. You might notice RP1 - this is the 9th bit for bigger PICs - you won't need to worry about this until you graduate to processors like the PIC16F877 that I used in the hi-fi preamp. This tells us that in theory, a PIC could have up to 512 locations in RAM.

Note that the STATUS register is available on both sides of the memory map. As above, compare the addresses given for STATUS:

    7 6 5 4 3 2 1 0
STATUS 03h 0 0 0 0 0 0 1 1
STATUS 83h 1 0 0 0 0 0 1 1

Again, the only difference is the MSB (most significant bit). This duplication of STATUS in both sides of the memory map is absolutely essential! Imagine you've set RP0 to move to Bank 1. What happens if STATUS was not in Bank 1? You would never be able to access STATUS again, meaning you could never change back to Bank 0!

So this background hopefully explains what RP0 is, and why we need to set it. If it doesn't make complete sense at this stage, don't worry too much. But do try to revisit it soon, because it is an essential topic to understand. Meanwhile, back to the program:

It's time to introduce two more instructions. PIC processors offer lots of bit-oriented instructions - that is, instructions that operate on just a single bit within a file register. That's convenient for setting and clearing RP0 - let's look at the initialisation subroutine again:

;*****Init - set up all ports, make unused ports outputs

Init     clrf     PORTA        ;all of porta low
         clrf     PORTB        ;all of portb low
         bsf      STATUS, RP0  ;change to bank1
         clrf     TRISA        ;all of porta outputs
         clrf     TRISB        ;all of portb outputs
         bcf      STATUS, RP0 ;back to bank0
         return
    

Note the "bsf" instruction - this means set a bit in a file register. Refer to the quick-ref sheet and you'll see that the syntax of this is:

        bsf      FileReg, bit
    

So if you had an LED connected to bit 0 of PORTB (RB0 for short), you could light that LED with bsf PORTB,0. There is a complimentary instruction of bcf (clear bit in a file register) - so to turn off the LED, you would use bcf PORTB,0. In the same way, we can use these instructions to set and clear RP0:

        bsf      STATUS, 5
    

You saw above that RP0 is bit 5 of STATUS. However, thanks to the .INC file, the assembler knows that RP0 means 5 (find and open the .INC file to prove this to yourself). So we can write:

        bsf      STATUS, RP0
    

Which is much easier to remember, and easier to understand when you revisit your code some time after you wrote it. Incidentally, this same syntax applied before when we wrote btfss STATUS, Z - the Z was actually converted to a 2 by the assembler.

Final program

Right. We've covered a lot of ground here. We've met a lot of new instructions, and concepts - especially in the last few sections. Let's bring together everything and show the complete program:

;*****************************************************************
; 4Bit.ASM
;
; This is a simple 4 bit counter, writing the result to PORTB...
;
;*****************************************************************

       LIST     P=16F84, R=DEC
       __FUSES  _XT_OSC & _WDT_OFF & _CP_OFF & _PWRTE_ON
       include  "P16F84.inc"

; RAM definitions

Ram      EQU      h'0C'
Count    EQU      Ram+0

; Main program starts here

         ORG      0            ;Reset vector
         call     Init         ;Setup hardware
Reset    clrf     Count        ;Reset Count
Loop     movfw    Count        ;Move Count into W
         movwf    PORTB        ;Write W to PORTB

         call     Wait1Second  ;Call the 1 second subroutine

         incf     Count,f      ;Count = Count + 1

         movlw    d'16'        ;W = 16
         xorwf    Count,w      ;W = 16 XOR Count
         btfss    STATUS,Z     ;Check the Z flag in the STATUS register
                                ;Is it set?
         goto     Loop         ;  No, so Count <> 16 - keep counting
         goto     Reset        ;Yes, so Count = 16 - reset counter




;*****Init - set up all ports, make unused ports outputs

Init     clrf     PORTA        ;all of porta low
         clrf     PORTB        ;all of portb low
         bsf      STATUS, RP0  ;change to bank1
         clrf     TRISA        ;all of porta outputs
         clrf     TRISB        ;all of portb outputs
         bcf      STATUS, RP0 ;back to bank0
         return

         END
    

This is everything apart from the time delay routine, which will be explained on a separate page. To save typing all of this in, you can download it here. You should be able to assemble the program, and program a PIC.

Build the circuit shown here (you might still have it built from before), and confirm that the counter works. Use your programing software to erase the PIC and confirm that an empty PIC does nothing. Feel free to experiment with the program - can you make it count more slowly? Can you change the highest number that the PIC counts to?

Click for larger version (5KB)

Summary and conclusion

We started this page by drawing up a program specification, and turned this into a detailed flowchart by breaking down each step into smaller, more manageable steps. Next we were able to translate each step into lines of code. But before we could make this work, we had to consider the general layout of a .ASM file, and write a subroutine to set up the hardware. Along the way, a detailed look at the memory layout of the PIC was required. But finally, we were able to assemble our source code and program a PIC and build it into a real circuit complete with flashing LEDs!

Let's summarise the instructions we've learnt:

addlw number Adds a number to the working register.
addwf FileReg, dest Adds the working register to the number in a file register and puts the result in dest.
bcf FileReg, bit Clear a bit in a file register.
bsf FileReg, bit Set a bit in a file register.
btfsc FileReg, bit Tests a bit in a file register, skips the next instruction if bit is clear.
btfss FileReg, bit Tests a bit in a file register, skips the next instruction if bit is set.
call Sub Calls a subroutine.
clrf FileReg clears the file register.
incf FileReg, dest Increments a file register and puts the result in dest.
goto label Go to the label.
movfw FileReg movies the number in the file register into the working register.
movlw number Moves a literal number into the working register.
movwf FileReg Moves the number in the working register into the file register.
return Returns from a subroutine.
xorwf FileReg, dest exclusive ORs the working register with the file register.

So after just the first program, we've used 15 instructions. That's nearly half of them! The next pages will gradually increase the pace, while reducing the amount of discussion. You should find that you become much more fluent with PICs before much longer!