PIC High-end Cores Introduction

        .cr     pic16   To load this cross overlay

This Cross overlay is only available as of Version 3.01.00 of the SB-Assembler. It is intended for the High-end family of PIC micro controllers, also known as the PIC18F family. I know, it may be a bit confusing that a Cross overlay called pic16 is for the PIC18 series. That's because I have named my Microchip Cross overlays after their instruction bit-size, rather than the family name. You can blame Microchip for making such a mess of PIC family names if you like.

Some of the PIC18F family members have a so called Extended High-end instruction set. This not only adds a hand full of extra instructions, it also changes the behaviour of many other instructions. As the Extended instruction set is purely added for compiler optimization, Microchip discourages the use of it by Assembly language programmers.
The Extended High-end instruction set not only adds extra instructions. It also changes the behaviour of Access bank GPR addressing of almost half of the PIC18's instructions too. Be aware that, enabling the Extended High-end instruction set alone, will almost certainly break your existing code, which ran perfectly in normal mode.
Therefore I've decided not to implement the Extended High-end instruction set in the pic16 Cross overlay. It's not worth adding more complexity for a features which is very likely never used.
Per default all PIC18F family devices have the Extended High-end instruction set disabled after reset. So you should be good with just the standard instruction set.

Programming Model

I only include a little summary about the features of the PIC High-end family. All these family members have a 16-bit instruction size in common. It is not my intention to make the original documentation obsolete, so please refer to the original documentation for further details.

Register File

General Purpose RAM (called GPR) and Special Function Registers (called SFR) share the same 4 kB linear memory space. Not necessarily all GPR locations are physically implemented on all devices. And the actual number of SFR can vary between devices too.
What they all do have in common though is that GPR starts at address $000 and upwards, while SFR space ends at address $FFF and extends downwards.

PIC18F register file

Although the Register file has a linear address range from $000 to $FFF, only blocks of 256 bytes each can be directly accessed. This comes down to a total of 16 blocks, or banks, to span the entire 4 kB range. The BSR register is used to hold the 4 upper bits of the Register file address, while the instruction's opcode holds the least significant 8 bits of the Register file address.
This means that you, as the programmer, have to make sure that the BSR register points to the intended bank, otherwise the wrong memory location will be accessed. To make life a little bit easier some part of GPR in bank 0 and SFR in bank 15 can be accessed through something called the Access Bank. Each device has its own division between the number of Access Bank GPR and SFR. The SB-Assembler assumes a division at $60. This means that Access Bank addresses below $60 come from Register file addresses $000 to $05F. And that Access Bank SFR starts at Register file address $F60, and extends through to the end $FFF.
Not all devices may have this division line at $60 though. Therefore you can set this devision yourself using the .AB Access Bank directive. This doesn't mean that you can select the actual division line yourself, it remains fixed by the device's design. It only tells the assembler where the division line really is.
Confused? So was I at first. If the 'a' bit of an instruction is 0, the processor will use the Access Bank, ignoring the contents of the BSR register. If the 'a' bit of an instruction is 1, the processor will use the BSR register to provide the upper 4 bits of the Register file address. That's really all that's to it. Remember that the Access Bank is also 256 bytes in size. The first part of this bank is occupied with GPR RAM, while the last part is occupied by SFR registers. The division line simply tells you where the GPR part ends and the SFR part begins.
You can specify whether you want to use the Access bank or not by supplying the access bit operand (always the last operand) in your operand field. An cacess bank bit value of 0 means that you force the use of the Access Bank, ignoring the BSR register's contents. An Access bank bit value of 1 means that you force the use of the BSR register, regardless of the linear address. Any other value will cause a Out of range error.
If you do not specify the access bit in your operand field the assembler will determine whether it can use the Access bank or not. If the 12-bit address is below the division line of bank 0, the Access bank bit will be cleared to 0. If the 12-bit address is above or equal to the division line of bank 15, the Access bank bit will be cleared to 0 too. In all other cases the Access bank bit will be set to 1, which uses the BSR register.

Warning:Forcing the assembler to use Access bank mode while the 12-bit Register file is not actually within the Access bank will very likely break your code. It will only work correctly when the BSR register accidentally is set to the correct bank. The assembler will not warn you for this!
Forcing the use of the non Access bank mode has no side effect, as long as the BSR register is loaded with the correct bank.

