« Back to home

Serial Programming - Part 1 (PIC16F628A)

This is the second 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 (here is the previous article). 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.


My apologies for this post being so late - a combination of illness and personal commitments contributed to the delay. It turns out there is a lot of ground to cover for this topic so I have split the very unwieldy first draft of the post into three separate ones that will appear on the site over the week. Here is what we will cover in the different parts:

  1. This post. Covers the basics of serial communications, introduces the serial periphial on the PIC, implements code to send data via RS232 and walks through using the integrated simulator in MPLAB/X to test the operation of the software.
  2. Introduces interrupt handling on the PIC and modifies the code to handle interrupt driven serial input. Once again the code is tested on the simulator to verify its operation.
  3. Covers the electrical characteristics of RS232 and provides the circuitry for RS232 communication with generic devices. This also covers the special case of communicating with the Raspberry Pi via RS232.

    I've also started including full MPLABX projects with the posts - you can find this one here (older posts have been updated with the appropriate links as well). So without further ado let's move on to serial programming on the PIC ...

Serial communciations and RS232

Two (or more) devices can exchange data with each other using either parallel or serial communication. In the parallel model all bits are communicated at the same time over a number of connecting wires (think of an old style printer port with 25 pins - 8 of those pins provided the data while the rest provided handshaking and power connections). With serial communication data is exchanged over a single wire - one bit at a time. Often there is one wire per direction (so one wire to transmit from device A to device B and another to transmit from device B to device A).

The speed at which data is sent on a serial link is measured in bits per second (bps, commonly referred to as the baud rate). Note that there is not necessarily a direct correlation between the baud rate and the number of bytes that are transmitted per second. Although it would seem that a 9600 baud would result in 1200 bytes per second (9600 / 8) there may be additional framing bits sent with each byte (start, stop and parity bits for example) as well as a small delay between each byte. The resulting byte rate will be slightly lower as a result of this.

Serial communications can be further broken down into synchronous and asynchronous methods. In a synchronous connection there will be an extra signal that indicates when data is valid - a clock signal. This allows the sender to indicate when the data line contains valid information (and allows for variable rate transfers). With asynchronous communication there is no such signal - both sides of the communication channel must already know the expected transfer speed.

The RS232 specification is a common asynchronous data transfer protocol. Although the data itself is transmitted asynchronously the full specification allows for some synchronisation (indications that the receiver is prepared to accept data, the sender has data available to transmit and that a device is connected at the other end). The synchronisation features are optional for RS232 (they are usually indicated as 'Flow Control' options in communications programs). Note that the Flow Control options do not change the speed (the baud rate) of the connection, they simply allow the partners to decide when a byte can be received

For minimal bidirectional communication an RS232 connection requires a reciever channel and a transmission channel at each end. These are defined as Tx (the transmitter) and Rx (the receiver). For a one to one communication connection the Tx at one end needs to be connected to the Rx at the other end and vice versa.

In this case we are going to use the PIC to provide an RS232 (asynchronous) connection with a transfer rate of 9600 baud. There will be no flow control, no parity checking and a single stop bit. This is normally represented as 9600 N81 in most communications software.


The 16F628A version of the PIC has an onboard USART or Universal Synchronous/Asynchronous Receiver/Transmitter) which does much of the heavy lifting for us in terms of serial communication. This particular USART can be used for protocols other than RS232 such as I2C or SPI but we are only going to examine the RS232 functionality in this post.

There are three main registers that control the function of the USART - TXSTA (Transmit Status and Control Register), RCSTA (Receive Status and Control Register) and SPBRG (Serial Protocol Baud Rate Generator). The first two registers are comprised of flag bits that determine the overall operation of the USART, the last register (SPBRG) contains a value that controls the bit rate of serial communications.

Bit rate generation uses an internal clock/counter and is dependant on the clock rate applied to the chip. We are using the internal 4MHz oscillator and all our calculations are based on that value. The datasheet for the 16F682 provides a handy set of lookup tables to determine the appropriate baud rate settings for a given system clock speed (these start at around page 75).

Reading and writing data over the serial connection is achieved by writing data to TXREG (for data to be transmitted) and reading from RCREG (for data that has been received). To determine if data can be sent or received requires checking individual bits in the control registers.

The Software

