Understanding the USART on 8-bit PIC Microcontrollers using XC8

note from Sun Nov 15, 2015

I've been using 8-bit PIC microcontrollers for a while now for various projects. Whenever I need serial communications, I always find myself having to re-learn how everything is set up. With the recent switch to Microchip's XC8 compiler as the preferred compiler, I've been finding myself confused about where to even start. Whereas the Hi-Tech C compiler included an example for using the USART that ended up being the de-facto "lib" of sorts, the new XC8 didn't come with anything to jump-start my serial communications code.

Below are my notes for helping myself understand how to use the USART, plus the header and source file I ended up with. These files are my starting point for any project that use the USART on 8-bit PICs. My latest project was with the 16f628a, so I will be using pin assignments and ports from that chip. Replace with the appropriate pins and ports for your chip.

First thing's first

Pins

First, make sure that you have enough i/o pins. The 16f628 uses RB1 and RB2 for receiving and transmitting serial data, respectively. Make sure that of the 12 i/o pins available, that you have enough after you reserve RB1 and RB2 for serial communications. Keep in mind that RA5 is always input, and if you dedicate pins for inline-circuit programming, it will take up RA5, RB6 and RB7. If you use an external oscillator, you will also lose RA6 and RA7.

Configuration

This is a step that I've often forgotten when I first started working with PIC chips. Incorrect (or not setting at all) configuration bits can cause your program to appear to act sporadically. I now always set the configuration bits to the basic vanilla setting. It's super easy if you use the built-in configuration generator in MPLABX. The only thing I do is set the chip to use the internal oscillator, and use RA6 and RA7 for i/o:

#include <xc.h>

// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.

// CONFIG
#pragma config FOSC = INTOSCIO  // Oscillator Selection bits (INTOSC oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = OFF      // RA5/MCLR/VPP Pin Function Select bit (RA5/MCLR/VPP pin function is digital input, MCLR internally tied to VDD)
#pragma config BOREN = OFF      // Brown-out Detect Enable bit (BOD disabled)
#pragma config LVP = OFF        // Low-Voltage Programming Enable bit (RB4/PGM pin has digital I/O function, HV on MCLR must be used for programming)
#pragma config CPD = OFF        // Data EE Memory Code Protection bit (Data memory code protection off)
#pragma config CP = OFF         // Flash Program Memory Code Protection bit (Code protection off)

Enabling the USART

The two registers that control the USART are RCSTA and TXSTA, which are mnemonics for "receive status" and "transmit status".

The bits map out like this:

Register bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0
RCSTA SPEN RX9 SREN CREN ADEN FERR OERR RX9D
TXSTA CSRC TX9 TXEN SYNC - BRGH TRMT TX9D

 

Setting SPEN = 1 enables the serial port, setting CREN = 1 enables (continuous) reception, and setting TXEN = 1 enables transmitting.

Other settings

For most serial communications (like RS-232 compatible serial comms), you would want asynchronous mode in 8-bit, so most of the other bits will stay 0 for now. So at the moment, our registers look like this:

RCSTA = 0b10010000; // 0x90 (SPEN RX9 SREN CREN ADEN FERR OERR RX9D)
TXSTA = 0b00100000; // 0x20 (CSRC TX9 TXEN SYNC - BRGH TRMT TX9D)

Baud rate genrator

OK, this is the part that I always find confusing. To set the baud rate, you will need to set the SPBRG register to a "divider" value (it divides the timer into correct periods for your desired baud rate). Most PIC chips will have an option for either low speed or high speed baud rate generation for asynchronous communications, as determined by the value set on BRGH (set 1 for high speed; refer to datasheet to see which speed setting supports your desired baud rate ). The equations for them are as follows:

Low speed (BRGH=0):
Baud Rate = Fosc / (64(X+1))

High speed (BRGH=1):
Baud Rate = Fosc / (16(X+1))

Where Fosc is the frequency of the clock and X is the divider that determines the period for the free running timer. The difference is the multiplier, which is 64 for low speed and 16 for high speed. Given a desired baud rate and solving for X, the equation looks like this for determining the divider:

divider = Fosc / (Multiplier * Baud) - 1

Whenever possible, the data sheet says to use high speed baud rate generation, so that's what I use. So for a desired baud rate of 9600, an internal clock of 4Mhz, we plug in the numbers and get this equation:

divider = 4000000 / (16 * 9600)  - 1
divider = 25.042

So we take the nearest integer and use 25. Our registers and SPBRG now look like this:

SPBRG = 25;
RCSTA = 0b10010000; // 0x90 (SPEN RX9 SREN CREN ADEN FERR OERR RX9D)
TXSTA = 0b00100100; // 0x20 (CSRC TX9 TXEN SYNC - BRGH TRMT TX9D)

Accessing the serial port

To read a byte from the serial port, the idea is to wait for register RCIF to become ready and then read the value of RCREG. In the most simple terms you basically do this:

unsigned char my_byte;
while(!RCIF)
    continue;
my_byte = RCREG;

Similarly, to write a byte to the serial port, we wait for TXIF to become ready and assign the value to TXREG:

