Tuesday, June 29, 2010

PIC Serial IO Example

I’ve been working on the PIC firmware for my ABPM project over the past few weeks and I thought it would be useful to combine many of the tips and tricks I learned into a little demo C program. I hope this will help others get up and running fast with similar applications.

The key features of this program are:
  • control and configuration via serial IO cable which can be detached when not required
  • very low power consumption – must run on batteries and last months
  • simple timing functions (eg awake every 20 minutes, take a reading, go back to sleep)
This example C program incorporates elements of all the above requirements. To get this setup up and running you will need the following:
  • a PIC 16F627A, 16F628A or equivalent. Other PICs of with similar functionality may work but a re-compile many be required. If ordering PICs make sure you order it in a plastic DIP package.
  • a prototyping breadboard
  • some way to interface logic voltage level serial IO to a computer (a discussion on this in a moment)
  • a PIC programmer (eg PICkit2), ideally configured to program in-circuit
  • one LED, a few resistors and some suitable jumper wires for the breadboard



Interfacing a PIC to a PC using Serial IO

Serial IO is a theoretically a very simple way of communicating with a PIC. Most PICs have hardware support for this protocol and until a few years ago most PCs had one or more serial ports (in the form of a 9 pin D connector RS232 port). Unfortunately even though the PC and the PIC speak the same serial IO ‘language’, they do so at incompatible voltage levels, so a direct connection is not possible. There are hacks involving resistors, but it doesn’t always work. The easiest ‘correct’ solution is to use the MAX232 chip which converts logic to RS232 voltage levels.

Leaving aside the voltage level problem, a more serious problem is that RS232 ports are increasingly rare having been replaced by the substantially faster and more flexible USB. Interfacing directly with USB is not trivial without the use of a support chipset such as the FTDI FT232RL.

So there are two approaches to achieve PC connectivity:
  1. Use USB <--> RS232 adapter (about €25) at the PC end and a MAX232 for voltage conversion at the PIC end
  2. Bypass the whole RS232 voltage level problem by using a Bus Pirate to act as a bridging device. The Bus Pirate costs about €25 and will avoid the complication of adding a MAX232 to your project.
I opted for approach #2 using a Bus Pirate.

Configuring a Bus Pirate to act as serial IO bridge

Connect to the Bus Pirate with a terminal emulator (eg minicom on Linux). At the Bus Pirate prompt enter:
m(return) (select mode) 
3(return)  (select UART mode) 
6(return)  (select baud rate of 19200)
(return) (select 8 bits, no parity - default)
(return) (select 1 stop bit - default)
(return) (select idle 1 - default)
(return) (select open drain, high = high impedance, low = GND)

Because we have chosen high to be represented by a high impedance state, a pull up resistor (eg 22K) to Vdd is required (see schematic).

Connect the Bus Pirate ground to Vss/GND, MISO to PIC TX and MOSI to PIC RX. Finally select the bridge mode macro: (1) In response to message “UART bridge. Space continues, anything else exits” press the SPACE key. Note: you can only exit this mode by power cycling the device.

Programming the PIC

A socket programmer can be used such as the Velleman K8048. But for development work the constant plugging and unplugging of the chip is a colossal waste of time and will quickly damage the chip’s pins.

Configuring a PICkit 2 as in in-circuit programmer is a far better option. In this configuration the 5 pin ISCP port (Vpp, Vdd, GND, PGD, PGC) is connected to corresponding pins on the PIC. If the Vpp pin is also used in your application (it can double as RA5 or MCLR) a protective diode should be utilized to protect your application circuitry from the ~ 13V programming voltage.

The ISCP port also optionally provides power to the circuit but there are current limits to be aware of: 25mA in the case of a PICkit2. Exceeding that may cause the USB port to switch off (although permanent damage is unlikely). Please refer to chapter 3 of the “PICkit™ 2 Programmer and ICSP™” guide downloadable from http://www.microchip.com for details on the operation of the ICSP port.

Loading the program from Linux

Download the PICkit2 command line Linux utility from http://www.microchip.com/


