MPLAB XC8 PIC Assembler Guide for Embedded Engineers

ยท

This guide provides practical examples and essential information for using the MPLAB XC8 PIC Assembler with mid-range and PIC18 microcontroller families. The examples demonstrate various programming concepts, assembler directives, operators, and command-line options to help you develop efficient assembly code for Microchip PIC devices.

Introduction to MPLAB XC8 PIC Assembler

The MPLAB XC8 PIC Assembler is a powerful tool for developing embedded applications using assembly language. This guide presents working examples that illustrate key programming techniques for both mid-range and PIC18 devices. These examples cover fundamental concepts such as configuration settings, memory management, interrupt handling, and optimization strategies.

When combined with the MPLAB XC8 PIC Assembler User Guide, this document provides a comprehensive resource for developers transitioning from MPASM assembler to the XC8 toolchain. The examples are designed to be practical and immediately applicable to real-world embedded development projects.

Basic PIC18 Device Example

The following complete assembly program demonstrates fundamental concepts for PIC18F47K42 devices, though most elements apply to other PIC18 devices as well. This code repeatedly reads values from PORTA and records the highest value detected.

PROCESSOR 18F47K42
#include <xc.inc>

; Configuration settings
CONFIG FEXTOSC = OFF
CONFIG RSTOSC = HFINTOSC_1MHZ
CONFIG CLKOUTEN = OFF
CONFIG PR1WAY = ON
CONFIG CSWEN = ON
CONFIG FCMEN = ON
CONFIG MCLRE=EXTMCLR
CONFIG PWRTS=PWRT_OFF
CONFIG MVECEN=OFF
CONFIG IVT1WAY=ON
CONFIG LPBOREN=OFF
CONFIG BOREN=SBORDIS
CONFIG BORV=VBOR_2P45
CONFIG ZCD=OFF
CONFIG PPS1WAY=ON
CONFIG STVREN=ON
CONFIG DEBUG=OFF
CONFIG XINST=OFF
CONFIG WDTCPS=WDTCPS_31
CONFIG WDTE=OFF
CONFIG WDTCWS=WDTCWS_7
CONFIG WDTCCS=SC
CONFIG BBSIZE=BBSIZE_512
CONFIG BBEN=OFF
CONFIG SAFEN=OFF
CONFIG WRTAPP=OFF
CONFIG WRTB=OFF
CONFIG WRTC=OFF
CONFIG WRTD=OFF
CONFIG WRTSAF=OFF
CONFIG LVP=ON
CONFIG CP=OFF

GLOBAL max
PSECT udata_acs
max:
    DS 1
tmp:
    DS 1

PSECT resetVec,class=CODE,reloc=2
resetVec:
    goto main

PSECT code
main:
    movlw 0x62
    movlb 57
    movwf BANKMASK(OSCCON1),b
    movlw 2
    movwf BANKMASK(OSCFRQ),b
    movlb 58
    clrf BANKMASK(ANSELA),b
    clrf max,c

loop:
    movff PORTA,tmp
    movf tmp,w,c
    subwf max,w,c
    bc loop
    movff tmp,max
    goto loop

END resetVec

Key Elements in PIC18 Programming

