Embedded Systems: PIC Microcontrollers
CS 301 Lecture, Dr. Lawlor
Tiny (about 1cm across), cheap (normally around $1), and very low power
(often less than one milliamp), fully programmable microprocessors can
disappear inside a variety of consumer electronic devices: microwave
ovens, watches, and even PC peripherals like mice, hard drives, and
such. These "embedded" computers typically run a fixed and simple
program on the bare CPU, without an operating system. The program
to run is stored in nonvolatile ROM or flash memory.
Plain old C is considered a "high level language" on embedded machines,
and many commercial embedded products are still written in assembly
language. 20MHz is considered "fast". Integer multiply
instructions are a rare luxury--floating point is almost never found. A $5 chip is considered "expensive".
Some major families of embedded microcontrollers out there include:
- The open ARM microcontrollers.
Instructions are 32 bit RISC, with 32 32-bit registers. Common in
cell phones, and scale out to Linux. Available as VHDL code, so you can drop one into
your ASIC.
- Amtel's AVR line, as used in the open source Arduino. Instructions are 16 bit RISC, with 32 8-bit registers.
- Microchip(tm) corporation's PIC line.
Instructions are fixed-size RISC, from 12 to 16 bits wide depending on
the model, with one 8-bit accumulator register W. Higher end
models can support a full-fledged OS, low end models don't even have
interrupts. The popular BASIC Stamp controller is build on a PIC.
- TI's MSP430 line is very popular in the UAF EE department.
- The venerable 68HC11 microcontrollers. Instructions are variable-length CISC, with two (A,B) or four (A,B,X,Y) registers.
I've written code for the 68HC11 and PIC micros. Microchip has
done a good job of selling a good $50 USB device programmer, the PICkit 2, for uploading a program onto a PIC chip, so I mostly use PICs nowadays.
PIC Microcontroller Hardware
Here's the Microchip PIC 12F675 reference manual.
The 12F675 is a tiny 8-pin chip about the size of your fingernail, and
it'll run on anything from 2v to 5v. It's got an internal 4MHz
oscillator, flash memory space for 1024 14-bit instructions, an 8-entry
hardware call stack, about 64 bytes of RAM, a 128-byte EEPROM, and a
4-channel 50kHz analog to digital converter. It retails for about $1.
The way it works is you feed in ground on pin 8 (marked Vss in the
datasheet), anything from +2V to +5V on pin 1 (marked Vdd on the
datasheet), and all the other pins are programmable as analog or
digital inputs or outputs in software. Pin 4, GP3, can only act
as an input line, not an output. GP3 is also used as a
high-voltage input to program the chip.
+5V -|-u-|- GND
GP5 -|. |- GP0
GP4 -| |- GP1
GP3 >|---|- GP2
(This is a top-down view. The "u" is the orienting notch in the chip. Chips inserted backwards may be fried!)
You upload a program from a "real" computer onto the PIC by hooking up
a PICkit 2 "device programmer", and routing the lines from the PICkit 2
snout pins:
> 1.) GP3 VPP programming voltage
2.) +5V (USB power)
3.) GND
4.) GP0 icspdat (programmed-in data)
5.) GP1 icspclk (programming data clock)
6.) not connected
By convention, there's always a triangle pointing to pin 1.
Again, if you hook the PICkit 2 up backwards, you might fry your
PIC. Frying one now and then is basically inevitable. It's
OK, they're $1.
Embedded Inputs
There are a bunch of different input devices you can hook up to
embedded microcontrollers. Basically, input pins read voltage, so
anything you can convert into voltage, you can input into the
controller. One really common device for this is a voltage divider, which is basically just two resistors in series, where you hook up the middle lead to the microcontroller.
For example, pushing a button normally closes a contact, brining the
resistance of the button from infinity down to zero. Alone,
that's useless. But if you put 5V on one terminal of the button,
and connect the other terminal to both the microcontroller input pin
and a 1Kohm "pull-down" resistor (the other end hooked to ground), then
you've effectively built a voltage divider with the button as the top
resistor. If the button is unpushed, its infinite resistance
allows the pull-down resistor to pull the micro's pin down to ground,
zero volts. When the button is pushed, it shorts the micro's
input pin up to 5v. A little current leaks through the pull-down
resistor, which is fine. Light switches, limit switches, keyboard
and mouse buttons, thermostat mercury bulbs, etc all boil down to just
contact or no contact, and are interfaced in exactly the same way.
One caveat: pushing a button may result in several hundred tiny
contact/no contact pulses a few microseconds wide. You typically
clean this up with a "debounce" circuit, either a hardware
resistor-capacitor filter circuit, or a software function that only
checks the button at 50Hz or so.
Another example, a "thermistor" is a resistor whose resistance varies
with temperature. If you set up a voltage divider as above, but
plug a thermistor into the top half, you can convert temperature to
voltage.
Analog TV, analog audio, and VGA signals are already just
quickly-changing voltage patterns, so you can run them straight into an
analog input pin of a microcontroller!
Embedded Outputs
Output pins output voltage, usually just either 0v or 5v, but can
supply up to a few dozen milliamps. This is just enough to light
up a small LED, although you usually want a 1Kohm (or so) current limiting resistor inline with the LED.
To switch a useful amount of current, say to run a little motor, you usually need an interface device like a transistor.
"Signal" transistors can switch up to a few hundred milliamps, and
"power" transistors can switch up to a few amps. FET transistors
can go up to dozens of amps fairly cheaply.
One annoying thing about transistors is they only conduct current in one direction. An electromechanical relay
can switch AC current, although usually you need a transistor to push
enough juice through the relay coil to get it to close. I like
relays, because they can't be hooked up backwards, and they're a lot
tougher to fry than transistors. Downside with relays is they're
slow, energy intensive, and electrically and even acoustically noisy.
To turn a DC motor in either direction, you need an H-Bridge.
In theory, you can build these yourself from transistors, but in
practice, it's way easier especially at high power to just buy a single-chip H-bridge (I really like the ST VNH3SP30TR, which switches up to 30 amps at 40 volts for $8).
One cool output device is a servo,
which is just a little motor, position sensor, H-bridge, and controller
circuit integrated into one handy case. You tell the servo what
position to go to with a pulse-width-modulated signal: 5v for 1ms means
all the way to the left, for 2ms means all the way to the right, and
1.5ms means halfway in between. Most servos can seek to several
hundred separate positions. You usually repeat the seek signal
every 15-30 milliseconds. Servos use just three wires: black for
ground, red (in the middle) for 5v, and white for the PWM position
signal. Servos are as low as $3 direct from China (although watch out for shipping!).
If talking to another big or little computer, you can speak USB (which is fast enough
you usually need special hardware to speak it), plain slow serial
(where bits are known fixed and *slow* times), I2C, SPIB, or any of a bunch of weirder protocols.
Embedded Programming Models
Typically, embedded programs look like this:
... initialize hardware ...
while (1) {
... read my inputs ...
... decide if anything needs to change ...
... if so, write changes to outputs ...
}
The main infinite loop is the "control loop". The idea is
microcontrollers usually are hardwired into known, fixed hardware, so
they only have one job to do.
You usually don't have files or permanent storage of any kind, don't
have an OS, don't have multiple threads or processes, and in general
are missing all the crap we've come to expect from computers. On
the minus side, your "debugger" is blinking LEDs and a voltmeter!
Here's an example that's runnable in NetRun. There are three LEDs
on GPIO pins 0, 1, and 2 (1<<0, 1<<1, and
1<<2). Pin 2 also hooks up to a servo.
#include "pic_setup.h" /* Dr. Lawlor's header, with "setup" function. */
void main(void) {
setup(); /* Dr. Lawlor's setup function */
while(1) { /* typical infinite loop */
GPIO=1; /* set LED pin */
busywait(30); /* do nothing for a while */
}
}
(Try this in NetRun now)
The whole setup is running inside a box in my office, and viewed with a webcam!
Embedded Programming: PIC Tools
To compile C code, I really like the version 9 "PICC-Lite" compiler
from HI-TECH. They give away the "Lite" version for windows, mac,
and linux. Although you do have to give them your email address,
I haven't gotten any spam from them yet.
On Linux, the header file listing the pic12F675's I/O ports is:
/usr/hitech/picc/lite/9.60/include/pic12f6x.h
And the main C compiler is used like this:
/usr/hitech/picc/lite/9.60PL2/bin/picl --chip=12F675 main.c -Omain.hex
Once you get a .hex file, you can upload it to the PIC via the PICkit 2 using pk2cmd (for Windows or Linux/OS X, also see Microchip's main PICkit2 page). Sadly, you first have to update the PICkit 2's firmware, like so:
wget http://ww1.microchip.com/downloads/en/DeviceDoc/PK2V023200.zip
unzip PK2V023200.zip
pk2cmd -DPK2V023200.hex
PICkit 2 found with Operating System v2.01.00
Use -D to download minimum required OS v2.32.00 or later
Downloading OS...
Verifying new OS...
Resetting PICkit 2...
OS Update Successful.
Once your PICkit is ready, pk2cmd works like this:
sudo pk2cmd /M /Ppic12f675 /T /Fmain.hex
"/M" means overwrite all PIC memory.
"/P" is the command to set the PIC model number.
"/T" Turns the target PIC on.
"/F" reads a .hex file and programs it into the PIC.
I usually just give in and make pk2cmd setuid root so I don't have to deal with "sudo".
Here's a tiny working "main.c" to flash one of the LEDs on my tiny board:
/**
Flash LEDs on 2008-2009 TAB boards
Orion Sky Lawlor, olawlor@acm.org, 2009-02-23 (Public Domain)
*/
#include <pic.h> /* magic HI-TECH header file; brings in /usr/hitech/picc/lite/9.60/include/pic12f6x.h */
/* Give my chip "serial number" 1. These can be used for anything you like. */
__IDLOC(1);
/* Set the "configuration word" entries as follows: turn everything off! */
__CONFIG(INTIO & WDTDIS & MCLRDIS & BORDIS & UNPROTECT & PWRTEN);
typedef unsigned char byte;
/** Busywait for this many milliseconds (FIXME: should use sleep & timer). */
void busywait(byte ms) {
for (;ms!=0;ms--)
{ // Wait for one millisecond (or so: depends on compiler, optimizer)
byte c;
for (c=250;c!=0;c--) { NOP(); }
}
}
void main(void) {
TRISIO = 0; /* all pins are outputs (not tri-stated) */
GPIO=0xff; /* turn on all LEDs at startup (debugging) */
busywait(250); /* wait for user to see flashed LEDs */
GPIO=0x00; /* turn off all the LEDs */
/* Main microcontroller control loop */
while (1) {
GPIO=4; /* blink pin-2's LED */
busywait(250);
GPIO=0;
busywait(100);
}
}
Again, you compile and upload this as follows:
/usr/hitech/picc/lite/9.60PL2/bin/picl --chip=12F675 main.c -Omain.hex
sudo pk2cmd /M /Ppic12f675 /T /Fmain.hex
Here's a more complete "main.c" with lots of optional features turned
off explicitly: this is handy if you forget how to turn them on!
/**
Flash LEDs on 2008-2009 TAB boards
Orion Sky Lawlor, olawlor@acm.org, 2009-02-23 (Public Domain)
*/
#include <pic.h> /* magic HI-TECH header file; brings in /usr/hitech/picc/lite/9.60/include/pic12f6x.h */
/* Give my chip "serial number" 1. These can be used for anything you like. */
__IDLOC(1);
/* Set the "configuration word" entries as follows: turn everything off! */
__CONFIG(INTIO & WDTDIS & MCLRDIS & BORDIS & UNPROTECT & PWRTEN);
typedef unsigned char byte;
/** Busywait for this many milliseconds (FIXME: should use sleep & timer). */
void busywait(byte ms) {
for (;ms!=0;ms--)
{ // Wait for one millisecond (or so: depends on compiler, optimizer)
byte c;
for (c=250;c!=0;c--) { NOP(); }
}
}
void main(void) {
byte count=0;
/* Weird aspect of the 12F675 and 16F676: they keep an internal
oscillator calibration word at address 0x3ff. This reduces
chip-to-chip timing variation substantially.
This is optional, and should NOT be done on most other PICs!
*/
typedef byte (*osccal_fn)(void);
osccal_fn fn=(osccal_fn)0x3ff;
OSCCAL=fn();
/* Turn *everything* off! */
TRISIO = 0; /* all pins are outputs */
CMCON = 0x07; /* comparator off */
WPU=0; /* disable weak pull-up on all pins*/
ANSEL = 0x00; /* disable A/D */
ADCON0 = 0x00;
OPTION = 0x80; /* disable weak pull-up globally */
TMR0 = 0; /* turn off the timer */
GPIO = 0x0; /* clear output pins */
GPIF=0; /* clear interrupt flag */
GPIE=0; /* disable GPIO interrupts */
GIE=0; /* turn off interrupts */
GPIO=0xff; /* turn on all LEDs at startup (debugging) */
busywait(250); /* wait for user to see flashed LEDs */
GPIO=0x00; /* turn off all the LEDs */
/* Main microcontroller control loop */
while (1) {
GPIO=4; /* blink pin-2's LED */
busywait(250);
GPIO=0;
busywait(100);
count++;
}