pk2cmd -PPIC16F627A -FSIODemo.hex -M -T -R

You can power off the PIC at any time with this command:

pk2cmd -PPIC16F627A -W

and power it back up with:

pk2cmd -PPIC16F627A -T -R


Loading the program from Windows

From PICkit 2 Programmer (free download from http://www.microchip.com) choose File -> Import Hex. Then click the “Write” button and if all goes well the status window should turn green with the message “Programming Successful”.


 

Running the program

After successful upload of the firmware to the PIC, all going will you should see a welcome message “Demo PIC application. Version 1.0” on the serial port. You can now enter commands at the prompt. These commands are implemented in the execCmd() function. Additional commands can be added as programming space allows. A command comprises a single letter (upper or lower case) followed by optional arguments and terminated by the RETURN key. 




Command summary:


L no params Query LED state. Will return “ON” or “OFF”.
L ‘0’ or ‘1’ Turn LED on or off respectively.
L ‘t’ Toggle LED state
R no params Reset device
T no params Return toc timer
Z no params Put device to sleep. Will confirm with “SLEEP” message. Hit any key to wake device. Will display “WAKE” message on waking.

Command error codes:

On successful command execution an “OK” message will be displayed. If an error is encountered “ERR” followed by an error number will be displayed.

ERR 2 Unrecognized command
ERR 3 Bad command parameters

Compiling the C code

As it stands this application doesn’t do anything very useful. You will need to edit the C to customize for your application.

You will need the MPLAB IDE from Microchip (a free download at http://www.microchip.com). This is a Windows application, but will run under Wine emulation on Linux (just run the installer in Wine).

Download the ZIP archive from http://wombat.ie/software/electronics/PIC/ and unpack it into a directory.

In MPLAB chooise Project -> Open... from the menu and navigate to the directory into which you unpacked the ZIP archive. Choose the SIODemo.mcp project.

Select Configure -> Select Device... and ensure you have the correct target chip selected.

Key CTRL-F10 (or Project -> Project Rebuild from the menu) to rebuild everything.

If all goes well you should have SIODemo.hex in the same directory. Upload the HEX file to the device with the programmer.


The Code

The code and compiled HEX file can be downloaded from http://wombat.ie/software/electronics/PIC/

/**
 * Example of a serial IO shell for a PIC.
 * Rev 27 June 2010.
 * (c) 2010 Joe Desbonnet, jdesbonnet@gmail.com.
 * Distributed under the "Simplified BSD License".
 *
 * This example is intended as a starting point for writing low power PIC apps
 * that need to be controlled over a serial port.
 *
 * This program will listen on the PIC's serial port for commands sent from a 
 * human user via a terminal emulator or commands issued from another application. 
 * In this example commands are L, L0, L1, Lt (LED status, off, on, toggle 
 * respectively, R (reset), T (get sleep clock 'tocs'), Z (go to sleep). See 
 * comments at execCmd() function for more details.
 *
 * During sleep mode the device uses very little power (micro watts) and can 
 * stay powered on 2 x AAA batteries for months. 
 *
 * Tested with 'Lite' version of HiTech C compiler for PIC on target
 * platform PIC16F627A and 16F628A. This code should be read in together with the
 * PIC datasheet which can be downloaded from http://www.microchip.com
 *
 * This program occupies most of the available space of a 16F627A. However the
 * use of stdio functions and liberal use of printf() accounts for a large portion
 * of the space used. For practical apps long string messages are not required and
 * the use of printf() can often be avoided.
 * 
 * It is assumed that there is a LED on RB3 (pin 9) which must have a limiting
 * resistor of ~ 500 ohms. The serial port RX and TX and connected to pins 
 * 7 and 8 respectively. Also the RX line (pin 7) and INT (pin 6) are tied together
 * to facilitate wake on serial port activity.
 *
 * NB: Do not connect RX and TX directly to RS232 as the voltage levels (typically
 * -15V to -3V for logic 1 and +3V to +15V for logic 0) are not compatible 
 * with PIC logic levels 0/ ~5V.
 *
 * Simplified schematic:
 *
 *          +-----------+
 *          |   PIC     |
 *          |16F627/8(A)|
 *          |           |
 * GND------|Vss     Vdd|------ 2 - 5V
 *          |           |
 *      +---|INT        |
 *      |   |           |
 *  ----+---|RX         |
 *          |           |
 *  --------|TX         |
 *          |           |
 *    +-----|RB3        |
 *    |     +-----------+
 *  500ohm
 *    |
 *   LED
 *    |
 *   GND
 *
 * Not shown: logic <--> RS232 conversion, optional crystal resonator.
 *
 * It is generally recommended that unused input pins are tied either high
 * or low through a ~ 10K resistor. However I have found that if setup correctly
 * (specifically MCLR and LVP are disabled) unused pins can be left floating.
 *
 */

#include <htc.h>
#include "stdio.h"

// Bit manipulation macros
#define BIT(x)   (1 << (x))
#define SETBIT(p,b) (p) |= BIT(b)
#define CLRBIT(p,b) (p) &= ~BIT(b)
#define TOGBIT(p,b) (p) ^= BIT(b)
#define TSTBIT(p,b) (p) & BIT(b) 

/*
Configuration Word:

Oscillator
--------------
HS:    High speed external oscillator
INTIO: Internal oscillator (4MHz)

Remember to set FOSC accordingly.


Watch Dog Timer (WDT)
--------------
WDTEN: Watch dog timer enable
WDDIS: Disable watch dog timer

When enabled a SLEEP operation will resume at the next instruction when the timer 
rolls over.  Otherwise SLEEP can only be interrupted by an external event. It takes 
about 2 seconds for the timer to roll over. A simple low-accuracy sleep clock can 
be made by counting the SLEEP/wake cycles. The device can be awoken after a set 
period of time this way (not implemented in this version). When the WDT is enabled 
a WDT roll over while awake will cause a device reset which we don't want in the
application. So it's important that the WDT is cleared periodically by calling
CLRWDT().

Other features
--------------

Unused features are disabled.

PWRTDIS:   Power up timer disable
BODIS:     Brownout reset disable
UNPROTECT: Code protection disable 
MCLRDIS:   Master Clear disable. Prevents spontaneous resets if MCLR left floating.
LVPDIS:    Low Voltage Programming disable. Prevents problems if RB4/PGM pin left floating.

A note about MCLR and LVP features: if you do enable those features, it is vital 
that you *don't* allow MCRL and PGM pins to float. Tie them high or  low via a 
~ 10K resistor. If you don't you will probably experience spontaneous  resets 
and intermittent failures.

*/

__CONFIG(INTIO & WDTEN & PWRTDIS & BORDIS  & UNPROTECT & MCLRDIS & LVPDIS);

// Oscillator frequency. Set to 4000000UL if using internal oscillator. Else
// set to the external crystal frequency.
//#define FOSC 10000000UL
#define FOSC 4200000UL

// Desired serial port baud rate. In theory any value is possible, but the
// traditional values are 300, 1200, 2400, 4800, 9600, 19200, 38400, 112500
// [Note about max speed, 19200 max with internal 4MHz osc?]
#define BAUD 19200

#define BUF_LEN 8  // Define the length of the command buffer
#define LED_PIN RB3  // Define LED pin.
#define LED_TRIS TRISB3 // Define LED tri-state bit (for configuration to output)


// Function prototypes
unsigned char execCmd();
void prompt();
void crlf();

// Flags shared with ISR. For convenience using an entire 8 bit value to represent
// a single flag. A value 1 being set, 0 being clear. This is wasteful and you may 
// want to use individual bits of a one general flags variable if space becomes tight.
volatile unsigned char sleepMode; // set to 1 if currently SLEEPing as much as possible
volatile unsigned char wakeFlag; // set to 1 by ISR to signal to main loop to wake up
volatile unsigned char cmdFlag;  // set to 1 by ISR to signal to main loop that ENTER was received

volatile unsigned char bufp;  // indicates the position of the next free char in the buffer
char buf[BUF_LEN];     // command buffer

unsigned int toc = 0;    // initialize our simple clock to 0

/**
 * Initialize hardware
 */
void init(void) {

 //
 // Configure serial port
 //

 // Serial Port Baud Rate Generator. Ref sect 12.1
 // "USART Baud Rate Generator (BRG)", page 75 of datasheet.
 SPBRG = (int)( FOSC / (16UL * BAUD) - 1 ); 

 // Receive Status register
 // 1--- ----  SPEN (Serial Port Enable)
 // ---1 ----  CREN (Continuous Receive Enable)  
 RCSTA = 0x90;

 // Transmit Status register
 // --1- ----  TXEN (Transmit Enable)
 // ---- -1--  BRGH (High baurd rate select)
 TXSTA = 0x24;


 //
 // Configure interrupts
 //

 GIE  = 1;      // Enable all unmasked interrupts
 PEIE = 1;      // Enable all unmasked peripheral interrupts
 INTE = 1;      // Enable external interrupt on RB0 (also tied to RX for wake-on-RX)
 RCIE = 1;      // Enable RX interrupt (only works while awake)


 //
 // Configure LED pin as output
 //

 LED_PIN = 0;     // Set to off on latch
 LED_TRIS = 0;     // Set pin as output

}


void main(void){
 unsigned char status;

 init();       // Initialize hardware

 // Display welcome message. NB: use of printf draws in 100s of bytes of library
 // code. Use only if you have to. I recommend not using stdio.h functions and
 // using the using putch() function defined here to output short messages.
 crlf();
 printf ("Demo PIC application. Version 1.0");
 crlf();
 prompt();

 // Main loop
 while (1) {

  // Display wake message if we are awakened from SLEEP
  if ( wakeFlag ) {
   sleepMode = 0;
   wakeFlag = 0;  // reset wakeup flag

   // Display wake message and prompt
   crlf();
   crlf();
   printf ("WAKE");
   crlf();
   prompt();
  }

  // If command entered (chars + ENTER key on serial port) execute command
  if ( cmdFlag ) {

   crlf();

   // Less than 2 chars (including CR) in command buffer is empty commmand.
   if (bufp >= 2) {

    // Execute the command 
    status = execCmd();

    // Display OK  message if successful, error message otherwise.
    if ( status == 0 ) {
     printf ("OK");
    } else {
     printf ("ERR %d", status); // NB: %d pulls in *lots* of lib code
    }
    crlf();

    // If sleepMode set it must mean that we have just been issued
    // a sleep command (as we have to be awake to receive a command).
    // Display a SLEEP message to indicated to the user that the device 
    // is now asleep.
    if (sleepMode) {
     printf ("SLEEP");
     crlf();
    }
   
   }

   // Reset command buffer
   bufp = 0;
   cmdFlag = 0;

   // Display prompt for next command if in wake mode
   if ( ! sleepMode) {
    prompt();
   }

  }

  if (sleepMode) {

   putch ('Z');

   // Can (optionally) disable INT interrupt while awake. 
   // If INT is left running the ISR will be called for every
   // incoming bit transition in the RX line. While this should
   // not cause a problem, it is causing unnecessary CPU activity
   // which generally should be avoided.
   INTE=1;   // Enable INT to facilitate wake on RX
   SLEEP();  // Sleep until INT or Watch Dog Timer expires
   INTE=0;   // INT not needed while awake.

   toc++;   // Update our simple clock

  } else {
   CLRWDT();  // If awake, clear watch dog timer as often as possible
  }

 }

}

/**
 * This function parses the command buffer (buf) after the user hits the 
 * RETURN key and executes whatever command that may have been entered.
 * Commands are letters (upper or lower) followed by optional arguments.
 * 
 * L <RETURN> - Display LED status
 * L0 <RETURN> - LED on
 * L1 <RETURN> - LED off
 * Lt <RETURN> - LED toggle
 * R <RETURN> - Reset device
 * T <RETURN> - Display sleep clock
 * Z <RETURN> - Put device to sleep. Any key will re-awaken.
 * 
 * @return status code. 
 *  0: success
 *  2: unrecognized command
 *  3: bad parameter
 */
unsigned char execCmd() {

 // Get command letter (assume first char is letter) and convert to
 // uppercase using bit manipulation (set bit 5 low)
 char c = buf[0];
 
 CLRBIT (c,5);  // Convert to uppercase by clearing bit 5 to 0.
 

 switch (c) {

  // LED command.
  case 'L':
   if (bufp == 2) {
    printf ("LED is ");
    if (LED_PIN) {
     printf ("ON");
    } else {
     printf ("OFF");
    }
    crlf();
    return 0;
   }
   c = buf[1];
   if (c == '0') {
    LED_PIN = 0;  // LED off
    return 0;   // Return OK
   } 
   if (c == '1') {
    LED_PIN = 1;  // LED on
    return 0;   // Return OK
   }
   if (c == 't') {
    LED_PIN = ! LED_PIN;// LED toggle
    return 0;
   }
   return 3;    // Return bad parameter error
   // break not required

  // Reset command
  case 'R':
   printf ("RESET");  // Indicate to user that device is about to be reset
   crlf();
   asm ("goto 0");   // Jump to addr 0 achieves a reset
   // break not required

  // Sleep timer query
  case 'T':
   printf ("%d",toc);  // WDT takes about 2s to roll over. x2 to get seconds
   crlf();
   return 0;
   // break not required

  case 'Z':
   sleepMode = 1;   // Enable sleep mode. Next iter of main loop will put dev to sleep
   break;

  // Unrecognized command
  default:
   return 2;
 }

 // Return success code
 return 0;
}


/**
 * Interrupt Service Handler (ISR)
 *
 * Any enabled interrupt will cause the CPU to jump to this routine. The C compiler
 * takes care of saving context at the start and restoring it when finished. The 
 * code in the ISR needs to check interrupt status flags to see which interrupt(s)
 * caused the ISR to be invoked. The ISR should be kept as short as possible. In
 * general if work needs to be performed as a result of an interrupt *don't* do it
 * in the ISR. Instead set a flag and have the main loop poll the flag and have it 
 * do the work.
 *
 * Variables modified by ISR:
 *   wakeFlag
 *   bufp
 * 
 * Note variables that are modified here
 * must be declared 'volatile'. This is because an optimising C compiler will
 * make incorrect optimisations based on the false assumption that the
 * value cannot spontaneously change. See this article for a discussion:
 * http://www.embedded.com/story/OEG20010615S0107
 */
interrupt isr () {

 unsigned char c;
 
 // Check if INT interrupt was triggered
 if ( INTF ) {
  if (sleepMode) {
   wakeFlag=1;   // Tell main loop we are awake now
  }
  INTF = 0;    // Clear interrupt flag (else isr will be called again on exit)
 }

 // May have more than one char in the buffer - so need to loop until buffer empty. Else we can have lag (?)
 while (RCIF) {
 
  c = RCREG;    // Read from USART and add to command buffer

  // ESC (ASCII code 27) key causes immediate reset at all times.
  if (c == 27) {
   asm("GOTO 0");  // Jumping to address 0 is how device reset is achieved
  }

  TXREG = c;    // Echo back char if possible

  buf[bufp++] = c;  // Add char to buffer

  // If ENTER or buffer full signal to main loop that we have command
  if (c == '\r' || bufp == BUF_LEN -1) {
   buf[bufp]=0;  // Zero terminate for safety (TODO: this added lots of bytes to code -- can we optimize?)
   cmdFlag = 1;  // tell main loop we have a command entered
   break;    // Exit loop: any further chars in buffer can be ignored
  }

 }

}

/**
 * Send a character to serial port. If buffer is full then wait until space
 * is available. Will wait indefinitely.
 */
void putch(unsigned char c) {
 while ( ! TXIF ) ;   // Loop until TX register available
 TXREG = c;     // Write character to TX register
}


/**
 * Write CR LF (new line) sequence to serial port.
 */
void crlf () {
 putch ('\r');
 putch ('\n');
}
/**
 * Write command prompt to serial port.
 */
void prompt () {
 putch ('>');
 putch (' ');
}


No comments: