Digital Electronics

PHYS 432 - PIC Information

http://physics.uoregon.edu/~torrence/432/picinfo.html
Updated Wednesday May 05, 2021

Introduction

Modern society is awash in computer-controlled devices. Even consumer devices as mundane as your toaster typically have at least one microprocessor. These imbedded devices can be as powerful as high-end desktop computers from a few years ago or extremely simple and cheap with very specific functionality. We will be using the very popular PIC family of microcontrollers from Microchip. The mid-range PIC is a cheap but relatively powerful device which includes a lot of useful functionality including built-in circuitry for ADCs, counters, timers, and comparators. With all this stuffed into a package as small as an 8-pin DIP costing as little as $2, these microcontrollers can be effectively used in place of discrete components to do a wide range of interfacing and data acquisition tasks.

Hardware Overview

A PIC is a computer, with memory and programs, albeit on a very limited scale. The PIC, unlike most commercial processors used in desktop computers, is an example of a Harvard architecture rather than a Princeton (or Von Neumann) architecture. What this means is that data and program memory are separate, and follow separate paths in the processor. This allows for a very compact RISC instruction set and most operations can be executed in one machine cycle, since the instruction and data can be read simultaneously. A simplified diagram of a PIC is shown below.

A PIC can have up to 4 banks of 8-bit memory, called File Registers, which can be used to store values. Certain memory locations contain special functionality related to the operation of the PIC, such as the values on the I/O pins. These special registers vary from chip to chip, so it is important to have the proper Data Sheet for the device you are using. Control of the chip is implemented through instructions which change bit values in these registers. In a typical PIC, due to the Harvard architecture, it is impossible to directly change the PIC program while the PIC is running.

The PIC has one (and only one) operation register called the working (or W) register. All operations on data (like adding, subtracting, and moving from one file register location to another) must go through the W register. This may seem like a huge limitation, but it really isn't. Values in the W register can easily be transferred back into the file registers for storage if desired, and many operations allow you to do this automatically.

In very simple terms, the operation of a PIC is as follows. On every instruction cycle, a pointer into the program memory (called the Program Clock or PCL) is incremented, and the next instruction is read. Depending upon the instruction, the 8-bit word in the W register can be loaded from a file register location, the W register value can be manipulated, or the value can be written back to a file register location. It is also possible to directly manipulate the PCL, allowing for conditional execution statements.

Most PICs have a rather paltry amount of program and file register memory. The popularity of modern PICs is largely due to the additional functionality Microchip has managed to stuff into these compact devices. The 12F675, an 8-pin device, incorporates analog comparators, a 10-bit ADC, two separate timers, brown-out detection, and an internal 4 MHz oscillator. The interaction with all of these features is through specific file registers. This chip can only store 1024 instructions, however, and has a grand total of 64 bytes of file register memory available to the user.

A more comprehensive description of PIC microcontroller architecture can be found in Chapter 3 of the book Programming & Customizing PICmicro Microcontrollers by Myke Predko. A copy of this book is on reserve in the Science library.

PIC Example

For a low to mid-range PIC there are typically only 35 distinct operations which the processor can execute. This small instruction set makes coding the PIC directly in assembly language rather painless. While it is possible to program a PIC in a higher-level language like C, you end up writing very similar code, and I typically find it unnecessary.

We will be using the 12F675 PIC, which comes in a very small 8-pin DIP, but has an amazing amount of built-in functionality. To really understanding this device, it is important to have a copy of the Data Sheet close at hand. Section 10 describes the instruction set, although there is also a nice instruction set summary available. You will need this to understand the code below.

Here is a non-trivial example of a PIC assembly file implementing a state machine to switch on the eight LEDs on the PICkit 1 programming board: state.asm. This demonstrates a variety of important features, like debouncing switches and state machines. This is also the second lesson in Appendix C of the PICkit 1 User's Guide. For anybody serious about learning PICs, these lessons are very useful. The source code for these lessons is available.

Header

    list      p=12f675            ; list directive to define processor
    #include <p12f675.inc>        ; processor specific variable defs
    errorlevel  -302              ; suppress message 302

These are all compiler directives, meaning they are not part of the actual code, but rather instruct the compiler to do certain things. The first line specifies the processor (and must match the chip), while the second loads a "header file" which pre-defines human-readable names for system registers and bits.

	__CONFIG   _CP_OFF & _CPD_OFF & _BODEN_OFF & _MCLRE_OFF & _WDT_ON & _PWRTE_ON & _INTRC_OSC_NOCLKOUT

This line sets a series of hardware fuses to control some of the low-level PIC behavior. This is how you control whether the PIC uses an internal or external oscillator, whether people can upload your code back out of the PIC, whether the PIC can be rewritten, and so on. Complete details for each chip are in the spec sheet under Configuration Bits. Note these can only be set by the programmer, not during run time. One particularly interesting bit is _WDT_ON, which enables the "Watchdog Timer". If this timer is not periodically reset in the program, the PIC assumes it has locked up and will initiate a reset cycle.

