Encoder Front Page
SRS Home | Front Page | Monthly Issue | Index
Google
Search WWW Search seattlerobotics.org

Interrupt driven serial routines

Kevin Ross

kevinro@nwlink.com

Overview

Communicating with a microcontroller is a fairly common and very useful thing to do. One issue that always comes up when using serial communications is how to handle the case where your controller is busy doing something, but you are trying to send data to it. There is also an issue of having to wait while multiple data bytes need to be written.

In the first case, when you are trying to send data to your device, you need to insure that the device is prepared to receive. If your controller is off doing something else, it could miss a byte that you are sending. The HC11 and HC12 SCI ports have the ability to buffer 1 full byte, even while another is being recieved. However, if you miss a byte, it is gone forever.

Another potential sore spot is when you are sending data out the serial port. If you need to send more than one character, then your output routine typically needs to wait for the previous character to be sent. This can be quite time consuming, especially when your CPU is connected at 9600 baud or slower. What would be really nice is if the processor could find out at a later time when the byte was finished being sent.

A potential solution to this issue is the use of interrupt driven serial drivers. This allows your main program to execute its code without missing data on the serial port, or waiting for data to be sent.

In this article, I am going to describe how you can implement interrupt driven serial communications in your own program. I have an example, written in Imagecraft C, which you can download and look at. I have implemented this article for the HC12 and the HC11. In fact, the code between the HC11 and HC12 only differ in the names of the registers. The HC12 has two serial ports, so the register names are set to the appropriate control registers. The only other change is the baud rate divisor on the HC12 is a 16-bit value, where the HC11 uses a pre-determined scalar value.

Click here to download sciint12.c for the HC12

Click here to download sciint11.c for the HC11

Interrupts

An interrupt is a common mechanism on most microprocessors that allows the software designer to execute specific code routines as part of some trigger event. In our case, we are interesting in knowing when the status of the SCI port (Serial Communications Interface) changes by either receiving a byte, or finishes sending a byte. An interrupt is a mechanism by which the running program on the microcontroller can be interrupted, a specialized routine called, then the running program restored.

I have written articles about interrupts before. If you are completely new to the idea of interrupts, it might be good to take a look at http://www.seattlerobotics.org/encoder/sep98/68hc12intr.html which is written for the HC12, but the mechanisms on the HC11 are nearly identical. In fact, even most of the bit assignments in the registers are the same!

The Program

There are three major sections to the source files. I am going to pick on the HC11 version of this file, though as previously mentioned, the HC12 file is nearly identical.

The first section implements a FIFO (First In First Out) queue using a ring buffer. A ring buffer is basically an array of bytes with a couple of pointers that keep track of where the head and the tail of the queue are.

The second section is the interrupt routine that does a majority of the work.

The third section is the 'user program' which uses the previous two sections.

The best way to explain is by checking out the source code. Though there are quite a few comments in the code, I am going to add some additional comments to the source files for the rest of this article.

Remember, the full code is available here:

Click here to download sciint12.c for the HC12

Click here to download sciint11.c for the HC11

The Ring Buffer

The first place I would like to start is taking a quick look at the ring buffer implementation. Its actually pretty straight forward. It has only two functions: rbPutByte() and rbGetByte(). These are general purpose routines which accept a ring buffer in the form of a pointer as an argument. This allows us to use the same routines for both the transmit and receive buffers. Remember that these are FIFO queues. If I call rbPutByte() on a series of bytes A, B, and C, then calling rbGetByte three times will return A, B, then C.

As each byte is added to the ring buffer, it is appended to the tail of the buffer. If the buffer is full, then the byte is not added, and an error is returned. The buffer record keeps track of how many bytes are currently available in the buffer. To make this buffer larger, you need to change the definition of RB_BUF_SIZE.

