PIC 16cxxx Introduction

Microchip offers a wide range of PICmicro RISC controllers. This page describes the SB-Assembler's behaviour for the Mid Range PIC16Cxxx family, which also includes the popular PIC16F84.

According to Microchip all these controllers are easy to learn because there are only some 30 to 40 instructions to learn. Unfortunately not everything is as easy as it may seem. I must admit that the base models are quite easy to comprehend. But things get complicated very quickly when larger controllers start working with memory paging and register bank switching.

The RISC architecture of the devices requires some getting used to. All instructions occupy only one instruction word. These instruction words are 12 to 16 bits wide, which reflects on the memory layout of the target file.

The maximum ROM capacity for this series of controllers is 8 k words of 14-bits each. This address range is divided into 4 memory pages of 2 k words each.

Data memory is only 8-bits wide and is organized as the so-called register file F, which is described shortly.

SB-Assembler Version Differences

This page describes the PIC Mid-range cross overlay for SB-Assembler Version 2 only. Microchip has made quite a little mess of the instruction sets throughout the entire family that it has become impractical to handle both versions on one page. After all Version 2 of the SB-Assembler is no longer maintained, therefore new instructions are not added to that version any more.
If you want the latest and greatest cross overlay for the PIC Mid-range family please go to the Mid-range page for SB-Assembler V3.

Programming Model

I only include a little summary about the features of the PIC16Cxxx programming model here. It is not my intention to make the original documentation obsolete, so please refer to the original documentation for further details.

PIC16Cxxx programming model

The register file can be addressed by a 7-bit pointer, giving a total addressing range of 128 bytes.
The first 32 bytes of this range contains up to 4 banks of special function registers. Those registers are used to set options, control peripherals, and address the I/O ports. I will only describe the most important control registers that are present on almost all devices here.
The 96 remaining addresses of the register file are general purpose registers. The main part of this area can also be bank switched with up to 4 banks, depending on the device. Only the registers $070 to $07F are never bank switched and are accessible from within any bank. This means that addresses $070 to $07F are equal to addresses $F0 to $FF and addresses $170 to $17F and addresses $1F0 to $1FF.
Some devices may implement unused Special Function Registers as normal general purpose registers.

All registers can be accessed directly or indirectly. Addresses from bank 0 range from $000 to $07F, bank 1 ranges from $080 to $0FF, bank 2 ranges from $100 to $17F, and finally bank 3 ranges from $180 to $1FF.
The bank switching itself is done by setting 2 bits in the Status register appropriately.


The Working register W is not part of the register file and is used as an operand in most instructions. On other microprocessors this register would have been called the Accumulator.

f$00 Indirect Data Addressing

This is not a physical register, but is used to indirectly address any of the register file's data. If you refer to address f$00, you actually address the file register pointed to by the contents in FSR (f$04). The IRP bit in the Status register is used as the ninth bit of the register's address.

INDF is never bank switched and can be reached at addresses $000, $080, $100 and $180.

f$02 PCL

This is the low byte of the program counter. Normally the entire program counter is incremented after fetching each instruction. The program counter can access up to 8 k words of memory. After reset the program counter is initialized to all 0's, effectively starting the program at the lowest available address.
Only the low byte of the program counter can be read and written to under program control. All other bits of the program counter can only be written to by means of the PCLATH f$0A register.
Writing to the PCL register in any way effectively causes a computed jump. Bit b12..b8 of the program counter will get the value of the PCLATH register, effectively loading the program counter with a 13-bit new value. Devices with less memory will mirror their normal ROM memory into the unoccupied areas (unimplemented bits of the program counter are ignored).
GOTO and CALL instructions can only change the bits b10..b0 directly. Bits b12 and b11 will get their value from the corresponding bits (b4 and b3) of the PCLATH register, when implemented in the device.
It is the programmer's responsibility to set PCLATH properly before any GOTO or CALL instruction.

PCL is never bank switched and can be reached at addresses $002, $082, $102 and $182.

The PIC16Cxxx series employs a 13-bits wide eight level deep hardware stack for return addresses. This means that subroutines may be nested 8 levels deep (including one level for a pending interrupt) before the stack overflows which looses the oldest return address. The stack can not be manipulated in any other way then by the CALL, RETLW, RETURN and RETFIE instructions or by an interrupt.

f$03 Status word register

This register contains 8 system flags.