The full source listing can be ``` ;============================================================================ ; Serial communication for PIC16F628A chips. ;---------------------------------------------------------------------------- ; 09-OCT-2012 shaneg ; ; A simple test of serial transmission using the USART. ;============================================================================

             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 ;----------------------------------------------------------------------------

; Constants CHARBASE equ 0x41 ; First character (ASCII for 'A') CHARCOUNT equ 0x1A ; Number of characters (26: 'A' thru 'Z') ENDOFLINE equ 0x0A ; End of line character

; Registers CHAR_CURRENT equ 0x20 ; Index of current character

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

             ; Reset vector                 org     0x0000                 goto    program

             ; Interrupt vector                 org     0x0004                 goto    interrupt

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

; This function sends a single ASCII character (contained in W) ; ; The function waits until the output register is available by testing the ; 'TRMT' bit in the 'TXSTA' register sendchar: banksel TXSTA waittosend: btfss TXSTA, TRMT goto waitto_send ; Load the output register banksel TXREG movwf TXREG 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 0x06 ; Set RB1/RB2 to 1 for USART operation movwf TRISB ; Set baud rate to 9600 movlw 0x19 ; 25 decimal movwf SPBRG ; Now configure the transmitter banksel TXSTA bcf TXSTA, SYNC ; Enable asynchronous mode bsf TXSTA, BRGH ; Enable 'high speed' mode bsf TXSTA, TXEN ; Enable transmission ; And the receiver banksel RCSTA bsf RCSTA, SPEN ; Enable the serial functionality ; Set up our variables banksel CHARCURRENT movlw 0x00 movwf CHARCURRENT mainloop: ; Send the current character banksel CHARCURRENT movlw CHARBASE addwf CHARCURRENT, W call sendchar ; Move to the next character banksel CHARCURRENT incf CHARCURRENT, F movlw CHARCOUNT subwf CHARCURRENT, W btfss STATUS, Z goto mainloop ; Roll characters around (return to 0 offset) movlw 0x00 movwf CHARCURRENT ; Send a carriage return movlw ENDOFLINE call sendchar ; Main program loop (runs forever) goto main_loop

             end ``` . I'll go through the important parts of the source (with examples) here but it is probably worth opening the listing in another tab or window for reference. In this post we are simply going to set up the USART to enable sending at 9600 baud and then repeatedly send upper case letters from 'A' to 'Z'. Not very interesting but it's enough to show how it all works.

The first thing we need to do is configure the IO pins we will be using and set up the USART. We do this at the start of the program as shown below ...

 The IO pins **RB1** and **RB2** serve the **RX** and **TX** functions respectively. The datasheet for the PIC indicates that both of these pins should be configured as inputs by setting the appropriate bits in the **TRISB** register to '1'. The baud rate is configured according to the look up tables in the datasheet, when running at 4MHz this is considered a *high speed* so we have to set the **BRGH** bit of **TXSTA** (which is done a few instructions later when we configure that register) as well as loading the counter value (25 decimal in this case) into **SPBRG** here.

 We then configure the USART by setting asynchronous mode (clearing the **SYNC** bit of **TXSTA**), enabling high speed mode (see above) and enabling transmission (setting the **TXEN** bit). The only flag we are interested at the moment for the receiver is **SPEN** which globally enables the USART.

 To send a byte of data we simply have to load the value you wish to send into the **TXREG** register. The USART will then send a single start bit, the 8 bits of data we loaded into **TXREG** and then a single stop bit (the *start* and *stop* bits are used to frame the data so the receiver can interpret it). At 9600 baud though each bit takes 1/9600 seconds to transmit so it takes around 1ms to send the entire frame. With the PIC running at 4MHz this means that around 1000 instruction cycles pass before the buffer can be loaded with the next character. Rather than force us to use a manual timing loop (as we did in the previous article) the PIC provides a flag bit (the **TRMT** bit) in the **TXSTA** register that indicates when the buffer is free. The helper function defined below waits until the buffer is ready to accept more data before loading the current contents of the **W** register into the transmission buffer for sending. There is nothing stopping you from loading a new value in the transmission register before it is ready but this will cause the PIC to start sending the new value immediately before the old value is completely transmitted - you simply get garbled communications.
 ; This function sends a single ASCII character (contained in W)     ;     ; The function waits until the output register is available by testing the     ; 'TRMT' bit in the 'TXSTA' register     send_char:                     banksel TXSTA     wait_to_send:                     btfss   TXSTA, TRMT                     goto    wait_to_send                     ; Load the output register                     banksel TXREG                     movwf   TXREG                     return

The btfss instruction above is one of the two conditional branching instructions supported by the PIC (essentially an IF THEN statement). The acronym stands for Bit Test File, Skip if Set (the other is btfsc - Bit Test File, Skip if Clear). The first parameter to the instruction is the file register to operated on and the second parameter is the bit number (0 to 7) to test. If the test passes (ie: the specified bit is set or clear depending on the instruction) the very next instruction will not be executed (it is skipped). The PIC assembler helpfully defines constants for all the special bits in the system registers for us so the code above will continue to loop to the wait_to_send label until the TRMT flag is set.