Comments: Assembly source code can use multiple comment styles. Semicolon comments work anywhere in your code, while C-style comments (// and / /) require preprocessing when using the .S extension or the -xassembler-with-cpp option.

Configuration Bits: Proper configuration of device settings is essential for correct program execution. Use the CONFIG directive to set individual configuration bits or entire configuration words. Always consult your device datasheet to understand the function and appropriate values for each configuration setting.

Include Files: Use the #include directive to incorporate other files into your source code. The standard include file <xc.inc> provides access to special function registers and other device features. Preprocessor searches standard directories for files specified in angle brackets and checks the current working directory first for files in quotes.

Common Directives: Essential directives include PROCESSOR to specify the target device and END to mark the end of source code. The PROCESSOR directive ensures code compatibility with the specified device, while the END directive can specify the program entry point.

Predefined Psects: Program sections (psects) group related code and data elements. The PIC assembler provides predefined psects like 'code' for executable instructions and 'udata_acs' for access bank memory objects. Using predefined psects simplifies linking as they automatically associate with appropriate linker classes.

User-Defined Psects: For special memory placement requirements, create custom psects. The example uses a resetVec psect with specific flags: class=CODE associates it with the code memory class, and reloc=2 ensures word alignment for PIC18 instructions.

๐Ÿ‘‰ Explore more assembly programming techniques

Mid-Range Device Programming Example

This example demonstrates assembly programming for PIC16F18446 devices, showing how to handle banked memory and other mid-range specific features:

PROCESSOR 16F18446
#include <xc.inc>

; Configuration bits omitted for brevity

skipnc MACRO
    btfsc CARRY
ENDM

PSECT udata_bank0
max:
    DS 1
tmp:
    DS 1

PSECT resetVec,class=CODE,delta=2
resetVec:
    PAGESEL main
    goto main

PSECT code
main:
    movlw 0x62
    movlb 17
    movwf OSCCON1
    movlw 2
    movwf OSCFRQ
    PAGESEL loop
    BANKSEL max
    clrf BANKMASK(max)
    BANKSEL ANSELC
    clrf BANKMASK(ANSELC)

loop:
    BANKSEL PORTC
    movf BANKMASK(PORTC),w
    BANKSEL tmp
    movwf BANKMASK(tmp)
    subwf max^(tmp&0ff80h),w
    skipnc
    goto loop
    movf BANKMASK(tmp),w
    movwf BANKMASK(max)
    goto loop

END resetVec

Mid-Range Specific Considerations

Assembler Macros: Macros define identifiers that represent code sequences. The example creates a 'skipnc' macro that skips the next instruction if the carry flag is not set. Macros can accept parameters and use special characters in their definitions for flexible code generation.

Banked Memory Usage: Mid-range devices lack the movff instruction found in PIC18 devices, requiring careful bank selection for all data access operations. The BANKSEL directive simplifies bank selection, while the BANKMASK() macro ensures proper address masking for instruction operands.

Delta Flag: Psects containing executable code for mid-range devices must use the delta=2 flag, indicating 2 bytes of storage per address location. This matches the program memory width (14 bits) of mid-range devices.

Working with Multiple Source Files and Paging

Larger projects often benefit from splitting code across multiple source files. This example demonstrates how to share objects and procedures between modules while handling program memory paging:

; File 1: main.S
PROCESSOR 16F18446
#include <xc.inc>
; Configuration bits omitted

PSECT code
readPort:
    BANKSEL PORTC
    movf BANKMASK(PORTC),w
    return

GLOBAL count
PSECT udata_shr
count:
    DS 1

PSECT resetVec,class=CODE,delta=2
resetVec:
    PAGESEL main
    goto main

GLOBAL storeLevel
PSECT code
main:
    BANKSEL ANSELC
    clrf BANKMASK(ANSELC)
    clrf count

loop:
    call readPort
    PAGESEL storeLevel
    call storeLevel
    PAGESEL $
    movlw 0xFF
delay:
    decfsz WREG,f
    goto delay
    movlw NUM_TO_READ
    incf count,f
    xorwf count,w
    btfss ZERO
    goto loop
    goto $
END resetVec

; File 2: storage.S
PROCESSOR 16F18446
#include <xc.inc>

GLOBAL storeLevel
GLOBAL count

PSECT udata_shr
tmp:
    DS 1

DLABS 1,0x120,NUM_TO_READ,levels

PSECT code
storeLevel:
    movwf tmp
    movf count,w
    addlw low(levels)
    movwf FSR1L
    movlw high(levels)
    clrf FSR1H
    addwfc FSR1H
    movf tmp,w
    movwf INDF1
    return
END

Multi-Module Development Strategies

Global Symbol Management: Use the GLOBAL directive to make symbols accessible across modules and declare external symbols in files that reference them. The EXTRN directive provides an alternative for declaring external symbols without defining them.

Psect Linking and Paging: Identically named psects within the same module are concatenated by the linker, while psects with the same name in different modules are linked separately. This affects paging considerations for calls and jumps on mid-range devices.

Linear Memory Addressing: Enhanced mid-range devices support linear addressing mode, which accesses banked data memory as a contiguous block. The DLABS directive defines objects that span multiple banks, and FSR registers enable indirect access using linear addresses without bank selection.

Compiled Stack Implementation

The compiled stack provides static allocation for local objects, allowing memory reuse across different procedures:

#include <xc.inc>
; Configuration bits omitted

FNCONF udata_acs,?au_,?pa_

PSECT resetVec,class=CODE,reloc=2
resetVec:
    goto main

PSECT code
; Function definitions with stack usage
FNSIZE add,0,4
GLOBAL ?pa_add
add:
    movf ?pa_add+2,w,c
    addwf ?pa_add+0,f,c
    movf ?pa_add+3,w,c
    addwfc ?pa_add+1,f,c
    return

FNSIZE incr,0,2
GLOBAL ?pa_incr
incr:
    addwf ?pa_incr+0,c
    movlw 0h
    addwfc ?pa_incr+1,c
    return

GLOBAL ?au_main
GLOBAL result
result EQU ?au_main+0

GLOBAL incval
incval EQU ?au_main+2

FNROOT main
FNSIZE main,4,0
FNCALL main,add
FNCALL main,incr

PSECT code
main:
    clrf result+0,c
    clrf result+1,c
    movlw 2
    movwf incval+0,c
    clrf incval+1,c

loop:
    movff result+0,?pa_add+0
    movff result+1,?pa_add+1
    movff incval+0,?pa_add+2
    movff incval+1,?pa_add+3
    call add
    movff ?pa_add+0,result+0
    movff ?pa_add+1,result+1
    movff incval+0,?pa_incr+0
    movff incval+1,?pa_incr+1
    movlw 2
    call incr
    movff ?pa_incr+0,incval+0
    movff ?pa_incr+1,incval+1
    goto loop

END resetVec

Compiled Stack Directives

FNCONF: Configures the compiled stack, specifying the psect for stack storage and prefixes for automatic and parameter objects.

FNSIZE: Declares the memory requirements for a procedure's automatic and parameter objects.

FNROOT and FNCALL: Define call graph relationships to enable memory overlapping between procedures that are never active simultaneously.

The linker processes these directives to create a call graph and allocate storage efficiently, with overlapping memory blocks for procedures that don't call each other.

Interrupt Handling and Bit Manipulation

This example demonstrates interrupt service routines and bit manipulation for a PIC16F18446 device, toggling an LED using timer interrupts:

#include <xc.inc>
; Configuration bits omitted

GLOBAL resetVec,isr
GLOBAL LEDState

PSECT bitbss,bit,class=BANK1,space=1
LEDState:
    DS 1

PSECT resetVec,class=CODE,delta=2
resetVec:
    ljmp start

PSECT isrVec,class=CODE,delta=2
isr:
    PAGESEL $
    BANKSEL PIE0
    btfsc TMR0IE
    btfss TMR0IF
    goto notTimerInt
    bcf TMR0IF
    movlw 1 shl (LEDState&7)
    BANKSEL LEDState/8
    xorwf BANKMASK(LEDState/8),f

notTimerInt:
exitISR:
    retfie

PSECT code
start:
    movlw 0x33
    BANKSEL TRISA
    movwf TRISA
    movlw 2
    BANKSEL OSCFRQ
    movwf OSCFRQ
    movlw 0x89
    BANKSEL T0CON1
    movwf T0CON1
    movlw 0x1D
    movwf TMR0H
    clrf TMR0L
    BANKSEL PIE0
    bcf TMR0IF
    bsf TMR0IE
    movlw 0x80
    BANKSEL T0CON0
    movwf T0CON0 
    bsf GIE
    bsf PEIE

loop:
    BANKSEL LEDState/8
    btfss BANKMASK(LEDState/8),LEDState&7
    goto lightLED
    BANKSEL PORTA
    bsf RA2
    goto loop

lightLED:
    BANKSEL PORTA
    bcf RA2
    goto loop

END resetVec

Interrupt Service Routine Considerations

ISR Placement: Interrupt service routines must be linked to the appropriate interrupt vector address using unique psects.

Context Saving: Many modern PIC devices automatically save core register states to shadow registers on interrupt entry. For devices without this feature, manual context saving is required.

Interrupt Source Identification: Check both the interrupt enable flag and interrupt flag to confirm the interrupt source before processing.

Bit Objects: Use the 'bit' psect flag to create single-bit objects that conserve memory. Bit addresses require conversion to byte addresses and bit positions for use with bit-oriented instructions.

๐Ÿ‘‰ Learn advanced interrupt handling techniques

PIC18 Interrupts and Vector Tables

PIC18 devices with vector interrupt controllers use a different approach to interrupt handling:

#include <xc.inc>
; Configuration bits omitted

GLOBAL resetVec
GLOBAL LEDState
GLOBAL __Livt

PSECT bitbssCOMMON,bit,class=COMRAM,space=1
LEDState:
    DS 1

PSECT resetVec,class=CODE,reloc=2
resetVec:
    goto start

PSECT ivt,class=CODE,reloc=2,ovrld
ivtbase:
    ORG 31*2
    DW tmr0Isr shr 2 

PSECT code
tmr0Isr:
    bcf TMR0IF 
    movlw 1 shl (LEDState&7)
    xorwf LEDState/(0+8),c
    retfie f

PSECT code
start:
    bsf BANKMASK(INTCON0),INTCON0_IPEN_POSN,c
    bcf GIE
    movlw 0x55
    movwf BANKMASK(IVTLOCK),c
    movlw 0xAA
    movwf BANKMASK(IVTLOCK),c
    bcf IVTLOCKED
    movlw low highword __Livt
    movwf BANKMASK(IVTBASEU),c
    movlw high __Livt
    movwf BANKMASK(IVTBASEH),c
    movlw low __Livt
    movwf BANKMASK(IVTBASEL),c
    movlw 0x55
    movwf BANKMASK(IVTLOCK),c
    movlw 0xAA
    movwf BANKMASK(IVTLOCK),c
    bsf IVTLOCKED
    
    ; Peripheral setup code omitted
    
loop:
    btfss LEDState/8,LEDState&7,c 
    goto lightLED
    bsf RE0
    goto loop

lightLED:
    bcf RE0
    goto loop

END resetVec

PIC18 Vector Interrupt Controller

Vector Tables: PIC18 devices with MVECEN enabled use an interrupt vector table rather than fixed interrupt vectors. Each interrupt source has its own ISR with an entry in the vector table.

Table Placement: The vector table psect uses the ovrld flag to allow overlapping placements from different source files. The ORG directive positions vectors at appropriate offsets within the table.

IVTBASE Registers: The vector table base address is loaded into IVTBASE registers using a special unlock sequence. The linker-defined symbol __Livt provides the table's linked address.

Fast Interrupt Return: The retfie f instruction uses the fast form that restores shadow register contents automatically.

Frequently Asked Questions

What is the difference between MPASM and MPLAB XC8 PIC Assembler?
The MPLAB XC8 PIC Assembler is the modern replacement for MPASM, with improved optimization capabilities, better integration with the XC8 compiler toolchain, and enhanced support for newer PIC devices. It uses a different directive syntax and offers better memory utilization through features like the compiled stack.

How do I set configuration bits in assembly code?
Use the CONFIG directive with setting-value pairs for individual configuration bits or entire configuration words. You can generate the appropriate configuration code using MPLAB X IDE's configuration bits tool or consult the device-specific HTML documentation in the compiler's docs directory.

What are psects and why are they important?
Program sections (psects) are containers that group related code and data elements. They are the smallest entities that the linker places into memory. Psects control memory placement, alignment, and linking behavior, making them essential for effective memory management in assembly programs.

How do I handle banked memory in mid-range devices?
Use the BANKSEL directive before accessing objects in banked memory, and the BANKMASK() macro to ensure proper address masking in instruction operands. Always verify that objects accessed in the same code sequence reside in the same memory bank to avoid runtime errors.

What is the compiled stack and when should I use it?
The compiled stack provides static allocation for local objects that can be reused across different procedures. Use it when you need to conserve data memory by overlapping storage for objects that are never active simultaneously. The compiled stack is particularly useful for reducing memory usage in applications with many functions that have local variables.

How do I create and use bit variables?
Define bit variables in psects with the 'bit' flag, which specifies that the psect uses bit addressing rather than byte addressing. Access bit variables using their byte address (bit_address/8) and bit position (bit_address&7). Bit variables conserve memory when you need to store single-bit flags or status indicators.

What's the best way to handle interrupts in assembly code?
Create a unique psect for your interrupt service routine linked to the appropriate vector address. Implement proper context saving if your device doesn't provide automatic shadow registers. Always check both the interrupt enable flag and interrupt flag to confirm the interrupt source, and use the appropriate return instruction (retfie or retfie f) to exit the ISR.