unsigned char my_byte = 'A';
while(!TXIF)
    continue;
TXREG = my_byte;

Pulling it all together

So, pulling this all together, we will want to create a header file with all of this. We should then implement some functions to access the transmit and receive registers that the stdio library can use. In short, if we define putch(), we'll get printf() functionality to the serial port :) Below are the files I now use for my serial communications projects. They are modified versions of the example files that were originally provided by Hi-Tech C:

usart.h

/* 
 * File:   usart.h
 * Author: Henry Chung
 *
 */

#ifndef _SERIAL_H_
#define _SERIAL_H_

// config communications here
#define BAUD 9600
#define FREQUENCY 4000000L

// refer to data sheet to set these. Typical settings are below
#define MULTIPLIER 16UL // muiltiplier is typically 64UL, 16UL or 4UL
#define HIGH_SPEED 1 // refer to BRGH in datasheet to determine if you need high speed

// not common
#define NINE 0     /* Use 9bit communication? FALSE=8bit */

// Set TX & RX pins. Below are for 16f628; update for your chip
#define RX_PIN TRISB1
#define TX_PIN TRISB2


/*
 * The rest is calculated, so no need to edit below this line
 */

#define DIVIDER ((int)(FREQUENCY/(MULTIPLIER * BAUD) -1))

#if NINE == 1
#define NINE_BITS 0x40
#else
#define NINE_BITS 0
#endif

#if HIGH_SPEED == 1
#define SPEED 0x4
#else
#define SPEED 0
#endif

// basic settings to enable serial
// RCSTA is 0x90: 10010000 (SPEN RX9 SREN CREN ADEN FERR OERR RX9D)
// TXSTA is 0x20: 00100000 (CSRC TX9 TXEN SYNC - BRGH TRMT TX9D)
#define RCSTA_DEFAULT 0x90
#define TXSTA_DEFAULT 0x20

/* Serial initialization */
#define init_comms()\
    RX_PIN = 1;    \
    TX_PIN = 1;          \
    SPBRG = DIVIDER;         \
    RCSTA = (NINE_BITS|RCSTA_DEFAULT);    \
    TXSTA = (SPEED|NINE_BITS|TXSTA_DEFAULT)

void putch(unsigned char);
unsigned char getch(void);
unsigned char getche(void);

#endif

usart.c

#include <xc.h>
#include <stdio.h>
#include "usart.h"

void putch(unsigned char byte) {
    /* output one byte */
    while(!TXIF)    /* set when register is empty */
        continue;
    TXREG = byte;
}

unsigned char getch() {
    /* retrieve one byte */
    while(!RCIF)    /* set when register is not empty */
        continue;
    return RCREG;
}

unsigned char getche(void) {
    unsigned char c;
    putch(c = getch());
    return c;
}

example main.c

/* 
 * File:   main.c
 * Author: henry
 *
 */

#include <stdio.h>
#include <stdlib.h>

#include <xc.h>

// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.

// CONFIG
#pragma config FOSC = INTOSCIO  // Oscillator Selection bits (INTOSC oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = OFF      // RA5/MCLR/VPP Pin Function Select bit (RA5/MCLR/VPP pin function is digital input, MCLR internally tied to VDD)
#pragma config BOREN = OFF      // Brown-out Detect Enable bit (BOD disabled)
#pragma config LVP = OFF        // Low-Voltage Programming Enable bit (RB4/PGM pin has digital I/O function, HV on MCLR must be used for programming)
#pragma config CPD = OFF        // Data EE Memory Code Protection bit (Data memory code protection off)
#pragma config CP = OFF         // Flash Program Memory Code Protection bit (Code protection off)


#include "usart.h"

int main(int argc, char** argv) {
    unsigned char input;

    INTCON=0; //disable interrupts
    CMCON=0b00000111; // configure for IO

    // config for serial comms is set in init_comms
    init_comms();

    for(;;) {
        printf("\rPress a key and I will echo it back\n");
        input = getch();
        printf("\rIt was [%c]\n", input);
    }

    return (EXIT_SUCCESS);
}

Where to get your PICs

I try to get my supplies from the smaller online shops like Sparkfun or Adafruit because as a small business, I believe that one should support other small businesses. Unfortunately, Adafruit doesn't carry the PICs. Sparkfun carried a small handful but it seems like they've dropped the entire line recently. I get them now on the cheap(ish) at Jameco. Their site even has the specsheets on the product page. Of course, everything is available on the Microchip site as well

Happy pic'ing!

tags: #pic #microcontroller #16f628a #MPLABX #XC8 #USART #UART

PIC Microcontrollers

The recent transition to MPLABX and the XC8 C compilers had all of a sudden made most books obsolete. Here is one book that is pretty recent and includes examples using MPLABX and XC8.

The Evil Genius book is a bit out-of-date, but I still like it and I refer to it for ideas on how to solve some common problems. It provides code in both assembly and C.

CanaKit CK1301 PIC Programmer

I've been using the CK1301 programmer from CanaKit for a while now. It's a great device that is PICKit2 compatible. It has a nice ZIF socket and also header pins for ICSP.