The Register file can also be accessed indirectly. A total of 3 pairs of SFRs can be used as 12 bit pointers, FSR0, FSR1 and FSR2. Any of these pairs can hold the 12-bit Register file target address. FSR0 for instance is divided in to SFR registers called SFR0H and SFR0L. SFR0H holds the 4 upper bits of the 12-bit address, while SFR0L holds the lower 8-bits of the address.
With these pointers we can access any of the File registers. You do that by accessing one of the three registers INDF0, INDF1 or INDF2.
You can also increment or decrement the pointer register pairs by using any of the registers POSTDECx, POSTINCx or PREINCx, where x is 0, 1 or 2. Please note that there is no PREDECx register., so pre-decrementing the pointers is not possible.
Indexed indirect mode is also possible by using the PLUSWx register to access the required address. The 2s complement value of the W register is added to the pointer register pair to calculate the target address.


The Working register W is not part of the register file and is used as an operand in most instructions. On other micro processors this register would have been called the Accumulator.
This register is mirrored as an SFR, which is called WREG.

Status byte register

This SFR register contains up to 8 system flags.

Bit 7-Unimplemented, reads as 0
Bit 6-Unimplemented, reads as 0
Bit 5-Unimplemented, reads as 0
Bit 4NNegative Flag
Bit 3OVOverflow Flag
Bit 2ZZero Flag
Bit 1DCBCD Digit Carry Flag
Bit 0CCarry Flag

The N, OV, C, DC and Z flags reflect the status of previous instructions.

Program pointer

The PIC18F family of processors can address up to 2 M Bytes of program memory. You'll need a 21-bit program pointer to address 2 MB, however since instructions always start at an even address bit 0 will always be 0, making it an even numbered address. Only data bytes can be stored at odd numbered addresses. The program counter is always incremented by 2 after fetching a word sized instruction. It is incremented by 4 after fetching a double word sized instruction.
The PIC18F family has a traditional RETLW instruction, which is used to store literal data. However you can also store single data bytes in program memory too. Labels can even be assigned to odd numbered addresses, as long as the next generated byte is from a data directive. If the next generated byte comes from an instruction a padding byte is added to force the address to become even numbered again, which will automatically make the label's value even numbered as well.

The program pointer is accessible through SRFs PCLATU, PCLATH and PCL. Reading PCL will provide the lower 8 bits of the current program pointer, while latching the other bits in the PCLATH and PCLATU registers. That way you can be sure that all 3 bytes remain atomic.
Writing to PCL will write 8 bits to the lowest 8 bits of the program counter and copies PCLATU and PCLATH to the higher bits of the program counter. This is useful for computed jumps.
Bit 0 of PCL is fixed to 0, which forces the program counter to be even numbered at all times.


The stack on the PIC18F is 31 levels deep and can only hold 21-bit return addresses. Level 0 is not really a stack level and can't hold any data. The stack pointer is incremented before a return address is pushed to it.
The stack is also readable/writable through SFR, which allows you to manipulate the top of stack pointer and the contents of that location. Some status bits exist which show you the stack's health. These bits indicate whether the stack is empty, full, overflowed, or underflowed. You can also enable a device reset on overflow or underflow condition.

Interrupt return addresses are also stored on the stack. Fast content switching is done by copying the SFRs STATUS, WREG and BSR to their shadow registers.
This can be seen as a one level stack! So if you enable both low level and high level interrupts you'll have to provide for content switching yourself on the low level interrupts, as the shadow registers will be destroyed when a high level interrupt interrupts a low level interrupt.

Reserved Words

The SB-Assembler PIC16 High-end cross overlay has only 2 reserved words, W and F. Avoid assigning labels with these two names, and you're safe.

Target Files

All 16-bit instruction words are stored as pairs of bytes in the target file. At the same time the program counter is always incremented by 2. This is different than the usual approach, but quite straight forward all the same.
All program memory references are therefore multiplied by 2. A CALL, GOTO and Branch destinations all point to the byte address of the destination, not the word address, as is usually the case for smaller PIC micro controllers. This means that the assembler will have to divide the destination address by 2 in order to point to the proper location.
This also implies that a CALL, GOTO or branch destination never can be an odd numbered byte address! A Out of range error will be reported if you do use an odd numbered destination address.

