Cornell University ECE4760
PLIB, C and Assembler considerations



Most of this material comes from the MPLAB® XC32 C/C++ Compiler User’s Guide. Section numbers below refer to this document. I chose to include topics here that I found to be useful or tricky to think about. Feel free to suggest additions.


  1. PLIB examples (and all the examples ZIPPED)
  2. Using PPS -- PIC32 Family Reference Manual section 12.3
  3. Using PPS -- In the MPLABX GUI, open HELP, then search PPS
  4. Using PPS input-- PIC32MX250F128B Peripheral Pin Select (PPS) input table
    -- example -- UART receive pin
    specify PPS group, signal, logical pin name
    PPSInput(2, U2RX, RPB11); //Assign U2RX to pin RPB11 -- Physical pin 22 on 28 PDIP
    Note that the groups (1 to 4) are delimited by horzontal lines in the righthand column..
  5. Using PPS output -- PIC32MX250F128B Peripheral Pin Select (PPS) output table
    -- example -- UART transmit pin
    specify PPS group, logical pin name, signal
    PPSOutput(4, RPB10, U2TX); //Assign U2TX to pin RPB10 -- Physical pin 21 on 28 PDIP
    Note that the groups (1 to 4) are delimited by horzontal lines in the righthand column.
    -- example -- set up compare 1
    PPSOutput(4, RPB9, C1OUT); //pin18
    mPORTBSetPinsDigitalIn(BIT_3); //Set port as input (pin 7 is RB3)

    Note that PORTB/bit3 is NOT on PPS. It has a fixed pin location.
  6. PDIP pinout by pin for PIC32MX250F128B :::
    Signal Names=>Pins ::: 1, 2, 3, 4, 5, 6, 7 PDIP highlighted in green (for PPS refer above)
  7. Interrupt Table 1, 2 and int_1xx_2xx.h -- interrupt IRQ names and vector names


  1. Device Header Files (Section 4.3)
    There is one header file that is recommended be included into each source file you write. The file is <xc.h> and is a generic file that will include other device-specific header files when you build your project. Inclusion of this file will allow access to SFRs via special variables, as well as #defines which allow the use of conventional register names from within assembly language files.
  2. The Stack (Section 4.4)
    The PIC32 devices use what is referred to in this user’s guide as a “software stack”. This is the typical stack arrangement employed by most computers and is ordinary data memory accessed by a push-and-pop type instruction and a stack pointer register. The PIC32 devices use a dedicated stack pointer register sp (register 29) for use as a software Stack Pointer. All processor stack operations, including function calls, interrupts and exceptions, use the software stack. It points to the next free location on the stack. The stack grows downward, towards lower memory addresses. By default, the size of the stack is 1024 bytes.The run-time stack grows downward from higher addresses to lower addresses. Two working registers are used to manage the stack:
    • Register 29 (sp) – This is the Stack Pointer. It points to the next free location on the stack.
    • Register 30 (fp) – This is the Frame Pointer. It points to the current function’s frame.
    No stack overflow detection is supplied.
  3. Data Types (Chapter 6)
    Types, their ranges, how to specify the types, and how structs, bit fields, and unions are defined.
    The compiler stores multibyte values in little-endian format. That is, the Least Significant Byte is stored at the lowest address.
    integer types
    float types
  4. Integer Promotion (Section 8.2)
    Integral promotion is the implicit conversion of enumerated types, signed or unsigned varieties of char, short int or bit field types to either signed int or
    unsigned int. If the result of the conversion can be represented by an signed int, then that is the destination type, otherwise the conversion is to unsigned int.
    Consider the following example:
    unsigned char count, a=0, b=50;
    if(a - b < 10)

    The unsigned char result of a - b is 206 (which is not less than 10), but both a and b are converted to signed int via integral promotion before the subtraction takes
    place. The result of the subtraction with these data types is -50 (which is less than 10) and hence the body of the if() statement is executed. If the result of the subtraction is to be an unsigned quantity, then apply a cast.
    For example:
    if((unsigned int)(a - b) < 10)

    The comparison is then done using unsigned int, in this case, and the body of the if() would not be executed. Another problem that frequently occurs is with the bitwise compliment operator, ~. This operator toggles each bit within a value. Consider the following code:
    unsigned char count, c;
    c = 0x55;
    if( ~c == 0xAA)

    If c contains the value 0x55, it often assumed that ~c will produce 0xAA, however the result is 0xFFFFFFAA and so the comparison in the above example would fail. The compiler may be able to issue a mismatched comparison error to this effect in some circumstances. Again, a cast could be used to change this behavior.
  5. Interrupt mechanism and syntax (Section 11.3. This section also has several ISR examples)
    An interrupt function must be declared as type void and may not have parameters. This is the only function prototype that makes sense for an interrupt function since they are never directly called in the source code. Interrupt functions must not be called directly from C/C++ code (due to the different return instruction that is used), but they themselves may call other functions, both user-defined and library functions, but be aware that this may use additional registers (and therefore cycles) which will need to be saved and restored by the context switch code. A function is marked as an interrupt handler function (also known as an Interrupt Service Routine or ISR) via either the interrupt attribute or the interrupt pragma1. While each method is functionally equivalent to the other, the interrupt attribute is more commonly used and therefore the recommended method. The interrupt is specified as handling interrupts of a specific priority level or for operating in single vector mode.
  6. Preprocessor (Section 16)
    Macro expansion using arguments can use the # character to convert an argument to a string, and the ## sequence to concatenate arguments. If two expressions are being concatenated, consider using two macros in case either expression requires substitution itself, so for example,
    #define paste1(a,b) a##b
    #define paste(a,b) paste1(a,b)

    lets you use the paste macro to concatenate two expressions that themselves may require further expansion. The replacement token is rescanned for more macro identifiers, but remember that once a particular macro identifier has been expanded, it will not be expanded again if it appears after concatenation. The type and conversion of numeric values in the preprocessor domain is the same as in the C domain. Preprocessor values do not have a type, but acquire one as soon as they are converted by the preprocessor. Expressions may overflow their allocated type in the same way that C expressions may overflow. Overflow may be avoided by using a constant suffix. For example, an L after the number indicates it should be interpreted as a long once converted.
    So for example:
    #define MAX 100000*100000
    #define MAX 100000*100000L

    (note the L suffix) will define the values 0x540be400 and 0x2540be400, respectively.

