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