#include <hc11.h>
#include <stdio.h>
//*******************************************************************
// Some definitions used by the serial port routines. 
//*******************************************************************
#define SCI_OK	0x00
//
// These definitions basically map to the bits in the SCSR2 
// register. They are also used as error codes.
//
#define SCI_FRAMEERR 0x02
#define SCI_NOISE	 0x04
#define SCI_OVERFLOW 0x08
#define SCI_IDLE	 0x10
#define SCI_RDRF	 0x20
#define	SCI_TC		 0x40
#define	SCI_TDRE	 0x80
#define SCI_DATAERROR_MASK (SCI_FRAMEERR+SCI_NOISE)
//*******************************************************************
// A couple of quick utility routines before the program get started!
// The meat of the serial port stuff is father down the program
//*******************************************************************
//
// These first routines create a small ring buffer utility. 
// Acting as a standard queue, the ring buffer uses a small data buffer as 
// the storage area. These have limited storage space, and reuse things in a
// first in, first out fashion. 
//
// To make a larger ring buffer, you should define
// the value RB_BUF_SIZE as something else. It MUST
// be a power of 2
//
#ifndef RB_BUF_SIZE
#define RB_BUF_SIZE	0x08
#endif
#define RB_SIZE_MASK (RB_BUF_SIZE-1)
#define RB_ERROR_OVERFLOW	0x8000
#define RB_ERROR_EMPTY		0x8100
#define RB_OK				0x0
typedef struct _ringbuf
{
	unsigned char ucCount;
	unsigned char ucHead;
	unsigned char ucTail;
	unsigned char aData[RB_BUF_SIZE];
} RingBuf;
#define rbInit(rb) {rb.ucCount = 0 ; rb.ucHead = 0 ; rb.ucTail = 0 ; }
#define rbIsFull(rb) (rb.ucCount >= RB_BUF_SIZE)
#define rbIsEmpty(rb) (rb.ucCount == 0)
#define rbCount(rb) (rb.ucCount)
//
// rbPutByte inserts a byte into the ring buffer.
//
unsigned int rbPutByte(RingBuf *pBuf, unsigned char ucByte)
{
	unsigned int res = RB_OK;
	unsigned char ucTail = pBuf->ucTail;
	unsigned char ucCount = pBuf->ucCount;
	
	if(ucCount >= RB_BUF_SIZE)
	{
		res = RB_ERROR_OVERFLOW;
	}
	else
	{
		pBuf->aData[ucTail] = ucByte;
		pBuf->ucCount = ucCount + 1;
		pBuf->ucTail = (ucTail + 1) & RB_SIZE_MASK;
	}
	return res;
}
//
// rbGetByte retrieves a byte from the ring buffer.
//
	
unsigned int rbGetByte(RingBuf *pBuf)
{
	unsigned char ucCount = pBuf->ucCount;
	unsigned int uReturn = RB_ERROR_EMPTY;
	if(ucCount > 0)
	{
		unsigned char ucHead = pBuf->ucHead;
		uReturn = pBuf->aData[ucHead];
		pBuf->ucHead = (ucHead + 1) & RB_SIZE_MASK;
		pBuf->ucCount = ucCount - 1;
	}
	return uReturn;
}

Potential enhancements to what I have done here would be to make the sizes of the ring buffers be parameters stored in the buffers. This would allow you to specify input and output buffers that are of different sizes. This is something you might consider doing if you expect to have differences in the expected data streams. For example, if your program is likely to output more than it receives, it would make sense to have a smaller input buffer.

The SCI Interrupt routines

Here come the meat of the routines. There are three functions that are most important to us. sciGetByte(), sciPutByte(), and sci_interrupt().

sciGetByte() is called to see if any bytes are available in the buffer rbInput.

sciPutByte() is called to place bytes into the output buffer rbOutput.

sci_interrupt() is only called when an SCI interrupt occurs. Its job is to accept bytes from the serial port, then to write bytes from the output queue to the SCI port.

As a program user, you will be calling sciGetByte() in your program to accept input bytes, and sciPutByte() to send bytes.

//
// Here we declare our two ring buffers. One for input, the other for
// output.
//
RingBuf rbInput;
RingBuf rbOutput;
//
// sciLastError will keep track of the last known error from the serial 
// port. Usually this would be a framing or line noise error, though it
// could also be a buffer overrun. If you need to be critical about the quality
// of the serial stream, you should keep checking sciLastError after getting
// each byte. This tells you if you should be concerned about the quality of 
// the byte. 
//
unsigned int sciLastError;
//
// sciGetLastError returns the most recent error from the serial routines
//
//
unsigned int sciGetLastError()
{
 unsigned int ReturnCode;
 INTR_OFF();
 ReturnCode = sciLastError;
 sciLastError = 0;
 INTR_ON();
 return ReturnCode;
}
 
//
// sciGetByte is used to retrieve one byte from the input queue. The
// return value is actually a 16-bit word. The high bit will tell you
// if the contents of the low byte are valid. Specifically, if the returned
// value is negative (the high bit set), then there was no valid data in the
// queue. 
//
unsigned int sciGetByte()
{
	unsigned int rval;
	INTR_OFF();
	rval = rbGetByte(&rbInput);
	INTR_ON();
	
	//
	// On output, if the high bit is set, then there was no data
	// available
	//
	return rval;
}