GCC general (Also see Common C errors)

  1. Never ignore a warning!
    This version of C will accept misspelled ISR names and nonexistant functions with just a warning, among other nasty surprises.

  2. Use static, const and volatile variables correctly.
    Incorrect use may affect program execution and efficiency. Every variable which is set in an interrupt and read in main (or vice-versa) must be declared as volatile. The optimizer may remove variables which change asynchronously in an ISR unless you use the volatile keyword.

  3. Distinguish between = and ==.
    When xyzzy is a variable, the following syntax using = instead of ==:
    if (xyzzy=0){stuff};
    is completely legal and will never enter the if-block because the variable is first assigned the value zero, then checked to see if it is not-zero.

  4. The operators ! (logical not) and ~ (bitwise not) are not equivalent.
    Let's say we have the char variable xyzzy=1. The syntax:
    if (~xyzzy){stuff};
    will always enter the if-block because ~1 for a char variable is 0xFE, which is non-zero.

  5. The operators & (bitwise and) and && (logical and) are not equivalent.
    Let's say that xyzzy=1 and abx=2.
    Then (xyzzy & abx)=0 but (xyzzy && abx) = 1 (logical TRUE)
    Similarly for | and || (bitwise and logical or).

  6. Variables seem to change without reason.
    If variables seem to be changing on their own, or inserting debug variables seems to 'fix' the problem, you probably have a memory issue. Check the compiler-generated .map file. Look for zero-based versus one-based array errors. Look for buffer overruns! You can overrun array boundaries at run time (C has no bounds checks). This includes strings which are longer than the declared array size.

  7. Watch out for the radix of constants.
    When mixing binary and hex (0b, 0x) in assignments it is very easy to miss mistakes like
    REGISTER=0x00001000; when you meant to use binary.

  8. Buffer overrun errors are easy when printing or copying strings.
    Use snprintf instead of sprintf to print a given number of characters.
    There is also strncopy instead of strcopy as well as other number-limited string commands.

  9. Putting a semicolon at the end of a for statement makes the statement a null loop!
    for (i=0; i<10; i++); <--wrong (unless for some reason you want a null loop).

Assembler (see also MPLAB® XC32 ASSEMBLER, LINKER AND UTILITIES User’s Guide)

  1. Assembler Listing
    To get a listing: menu item Window>Debugging>output>Dissasembly
    But note that in Project Properties>Loading you need to check Load symbols when programming
  2. Mixing C and assembler (Section 14)
    If assembly must be added, it is preferable to write this as self-contained routine in a separate assembly module rather than in-lining it in C code.
    I do sometimes write inline assembler, but keep it simple and short.
  3. Register Conventions (Section 9.3)
    table 9.1
  4. Register Definitions (Section 4.5.1)
    When the CP0 register definitions header file is included from an Assembly file,
    the CP0 registers are defined as:
    #define _CP0_register_name $register_number, select_number
    For example, the IntCtl register is defined as:
    #define _CP0_INTCTL $12, 1
  5. Interrupt context switch (Section 11.6)
    The standard calling convention for C/C++ functions will already preserve zero, s0-s7, gp, sp, and fp. k0 and k1 are used by the compiler to access and preserve non-GPR context, but are always accessed atomically (i.e., in sequences with global interrupts disabled), so they need not be preserved actively. A handler function will actively preserve the a0-a3, t0-t9, v0, v1 and ra registers in addition to the standard registers. An interrupt handler function will also actively save and restore processor status registers that are utilized by the handler function. Specifically, the EPC, SR, hi and lo registers are preserved as context. Handler functions may use a shadow register set to preserve the General Purpose Registers, enabling lower latency entry into the application code of the handler function. On some devices, the shadow register set is assigned to an interrupt priority level (IPL) using the device Configuration bit settings (e.g., #pragma config FSRSSEL=PRIORITY_6). While on other devices, the shadow register set may be hard wired to IPL7.

See also:
PIC32 Architecture and Programming John Loomis

Copyright Cornell University January 4, 2017