NU32: A Detailed Look at Programming the PIC32 on the NU32

From Mech
Jump to navigationJump to search


After you have programmed your NU32 for the first time and verified that you can create a new project, compile it, and run it on your NU32, it is useful to take a step back and understand the basics of the programming process, beginning with a PIC32 fresh from the factory. We will do that on this page. We will begin by discussing the virtual memory map of the PIC32. To discuss the virtual memory map, it is useful to know hexadecimal (hex, or base 16) notation, where each digit of a hex number takes one of 16 values, 0...9, A...F. Since 16 = 2^4, a single hex digit represents four digits of a number written in binary (base 2). The table below gives examples.

Hex Binary Base 10
7 0111 7
D 1101 13
B5 1011 0101 181

To distinguish hex and binary numbers from base 10 numbers, we begin the numbers with 0x and 0b, respectively. For example,

0xA9 = 10*16^1 + 9*16^0 = 0b10101001 = 1*2^7 + 0*2^6 + 1*2^5 + 0*2^4 + 1*2^3 + 0*2^2 + 0*2^1 + 1*2^0 = 160 = 1*10^2 + 6*10^1 + 0*10^0.

The 0x convention is standard in C, but the 0b is not universally used.

The PIC32 Virtual Memory Map

The PIC32 has a virtual memory map consisting of 4 GB (four gigabytes, or 2^32 bytes, where each byte equals 8 bits) of addressable memory. All memory regions reside in this virtual memory space at their unique respective addresses. This includes program memory (flash), data memory (RAM), peripheral special function registers (SFRs), etc. For example, the peripheral SFRs begin at virtual memory location 0xBF800000 and end at virtual memory address 0xBF8FFFFF. Subtracting the begin address from the end address, and adding one byte, we get 0x100000, which is 1*16^5 = 1,048,576 bytes, commonly written as 1 MB. (Note: Section 3 of the reference manual incorrectly indicates that the size of this region is 4 KB.)

In addition to this virtual memory map, there is also a physical memory map. When you are writing a program, you only deal with the virtual memory map. The PIC's CPU implements a Fixed Mapping Translation (FMT) unit that takes the virtual memory address and maps it to a physical memory address. In other words, the virtual memory address is translated to a set of bit values on an addressing bus that allows the PIC's CPU to physically address the appropriate peripheral, flash memory location, RAM location, etc. We will focus on the virtual memory map, not the physical memory map, since our goal is to program the PIC and we don't need to concern ourselves with how the FMT works. If you're curious, however, the translation is a simple bitwise AND:

     physical address = virtual address & 0x1FFFFFFF

If you are directly accessing memory, independent of the CPU, as when an external device is engaged in direct memory access (DMA) with the PIC, you must use physical addresses. To learn more about physical addresses, see Section 3 of the PIC32 Family Reference Manual.

Virtual memory is partitioned into two types of address space: user address space (the lower 2 GB) and kernel address space (the upper 2 GB). By analogy to your personal computer, the kernel address space is to hold the computer's operating system, while the user address space is to hold a program that runs under the operating system. This is for safety: the user's program should not interfere with or compromise the operating system, e.g., it shouldn't be able to overwrite data that the operating system needs to function. We will not be using an operating system, so our programs will reside in the kernel address space.

The kernel virtual address space contains two subsections: one that is cacheable and one that is not. "Cacheable" means that instructions or data can be stored in the cache by the pre-fetch cache module, which speeds up execution by eliminating some wait states needed when fetching data or instructions from flash. The pre-fetch cache module is activated when we execute the command SYSTEMConfig() in our C code.

The three major partitions of PIC32 virtual memory, then, are called KSEG0, KSEG1, and USEG/KUSEG, where KSEG0 corresponds to the cacheable kernel address space, KSEG1 corresponds to the non-cacheable kernel address space, and USEG/KUSEG corresponds to the user address space. The "K" in this last name indicates that programs in the kernel can address the user address space. Programs in the user address space cannot access the kernel address space.

Each of KSEG0, KSEG1, and USEG/KUSEG are further broken into the following sections: program flash, data RAM, and program RAM. The PIC may be made to run a program that is stored in RAM (as opposed to the usual case of a program stored in flash), which is why we have this last category.

Finally, we have two more areas of kernel memory for (1) the peripheral SFRs and (2) boot flash, the code that is executed upon reset of the PIC.

