Here are some assembly language macros that make programming Microchip's 8-bit PIC microcontrollers more pleasant. This is specifically for the medium and small devices: PIC16Fxxx, PIC12Fxxx, PIC10Fxx.
This is somewhat inspired by the Parallax PIC assembler "PASM" that was available in the early 1990s.
The general idea of these macros is to hide the W register and make PIC look like a two-address machine with mnemonics similar to 6800 / 6502 / PDP-11. They also provide essential, but missing features of PIC, such as macros for multi-precision arithmetic.
The syntax used is compatible with Microchip's MPASM and the open source GPASM.
Note that this is the old syntax, which is very different from that used in the newer XC8 PIC Assembler.
Simply include "macros.inc" into your assembly source file with:
include p10f200.inc
include macros.inc
- The macros make the PIC look like a two address machine. This means that dual-operand instructions expect two register arguments, or one register and one immediate value:
add 0x14, 0x18 ; Add register 0x18 to register 0x14
addi 0x14, 3 ; Add immediate value 3 to register 0x14
-
The macros hide direct address register banking. On entry, each macro assumes that the current bank is bank 0 (the RP bits in STATUS are all set to zero). If the macro needs to switch banks to access a register which is located beyond bank 0, it automatically does so. It then restores the current bank back to 0.
-
Macros are provided for multi-precision arithmetic, including ADC and SBC (add with carry and subtract with carry). For example, to add two 32-bit numbers, this sequence can be used:
a equ 0x10 ; Location of first 32-bit number
b equ 0x14 ; Location of second 32-bit number
add a+0, b+0 ; Add, set carry correctly
adc a+1, b+1 ; Add with carry, set carry correctly
adc a+2, b+2 ; Add with carry, set carry correctly
qadc a+3, b+3 ; Quick add with carry (does not set carry correctly)
- A complete set of branches is provided for unsigned comparisons, using Motorola syntax (so you don't have to think about the skip instructions):
cmp a, b ; Compare a with b, then one of:
jeq target ; Jump to target if equal
jne target ; Jump to target if not equal
jhi target ; Jump to target if a is higher than b
jhs target ; Jump to target if a is higher or same as than b
jlo target ; Jump to target if a is lower than b
jls target ; Jump to target if a is lower than or same as b
- Macros are provided for proper code page switching:
Use jsr when calling a subroutine that is located within the same page:
jsr fred ; Jump to subroutine fred
fred . . . ; Do something
rts ; Return
Use farjsr when calling a subroutine that is located in a different page:
farjsr bob ; Jump to subroutine bob
bob . . . ; Do something
rts ; Return
- Macros for flash-memory (code-space) based table indexing
Create a table of bytes using the "table" and "val" macros:
mytable table
val 0x10 ; Table index 0
val 0x20 ; Table index 1
val 0x30 ; Table index 2
Use short_lookup, lookup or far_lookup depending on the location of the table:
ldi 0x10, 2 ; Table index of 2 in register 0x10
lookup mytable, 0x14, 0x10 ; Index mytable
; Register 0x14 now has 0x30
Which lookup should you use?
- short_lookup if table is in same 256 word page
- lookup if table is in same 2K word page
- farlookup if table is in a different page
You do not have to use these except as a prefix for this OPTION or TRIS instructions. You may want to use them if you need to store the same value into many registers or for chained accumulator calculations.
ldwi <imm> ; Load immediate value <imm> into W
addwi <imm> ; Add immediate value <imm> into W
rsbwi <imm> ; Reverse subtract immediate: immediate - W -> W
orwi <imm> ; Bit-wise OR immediate value <imm> into W
andwi <imm> ; Bit-wise AND immediate value <imm> into W
xorwi <imm> ; Bit-wise exclusive-OR immediate value <imm> into W
ldw <src> ; Load register <src> into W
stw <dest> ; Store W into register <dest>
clrw ; Clear W
negw ; 2's complement negate W (rsbwi 0)
comw ; Bit-wise complement W (xorwi 0xFF)
ld <dest>, <src> ; Load register <src> into register <dest>
ldi <dest>, <imm> ; Load immediate value <imm> into register <dest>
add <dest>, <src> ; Add register <src> to register <dest>
addi <dest>, <imm> ; Add immediate value <imm> to register <dest>
adc <dest>, <src> ; Add with carry register <src> to register <dest>
adci <dest>, <imm> ; Add with carry immediate value <imm> to register <dest>
qadc <dest>, <src> ; Quick version of adc: does not set resulting carry correctly
qadci <dest>, <imm> ; Quick version of adci: does not set resulting carry correctly
sub <dest>, <src> ; Subtract register <src> from register <dest>
subi <dest>, <imm> ; Subtract immediate value <imm> from register <dest>
sbc <dest>, <src> ; Subtract with carry register <src> from register <dest>
sbci <dest>, <imm> ; Subtract with carry immediate value <imm> from register <dest>
qsbc <dest>, <src> ; Quick version of sbc: does not set resulting carry correctly
qsbci <dest>, <imm> ; Quick version of sbci: does not set resulting carry correctly
cmp <dest>, <src> ; Compare register <dest> with register <src>
cmpi <dest>, <imm> ; Compare register <dest> with immediate value <imm>
and <dest>, <src> ; Bitwise-AND register <src> into register <dest>
andi <dest>, <imm> ; Bitwise-AND immediate value <imm> into register <dest>
or <dest>, <src> ; Bitwise-OR regiter <src> into register <dest>
ori <dest>, <imm> ; Bitwise-OR immediate value <imm> into register <dest>
xor <dest>, <src> ; Bitwise exclusive-OR register <src> into register <dest>
xori <dest>, <imm> ; Bitwise exclusive-OR immediate value <imm> into register <dest>
ldb <dest>, <dest-b>, <src>, <src-b> ; Load bit number <dest-b> of register <dest> from bit number <src-b> of register <src>
ldnb <dest>, <dest-b>, <src>, <src-b> ; Same as above, but load the complement of the bit
Incremement or decrement register depending on bit value:
addb <dest>, <src>, <b> ; Add bit number <b> of register <src> to register <dest>
; This one affects Carry and Zero.
incb <dest>, <src>, <b> ; Increment register <dest> if bit number <b> of register <src> is set
; This one does not affect Carry. Zero is affected only if the bit was set.
subnb <dest>, <src>, <b> ; Subtract inverse of bit number <b> from register <src> from register <dest>
; This one affects Carry and Zero.
decb <dest>, <src>, <b> ; Decrement register <dest> if bit number <b> or register <src> is clear
; this one does not affect Carry. Zero is affected only if the bit was clear.
clr <dest> ; Clear register <dest>
inc <dest> ; Increment register <dest>
dec <dest> ; Decrement register <dest>
rol <dest> ; Rotate register <dest> left through carry
lsl <dest> ; Logical shift left register <dest>
asl <dest> ; Arithmetic shift right register <dest>
ror <dest> ; Rotate register <dest> right through carry
lsr <dest> ; Logical shift right register <dest>
asr <dest> ; Arithmetic shift right register <dest>
com <dest> ; Bitwise complement register <dest>
neg <dest> ; 2s complement negate register <dest>
swap <dest> ; Swap halves of register <dest>
addc <dest> ; Add carry to register <dest>
qaddc <dest> ; Quick version of above: does not set Carry correctly
subb <dest> ; Subtract borrow (negative of carry flag) from register <dest>
qsubb <dest> ; Quick verison of above: does not set Carry correctly
bic <dest>, <b> ; Clear bit number <b> of register <dest>
bis <dest>, <b> ; Set bit number <b> of register <dest>
clc ; Clear carry flag
stc ; Set carry flag
clz ; Clear zero flag
stz ; Set zero flag
jbc <src>, <b>, label ; Jump if bit number <b> of <src> is clear
jbs <src>, <b>, label ; Jump if bit number <b> of <src> is set
jeq label ; Jump if equal (Z set)
jne label ; Jump if not equal (Z clear)
jcs label ; Jump if Carry set
jcc label ; Jump if Carry clear
jlo label ; Jump if lower (same as jcc)
jls label ; Jump if lower or same (jump if C=0 || Z=1)
jhi label ; Jump if higher (jump if C=1 && Z=0)
jhs label ; Jump if higher or same (same as jcs)
jac label ; Jump if digit (aux) carry is clear
jas label ; Jump if digit (aux) carry is set
decjne <dest>, label ; Decrement register <dest>, jump if it's not zero
incjne <dest>, label ; Increment register <dest>, jump if it's not zero
jsr label ; Jump to subroutine at label (same page), (same as CALL)
farjsr label ; Jump to subroutine at label (different page)
rts ; Return from subroutine (same as RETURN)
jmp label ; Jump to label (same as GOTO)
farjmp label ; Jump to label (different page)
nop ; No Operation
sleep ; Sleep until interrupt
clrwdt ; Clear watchdog timer
tris ; Load TRIS register (use with ldwi)
option ; Load option register (use with ldwi)
; Index <table> with regsiter <src>, put result into <dest>
short_lookup <table>, <dest>, <src> ; Use if table is in same 256 word page
lookup <table>, <dest>, <src> ; Use if table is in same 2K word page
far_lookup <table>, <dest>, <src> ; Use if table is in a different page
fred table ; Create a byte table called fred (alias for "addwf pcl, 1")
val 1 ; val is alias for "retlw"
val 2
val 3
. . .
Multi-byte increment can be done quickly like this (does not affect any flags)
incfsz dest, 1 ; Inc dest, skip if result is zero (skip if we carried)
goto done
incfsz dest+1, 1 ; Inc dest+1, skip if result 0
goto done
incfsz dest+2, 1
goto done
incfsz dest+3, 1
goto overflow
done
overflow
There are up to 512 registers [? depends on PIC].
One way to access them is through the IND / FSR indirect addressing mechanism. In this case, FSR supplies the lower 8 bits of the address and the IRP bit of the status register supplies the top bit.
Direct addressing is only 7 bits [? depends on PIC], so there are multiple direct address banks. The RP bits of the status register selects the bank.
The following registers are aliased into all of the banks:
indf
pcl
status
fsr
pclath
intcon
general purpose registers 0x70 - 0x7F
For direct address access to the other registers, you use the "banksel" pseudo-instruction:
banksel ANSELA
clrf ANSELA
banksel PORTA
Borrow is inverted (0 - 1 give a clear carry).
There are multiple code page sizes to worry about: 256 bytes for adding to PC for table lookup and 2K bytes for jmp (GOTO) and jsr (CALL) [The page size depends on the PIC]. PCLATH supplies the upper bits so that the entire memory map can be reached with GOTO and CALL.
MPASM has "pagesel" pseudo-op to help deal with this. I provide farjmp and farjsr, which use pagesel.
We adhere to the convention that PCLATH holds the current CALL / GOTO page by default. This way, we can CALL subroutines in the current page without having to modify PCLATH.
Thus when calling a subroutine in a different page, use the farjsr macro. It sets up PCLATH for the target bank, calls the subroutine, then restores PCLATH back to the current bank. Note that the RETURN and RETLW instructions do not modify PCLATH.
[PIC16F720 has only a single 2K page, which equals the reach of CALL and GOTO, so no need to worry about farjsr..]
[PIC10F200 does not have RETURN, only RETLW]
[Some PICs (for example PIC10F200 and PIC16F59) do not have PCLATH, instead the page number is provided by bits in the STATUS register. On these, the CALL address is shorter than the GOTO address. The missing bits are set to 0. Likewise, when jumping by modifying PCL, the missing bits are set to 0. It means CALL and jumps via PCL modification can not reach all of code memory].
With MPASM, correct sequence is:
pagesel dest
call dest
pagesel here
here
If you do a bit operation on a PORT, the other bits could be affected since it's a RMW operation (for example if input give different value than output, you will end up changing the output).
Don't forget to write to ANSEL bits.. pins selected for analog input (which is the default) always read 0.
On 10F200, you must clear the T0CS bit of OPTION since it prevents GP2 from being used as an output.
The interrupt handler must save the context without changing it. This means that we need at least one register which is reachable no matter what the current bank is set to. Luckily registers 0x70 - 0x7F are aliased into every bank [this is PIC dependent].
This sequence saves and restores the full CPU context for proper interrupt handling.
; Code assumes these are in bank 0, but could be changed
saved_fsr equ 0x7c
saved_pclath equ 0x7d
saved_status equ 0x7e
; This one must be always visible, no matter what the RP bits are set to!
saved_w equ 0x7f
; Reset vector
rstvec org 0x0
jmp run
; Interrupt vector
org 0x4
; Interrupt!
; Save full context: STATUS, W, PCLATH and FSR
movwf saved_w ; Saved_w must be visible in all banks (0x70 - 0x7F)
swapf STATUS, W ; Pre-swap status, this is undone in restore
banksel 0 ; The rest can be anywhere, we choose bank 0
movwf saved_status
movf PCLATH, W
movwf saved_pclath
movf FSR, W
movwf saved_fsr
; You ISR goes here
. . . .
; Restore full context
; Assumes we are still in bank 0
movf saved_fsr, W
movwf FSR
movf saved_pclath, W
movwf PCLATH
swapf saved_status, W ; Swap status nibbles back to correct order
movwf status
swapf saved_w, F ; Swap nibbles in saved_w (does not affect STATUS)
swapf saved_w, W ; Swap back to normal when transferring to W (does not affect STATUS)
; Return from interrupt...
retfie