Of interest here is the fact that sciGetByte() does much of its work with interrupts turned off. The reason for this is that the rbGetByte() routine is going to mess around with a data structure that is also touched by an interrupt routine. It is generally a poor idea to allow two different routines, one being run asynchronously like an interrupt, to touch the same data structures. They could end up clobbering the data structure at the same time, and you could generate errors.

Note that we didn't put the interrupt code in rbGetByte() because it is called from an interrupt routine. Interrupt routines are only called with interrupts turned off, therefore we didn't need them there.

sciGetByte() is going to return a large number (the high bit set) if the buffer was empty. The input buffer is only filled by bytes coming in on the SCI port. The input buffer is added to by the sci_interrupt() routine, shown later.

//
// This routine does the actual work of putting data into the output queue.
// It turns interrupts off, attempts to queue the data, then turns interrupts
// back on. If the high bit of the return value is set, then there wasn't room
// in the queue for the byte. In that case, you need to call this routine again
// either at a later time, or when you know there is room in the queue. 
//
unsigned int sciPutByte(char cData)
{
	unsigned int rval;
	//
	// Disabling interrupts here so that the interrupt routine and this 
	// routine don't interfere with each other while writing the queue
	// data header. 
	//
	INTR_OFF();
	//
	// Put a byte into the output queue.
	//
	rval = rbPutByte(&rbOutput,cData);
	
	//
	// There is now a byte ready to go in the output buffer. Enable the
	// Transmit Interrupt to trigger when TDRE is set. This causes the
	// interrupt to occur right away, and the byte to be sent. All of the
	// work of actually sending the data is performed in the sci_interrupt
	// routine. 
	//
	SCCR2 |= 0x80;
	
	//
	// Enabling interrupts now. After this instruction, an interrupt is sure
	// to happen, and the byte will be sent out from sci_interrupt. In the
	// event that the queue already had data in it, the interrupt will happen
	// at some later time. 
	//
	INTR_ON();
	
	//
	// It is possible that the buffer was full. The error code from 
	// rbPutByte informs our caller that the operation failed, and should
	// be tried again later. If it succeeded, it will return zero
	//
	return rval;
}

sciPutByte() is an interesting routine. Its main job is to put bytes into the output queue, rbOutput. It also enables a transmit interrupt. When the transmit interrupt occurs, it will remove bytes from the output queue and send them to the SCI port. Just like the serGetByte() routine, most of the work is done with interrupts disabled.

A special note about how the send mechanism works. When this routine queues a byte, it sets the TIE bit in the control register. This is the Transmit Interrupt Enable bit. If this bit is set in the control register, AND the Transmit Data Register Empty bit is set in the SCSR2 status register, then an interrupt is triggered. There are two possible cases here. If this is the first byte in the queue, then the chances are pretty high that the Transmit Data Register Empty bit is already set. Enabling the TIE bit, as is done above, will cause an interrupt to occur right away. When that occurs, the interrupt routine will start outputting bytes to the SCI port. So, in essence, this routine sets up one or more bytes in the queue, then 'kicks' the interrupt routine to do its work.

If the TIE bit was already set, then there are/were bytes in the queue, and the interrupt mechanism is already working on clearing them out. Thats fine, setting the bit again will just keep things going as they were.

Now we look at the interrupt routine. On the HC11/HC12 parts, each SCI port has its own interrupt vector. All of the possible interrupt conditions for that SCI port will route to the same interrupt vector. You need to check the contents of a status register to determine which interrupt type you are handling. Due to the asynchronous nature of the SCI port, it is possible that both a transmit and a receive interrupt have been triggered at the same time, so we need to handle both cases.