Bit 7IRPIndirect Register bank select bit
Bit 6RB1Select Register Bank bit 1
Bit 5RP0Select Register Bank bit 0
Bit 4TOTime Out Flag
Bit 3PDPower down Flag
Bit 2ZZero Flag
Bit 1DCBCD Digit Carry Flag
Bit 0CCarry Flag

The C, DC and Z flags reflect the status of previous instructions. The PD and TO bits can be used to find the reason why the device was restarted and are read-only bits. The bits RP1 and RP0 are used to select one of the 4 possible register file banks during direct addressing. The IRP bit is the ninth bit (b8) during indirect addressing of the register file, while the FSR register holds the other 8 addressing bits.

STATUS is never bank switched and can be reached at addresses $003, $083, $103 and $183.

f$04 FSR

The File Select Register is used as a data pointer for indirect addressing of any of the registers in the file. The FSR register holds 8 bits of the 9 bits which are needed to address up to 4 banks of 128 registers each. The ninth bit (b8) is the IRP bit in the Status register.
Reading or writing to register f$00 will effectively read or write to the address pointed to by the contents of the FSR register and the IRP bit in the Status register.

FSR is never bank switched and can be reached at addresses $004, $084, $104 and $184.


This register is used as a buffer for the 5 most significant address bits.
The bits b4 and b3 of this register are copied to the program counter whenever a GOTO or CALL instruction is executed.
The bits b4..b0 of this register are copied to the 5 most significant bits of the program counter whenever the PCL register is directly written to.

It's the programmer's responsibility to set PCLATH before any displacement operation.

PCLATH is never bank switched and can be reached at addresses $00A, $08A, $10A and $18A.

Please note that none of the special function register names are pre-defined by the SB-Assembler. You can use normal labels in combination with the .EQ directive to declare names for all the file registers.

Reserved Words

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

Target Files

Storing 14-bit instruction words in an 8-bit oriented target file requires special treatment. You may choose any target file format you like. Most PIC programming devices require you to supply an Intel HEX file though, so I advise you to use only the Intel format for your target files when writing programs for the PICmicro families.

Microchip recommends 2 methods of storing your target files. The first method is to split the 14-bit instruction words and store the low bytes in one target file and the remaining bits in a second target file. The SB-Assembler doesn't support this method, which is not such a big problem because the second method is accepted by all PIC programming devices I have seen so far.
The second method that is recommended by Microchip is to store all instruction words in one target file, low byte first, and the remaining bits in the next byte. This method is supported by the SB-Assembler.

Please note that the SB-Assembler will always store pairs of bytes and that all source addresses are doubled in the target file because of this. So if you write an instruction at address $0123, it will end up at address $0246 and $0247 in the target file.

Every processor allows you to write 4 ID words at the end of the ROM area of the device. These ID words can be used to identify your software version, or may contain any other information you like. The ID words can not be read under program control, they are only accessible by programming devices.
You can include the ID words in your target file at the end of the program memory. Please refer to the new .ID directive for more information on how to include the ID words in your program.

Every device also has a Config word, allowing you to select the type of oscillator and some other configuration options. This Config word can be included in the target file too and is usually written at address $2007. Please refer to the new .CW directive for more information.

Some devices are also equipped with data EEPROM memory. You can initialize this memory with data when the device is programmed. Any data generating directive can be used to fill the EEPROM with data. Usually the data is stored from address $2100, so it will effectively start at address $4200 in the target file.
All data is still saved as 2 bytes, so all target file addresses will still be doubled. The high byte of each pair simply remains 0.

Please note that all data generating directives will generate RETLW instructions as long as the program counter is below $2000. Above this address all data is stored as 2 bytes, with the 2nd byte (odd address in target file) always being $00.

Special Features

New Directives

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

Altered behaviour of Directives

The memory organization of the PIC devices forced me to change the behaviour of all data generating directives, like: .AS, .AT, .AZ, .BI, .DA, .DL, .DR, .HS and .RF
All data generating directives will now generate RETLW instructions instead of normal data bytes when the destination address is below $2000. If the destination address is above $2000 all data will be saved as words, with the low byte filled with the data and the high byte of the word being $00.

Please note that words of data are stored in 4 bytes in the target file when the destination address is above $2000, all odd addresses will still be filled with $00!

Disabled Directives

I had to disable a few standard directives due to the way the PIC cross overlay handles the target files. The disabled directives are: .DU, .ED, .PH and .EP
These directives were not very useful to the PIC crosses anyway, so we can do very well without them.