Variable Definitions

   cblock   0x20
     STATE_LED        ; LED state machine counter
     STATE_DEBOUNCE   ; button debounce state machine counter
   endc

While you can always write to most any file register (memory) location by its numeric value, it is better to have specific named variables for specific information. Here we have assigned specific names to a 2-byte block of memory starting at absolute address 0x20. You must make sure your variables sit in a location which is not otherwise used by the PIC itself. See the memory map in the spec sheet for details. For the 12F629, 0x20 starts the general purpose register section. Note that the apostrophie is not necessary at the end of a line. This denotes a comment. The apostrophie, and anything after it on the line, is ignored.

Define Statements

#define SW1	        GPIO,3      ; toggle switch
#define TRIS_D0_D1  B'00001111' ; TRISIO setting for D0 and D1
#define D0_ON       B'00010000' ; D0 LED

Next comes a series of define statements. Three examples are given here. The define command is actually an instruction to the compiler to replace the first text string with the second text string before compiling the code. It is generally bad practice to have any hard-coded "Magic Numbers" in your code, so by defining these up top, it is much easier to change things later. In this case, SW1 is used to identify the bit in the GPIO word where the pushbutton switch is read. The other two define bit patters for the TRISIO and GPIO registers to turn on specific LEDs on the programming board. Note that numeric values can be defined as either bit fields prefixed with B, as hex values prefixed with the hex radix such as 0x20.

Start of the Program

    org     0x000			        ; processor reset vector
    goto    Initialize

When the processor starts up or resets, the Program Counter (PC) which controls what instruction to execute next is automatically set to 0x000. The org command here tells the compiler to start the following code at program address 0x000. Remember, in a PIC the program memory space is held in Flash RAM and is not the same as the Special Function Registers, where you can read and write values. The next instruction is a goto, which immediatly causes the PC to jump to the Initialize label. You could also specify a specific numeric location in the program memory, but this is very prone to errors. The compiler replaces the Initialize label with the actual memory location of the label, which is defined below. Since this is the very next thing, you may wonder why this is necessary. The reason is because if you are using interrupts (which we are not here) the interrupt handler routine always starts at location 0x004, and we want our main code to avoid this location. We really don't need this, but it is a good habit to get into.

Initialization

    org     0x005           ; Start of Programm Memory Vector
Initialize:
    call    0x3FF           ; retrieve factory calibration value
    bsf     STATUS,RP0      ; Bank 1 
    movwf   OSCCAL          ; update register with factory cal value 
    movlw   B'00111111'     ; Set all I/O pins as inputs
    movwf   TRISIO
	
    movlw   B'10000100'     ; Weak pullups: disabled
    movwf   OPTION_REG      ; TMR0 prescaler: 1:32 (TMR0 will overflow in 8.2ms)
    clrf    INTCON          ; disable all interrupts, clear all flags	
    bcf     STATUS,RP0      ; Bank 0
    clrf    GPIO            ; clear all outputs
    clrf    TMR0            ; clear Timer 0
    clrf    STATE_LED       ; clear LED state machine counter
    clrf    STATE_DEBOUNCE  ; clear debounce state machine counter