//
// Using Imagecraft C, a #pragma statement allows you to declare a routine as
// an interrupt handler. The basic difference is that an interrupt handler
// ends with an RTI instruction, which is Return From Interrupt. 
//
#pragma interrupt_handler sci_interrupt
void sci_interrupt()
{
	unsigned char lscsr;	// Local SCSR - Serial Status Register
	unsigned char lscdr;	// Local SCDR - Serial Data Register
	
	//
	// Ok, we just had an SCI interrupt. This could mean a byte was
	// recieved, transmitted, or perhaps both conditions! 
	//
	// We do know that interrupts are off, and that we can play with the
	// ring buffers as we see fit. This is due to the fact that we are in
	// an interrupt handler, which are always called with interrupts off. 
	// You should leave them off in this routine!
	//
	// First, handle the recieve case. The following two steps get the
	// status and data. It also clears the recieve flag if a recieved
	// byte was valid. Do NOT overwrite the values in lscsr and lscdr until
	// we are done with them completely. The following two reads cause the
	// actual SCSR and SCDR registers to change value.
	//
	lscsr = SCSR;
	lscdr = SCDR;
	
	// Determine if there is an incoming byte in the data register
	if(lscsr & SCI_RDRF)
	{
		//
		// There is a byte in the data register. Determine if it is a
		// valid byte. If there is a potential for error, then make 
		// note of it in the sciLastError value.
		//
		// There are bits in the SCSR that inform you if the SCI port detected
		// anything bad, like a framing error or perhaps some line noise.
		//
		if(lscsr & SCI_DATAERROR_MASK)
		{
			//
			// There is a data error! That means the contents of a byte in 
			// the serial stream is in question. Make a note of it.
			//
			// Other possibilities are to be more robust in how the byte is
			// noted. You might, for example, keep another queue that keeps 
			// track of the error bytes on a per byte basis. This is a good 
			// idea if you really need to keep track of the quality of the 
			// data stream. 
			//
			sciLastError = lscsr & SCI_DATAERROR_MASK;
		}	
		
		//
		// Queue the byte. We could lose a byte to a buffer overrun
		// Then, check to see if the hardware has detected a buffer 
		// overrun. If so, then make a note of this problem in the 
		// last error variable.
		// 
		if(rbPutByte(&rbInput,lscdr) || (lscsr & SCI_OVERFLOW))
		{
			// The OverRun flag is set, meaning we dropped at least
			// one byte. 
			sciLastError = SCI_OVERFLOW;
		}
	}

Ok, that was the receive portion of the interrupt routine. To keep track in your mind, you can see that this routine does the rbPutByte(&rbInput) to place incoming bytes into the input buffer. If you recall, sciGetByte() is the place were bytes are taken out of that queue.

Also note that this implementation attempts to keep track of data errors. If you check out some of the flags in the SCSR status register, it will try to tell you if it detected some forms of communication errors, such as line noise or framing errors. It will also tell you if a buffer overrun occured. That would only happen if the CPU was working on a task with interrupts turned off for a very long time. I have combined a hardware buffer overrun with a ring buffer overrun error code. In other words, I treat rbPutByte() failing the same as having the SCI_OVERFLOW bit set in the status register. In both cases, a byte was lost. A note is made in sciLastError. You can check and sciLastError from time to time to determine if an error occurs.

	// 
	// That is the end of the receive portion of the handler. Now for the
	// transmit	
	//
	// Determine if the Transmit Data Register is Empty. Note that the register
	// is re-read here to insure that the SCSR read / SCDR write sequence is
	// satisfied thus clearing the flag.
	//
	// A key point is what happens when no data is ready to be written. In this
	// case, the write to the SCDR will not happen. This leaves the TDRE bit
	// set in the SCSR. Normally, this would be a bad thing to do, since the 
	// interrupt handler will be called again immediately. However, if we 
	// don't write anything to SCDR, then we are going to turn the TIE interrupt
	// (Transmit Interrupt Enable) off, so it won't trigger again. The next time
	// something is put into the queue using sciPutByteNoWait, then the TIE
	// is reset to 1, and an interrupt occurs right away.
	//
	//
	if(SCSR & SCI_TDRE)
	{
		// Now determine if there are bytes in the output queue
		if(rbCount(rbOutput))
		{
			//
			// There are bytes. Get one of them, and send it by writing it
			// to the SCDR 
			//
			SCDR = rbGetByte(&rbOutput);
		}
		else
		{
			//
			// No bytes are available for sending. Turn off the 
			// TIE interrupt. It will be enabled again when a byte has
			// been queued for transmission
			//
			SCCR2 &= 0x7F;
		}
	}
}

The transmit section of the routine is quite a bit shorter. Note that the hardware will clear the interrupt status flag automatically if and only if a read of SCSR is followed by a write to the SCDR. In other words, you need to read the status then write a byte.

Remember that writing a byte to SCDR will cause it to be sent to the serial port. In the case where no bytes are left to send, you really don't want to send any extra bytes. The solution is that if no bytes are ready to be written, then this routine turns the TDRE (Transmit Data Register Empty) interrupts off. This implies that the sciPutByte() routine MUST turn that interrupt back on each time it queues a byte.

A mental note: In sciPutByte(), we put bytes into the rbOutput queue. This transmit section is where those bytes are removed from the queue.

The Rest of the Program

Just to be complete, I present the rest of the program right here. Note that I replaced the stdio routine 'putchar()' with my own version that uses by sciPutByte() routine. Note that sciPutByte() is a routine that will not block. The only error that I am tracking is to determine if the byte was queued or not. In this routine, I sit and wait until the byte is placed in the queue.

