//
// serial.c - An interrupt driven communications queue for the HC12
//
// Copyright (c) 2000, Kevin W Ross - All rights reserved.
//
// This file will show you how to write an interrupt driven serial routine
// for the HC12. The routines are
// bi-directional, so both transmit and receive are done using interrupts.
//
// The flow of control for a program like this is a little contorted to follow
// until you learn how the data is queued and sent. There are really two cases.
//
// In the first case, there is an input queue called rbInput. It is filled when
// the interrupt handler is called by the serial port interrupt occuring.
// It is emptied each time you call serGetChar().
//
// The second case, there is an output queue called rbOutput. It is filled when
// you call sciPutByte(), and is emptied by the interrupt handler.
//
// To follow the flow, you need to remember where things happen and when.
// Interrupts happen asynchronously to the rest of the program. That means they
// happen without warning or care as to where in your program you are currently
// running code. It will interrupt whatever your program is currently doing and
// jump to an interrupt handler.
//
#include
#include
//*******************************************************************
// 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
//
// On the HC12, the default buffer size is 0x10 bytes for each direction
#ifndef RB_BUF_SIZE
#define RB_BUF_SIZE 0x10
#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;
}
//*******************************************************************
//*******************************************************************
//
// The start of the serial port specific routines
//
//*******************************************************************
//*******************************************************************
//
// 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.
//
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.
//
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;
}
//
// 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.
//
SC0CR2 |= 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;
}
//
// 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 SC0SR1 - Serial Status Register
unsigned char lscdr; // Local SC0DRL - 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 SC0SR1 and SC0DRL registers to change value.
//
lscsr = SC0SR1;
lscdr = SC0DRL;
// 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 SC0SR1 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;
}
}
//
// 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 SC0SR1 read / SC0DRL 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 SC0DRL will not happen. This leaves the TDRE bit
// set in the SC0SR1. 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 SC0DRL, 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(SC0SR1 & 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 SC0DRL
//
SC0DRL = 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
//
SC0CR2 &= 0x7F;
}
}
}
//
// 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
// SC0BD = 52;
// Setup at 19200
// SC0BD = 26;
// Setup at 38400
SC0BD = 13;
//
// 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
//
SC0CR2 = 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, /* SCI0 */
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