Because of the large program memory range of 2 MB, you'll have to use a target file format which is not restricted to the traditional 64 kB maximum size. Choosing the Intel Hex file format is a safe choice, also because most PIC programmers will understand that format.

In case a device has built in EEPROM memory, this memory is stored in the target file from memory address $F00000 upwards. EEPROM data is stored in byte sizes. No padding bytes are used to make EEPROM data word sized.
Don't forget to raise the memory size to infinite before you start storing bytes there.

Eight ID bytes are available from address $200000 to $200007. You can store any 8 byte sized data you like there. These ID words can simply be generated by any data generating directive. The pic16 Cross overlay doesn't have a dedicated .ID directive any more, like the other PIC Cross overlays have.
Don't forget to raise the memory size to infinite before you start storing bytes there.

Configuration words start from address $300000 in memory. There can be many. Every configuration word consists of 16 bits. These configuration words can be written by any data generating directive. The pic16 Cross overlay doesn't have a dedicated .CW directive any more, like the other PIC Cross overlays have.
Don't forget to raise the memory size to infinite before you start storing bytes there.

Special Features

I've added a few new directives specifically tailored to suit the PIC cross overlay. All new directives are explained later on this page.

Altered behaviour of Directives

Only one data byte per instruction word could be stored by PIC families with less than 16-bit instruction sizes. Therefore they had to rely on the RETLW instruction to create data tables. With those families the SB-Assembler simply translated every generated data byte into a RETLW instruction.
The PIC18F can still use the RETLW instruction to generate data tables. However the preferred way of storing data tables is to store up to 2 bytes per instruction word, which makes better use of the available Flash memory.

Therefore the pic16 Cross overlay does not translate generated data bytes into RETLW instructions any more. No restrictions apply any more regarding the generation of data tables.


Data bytes can be stored as RETLW instruction words in the program memory. If you want to read such data you should CALL to the desired location. The RETLW instruction at that location will immediately return to the calling routine with a literal value loaded in W.
You may add multiple expressions behind a single RETLW instruction, effectively generating multiple RETLW instructions, each with its own return value.

You may also omit the data byte completely after a RETLW instruction, effectively making it a RETURN instruction. Keep in mind that the RETLW instruction without return value will return with the W register cleared to 0. Fortunately the High-end cores have a real RETURN instruction, which doesn't affect the W register.
If you do want to omit the data value, make sure any comments will follow the RETLW instruction after at least 10 spaces, otherwise the comment will be interpreted as return value.


        RETLW  $12,$34,$56
        RETLW          Comment after at least 10 spaces

RETLW Table Page crossing

The SB-Assembler will not warn you if a RETLW data table spans across two or more pages of program memory. A RETLW data table can span multiple pages, but you'll have to make special arrangements to allow a carry from PCL into the higher bytes of the program counter if a table crosses page boundaries. Please also keep in mind that bit 0 of PCL is fixed to 0, which means that a RETLW table can not be larger than 128 bytes to keep it in one page of program memory.

You can use the .OT and .CT directives to warn you for page crossings if you like.

Immediate prefix is optional

Immediate data is called literal data in Microchip's documentation. Usually instructions that can handle immediate data end with the letters LW, which indicates that a literal value is to be used. The immediate pre-fixes #, /, = or \ may be used to identify an immediate value. The assembler will assume the default # pre-fix if no pre-fix symbol precedes the operand value, effectively using the least significant bits (as many as will fit) of the 32-bit value as operand.

Destination flag

Some instructions allow you to store the result in the W register or in the register file. You tell that to the assembler by adding ,W or ,F behind the operand of the instruction.
If you don't specify the destination flag the default destination will be F.

You can also use an expression as destination flag. The W register will be the destination if the expression evaluates to 0. And it will use the register file if the expression evaluates to 1. Any other value will generate a Out of range error.

Access Bank bit

Many instructions can operate on the so called Access Bank to determine the upper 4 bits of the register file address. The Access Bank parameter is optional. If you omit it, the assembler will determine the Access Bank bit for you. This process has been described above.

Many instructions also have an optional Destination flag, in which case both parameters are optional. If you do want to specify the Access Bank parameter, the Destination flag is no longer optional.

Pseudo instructions