The virtual memory map is summarized in the table below:

Start Address Size (bytes) Partition Kind Notes
0x80000000 BMXDKPBA (max 128 K) KSEG0 (cacheable) data RAM same physical address as KSEG1 data RAM
0x80000000 + BMXDKPBA BMXDUDBA - BMXDKPBA KSEG0 (cacheable) program RAM same physical address as KSEG1 program RAM
0x9D000000 BMXPUPBA (max 512 K) KSEG0 (cacheable) program flash same physical address as KSEG1 program flash
0xA0000000 BMXPUPBA (max 128 K) KSEG1 data RAM
0xBD000000 BMXPUPBA (max 512 K) KSEG1 program flash
0xBF800000 1 MB kernel (KSEG1, non-cacheable) peripheral SFRs
0xBFC00000 12 KB kernel (KSEG1, non-cacheable) boot flash

In the table above, BMXPFMSZ and BMXDRMSZ are read-only registers containing the size of the program flash (512 K for us) and data RAM (128 K for us). The registers BMXqBA stand for "bus matrix" (BMX) and "base address offset" (BA), where q = PUP is for the user's segment of program flash, q = DUP is for the user's program space in RAM, q = DUD is for the user's data space in RAM, and q = DK is for the kernel program space in RAM. In our programs, we do not need to set the BMX registers. Leaving them at their default values allows maximum RAM and flash for kernel mode applications. If we want to run code from RAM or set up a user mode partition, we need to configure the BMX registers.

What Happens When the PIC Is Reset

When the PIC is reset, it goes to the reset address 0xBFC00000, which is the location of the boot flash, and executes the code there. The (assembly version of the) code that typically sits there, which is put there by a PIC programmer device, can be found in pic32-libs/c/startup/crt0.S. This code takes care of some initialization tasks, then calls the code for the program you have written, which typically resides in the KSEG0 program flash memory block.

With the NU32, we have a "bootloader" program that executes upon reset. This program was placed in the PIC's flash memory by a programmer device. More on the bootloader below.

Programming a PIC32 with a Programmer

No code is installed on the PIC32 when it arrives from the factory. To put a program on the microcontroller, a programmer is used. There are a variety of programmers available, including many from Microchip, the manufacturer of the PIC32, listed here. These programmers have many functions, including programming and debugging, with more functionality built into the more expensive programmers.

The NU32 can easily be programmed with any of these programmers, but has been designed to work with the PICkit 3, available for around $45.

The PIC32 Bootloader and Bootloader App

To avoid the expense of providing a PICkit 3 programmer with every NU32 kit, we opted to use our PICkit 3 programmer to install a bootloading program on your NU32. This allows you to program your PIC using only a USB cable and the free MPLAB X IDE and compiler. By not using a programmer, you have lost the ability to do "in-circuit debugging," such as adding breakpoints and watches to your code as was done in the simulator. We will discuss alternative ways of debugging your code as the course progresses.

The bootloader code executes upon reset of the PIC. This code tests whether a digital input (C13) is low (i.e., whether the user is pressing a button) when the PIC is reset, or if UART4 has received a 'B'.

  • If the digital input is high (no button press) or has not received a 'B', the bootloader jumps to a hard-coded virtual memory address to begin executing code there. In the case of our bootloader, that memory address is 0x9D001000 (i.e., in KSEG0 program memory).
  • If the digital input is low (the button is being pressed), the PIC attempts to establish communication with the NU32 Utility "bootloader app" on you computer. The bootloader app will send over your new .hex program, and the PIC will write the program into KSEG0 program flash beginning at 0x9D001000. The next time the PIC is reset (and not asked to stay in bootloader mode), it will begin executing the program you loaded.

The bootloader on the NU32 is based on Microchip's application note AN1388. Microchip provides source code for this application note, which has been modified for the NU32. The NU32 bootloader project is here.

Important: The bootloader does one other important thing: it sets configuration bits for the PIC. Configuration bits are SFRs that are set on startup and control some basic behavior of the PIC. For example, the configuration bits are used to configure phase-locked loops on the PIC to turn our 8 MHz external oscillator into an 80 MHz system clock, 80 MHz peripheral bus clock, 48 MHz USB clock, etc.

