Monday, October 5, 2009

Writing your first programme..

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 too 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.

We first need to consider the "programming cycle".



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 there are any errors 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 actually 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 MPLAP - 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 draw 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!

Start by drawing up a simple flowchart:

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

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.


The exact position of the output stage could appear 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 output, 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 is then incremented to equal "15". But, as we get to the decision box, the programme realises that the counter has reached 15, and it will take the alternative path and the counter will be reset. 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 think about what happens the very first time the program runs - the output won't be updated until after the first delay routine. We don't know what happens when the PIC is initially powered up, so it safest to take control as soon as we can.


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:

So, 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.

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 also 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 the 16F84 only has 1024 words available in programme memory, it pays to think about code-efficiency right from the beginning. You can do this using only one instruction:

clrf Count ;Reset Count

This instruction clears a file register. Clearly, in this context, means set to 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 touch or affect the working register in any way.

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 LED's connected to PORTB, they will light up to show a binary representation of the value of count. Suppose Count was 15, then four LED's 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.

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 has deliberately been 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.

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 often see the "dest" part of the instruction, and think 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 programme doesn't behave as expected. Also, if you omit the destination, the assembler won't generate an error - rather it puts a warning in the .LST file, which is easy to miss. In that instance, it assumes F - which obviously will work if that is 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.

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.

The Z flag is set whenever the PIC performs an arithmetic operation - have a look at the third column of the instruction set sheet, and you'll see that some instructions affect Z, others don't.

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 logic gate produces a logic "1" output 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. So, for 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 if you decided that your counter needed to count up to some other number, you would have to re-programme the PIC. 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 into the file register, because the result from the sum is absolutely meaningless. As mentioned above, if you neglected to type the ",W" after "Count", the assembler would assume a destination of "F", and the programme would appear to count in a random sequence!

So, we've effectively "thrown away" the result from the XOR. 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 programme 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.

It's worth mentioning 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.

So 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 programme?

The best way to start is 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

Looking 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 is really convenient. The default radix setting tells the assembler how to interpret any numbers that it finds without the letter and quotes. This is the most useful setting, but you can change this here.

Next, fuses. This is something that hasn't been mentioned yet, but fuses are a separate section of memory that is outside the normal programme 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 your own sub-routines. Personally I prefer to copy and paste in the subroutines are needed, as you can see everything on one screen, but when programs get bigger and more complex, this might be a useful technique.

3. Memory definition:

The next thing is to list all the variables that your programme wants to use - this pre-warns the the assembler of all the words that is likely to come across. This can cause some confusion at first, but the key thing to remember 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 (that's 12 in decimal)

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

There's 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, 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)

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 programme. 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 for 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 refer to the memory map. Notice how TRISA and TRISB is on the right-hand half of the memory map?

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 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 programme memory, and when the core of the process 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. But we're interested in something called RP0, which is our 8th bit. You might notice RP1 - this is the 9th bit for even bigger PICs - you won't need to worry about this until you graduate to bigger processors like the PIC16F877 that I used in the hi-fi preamp. This tells us that in theory, a PIC can have up to 512 locations in RAM.

Note that 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 programme:

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 (RP0 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 programme:

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 programme:

;*****************************************************************
; 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 in a separate section. 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 programmer 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?

Summary and conclusion:

We started this page by drawing up a programme 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 programme 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 around 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 in the language of PICs before much longer!

On to the next Post >> Introducing time delays.