« Back to home

Serial Programming - Part 2 (PIC16F628A)

This is the third 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; previous posts are ...

  1. Microcontrollers - The PIC16F628A
  2. Serial Programming - Part 1 (PIC16F628A)

    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.

Introduction

In this, the second part of the serial programming tutorial, we implement the code needed to read data sent via serial connection from a remote system. Rather than depend on a polling loop we will use the interrupt functionality provided by the USART to deal with input as soon as it comes in. Before we get into the actual serial code we will look at what interrupts are, how they behave specifically on the PIC processor and how to handle them. Once we have covered those basics we will look specifically at the serial input interrupt and how to write serial handler code that makes use of interrupts.

The code for this post can be ``` ;============================================================================ ; Serial communication for PIC16F628A chips. ;---------------------------------------------------------------------------- ; 12-OCT-2012 shaneg ; ; A simple test of serial reception 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 ;----------------------------------------------------------------------------

; State information for interrupt handler (in shared memory area) SAVEDW equ 0x70 SAVEDSTATUS equ 0x71

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

; This routine handles interrupt driven RS232 input ; ; It reads the value received, increments it by one and sends it back out. ; Errors such as framing and overrun and handled and quitely ignored. rs232input: banksel RCSTA btfsc RCSTA, OERR ; Check for overrun error goto rs232overrun btfsc RCSTA, FERR ; Check for framing error goto rs232framing banksel RCREG incf RCREG, W ; Read the received value and increment call sendchar ; Send out the incremented value goto rs232done ; All finished rs232overrun: ; Indicates that data has come in faster than we can process ; it. We need to reset the receiver circuit and flush the ; (3 byte) input buffer of the USART. banksel RCSTA bcf RCSTA, CREN bsf RCSTA, CREN banksel RCREG movf RCREG, W ; Make sure all buffers are empty movf RCREG, W movf RCREG, W goto rs232done rs232framing: ; Indicates that a malformed packet has been received (invalid ; stop bit count, 9 data bits, etc). Simply read and discard ; what we have in the receive register. banksel RCREG movf RCREG, W rs232_done: return

; This is the main interrupt service routine. It delegates to support functions ; depending on the interrupt source detected. interrupt: ; Save current state movwf SAVEDW ; Save W register first movf STATUS, W ; Move the STATUS register to W movwf SAVEDSTATUS ; And save that as well ; Process and clear outstanding interrupts banksel PIR1 btfsc PIR1, RCIF ; Check for RX interrupt call rs232input ; Restore state movf SAVEDSTATUS, W movwf STATUS swapf SAVEDW, F ; Loads saved W without changing STATUS swapf SAVEDW, W 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 bsf RCSTA, CREN ; Enable data reception ; Enable global interrupts banksel INTCON bsf INTCON, GIE bsf INTCON, PEIE ; Enable interrupts on incoming data banksel PIE1 bsf PIE1, RCIE mainloop: ; Main program loop (runs forever) goto mainloop

             end ```

and the MPLAB X project can be downloaded from here.

What are Interrupts?

An interrupt behaves pretty much the way you expect it would - it interrupts the current sequence of events to allow a separate (generally small) sequence of events to be performed before returning to the point of interruption.

Let's look at a simplistic example: You are watching a DVD when the phone rings, you interrupt your DVD (pressing pause) and answer the phone. Once you have responded to the phone call you sit back down and resume the DVD from where you had paused it. Even though you had to stop watching the DVD for a while you haven't missed anything - you resumed from the point you were at when the phone rang.

In a lot of cases the interrupting event will have no effect on the task you were involved in at all - the phone call was from a friend who wanted to know the name of the Thai restuarant you always rave about - you look it up and give it to him. The process doesn't change the way you look at the DVD or your expectations of it.

In other cases the interrupt may change the way you handle the task you were involved in - your friend rings to tell you 'Hey, that film Titanic you rented? The boat sinks!'. In this case you may stop watching the DVD and move on to another task (because now you know the ending) or you may continue to watch but be paying more attention to indications of the ending.

In computing terms interrupts are generally used to indicate conditions that are time critical - they must be dealt with immediately; failure to do so may result in catastrophic failure or loss of data.

Interrupt Handling on the PIC

Interrupt handling is software controlled on the PIC - there is a global interrupt enable flag (the GIE bit of the INTCON register) and individual enable/disable bits for all 10 of the possible interrupt sources on the processor.