There is a lot going on here. First, the Initialize label is not an instruction, but a compiler directive which can be used as the target of GOTO statements. The trailing colon is by convention to make it clear this is a label, but is not strictly necessary. What is necessary is to have no white space before the label. White space (usually a tab) is needed for instructions. No white space generally indicates a compiler directive (label or #define).

I am not going to try to explain this section in detail. In summary, various Special Function Registers are being loaded with default values using a MOVLW/MOVWF pair of instructions, or else they are being cleared with CLRF. From the Data Sheet, you should be able to identify every one of these registers and understand what each bit means. The two registers which control the I/O pins are TRISIO and GPIO. One important subtlety to PIC programming is that there are two banks of Function Register memory. Some registers exist in both banks, but others do not. You must be careful when reading or writing to function registers that you are properly set to be in Bank0 or Bank 1. The BSF (bit set) and BCF (bit clear) instructions are used on bit RP0 of the STATUS register to switch between the two.

State Machine

State_Machine:
    clrwdt                  ; clear Watch Dog Timer
    call    Button_Press    ; Increments STATE if button is pressed
    movf    STATE_LED, w    ; Mask out the high order bits of 
    andlw   B'00000111'     ;   STATE_LED
    addwf   PCL, f          ; The program clock (PCL) is incre-
    goto    State0          ;   mented by STATE_LED in order
    goto    State1          ;   to go to the appropiate routine
    goto    State2			
    goto    State3			
    goto    State4
    goto    State5
    goto    State6
    goto    State7

After all of that setup, we are now ready to look at the real algorithm. The watchdog timer is the thing that will reset the chip if it is not cleared periodically. For this chip, it must be reset every 18 ms! It is put here in the main loop so it is not forgotten. The lights are sequenced in order by means of a simple state machine. Pushing the button changes the state. The subroutine Button_Press increments the state if it finds the button has been pressed, or leaves it where it is if not. The PIC runs fast enough (MHz) such that it is impossible for a human to press and release the button without the PIC noticing. The next two lines copy the value from our pre-defined STATE_LED register into the working or W register, and masks off the lowest three bits. The next command adds this value directly to the Program Clock, the thing which tells the PIC what the next instruction is. By doing this, we can select between 8 different functions, each of which lights up a specific LED then goes back to the start of the State_Machine loop. This ability to directly modify PCL is somewhat dangerous, and should always be used in a tightly controlled fashion as it is done here. The 3-bit mask ensures that we will only eight possible values.

Output Registers

State0:      ; Turns on D0 LED
    bsf     STATUS, RP0	    ; Bank 1
    movlw   TRIS_D0_D1      ; move predefined value to TRISIO
    movwf   TRISIO
    bcf     STATUS, RP0     ; Bank 0
    movlw   D0_ON           ; move predefined value to GPIO
    movwf   GPIO
    goto    State_Machine   ; go back to state machine jump table

This code is a general method for turning on one LED. In summary, we first set the TRISIO register to control the tri-state value for each IO pin. This effectively defines whether each pin will be an input, output, or floating (tri-state). The second set of commands writes a particular pattern onto the pins defined as outputs. By manipulating the pins in this way, the PIC can manage to light 8 LEDs using only 5 pins. See page 3 of the Tips and Tricks file for a better idea how this is done.

Button Debouncing

The final part of the code looks for changes in the level of GPIO 3, which is always configured as an input, and on the PIC programming board has the push-button wired to it. Like anywhere else, buttons must be debounced, and this code does this in a very brute force manner, essentially waiting for about 10 ms for the bouncing to settle down before doing anything else. Look through Lesson 2 in the User's Guide for more details on this. If you need to use a button to interface to a PIC, you should probably just copy this code to deal with the button.

Software Resources

Microchip provides a free development envionment called MPLAB and an assembler which works with the PICkit 1 board. This is installed on the PCs in room 11. There is also a free C compiler called PICC Lite from HI-TECH software. This can integrated directly into MPLAB as a "Toolsuite", although it is a little tricky to get this working properly. MPLAB also handles downloading the compiled files to the chip via the PICkit 1 Classic application. This and other useful documentation can be found at the PICkit 1 webpage.

What I typically use instead is the GNU command-line assembler which is available for Linux, OSX, or DOS. To download the files to the PICkit, on a Mac or Linux you can use the usb_pickit 1.5 application. The original version of this doesn't work with recent PICkit firmware versions. On the PC, the PICkit Classic application can be used to directly download precompiled .hex files.

Software Walkthrough

This is intended to give the very briefest of instructions for how to actually use MPLAB to compile and load a program to a PIC. A more detailed walkthrough can be found in Chapter 4 of the PICkit User's Guide.

Step 1: Download the state.asm file to a local directory. Start the MPLAB application, and open the state.asm file.

Step 2: Set up the MPLAB options. The device should be automatically set by the list command in the state.asm file, but it doesn't hurt to do this by hand. Under Configure:Select Device, select the specific PIC you are using and make sure this matches what is specified in the .asm file. You can also select the programmer at this time, by selecting Programmer:Select Programmer:PICkit 1.

Step 3: Compile the ASM file. This can be done by selecting the state.asm window and selecting Project:Quickbuild state.asm. We have not defined a project, and Quickbuild will only work for single file programs. Setting up a project is fairly simple using Project:Project Wizard. See the User's Guide for more details on setting up projects.

Step 4: Debug (optional). If your code compiles, but doesn't seem to do exactly what you want, you may need to explore the MPLAB debugger. This can be used to single-step through your code and examine the register values after each step to find logic errors which can otherwise be hard to figure out. Consult the User's Guide for more information.

Step 5: Download the compiled .hex file to the PIC. If the chip isn't in the programming board yet, turn off the PICkit board by unplugging it from the USB cable. Insert the PIC very carefully into the "Evaluation Socket" with the notch pointing up. The chip should be at the very top of the socket, with any empty socket pins below the chip. With the chip in place, plug the board back into the USB cable. Make sure you have selected the correct programmer, then select Programmer:Program Device, or use the Program button in the menu bar.

Your PIC should now be programmed. You should be able to push the button on the programmer board and cycle through the LEDs. Being able to test out code without removing the PIC from the programmer is very useful, as there is always the danger of bending pins when removing chips from a socket. I find that sliding a small flat screwdriver between the chip and the socket is the best way to pop them out. Don't forget to disconnect the board first to turn off the power!

Documentation

There are a series of lessons in Appendix C of the PICkit 1 User's Guide. The source code for these lessons can be found here.

Microchip also has a huge number of application notes on their web site. Searching on their web site for a popular PIC model, like the 16F684, is a good way to find these. One example is AN964: Inverted Pendulum, which would make a great project.

There are two useful books by Myke Predko on reserve in the Science library: