Welcome to the first of a series of assembly tutorials for the Microchip PIC18F2550 (datasheet) or 2550 for short. I have been using this at university for about two years now, both programming in C and ASM. You might think “why would anyone ever write in assembly when you’ve got C, which looks a lot easier to write a program with”. The idea with writing code in assembly is full control. You directly control every operation the processor will execute which, if you’re an experienced programmer, will result in faster program execution and less overhead.
But enough of the jibber jabber, let’s get started. We’ll start simple and gradually increase complexity, ending with serial communication between 2 microcontrollers.
I’m assuming you are familiar with Microchip’s IDE called MPLab so I won’t be explaining how it works, it’s fairly straightforward anyway. The big difference between C and ASM compilation is that you do not use a linker file when compiling your ASM project (give it a try, include the linker, you’ll notice MPLab throws an error).
First of all, I’m using a bootloaded PIC so some vectors (such as the interrupt en reset vectors) have an “offset”. Let’s take a look at the following bit of code:
title "SPI MASTER"
list p=18F2550, r=hex, n=0
org 0x800 ; Reset Vector
org 0x808 ; Interrupt Vector HIGH priority
org 0x818 ; Interrupt Vector LOW priority
This is the very beginning of the assembly program. As said before, vector mapping several things as well as giving the program a name. The actual program begins after the “Start” label. Whenever you see such a label (which can be any word you like) you can use the GOTO instruction to jump to the code directly after the label.
Initializing the PIC’s control registers takes a long time but it’s essential for the correct behavior of the processor. DO NOT FORGET THIS or rest assured you will spend a long time trying to figure out why your code doesn’t work. So, we start with configuring the IO pins of PORT A through C:
CLRF PORTA ; Initialize PORTA by clearing output data latches
MOVLW 0x00 ; Set PORTA pins as output
MOVWF TRISA ; Value used to initialize data direction
CLRF PORTB ; Initialize PORTB by clearing output data latches
MOVLW 0x00 ; Set PORTB as output
MOVWF TRISB ; Value used to initialize data direction
CLRF PORTC ; Initialize PORTC by clearing output data latches
MOVLW 0x00 ; Value used to initialize data direction
We start by clearing anything that might be on the data latches of the ports, then we move a certain value to the WREG (working register, or its shorthand version: w). There’s an easy way to remember what defines input or output. You’ve got 1’s for Input (1 looks like an I) and 0 for output (0 looks like an O). Finally we move the value from the working register to the Tri-state register of each port. Note that it is recommended to configure any floating IO pin as output.
Next up we make sure to disable interrupts while we configure some more registers. BCF means “Bit Clear File” and BSF is the exact opposite, it sets a bit.
bcf INTCON,GIE ; Turn Off global interrupt
bcf UCON,3 ; to be sure to disable USB module
bsf UCFG,3 ; disable internal USB transceiver
MOVWF ADCON0 ; AD convertor OFF
movlw 0x00 ; No priority on interrupts
bsf INTCON, GIE
For this simple tutorial we do not have to set a lot of registers, let’s just make sure the AD convertor is turned off and the interrupt registers are set correctly.. After all the registers are configured we allow interrupts again.
Once this is done, we move on to the next important routine, which is the interrupt. In this example we’re not using interrupt priorities so there is just one label called “inter”.
inter ; using single interrupt priority
BTFSS INTCON,TMR0IF ; Check for TMR0 overflow
Let’s take a look at what we have here. We use the “inter” label to know the following code will be executed when an interrupt happens. We’re not actually doing anything in this example besides checking if Timer 0 has overflowed (we haven’t really configured timer 0, this is just to show you how interrupts work). When jumping into the interrupt routine, the interrupt enable bit will be cleared by the hardware, the “RETFIE” command will set the “GIE” bit again and return to wherever the program was when the interrupt happened.
I also prefer to put the interrupt routine at the end of the file, hence why there is a (mandatory) “END” at the last line of code.
Now that that’s out of our way we can create our first really simple program. We’ll start by making a simple upcounter using PORTB. If you have 8 resistors and LEDs, connect them to RB0 through 7 now. As the timer would go really, really quickly we’ll have to add a delay routine so you can actually see the counting. Start by defining 3 counter variables at the top of the file:
count1 equ 0x30 ; Add these directly above org 0x800
count2 equ 0x31
count3 equ 0x32
Then we come to the actual program, this code is located directly above “inter”:
BSF PORTB, 0 ; Set one bit so we can shift it around
RRCF PORTB ; Move the bit one spot to the right
SETF count1 ; Set count1 to 0xFF
MOVLW 0x05 ; Move 0x05 to the WREG
MOVWF count3 ; Move the 0x05 in the WREG to count3
DECFSZ count1 ; Decrement count1, skip the next instruction if count1 = 0x00
First of all we set RB0 to 1 so we can shift it around. Next up we do an actual shift with “RRCF”, we then set the counter values and continue into the Delay routine. The logic here is the following:
1. Decrement count1
2. If count1 is not equal to 0, go to 1.
3. Decrement count2
4. If count2 is not equal to 0, go to 3.
5. Decrement count3
6. If count3 is not equal to 0, go to 5.
7. Go back to Main
I explicitly set the counter values each loop to make sure they have the correct values I want. If you would prefer an upcounter simply remove the Init part and replace “RRCF” with “INCF”. For a downcounter use “DECF”.
That concludes this tutorial, we’ll cover some more advanced things in the next one. If you have any questions or remarks feel free to post them in the comments.
Continue with the second tutorial.