« Back to home

Microcontrollers - The PIC16F628A

This is the first in a series of articles describing the construction of an interface board suitable for controlling 2 motors, 2 servos and providing a collection of binary inputs and outputs. The board provides limited onboard intelligence (only enough to protect the motors and servos from invalid operations) - it is designed to be controlled from an external computer through an RS232 connection.

Update The MPLAB X project file for this post is available here.


PIC 16F628A

This article provides an introduction to the microcontroller that is at the heart of the project - the PIC 16F628A. All of the provided programs are in PIC assembly language, using C on a PIC (the 8 bit PIC processors at least) seems to be overkill - the resulting code looks a lot like assembly language but with a lot more boiler plate around it.


There are a number of things you are going to need for this to be more than an intellectual exercise ...

  1. You will need a way to convert the assembly language code into a binary file that can be loaded into the program memory on the PIC. The easiest solution is to download and install the MPLABX IDE which is available for Windows, Mac and Linux. You will need to download the appropriate installers for the MPLABX IDE and MPLAB XC8 Compiler from the previous link and install them in that order. If you prefer to use the command line and already have a preferred editor you might consider the gputils package (this can be installed via apt-get for Debian and Ubuntu).
  2. A simulator is handy to test your code before burning to a chip - it helps to iron out the more obvious bugs in a more friendly environment and reduce the chances of damaging any components in your circuit. MPLABX includes a simulator, you could also look at gpsim or PICsim which are available for both Linux and Windows. The gpsim project provides both a command line interface (similar to gdb) and a simple GUI. PICsim on the other hand provides a much more graphical interface as well as simulating some external hardware (switches, LED's, LCD displays, etc).
  3. A hardware programmer is required in order to burn the resulting code onto the chip itself. I'm using one known as the K150 (v2.1) - I'm not sure if this is available anymore (and if it is - from resellers for example - how long the supporting software will be available) as the main manufacturers site (kitsrus) seems to have expired. There are wide range of alternative programmers available (a google search should turn up something that suits your budget and needs).
  4. At least one PIC16F628A chip - preferably two in case of mistakes. In Australia these are available from Element14 at $AU 4.40 per unit, in the US you could source them from SparkFun for $US 3.18.

Programming Principles

I highly recommend downloading a copy of the datasheet for the processor and keeping it open as a reference (it's large, 180 pages long, so printing it out may not be an option). I'm going to gloss over a lot of details and just concentrate on the basics - for more information please refer to the datasheet to get all the nitty gritty details.

Chip Pinout

This particular model of PIC has 2048 words of program memory (each instruction consumes a single word so your maximum program length is 2048 instructions), 224 bytes of RAM, 128 bytes of EEPROM for persistant storage and 16 IO pins. If you look at the pinout to the left you can see that most of the IO pins have multiple purposes - only one function is available at any given time so choosing to use a particular function will exclude that pin from any other purpose. This is a very important factor to consider when designing your project.

Memory Map

Memory layout on the PIC is very different to what you are probably used to if you have developed programs for desktop computers. Each memory location is considered as a register and is accessed by it's address. To further complicate the issue not all registers are available at any given time - memory is organised as four banks of 128 registers each and only one bank can be accessed at one time. The first 32 registers in each bank are special purpose registers that are used to control and configure the processor and IO pins, the last 16 registers in each bank are shared across all banks allowing for values that can be accessed regardless of which bank is currently selected. Not all register addresses are implemented either which leaves some gaps in the memory map that need to be worked around. The full memory map is shown in the image on the right. The entries labeled as General Purpose Register and the block of 16 bytes at the end of each page are free for use by your program.

The PIC processor is a RISC based system so it has a very simple instruction set - only 35 instructions in total. As well as the registers mentioned above there is a single working register (refered to as W) that is required for many operations. Operations that modify values have the option to store the result in this register or the file register being operated on.

Each instruction takes 4 clock cycles to execute with the exception of conditional and jump instructions which may take 8 clock cycles. This model of PIC can operate at up to 20MHz (approximately 5 MIPS) using an external oscillator. There is an onboard oscillator which operates at 4MHz (approximately 1 MIPS) and can be slowed to 48 KHz to save power or facilitate debugging and testing. All examples in this series will use the internal oscillator.

Sample Project - Flashing an LED

The remainder of this article describes a circuit (and the software) required to flash an LED at a regular period (on for 250ms, off for 250ms). This project doesn't use any of the special periphials on the chip, timing is achieved purely though clock cycle counting and only a single output pin is used.

Flashing a LED - The Circuit

LED Flasher Circuit

Electrically the PIC will operate with an input voltage from 3.0V to 5.5V and draws very little current (typically less than 2 mA @ 5V for the chip itself). It does require a steady DC power supply but a voltage regulation circuit is generally not necessary. As most of my projects are designed to be mobile (or at least portable) I tend to use a 6V battery pack (4 x AA NiCD batteries).

The circuit shown to the left is the simple LED flashing example we are going to use. The upper left part of the circuit is a simple voltage regulator which uses the forward voltage drop across a 1N4004 diode to ensure the input voltage is always less than 5.5V.

Circuit on Breadboard

The LED itself is driven from the RB5 output through a 180 Ohm current limiting resistor. Outputs from a PIC can drive loads up to 25mA (so a single LED is fine) with a total of 200mA for all active outputs. For higher loads you would generally use the output from the PIC to drive a transistor which will bear the load rather than pulling it through the output pin itself.

Flashing a LED - The Code

For this project I've kept the code very simple, only the basic input and output functionality of the chip is being used. I have a simple

``` ;============================================================================ ; Template program for PIC16F628A chips. ;---------------------------------------------------------------------------- ; 28-SEP-2012 shaneg ; ; Use this file as a template for new projects. Sets up a common code layout ; and basic coding style. ;============================================================================

             list P = 16F628A     ; Identify the chip to use                 include  P16F628A.INC

             ; Set the configuration bits for this application                 __config _WDT_OFF & _BOREN_OFF & _INTOSC_OSC_NOCLKOUT

;---------------------------------------------------------------------------- ; Constants and definitions ;----------------------------------------------------------------------------

;---------------------------------------------------------------------------- ; Interrupt vector table ;----------------------------------------------------------------------------

             ; Reset vector                 org     0x0000                 goto    program

             ; Interrupt vector                 org     0x0004                 goto    interrupt

;---------------------------------------------------------------------------- ; Functions and routines ;----------------------------------------------------------------------------

;---------------------------------------------------------------------------- ; Interrupt handler ;----------------------------------------------------------------------------

interrupt: ; Save current state ; Process and clear outstanding interrupts ; Restore state retfie

;---------------------------------------------------------------------------- ; Main program ;----------------------------------------------------------------------------

program: ; Configure periphials and IO mainloop: ; Main program loop (runs forever) goto mainloop

             end ```  file that I use as a starting point for most projects and it is the basis for the code presented here.

The full code for the project is ``` ;============================================================================ ; Demonstration program for PIC16F628A chips. ;---------------------------------------------------------------------------- ; 29-SEP-2012 shaneg ; ; A simple test program to flash a LED on port RB5. ;============================================================================

             list P = 16F628A     ; Identify the chip to use                 include  P16F628A.INC

             ; Set the configuration bits for this application                 __config _WDT_OFF & _BOREN_OFF & _INTOSC_OSC_NOCLKOUT

;---------------------------------------------------------------------------- ; Constants and definitions ;----------------------------------------------------------------------------

REGLOOP0 equ 0x20 REGLOOP1 equ 0x21

;---------------------------------------------------------------------------- ; Interrupt vector table ;----------------------------------------------------------------------------

             ; Reset vector                 org     0x0000                 goto    program

             ; Interrupt vector                 org     0x0004                 goto    interrupt

;---------------------------------------------------------------------------- ; Functions and routines ;----------------------------------------------------------------------------

delay250: ; This routine simply enters a loop to delay the processor for approx 250ms ; by consuming the processor for around 1,000,000 instructions. movlw 0xff movwf REGLOOP0 loop0: ; Outer loop movlw 0xff movwf REGLOOP1 loop1: ; Inner loop nop ; NOP x 4 to get the delay nop nop nop ; Check for end condition decfsz REGLOOP1, F goto loop1 decfsz REG_LOOP0, F goto loop0 return

;---------------------------------------------------------------------------- ; Interrupt handler ;----------------------------------------------------------------------------

interrupt: ; Save current state ; Process and clear outstanding interrupts ; Restore state retfie

;---------------------------------------------------------------------------- ; Main program ;----------------------------------------------------------------------------

program: ; Configure periphials and IO banksel TRISB movlw 0x20 xorwf TRISB, F ; Set RB5 to output banksel PORTB movwf PORTB

mainloop: ; Main program loop (runs forever) movlw 0x20 xorwf PORTB, F ; Toggle RB5 ; Wait for a quarter second call delay250 ; Restart loop goto main_loop

             end ``` , this article will simply highlight the more important parts.

The start of the code is mostly control information for the assembler - it sets the type of processor to use, brings in specific definitions for that processor and defines the configuration bits we wish to use.

                     ; Set the configuration bits for this application                     __config _WDT_OFF & _BOREN_OFF & _INTOSC_OSC_NOCLKOUT

 The line starting with **__config** is worth pointing out - this enables and disables various features of the chip. The configuration information is burnt into the chip during the programming stage and cannot be modified at runtime. In this case we are disabling the watchdog timer (_WDT_OFF), brown out resets (_BOREN_OFF) and selecting the internal 4 MHz clock generator with no clock signal output (_INTOSC_OSC_NOCLKOUT). This leaves the designation of all the IO pins up to our program and ensures the chip will not reset on us as long as there is 3V or more on the power pins.

 When the processor starts it starts executing code at the very beginning of program memory (address 0). If you enable interrupts on some of the periphials (the serial controller, timers, and others) the processor will *interrupt* whatever code is currently running and start executing code at the interrupt vector (address 4). Once the interrupt has been handled the previous code will resume. Even though we are not using interrupts in this example we set up the start of program memory as if we were. This next bit of code does that for us - on startup we jump to the main program, if an interrupt occurs we jump to an interrupt handler to deal with it.
                 ; Reset vector                     org     0x0000                     goto    program

                 ; Interrupt vector                     org     0x0004                     goto    interrupt

Because we are not using the built in timers we need some way to force a delay. The easiest way to do this is to keep the processor busy executing instructions for the period we want to delay. In this case a delay of about 250ms is required so we need to do something for around 250,000 instructions. The routine shown next uses some nested loops (and some nop - no operation - instructions) to consume the time. In this case we use two nested loops of 255 cycles each and the inner loop includes some nop instructions to space out the timing. The time consumed is approximately 255 x 255 x 4 (260100) instructions. The time is actually slightly longer than that due to additional test and initialisation instructions.

 The variables used in the routine are defined at the top of the file. Each variable is simply assigning a friendly name to the address of a file register. Both of these registers are in bank 0 for simplicity (and for most of the program bank 0 is selected).
 REG_LOOP0       equ 0x20     REG_LOOP1       equ 0x21

Although this is a very rough and ready solution to the problem it is a solution that is commonly used with the PIC (although the timing is generally much more carefully designed). It is possible to use a single general purpose IO pin to simulate a more complex operation (such as serial communication) with careful timing.

Finally we have the main program which is usually divided into two parts;

  1. The initialisation part of the program is run once on startup and configures the appropriate periphials and IO pins. In this case we are setting a single pin to be in output mode.
  2. The main part of the program is run in an endless loop for as long as the processor has power.

    This is what it looks like ...

``` program: ; Configure periphials and IO banksel TRISB movlw 0x20 xorwf TRISB, F ; Set RB5 to output banksel PORTB movwf PORTB mainloop: ; Main program loop (runs forever) movlw 0x20 xorwf PORTB, F ; Toggle RB5 ; Wait for a quarter second call delay250 ; Restart loop goto main_loop

The first part of this code configures the IO pins - in this case we modify the TRISB port to set the RB5 pin as an output (by default all pins are configured as inputs). This is done by setting bit 5 of the TRISB register to a '1' - refer to the datasheet for more information on how this works. We then set the output pin high (by writing to the PORTB register) before going into the main loop.

As I mentioned at the start of the article the PIC has multiple banks of registers and only one bank can be active at any given time. The TRISB and PORTB registers are in different banks so depending on which one you are accessing you need to ensure that it's bank is currently selected. The banksel statement shown in the code above is a useful macro provided by the assembler to help you in this process. Using banksel TRISB emits the appropriate code needed to switch to the bank containing the TRISB register.

Programming the PIC

All that remains now is to compile the program to a HEX file and then use your programmer to load that code onto the PIC. Personally I prefer the gpasm tool from the gputils package mentioned at the start of the article. I use a K150 programmer (show in the image to the right) and the software that comes with it. When you do the burn always double check the code has loaded correctly (most programmers have a verify function you can use) and check that the configuration bits have been set as expected (these are also known as fuse bits) - some programmers have a bad habit of overriding the bits set in the hex file with whatever has been set in the GUI for the programmer which can lead to some unexpected behaviour.

What Comes Next?

This has been a very introductary article aimed at getting from the design to an actual implementation. The actual functionality has been deliberately simplified to make it easier to explain (and, quite frankly, this article has turned out a lot longer than I originally expected it to be).

The next step in the series will add the ability to communicate with and control the PIC software over an RS232 connection. This will introduce interrupt handling and dealing with some of the built in periphials on the PIC. The new circuit will be a little more complex than the one shown in this article but not overly so.

If you have any questions or suggestions please leave them in the comments, I look forward to hearing from you.