When an interrupt occurs on the PIC (and both the GIE and the individual interrupt enable bits are set) the following events occur:

  1. The current instruction is completed.
  2. The program counter (the address of the next instruction) is saved on the stack.
  3. The value 0x0004 is loaded into the program counter.
  4. Processing continues with the instruction at the new value of the program counter.

    An interrupt handler relinquishes control using the retfie (return from interrupt) instruction. When this instruction is executed the value on the top of the stack is loaded into the program counter and processing continues. It is important to note that between the interrupt being generated and the completion of the retfie instruction no other interrupts can be generated - the interrupt routine itself will not be interrupted.

    Another important thing to note is that an instruction will never be interrupted partway through - the instruction is allowed to complete before any interrupt routine is executed.

    As you can see there is only a single piece of state saved for you - the address of the instruction to return to after the interrupt has been processed. Your interrupt handler is going to modify the values of a number of registers and some of these (the W and STATUS registers at a minimum) are going to be critical to you main program - it is not expecting them to be changed between instructions to values it did not specify itself. It is the responsibility of the interrupt handler code to ensure that these values are the same on exit as they were when the routine was entered. The following code examples show how to achieve this.

    To start with we need to have a location to save the W and STATUS registers without actually modifying either of those registers. Because we don't know what bank of registers is selected when we enter the routine we need to have locations to save the current values that can be accessed regardless of the current bank (changing the bank would involve changing the STATUS register and losing the value that is currently stored). This means the storage locations must be in the shared registers which are available in all banks (on the 16F628A these are in the range 0x70 to 0x7F):

 Saving the state is straightforward. We save the **W** register first (which then allows us to use it), followed by the **STATUS** register:
                 ; Save current state                     movwf   SAVED_W          ; Save W register first                     movf    STATUS, W        ; Move the STATUS register to W                     movwf   SAVED_STATUS     ; And save that as well