If you are programming an NU32 without the bootloader (i.e. with a PICkit3) and want to use the same configuration bits, they are:

// Configuring the Device Configuration Registers
// 80Mhz Core/Periph, Pri Osc w/PLL, Write protect Boot Flash
#pragma config UPLLEN   = ON            // USB PLL Enabled
#pragma config UPLLIDIV = DIV_2         // USB PLL Input Divider
#pragma config POSCMOD = HS, FNOSC = PRIPLL, FPBDIV = DIV_1
#pragma config ICESEL = ICS_PGx2, BWP = OFF
#pragma config FSOSCEN = OFF // to make C13 an IO pin, for the USER switch

More information about configuration registers can be found in Section 32 of the Reference Manual.

Also note that pins A4 and A5, LEDs L1 and L2, are by default used for JTAG, so you must disable JTAG to make them digital IO:

// disable JTAG to get A4 and A5 back

Creating a .hex File Using MPLAB

OK, everything is in place. You've got a bootloader on the PIC and a bootloader app on your computer. Now we just need to create a program to put on the PIC!

Let's use the digital I/O code sample on this page as an example. Following the basic procedure on this page, you create a new project and add the code sample to Source Files. Let's call this file digio.c. You add the app.ld file from to the project folder. You choose the XC32 Compiler and the device PIC32MX795F512L. Then you choose "Build," and if all has gone well, you should get the message BUILD SUCCEEDED and a .hex file ready to be loaded on to your PIC.

So what was the process, really? Three things happened:

  • a compiler turned your C code into assembly language (you can see the assembly code by going to View->Disassembly Listing in MPLAB);
  • an assembler produced a machine-level "object" file (with suffix .o) for each .c file; and
  • a linker took the .o file(s) and created an executable and linkable format file (.elf) and a .hex file.

The purpose of the linker is to assign specific addresses in memory for each function and global variable defined in your C files (and therefore .o files) according to rules specified in a linker script file ending in .ld (e.g., procdefs.ld). One purpose of your procdefs.ld is to tell the linker to place your code in the virtual memory map at the memory location that your bootloader code will jump to. Thus your procdefs.ld should be designed to work with your particular bootloader code. (Even if you define your procdefs.ld incorrectly, the bootloader will not be able to overwrite itself.) More details on procdefs.ld.

Now let's dig a little deeper. In our C code, we used statements like

#define L1     LATAbits.LATA4



So it looks like we defined one constant (L1) to be equal to a variable, the field "LATA4" of a struct "LATAbits." But where was that struct defined? And where was the function SYSTEMConfig() defined?

The key is the first line of our code:

#include <plib.h>

Like Alice in Wonderland, we're about to go down a rabbit hole...