Our test software is going to send all 26 upper case ASCII letters followed by a newline character and then repeat the sequence endlessly. We need some constants to define the main values we are going to work with, we do this at the top of the file ...

 We also need a memory location to store the index of the next character to send, we'll use the first free general purpose register in bank 0 for that ...
 ; Registers     CHAR_CURRENT    equ     0x20        ; Index of current character

This variable contains the character offset, not the character itself. We add this to the CHAR_BASE value to get the actual ASCII code prior to sending it. That means we want to start it off at 0, let it count up to 26 and then reset it to zero again. The next block of code does exactly that ...

``` movlw 0x00 movwf CHARCURRENT mainloop: ; Send the current character banksel CHARCURRENT movlw CHARBASE addwf CHARCURRENT, W call sendchar ; Move to the next character banksel CHARCURRENT incf CHARCURRENT, F movlw CHARCOUNT subwf CHARCURRENT, W btfss STATUS, Z goto mainloop ; Roll characters around (return to 0 offset) movlw 0x00 movwf CHARCURRENT ; Send a carriage return movlw ENDOFLINE call sendchar ; Main program loop (runs forever) goto mainloop

To test for roll over we load W with the number of characters in the sequence (CHARCOUNT) and subtract that from the current character index (CHARCURRENT) making sure we put the result in W rather than the CHARCURRENT register. If the result of that operation is zero (so CHARCURRENT is equal to CHAR_COUNT) the Z (or zero) flag is set in the STATUS register. If this bit is set (has the value of 1) we skip the next goto instruction and fall through to the roll over code, if the bit is clear the goto instruction is executed and we run through another iteration of the main loop.

When we roll back to 0 we also send an END_OF_LINE character so each sequence of the alphabet appears on it's own line. As well as being more visually appealing this also allows use to check that our logic is correct.

Simulating the Software

Debugging code on hardware is very difficult - if it doesn't work there are a large number of potential failure points that need to be tested in order to locate the actual cause of the problem (is the circuit correct? Have the fuse bits been set correctly? Are we running at the right clock frequency? Is there a logic error?). Using a simulator or emulator to verify the operation of the software makes it easier to eliminate software related errors prior to running up the real system in hardware. Simulation allows you to pause the code, inspect the state of the chip (registers, flags, etc) and show the generated output to ensure it matches expectations.

Open a Project

The free MPLAB X IDE from Microchip includes a reasonable simulator for the entire PIC family of processors. I've provided an MPLAB X project for this post which can be downloaded here. Extract the project files (it should be compatible with Windows, OS/X and Linux versions of MPLAB) to a location of your choice and open the project in MPLAB by selecting Open Project from the File menu (shown in the left hand image) and navigating to where you extract the project files to (you need to select the directory name - in this case 'picserial1'). Navigate down to Source Files and open the 'picserial1.asm' file by double clicking on it as shown in the image below.

Viewing Source

Next we need to configure the simulator to match the hardware we will be using. There are three steps to this process (shown in the image to the right) ...

Simulator Settings

  1. Right click on the project name in the tree to the left of the screen (in this case - 'picserial1') and select Properties from the popup menu.
  2. Select the Simulator category from the tree and make sure Oscillator Options is selected in the Option Categories drop down list. We are using the internal 4MHz option so set the Processor Frequency to 4 and make sure MHz is selected as the Frequency In value.
  3. Select Uart1 IO Options from the Option Categories drop down list and make sure it is enabled. Select Window as the output (there is no need to select an output file at this time).

    Start Debugging

    To run the program under the simulator click the Debug Main Project button in the toolbar (shown in the image to the left) and a set of new tabs will open in the bottom right of the MPLABX window (shown in the image below). Select the Output tab and then the UART1 Output tab under that. You should see the alphabet (in uppercase) with one full sequence of the characters 'A' through 'Z' per line.

    Debug Output

    The simulator in MPLABX has a large range of features, far too many to cover here. In upcoming posts I'll introduce new features as they are required - in the meantime it is well worth reading over the help files for MPLAB. Some useful things to look for are watches (which allow you to inspect the content of memory and registers) and breakpoints (which allow you to pause execution at a specific point in the code to inspect the current state of the processor).

What Comes Next?

As I mentioned in the introduction we are not going to build any hardware for this part of the tutorial - our next step is to implement support for reading serial data from a remote host. Once again we will test this under the simulator before moving to the actual hardware implementation.

I hope you enjoyed this post and found it useful. Once again I apologise for the large delay in getting it on the site.