From this point on we are free to perform whatever operations we want to knowing that we have a copy of the previous codes state to fall back on when we are finished. Once the interrupt handler has finished doing what it needs to the state should be restored prior to executing the retfie instruction and restoring control to the main program. This is done as follows:

 An important thing to note here is how the **W** register is restored. If we were to use a **movf** instruction to load the saved **W** into the actual **W** we could potentially change the value of the **STATUS** register (the zero flag for example if the stored **W** had a value of 0). A slight trick is used here - the **swapf** instruction does not modify **STATUS**. This instruction swaps the high 4 bits of the register with the low 4 bits of the same register - the first **swapf** instruction swaps the value *in place* - only the contents of the SAVED_W memory location are modified. The second **swapf** swaps the values back but puts the result in the **W** register thus restoring the original value of the register without modifying **STATUS**.

 Now we can save and restore the essential state of the original program we can implement our actual interrupt handling code between those two sets of instructions. As was mentioned above the PIC16F628A has 10 sources of interrupts yet it only has a single interrupt vector. How do we know which interrupt source triggered the interrupt code? The answer is simple - each interrupt source has an associated flag bit associated with it that indicates if that interrupt source is currently active. When a specific interrupt has been handled you must clear the interrupt flag associated with it to avoid the same interrupt being retriggered after it has already been handled.

 It is possible to have multiple interrupts to handle in a single execution of the interrupt handler routine. You can simply handle one at a time (if you don't clear the interrupt source flag the interrupt will be regenerated at the next cycle and you can handle it then) or you can handle multiple interrupt sources in a single run of the handler.


# Software for PIC Serial Input

 The code for this posts extends the code we developed for [the last one](/serial-programming-part-1-pic16f628a/). You can see the full source lists ``` ;============================================================================ ; Serial communication for PIC16F628A chips. ;---------------------------------------------------------------------------- ; 12-OCT-2012 shaneg ; ; A simple test of serial reception 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 ;----------------------------------------------------------------------------

 ; State information for interrupt handler (in shared memory area) SAVED_W         equ     0x70 SAVED_STATUS    equ     0x71

 ;---------------------------------------------------------------------------- ; 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 send_char:                 banksel TXSTA wait_to_send:                 btfss   TXSTA, TRMT                 goto    wait_to_send                 ; Load the output register                 banksel TXREG                 movwf   TXREG                 return

 ;---------------------------------------------------------------------------- ; Interrupt handlers ;----------------------------------------------------------------------------

 ; This routine handles interrupt driven RS232 input ; ; It reads the value received, increments it by one and sends it back out. ; Errors such as framing and overrun and handled and quitely ignored. rs232_input:                 banksel RCSTA                 btfsc   RCSTA, OERR      ; Check for overrun error                 goto    rs232_overrun                 btfsc   RCSTA, FERR      ; Check for framing error                 goto    rs232_framing                 banksel RCREG                 incf    RCREG, W         ; Read the received value and increment                 call    send_char        ; Send out the incremented value                 goto    rs232_done       ; All finished rs232_overrun:                 ; Indicates that data has come in faster than we can process                 ; it. We need to reset the receiver circuit and flush the                 ; (3 byte) input buffer of the USART.                 banksel RCSTA                 bcf     RCSTA, CREN                 bsf     RCSTA, CREN                 banksel RCREG                 movf    RCREG, W         ; Make sure all buffers are empty                 movf    RCREG, W                 movf    RCREG, W                 goto    rs232_done rs232_framing:                 ; Indicates that a malformed packet has been received (invalid                 ; stop bit count, 9 data bits, etc). Simply read and discard                 ; what we have in the receive register.                 banksel RCREG                 movf    RCREG, W rs232_done:                 return

 ; This is the main interrupt service routine. It delegates to support functions ; depending on the interrupt source detected. interrupt:                 ; Save current state                 movwf   SAVED_W          ; Save W register first                 movf    STATUS, W        ; Move the STATUS register to W                 movwf   SAVED_STATUS     ; And save that as well                 ; Process and clear outstanding interrupts                 banksel PIR1                 btfsc   PIR1, RCIF       ; Check for RX interrupt                 call    rs232_input                 ; Restore state                 movf    SAVED_STATUS, W                 movwf   STATUS                 swapf   SAVED_W, F ; Loads saved W without changing STATUS                 swapf   SAVED_W, W                 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                 bsf     RCSTA, CREN ; Enable data reception                 ; Enable global interrupts                 banksel INTCON                 bsf     INTCON, GIE                 bsf     INTCON, PEIE                 ; Enable interrupts on incoming data                 banksel PIE1                 bsf     PIE1, RCIE main_loop:                 ; Main program loop (runs forever)                 goto    main_loop

                 end ``` , once again it is worth keeping open in another browser tab for reference.

 Initialisating the USART uses almost exactly the same code that we used last time with a few extra configuration bits set to enable data reception and to enable interrupts. This code is again placed at the start of the program block before entering the main loop:
 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                     bsf     RCSTA, CREN ; Enable data reception                     ; Enable global interrupts                     banksel INTCON                     bsf     INTCON, GIE                     bsf     INTCON, PEIE                     ; Enable interrupts on incoming data                     banksel PIE1                     bsf     PIE1, RCIE

The new functionality begins at the 'bsf RCSTA, CREN' instruction which enables the reciever part of the USART. The next few statements enable interrupt generation. The main control register for interrupts is the INTCON register - we set the GIE bit which globally enables interrupt generation. If this bit is not set the interrupt routine will never be called regardless of which other flags are set.

Interrupt Logic

Next we set the PEIE bit which enables interrupts generated by on chip periphials (this includes timers, the USART and comparators). This allows interrupts generated by the periphials to invoke the interrupt handler routine. The image to the left shows the interrupt logic. You can see that all periphials capable of generating interrupts have two flags associated with that functionality - an enable flag (with the suffix IE) which you must set in software to enable interrupts for that source and an interrupt flag (with the suffix IF) which is controlled by the periphial itself and set when the interrupt condition is met. In the case of the USART receiver module these flags are RCIE (in the PIE1 register) and RCIF (in the PIR1 register). Our last piece of setup code ensures the RCIE bit is set so we will get interrupts when new data is read on the serial port.

Next we need to implement the body of the interrupt handler between the state save and state restore code described above. In this case it's very simple - we test the value of the RCIF flag to see if the interrupt was generated by the USART receiver and, if so, call a routine to handle that interrupt for us as shown below:

 Using this model we can handle multiple interrupt sources in the interrupt handler very easily - simply test the appropriate bit for the interrupt source and invoke a function designed specifically to handle it if set. The result is much more readable code than if you tried to implement everything in a single block.

 The function to handle the USART interrupt is as follows:
 ; This routine handles interrupt driven RS232 input     ;     ; It reads the value received, increments it by one and sends it back out.     ; Errors such as framing and overrun and handled and quitely ignored.     rs232_input:                     banksel RCSTA                     btfsc   RCSTA, OERR      ; Check for overrun error                     goto    rs232_overrun                     btfsc   RCSTA, FERR      ; Check for framing error                     goto    rs232_framing                     banksel RCREG                     incf    RCREG, W         ; Read the received value and increment                     call    send_char        ; Send out the incremented value                     goto    rs232_done       ; All finished

The first thing we do is check for any error conditions. These are indicated in the RCSTA register by the bits OERR (for overrun errors) and FERR (for framing errors). If either of these states is detected we jump to the appropriate code to handle them. When no errors are indicated we simply read the received value from the RCREG register (which has the side effect of clearing the RCIF flag for us - it will be set again when new data has arrived), increment it by one and use the sendchar function defined in the last post to send it out. The rs232done label simply defines a common exit point for the function, it just executes the return instruction to return control back to the caller.

The USART receiver is capable of holding three incoming bytes - one that is in the process of being received, and a two byte buffer for data that has already been accepted. If the two byte buffer is already full when the third byte has been completed an overrun error will be flagged. This indicates that the data is coming in faster than we are processing it. To clear the error we must reset the receiver part of the USART (by clearing the CREN bit and then setting it again) and purge the input buffer. The following code does this for us:

 If the incoming data cannot be interpreted as a valid *frame* (invalid number of stop or data bits for example) a framing error is flagged. This usually indicates that the sender and receiver are not set to the same communication parameters. In this case the data in **RCREG** cannot be trusted so we simply read it and ignore it:
 rs232_framing:                     ; Indicates that a malformed packet has been received (invalid                     ; stop bit count, 9 data bits, etc). Simply read and discard                     ; what we have in the receive register.                     banksel RCREG                     movf    RCREG, W

Our main program loop remains the same - we simply sit in an empty loop consuming clock cycles.

Testing the Software

Try as I might I was unable to simulate serial input using the MPLAB X environment, as far as I can tell that functionality is simply unsupported for the 16F628A chip. Other alternatives are to use an older version of the MPLAB software (which is unfortunately only available for Windows) or to use the open source gpsim which is available for both Linux and Windows. In this case I used MPLAB v8.86 which seems to be the last release of MPLAB prior to migrating to MPLAB X. The screenshots and descriptions in this section all relate to that software.

An Appeal to Readers If anyone knows how to simulate serial input on mid-range PIC chips such as the 16F628A using MPLAB X could you please place some pointers in the comments? So far I have found a number of posts from people with the same problem but no actual solutions.

Once you have downloaded and installed the MPLAB software (the default install options are fine) you will need to download and extract the project files for this post (these are still in MPLAB X format for consistancy). To create and configure a project in MPLAB 8 format you need to perform the following steps:

Creating a Project - Pt 1

  1. Create a new directory to hold the project (I used the directory 'C:/working/picserial2'). Copy the files 'picserial2.asm' and 'picserial2.sim' from the code archive into this directory.
  2. Start MPLAB and select Project | New ... from the menu. Enter the project name and the directory you created in step 1.
  3. Select Project | Add Files to Project ... from the menu and add the 'picserial2.asm' file.
  4. Select Configure | Select Device ... from the menu and ensure the device type is set to PIC16F628A.
  5. Select Debugger | Select Tool | MPLAB SIM from the menu to enable the built in simulator.
  6. Select Debugger | Settings from the menu. In the dialog that appears change the Processor Frequency to 4MHz. Switch to the Uart1 IO tab and enable the UART.
  7. In the Input File field browse to the location of the 'picserial2.sim' file you copied from the code archive and select it. Select Rewind Input and set the output to Window.
  8. Click the Build All button in the toolbar. A dialog will appear asking if you want Absolute or Relocatable code - select Absolute.

    Creating a Project - Pt 2

    If all went well your project should have been built and an Output window will have opened. I'm not going to go into a lot of detail about MPLAB 8 as future posts will continue to use MPLAB X (once we've verified that the serial input code is working we can work around the inability to simulate it). I've provided some screenshots in the images above and to the right to help you set up the project - please refer to them if the instructions are not clear.

    The 'picserial2.sim' file that was provided is a stimulus file that specifies data to be sent to the simulated serial port. This file waits for 1 second and then sends the character sequence 'Hello World!' followed by the TAB character (ASCII 9) and then repeats the process endlessly.

    There is a random delay between each character to ensure a more realistic simulation. As provided the delay is between 5 and 20 milliseconds - if the delay is less than around 1.5 ms you will start to get overflow errors (and characters will be dropped), the file format is fairly straight forward - you can play around with the settings in it to see what the response is.

    To run the program in the simulator simply click the Run button in the toolbar (the small green 'Play' icon) and the output will be displayed in the Output window. Select the SIM Uart1 tab and you will see the output we are generating. In this case you should see the string 'Ifmmp!Xpsme"' repeated over and over with one iteration per line - this is the input text ('Hello World!') shifted up one character (the TAB character becomes a line feed - ASCII 10 - forcing a new line at each iteration) which is exactly what we expect.

What Comes Next?

This post concludes the software part of serial programming - our next step is to build and test the hardware. The next post will look at the electrical characteristics of RS232 interfaces and build up a circuit that can be connected to a standard PC for testing. Both the code for this article and the previous one will be tested on the circuit to make sure our real world operation matches the simulation.