When you installed MPLAB X and the XC32 compiler, it installed a large tree of directories under mplabc32/v.11a. To give you an idea of the directory structure, we highlight some of the important directories and files below:


  • doc
    • Microchip-PIC32MX-Peripheral-Library.chm
    • MPLAB C32 Libraries.pdf
    • MPLAB C32 User Guide.pdf
  • examples
    • plib_examples (lots of directories containing sample code using the peripherals; below are some examples)
      • adc10
      • timer
  • lib (contains various compiled libraries with .a extensions, and .h header files; ignore for now)
  • pic32-libs
    • dsp
      • wrapper
        • various .c files that call mips_XXX DSP functions
    • include
      • math.h (math function prototypes)
      • p32xxxx.h (uses the processor you chose when you created the project to include the right p32mx... file; see below. also does some other minor things.)
      • peripheral
        • adc10.h
        • system.h
        • lots of other peripheral library header files
      • plib.h (includes all the peripheral library headers)
      • proc
        • p32mx795f512l.h (huge file defining SFR names and constants for virtual memory addresses for the particular PIC)
        • ppic32mx.h (some more constant definitions, generic to all PIC32's, included by the file above)
    • peripheral
      • C source code for the peripheral library, one directory per peripheral/topic
  • pic32mx
    • include (looks similar to the include directory above)
    • lib (contains compiled libraries with .a extensions)
      • mips16
        • .a libraries for DSP functions for different PIC32's
      • proc
        • 32MX795F512L
          • processor.o (definitions of SFR virtual memory addresses for our PIC)

OK, that's a lot of stuff, and it's just a small fraction of all the stuff that's there. But for now, notice plib.h in bold above. When we put include <plib.h> in our program, the compiler searches an "include path" (a list of directories to look under) for a file of this name. By default, our include path allows us to find pic32-libs/include/plib.h. Try opening this file in a text editor. You'll see that all that plib.h does is include the peripheral header files pic32-libs/include/peripheral/{adc10.h, ports.h, system.h, etc.}.

Open pic32-libs/include/peripheral/system.h. The first thing it does is include p32xxxx.h (along with a number of other .h files).
Open pic32-libs/include/p32xxxx.h. This file checks which processor we told the IDE we are using and then includes the header file pic32-libs/include/proc/p32mx795f512l.h. (It also does some other things, like make some standard definitions of register names for assembly code.)
Open pic32-libs/include/proc/p32mx795f512l.h. Whoa! This is a big file defining a lot of the variables and variable types that we use to interact with the SFRs. We've also reached the bottom of the include chain, since this file doesn't include anything else. There is some syntax here that you're probably not familiar with, but we should be able to find something recognizable. Search in the file for LATGbits. About 13% into the file, you will see the following code.
extern volatile unsigned int        LATG __attribute__((section("sfrs")));
typedef union {
  struct {
    unsigned LATG0:1;
    unsigned LATG1:1;
    unsigned LATG2:1;
    unsigned LATG3:1;
    unsigned :2;
    unsigned LATG6:1;
    unsigned LATG7:1;
    unsigned LATG8:1;
    unsigned LATG9:1;
    unsigned :2;
    unsigned LATG12:1;
    unsigned LATG13:1;
    unsigned LATG14:1;
    unsigned LATG15:1;
  struct {
    unsigned w:32;
} __LATGbits_t;
extern volatile __LATGbits_t LATGbits __asm__ ("LATG") __attribute__((section("sfrs")));
The typedef command is creating a new variable type consisting of the union of two structs. The first struct clearly indicates which pins belong to port G: RG0...RG3, RG6...RG9, and RG 12...RG15. Confirm this by consulting Table 1-1 in the Data Sheet. The last line of code declares a variable of type __LATGbits_t, and the name of the variable is LATGbits. To reference bit/pin 12, for example, we would use the C code LATGbits.LATG12. (The :1 indicates one bit, :2 indicates two bits.)
We can also see that LATG, LATGCLR, LATGSET, and LATGINV are declared. The only thing that is missing, now, is the addresses of these SFRs. The compiler needs to know these addresses, because they are fixed by the hardware of the PIC. (Note also that LATG and LATGbits are actually pointing to the same locations in memory.)
If you scroll down further, about 30% of the way through the file you see variable names defined for assembly. These are our answers as to the virtual memory addresses of the various SFRs. If you search for LATG, about 34% into the file you will see that the address is 0xBF8861A0, and the other addresses are offset by four. (Why four?)
But the addresses are still not actually defined here (these addresses are in comments). The addresses are actually defined in pic32mx/lib/proc/32MX795F512L/processor.o, which is the first thing included at the top of our linker file procdefs.ld. The "extern" keyword allows this file to use the variable definitions in another file.
Searching further for LATG12, about 67% of the way into the file you see the code
#define _LATG_LATG12_POSITION                    0x0000000C
#define _LATG_LATG12_MASK                        0x00001000
#define _LATG_LATG12_LENGTH                      0x00000001
These define the offset of bit 12 from the base address of LATG (_LATG_LATG0_POSITION), a "mask" (if you do a bitwise AND of the mask with LATG, all bits will be made 0, except for bit 12, which will be left unchanged), and a length of 1 that indicates that LATG12 is only one bit. These mnemonic constants are available for the programmer's use.
OK, let's pop back up a couple of levels and return to pic32-libs/include/peripheral/system.h. At the end of this file, we see the definition of SYSTEMConfig, which we use in our digital I/O program to optimize the PIC flash wait states and turn on the pre-fetch cache.

Certainly lots of other files get included in your program, but you get the idea. The point is that by digging deeper into the code, you find that the software you are writing eventually interacts with the hardware, e.g., by modifying the bits at specific memory locations (the SFRs that control the peripherals, which correspond to transistors being on or off in the PIC's circuitry). These lower-level include files help you understand how the hardware information in the Data Sheet and Reference Manual is reflected in the software.