I haven't found any recommended Pseudo instructions in Microchip's documentation for the High-end devices. Granted most of the Pseudo instructions from the smaller PIC families are no longer necessary on the PIC18F series.
Anyway, I have taken the liberty to add some Pseudo instructions, which I think can be useful. Please consult the opcode test file for a list of them.

Extra Directives

The PIC High-end Cross overlay requires some extra directives. These are used to set some of the device specific options options.

.AB     Access Bank


        .AB     expression


With this directive you can change the division between GPR RAM memory and SFR register for the so called Access Bank. Per default this division is set to $60, but can be changed to anything between 0 and 255 using this directive. You'll have to set the division correctly for the SB-Assembler to choose the right Access Bank bit in case you omit the Access Bank parameter in the operand filed.


Many of the file register related instructions can either use the Access Bank or the BSR register to supply the upper 4 bits of the file register address. You can force either mode by supplying the Access Bank parameter at the end of the operand field. But if you don't, you let the assembler decide which source for the upper 4 address bits to use.
The BSR register is always used as the source of the 4 upper address bits when the 12 bit address is pointing to file register banks 1 to 14. Only when the file register bank evaluates to 0 or 15 it becomes a bit more complicated. If bank 0 is being addressed and the address is below the Access Bank division, the Access Bank addressing mode is chosen. Also when bank 15 is being addressed and the 8 least significant bits of the register file address is above or equal to the access bank division, the Access Bank addressing mode is chosen. Otherwise the BSR addressing mode is selected.
With the .AB directive you can set the Access Bank division to any value between 0 and 255. Any other value will result in a Out of range error during Pass 2. It goes without saying that you'll have to set this division to the same value as being used by your PIC18F micro controller, otherwise you're program will very likely not do what you want it to do, without even telling you why.
Please note that the actual division is fixed by the design of the processor you use. The .AB directive only tells the assembler where the division lies.

The default Access Bank boundary is set to $60.

              1        .AB     $60
0012-         2  FILE  .EQ     $12         Is in Access bank
0123-         3  GPR   .EQ     $123        Can't be in access bank
0F80-         4  SFR   .EQ     $F80        Is in Access bank
0000-         5  A     .EQ     0           Access bank select
0001-         6  B     .EQ     1           BSR select
0000-         7
0000-12 24    8 ADDWF  FILE,W             Auto Access bank
0002-12 26    9        ADDWF   FILE,F     Auto Access bank
0004-12 26   10        ADDWF   FILE       Dest is F, Auto Access bank
0006-        11
0006-23 25   12        ADDWF   GPR,W      Auto BSR mode
0008-23 27   13        ADDWF   GPR,F      Auto BSR mode
000A-        14
000A-23 24   15        ADDWF   GPR,W,A    Forced Access bank !!!
000C-23 26   16        ADDWF   GPR,F,A    Forced Access bank !!!
000E-12 25   17        ADDWF   FILE,W,B   Forced BSR mode
0010-12 27   18        ADDWF   FILE,F,B   Forced BSR mode
0012-        19
0012-80 24   20        ADDWF   SFR,W      Auto Access bank
0014-80 26   21        ADDWF   S          Auto Access bank

Please note that in line 15 and 16 I forced the use of the Access bank, even though the register file address is clearly outside the Access bank's range. This way I access the wrong register, which very likely breaks my code, without a warning from the assembler!
Forcing the use of the BSR register will always work, as long as you make sure that the BSR register holds the proper upper 4 bits of course.

.CT     Close Table




This directive signals the end of your table in memory. It will present a Table crossed page boundary error if this directive is located on a different page than which was stored by the previous .OT directive.


Tables of data will usually not allow the high bytes of the program counter to change in between. That's why the .OT and .CT directives are used to signal the beginning and the end of a table, verifying that a page crossing hasn't occurred in between.

The .OT directive should be placed at the beginning of the table. It will memorize the memory page of the beginning of the table. At the end of the table the closing .CT directive should still be on the same memory page. There's only one exception to this rule, and that is when the .CT is at the first location of the next page, because then the table ended just in time on the previous page.
No error message is reported when both pages are equal. A Table crossed page boundary error is reported when they do differ during pass 2 of the assembly process. The error message is only reported during pass 2 to enable you to find out at what location the table crossed the page boundary in order to fix the problem by either moving the table or moving away some code in front of the table.
No error or warning is shown when there was no matching .OT directive found.

