NU32v2: A Detailed Look at Programming the PIC32

THIS PAGE REFERS TO A PRE-RELEASE VERSION OF THE NU32 PIC32 DEVELOPMENT BOARD. FOR INFORMATION, SAMPLE CODE, AND VIDEOS RELATED TO THE PRODUCTION VERSION (2016 AND LATER), AND TO THE CORRESPONDING BOOK "EMBEDDED COMPUTING AND MECHATRONICS WITH THE PIC32 MICROCONTROLLER," VISIT THE NU32 PAGE.

After you have programmed your PIC32 for the first time and verified that you can create a new project, compile it, and run it on your NU32v2, 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:

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
0x7D000000 + BMXPUPBA BMXPFMSZ - BMXPUPBA USEG/KUSEG program flash
0x7F000000 BMXDUPBA - BMXDUDBA USEG/KUSEG data RAM
0x7F000000 BMXDRMSZ - BMXDUPBA USEG/KUSEG program RAM
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
0xA0000000 + BMXDKPBA BMXDUDBA - BMXDKPBA KSEG1 program 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 NU32v2, 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 NU32v2 can easily be programmed with any of these programmers, but has been designed to work with the PICkit 3, available for around \$45.

To avoid the expense of providing a PICkit 3 programmer with every NU32v2 kit, we opted to use our PICkit 3 programmer to install a bootloading program on your NU32v2. This allows you to program your PIC using only a USB cable and the free MPLAB 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 is low (i.e., whether the user is pressing a button) when the PIC is reset. (Aside: the bootloader code sets pin G6 as a digital input and configures an internal pull-up resistor to 3.3 V, which is why a simple external normally open switch, connecting G6 to ground when it is pressed, suffices for the digital input.)

• If the digital input is high (no button press), 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 a "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 the button is not being pressed), it will begin executing the program you loaded.

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. Unfortunately, you cannot see the configuration bits being set in the bootloader .S assembly code (below), because the bit settings were chosen in the IDE that compiled the bootloader. In future, we will set these configuration bits in the code itself, for portability and so you can see what's going on. This is also the standard for the MPLAB X IDE.

CODE

• The NU32v2 bootloader project, written in MPLAB Assembly, with a "hello_world" type blinking LED example tacked on, is provided here: PIC32 code.
• The NU32v2_serial_bootloader app source code, written in Processing, is provided here: PIC32 code. New for Feb 14 2011- Should crash less. Email Nick if you repeatedly get Error # in the message area.
• The Mac OSX executable version of the bootloader app is here: NU32v2 serial bootloader for OSX. New for Feb 14 2011- Should crash less. Email Nick if you repeatedly get Error # in the message area.

The NU32v2_serial_bootloader app allows the user to navigate to and select a .hex file. No other file type selection is allowed. The app also creates a button for each available serial COM port when the app loads, so the NU32v2 board must be powered up and the USB cable must be plugged in before the app is run.

Note about serial communication: The NU32v2 board has an FT232RL built on to allow for easy communication over USB with your computer. This chip translates between the PIC's UART (RS-232) and your PC's USB protocol. The communication is viewable as a "virtual com port" on your computer using the driver provided by FTDI. This chip provides an easy way for your computer and the NU32v2 to communicate, for bootloading, debugging, and general communication.

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 procdefs.ld file to Other or Important Files. You choose the C32 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.

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

#define LED0     LATGbits.LATG12

and

SYSTEMConfig(SYS_FREQ, SYS_CFG_ALL);

So it looks like we defined one constant (LED0) to be equal to a variable, the field "LATG12" of a struct "LATGbits." 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 and the C32 compiler, it installed a large tree of directories under Program Files\Microchip\MPLAB C32 for MPLAB 8.xx, or mplabc32/v.11a (or similar) for MPLAB X. To give you an idea of the directory structure, we highlight some of the important directories and files below:

MPLAB C32

• 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)
• 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
• 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