You'll get a fatal *** Illegal directive error if you do try to use any of the above directives.

.CR directive

The .CR directive hasn't changed actually. But you better don't rely on the SB-Assembler's multi-processor capabilities for the PIC family because of the way the target files are generated. This means that you better not use more than one .CR directive in your PIC programs.

File Register addressing

File registers are numbered from $000 to $1FF, while the actual addressing range runs from $00 to $7F. The SB-Assembler accepts addresses from $00 to $1FF when addressing any file register, although the actual instruction keeps only the 7 lowest bits. This allows you to access the file registers by their absolute addresses, but you must manually take care of the proper addressing of the upper two bits of the file address.

Immediate prefix is optional

Immediate data is called literal data in Microchip's documentation. All 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 LSB of the 32-bit value as operand.


Data bytes are stored as RETLW instruction words in the program memory. If you want to read some data from program memory 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.


        RETLW    $12,$34,$56

CALL and GOTO address range

Some restrictions apply to the destination addresses of CALL and GOTO instructions due to the nature of the memory organization of the PIC16Cxxx family of processors. Memory consists of up to 4 banks of 2048 instruction words each.

The GOTO and CALL instructions may cause a jump to any location within the 8 k word program memory, even though only 11 bits of the destination address can be supplied by the instruction itself. The upper two address bits are copied from the bits b4 and b3 of the PCLATH register every time a GOTO or CALL instruction is executed. It's the programmer's responsibility to set these bits appropriately before executing the GOTO or CALL instruction.

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.

Obsolete TRIS and OPTION instructions

Some of the 16Cxxx family of processors inherited the obsolete instructions TRIS and OPTION. Microchip discourages the use of these instructions because the latest devices don't understand these instructions anymore. Please use the Special Function registers TRIS and OPTION instead to set I/O direction and device options.

The SB-Assembler still accepts these two obsolete instructions for compatibility reasons, but this doesn't mean that your processor will understand them!

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

Please note that a page refers to 256 contiguous bytes here and not 2048 bytes when referring to memory mapping pages for the 16Cxxx family.


With the 16Cxxx family tables are not restricted to the traditional 256 bytes limit because PCLATH can be used to set the upper 5 bits of the program counter. However it is far easier to use small tables that do not cross a page boundary (i.e. PCLATH is the same for the entire table). That's why the .OT and .CT directives are used to signal the beginning and the end of a table, verifying page crossing hasn't occurred in between.

The .OT directive should be placed at the beginning of the table. Please remember to load the PCLATH register with the 5 most significant bits of the table's address before jumping to the table!

The .OT directive will memorize the memory page of the beginning of the table. At the end of the table the closing .CT directive should still be in 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. But 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 some code in front of the table.


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.

DEC2SEGM   MOVLW  /SEGMENTS        Load PCLATH with MSB of the
           MOVWF  PCLATH            table address
           MOVF   DIGIT,W          Get the digit to be displayed
           MOVWF  PCL              Jump to location in table

           .OT                     Mark the beginning
SEGMENTS   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

.CW     Config Word


        .CW  [flags]
        .CW  #expression


This directive is used to set the Config Word flags of the 16Cxxx device which select the oscillator type and other settings.


The Config Word of the 16Cxxx family selects various device options, like oscillator type, watchdog enable, start-up behaviour, and software protection options.
The Config word is usually located at address $2007 and will end up on addresses $400E and $400F in the target file. You may put the .CW directive anywhere outside the program memory, but Microchip recommends you to put it at address $2007 so it can be understood by all chip programmers.

You may use the named flags only for the PIC16F84 devices and maybe for some closely related devices. The table below shows the possible flags that are pre-defined for the PIC16F84. The oscillator names select 2 bits at a time and may only be entered once in the flag list. You'll get an *** Illegal Config bit error if you try to use more than one oscillator option or if you use an unrecognized flag name. Multiple flags must be separated from each other by commas.

FlagConfig word bit relationship
CPbit 3 is 0 if CP is listed
WDTEbit 2 is 1 if WDTE is listed
RCbits 1 and 0 are both set if RC is listed
HSbit 1 is set, bit 0 is cleared if HS is listed
XTbit 1 is cleared, bit 0 is set if XT is listed
LPbits 1 and 0 are both cleared if LP is listed