Remember that the PIC18F stores two bytes per instruction. Therefore bit 0 of PCL always remains 0. This means that a RETLW table is allowed to grow up to 128 bytes in length to fit on one memory page.
You can make larger tables though. Then you'll have to take the carry into PCLATH and PCLATU into account, making the table indexing a bit more complex.

A better method of storing data in program memory allows two bytes of data to be stored in each instruction location. Then you can use the TBLRD instruction to read those bytes a lot easier.


The example below shows a typical lookup table routine. It converts a decimal digit to a seven segment pattern. I think it clearly demonstrates the use of the .OT and .CT directives.

BCD7SEGM  ADDWF   WREG,W      Multiply BCD by 2
          .OT                 Mark the beginning
          ADDWF   PCL
          RETLW #%0011.1111   0  Return with pattern for '0'
          RETLW #%0000.0110   1  Return with pattern for '1'
          RETLW #%0101.1011   2  Return with pattern for '2'
          RETLW #%0100.1111   3  Return with pattern for '3'
          RETLW #%0110.0110   4  Return with pattern for '4'
          RETLW #%0110.1101   5  Return with pattern for '5'
          RETLW #%0111.1101   6  Return with pattern for '6'
          RETLW #%0000.0111   7  Return with pattern for '7'
          RETLW #%0111.1111   8  Return with pattern for '8'
          RETLW #%0110.1111   9  Return with pattern for '9'
          .CT                 End of table, checking page

.MS     Memory Size


        .MS  expression


This directive is used to set the expected program memory size of your particular device.


The .MS directive is preferably used before any code is generated. Although the SB-Assembler is perfectly happy if you use the directive just in time before you actually run out of the default memory size of 2048 bytes.

On the PIC18F there's only one reason to set the memory size, and that is to warn you if you run out of Flash memory. The first byte written beyond the current memory size will generate a Warning: Target program memory full warning.

The .MS directive will accept any expression resulting in a value between $0400 to $200000 (2 M Byte). It will also accept the value 0, which basically sets the limit higher than the maximum value of a 32-bit number, effectively setting it to infinite. This is useful when you want to write ID bytes, Configuration words, and EEPROM data, which is usually stored behind the program memory.
A Out of range error will be reported if the expression evaluates to a value above or below these limits. The expression may not contain forward referenced labels.
Any other size is accepted, even silly ones.

The default memory size is $0400 words.

.OT     Open Table




This directive signals the beginning of your table in memory.


.OT saves the current memory page address internally. Later on it should match the memory page address of the next .CT directive.

The .OT directive will automatically call the .CT function if you previously started a table without closing it properly with a .CT directive. After closing the previous table this way the new table will start at the current location as if you had inserted a .CT directive yourself. This way you can concatenate several tables after each other.
Remember though that it becomes more difficult to find a piece of memory large enough to hold all your concatenated tables if you rely on this automatic closing of previously opened tables.

Please note that the .OT directive would never generate the Table crossed page boundary error by itself if it wasn't for the automatic calling of the .CT routine when a previous table wasn't closed.

Extra Error Messages

Only one extra error messages is added to the standard repertoire of error messages.

Table crossed page boundary

This error is generated by the .CT directive and sometimes by the .OT directive.
The error is only generated if the .CT directive is on a different memory page than the previous .OT directive. If the error occurs it will be reported during pass 2 of the assembly process only. This is to simplify the search for a suitable new place for your table. You would have no way of telling at what address the page was crossed if the error was reported during pass 1 instead.

Overlay Initialization

Four things are set while initializing the PIC18 overlay every time it is loaded by the .CR directive.

  • Little endian mode is selected for the data generating directives. This means that words or long words are stored with their low byte first.
  • Default memory size is set to $800 bytes.
  • The target factor is set to 1.
  • One extra error message is added to the list of standard error messages.

Differences Between Other Assemblers

There are some differences between the SB-Assembler and other assemblers for the PIC18F family processor. These differences require you to adapt existing source files before they can be assembled by the SB-Assembler. This is not too difficult though, and is the (small) price you have to pay for having a very universal cross assembler.

  • Not all assemblers will allow multiple literal values, or none at all, following one RETLW mnemonic.
  • The obvious differences in notation of directives and radixes common to all SB-Assembler crosses.
  • None of the SFR registers and bit names are pre-defined. They can be declared using normal labels though.