In practice, while the program is being used, the queue usually will not fill up, so no waiting is done here. Only if I try to send more data than will fit into the queue will I have to wait. This is an important point that you need to consider in your design. You should attempt to make your queue large enough to handle normal traffic out the serial port. That keeps your interrupt routine busy without hogging the CPU from your mainline program.

I am currently not using the sciLastError global variable.

//
// This version of putchar will be linked into your program before the version
// in the libaries. printf() and puts() will end up using this routine to 
// output bytes to the serial port. They will be queued. Note that if the
// queue fills with data, then this routine will wait until there is room. 
//
int putchar(char cData)
{
	//
	// This routine will sit and wait for the current byte to be placed in
	// the output queue. This is a blocking call. Use sciPutByte() as the 
	// non-blocking version
	//
	if(cData == '\n')
	{
		putchar('\r');
	}
	//
	// Block waiting for room in the output queue. 
	//
	while(sciPutByte(cData));
	return cData;
}
//
// Here is a mini-test program for these routines. 
//
//
void main()
{
	//
	// First, be sure the two queues are initialized BEFORE enabling the 
	// serial port. 
	// 
	rbInit(rbInput);
	rbInit(rbOutput);
	
	//
	// sciLastError will tell you what the previous error was
	//
	sciLastError = 0;
	
	// Setup at 9600 baud
	BAUD = 0x30;
	
	//
	// Start out with Receive Interrupt Enable set to 1
	// and Transmit Interrupt Enable set to zero 
	// The TIE will be set to 1 only when there are bytes to be sent
	//
	SCCR2 = 0x2c;
	
	//
	// Enable interrupts. The sci_interrupt routine is now active
	//
	INTR_ON();
	
	//
	// puts will send bytes one at a time using putchar()
	//
	puts("serial.c\n");
	
	while(1)
	{
		//
		// This is just a test program. Echo bytes in blocks of 5 each.
		// Note that the bytes are queued up during the first loop,
		// then read very quickly during the second loop. 
		// 
		int ch;
		while(rbCount(rbInput) < 5);
		while((ch = sciGetByte()) > 0)
		{
			putchar(ch);
		}
	}
}
//
// The following definition creates a reset vector. This is used during the
// reset of the CPU, and also to direct the SCI interrupt vector to our own
// handler. 
//
#define DUMMY_ENTRY     (void (*)(void))0xFFFF
//
// A Reset vector for this program.
//
extern void _start(void);
#pragma abs_address:0xffd6
void (*interrupt_vectors[])(void) =
{
        sci_interrupt,    /* SCI */
        DUMMY_ENTRY,    /* SPI */
        DUMMY_ENTRY,    /* PAIE */
        DUMMY_ENTRY,    /* PAO */
        DUMMY_ENTRY,    /* TOF */
        DUMMY_ENTRY,    /* TOC5 */      /* HC12 TC7 */
        DUMMY_ENTRY,    /* TOC4 */      /* TC6 */
        DUMMY_ENTRY,    /* TOC3 */      /* TC5 */
        DUMMY_ENTRY,    /* TOC2 */      /* TC4 */
        DUMMY_ENTRY,    /* TOC1 */      /* TC3 */
        DUMMY_ENTRY,    /* TIC3 */      /* TC2 */
        DUMMY_ENTRY,    /* TIC2 */      /* TC1 */
        DUMMY_ENTRY,    /* TIC1 */      /* TC0 */
        DUMMY_ENTRY,    /* RTI */
        DUMMY_ENTRY,    /* IRQ */
        DUMMY_ENTRY,    /* XIRQ */
        DUMMY_ENTRY,    /* SWI */
        DUMMY_ENTRY,    /* ILLOP */
        DUMMY_ENTRY,    /* COP */
        DUMMY_ENTRY,    /* CLM */
        _start  /* RESET */
};
#pragma end_abs_address

 

Summary

In practice, interrupt driven serial routines can increase the speed of your program by allowing your CPU to do meaningful work instead of waiting for the SCI port to finish. As you can see, there really isn't a lot of code involved in writing an interrupt routine, but you do need to keep the following points in mind:

  1. Protect common data structures by turning interrupts off while manipulating them
  2. Take great care in insuring that you acknowledge the interrupt source. In this instance, the act of reading the status register then reading the data register is how we acknowledge the read interrupt. Reading the status register then writing the data register acknowledges the transmit interrupt.

I hope you find these routines useful.