WARNING The mid-range processor family has a multitude of Config word flag definition differences, making it next to impossible to use named flags for all derivatives. So please specify a bit mask yourself for the Config word, in which case the first character of the operand must be a # symbol. The value you enter will be truncated to the 14 least significant bits. You may not mix named flags and the immediate bit mask in one operand though.
Please consult the data sheet of your processor to ensure yourself of the proper setting of the Config word flags. Microchip has not been very consistent on the use, polarity and bit location of the Config word bits, so be vary careful! E.g. the PWRTE bit of the PIC16F84 has the inverse meaning on the PIC16C84!

You may also leave the operand field completely blank. The SB-Assembler will then assume a bit mask of %11.1111.1111.1011, disabling code protect, power-up timer, watchdog timer and selecting an RC oscillator on a PIC16F84. The exact meaning of this default mask may vary between derivatives though!

The .CW directive is only allowed outside the program memory space, which is above address $2000. You'll get a *** Data memory allocation error if you do try to use the .CW directive below address $2000.


          .OR    $0FFF         Put CW in recommended address
          .CW    XT,WDTE       Select crystal and watchdog enable

          .OR    $0FFF         2nd example (not in same source file)
          .CW    #%0100        Select LP oscillator and code protect

.ID     ID words


        .ID  expression


This directive allows you to set-up the four 4-bit ID words in the device.


Every device contains four ID words, which can be loaded with any value you like. They serve the purpose of identifying programmed parts. The ID words can only be read during program/verify mode and are not accessible during normal operation. Microchip recommends to place the 4 ID words above address $2000. The SB-Assembler will allow you to place the ID words anywhere above address $2000.

You'll get a fatal *** Data memory allocation error" message if you try to use the .ID directive below address $2000.
You should use the .ID directive only once in your program to set-up the ID words. No error is reported if you do have more occurrences of the .ID directive in your program though.

The expression will be split into 4 nibbles and these nibbles are stored each in their own 14-bit words. All other bits of the ID words remain 0. The range of the expression is not checked, the assembler will simply use the 4 least significant nibbles.


2000-                       .OR    $2000       Beginning of data memory

2000-01 00 02 00
2002-03 00 04 00            .ID    $1234

.MS     Memory Size


        .MS  expression


This directive is used to indicate 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 $0400.

There is only one reason why you are requested to specify the program memory size (in words, not in generated bytes). It gives the SB-Assembler the opportunity to warn you if you try to use more memory than is actually available.

The .MS directive will accept any expression resulting in a value up to 8192 or $2000 (i.e. 8k words). A *** Range error will be reported if the expression evaluates to a value above $2000. The expression may not contain forward referenced labels.
Any other address is accepted, even silly addresses like $0000. Please feel free to make your programming job a nightmare by selecting silly program memory sizes.

The default memory size is $0400, which is the ROM memory size of the PIC16F84.

.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 itself if it wasn't for the automatic calling of the .CT routine when a previous table wasn't closed.

Extra Error Messages

A few extra error messages are added to the standard repertoire of error messages.

*** Table crossed page boundary error

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.

*** Illegal config bit error

This error is given because one of the flag names behind the .CW directive could not be recognized, or because an oscillator was selected for the 2nd time.

*** Illegal directive error

This is a fatal error, triggered by the use of any of the illegal directives for the PIC cross overlay. The illegal directives are: .DU, .ED, .PH and .EP

** Program memory allocation error

This fatal error is reported because you tried to generate instruction words beyond the end of the program memory space. You're only allowed to save data, ID words and a Config word beyond the end of the program memory space.

*** Data memory allocation error

This fatal error is reported because you tried to use the .ID or .CW directive while still being somewhere in program memory space.

Overlay Initialization

Three things are set while initializing the 16Cxxx overlay every time it is loaded by the .CR directive.

  • Little endian model is selected for the .DA and .DL directives. This means that words or long words are stored with their low byte first.
  • Default memory size is set to $0400.
  • Control of storing bytes in the target file is taken over from the main assembler to enable the saving of words instead of bytes.

Differences Between Other Assemblers

There are some differences between the SB-Assembler and other assemblers for the 16Cxxx 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.

  • The handling of ID words and the Config word may differ from other assemblers.
  • Not all assemblers translate data bytes to RETLW instructions automatically.
  • Not all assemblers will allow multiple literal values following one RETLW mnemonic.
  • The obvious differences in notation of directives and radixes common to all SB-Assembler crosses.
  • Don't forget that the SB-Assembler does not allow spaces in or between operands.