Software UART on PIC18F4321: Interrupt-Driven Serial Communication in C
Software UART on PIC18F4321 with INT0 and Timer Ticks
The following code is a software UART implementation for the PIC18F4321 microcontroller. It uses the xc.h and pic18f4321.h headers and defines the transmit and receive pins, buffer sizes, and masks at the top of the file.
#include <xc.h>
#include <pic18f4321.h>
#define TX_PIN LATBbits.LATB1
#define RX_PIN PORTBbits.RB0
#define RES 2
#define MAX_RX 8
#define MASK_RX 7
#define MAX_TX 8
#define MASK_TX 7
static unsigned char QueueRX[MAX_RX], StartRX = 0, EndRX = 0, HowManyRX = 0;
static unsigned char QueueTX[MAX_TX], StartTX = 0, EndTX = 0, HowManyTX = 0;
static unsigned char rxState = 0, rxTickCnt = 0, rxBitCnt = 0, rxShift = 0;
static unsigned char txState = 0, txTickCnt = 0, txBitCnt = 0, txShift = 0;
Initialization
The SIOTime_Init() function configures RB1 as an output and RB0 as an input, sets the idle level of the transmit line high, clears all queues and state variables, and enables the INT0 external interrupt on the falling edge.
void SIOTime_Init(void) {
TRISBbits.TRISB1 = 0;
TX_PIN = 1;
TRISBbits.TRISB0 = 1;
StartRX = EndRX = HowManyRX = 0;
StartTX = EndTX = HowManyTX = 0;
rxState = txState = 0;
INTCON2bits.INTEDG0 = 0;
INTCONbits.INT0IF = 0;
INTCONbits.INT0IE = 1;
}
INT0 Interrupt Handler
On a start-bit edge detected on INT0, the receiver leaves the idle state, resets its counters, and disables INT0 so that the timer-driven state machine can take over sampling.
void SIOTime_InterruptINT0(void) {
INTCONbits.INT0IF = 0;
if (!rxState) {
rxState = 1;
rxTickCnt = rxBitCnt = rxShift = 0;
INTCONbits.INT0IE = 0;
}
}
Buffer Access Functions
unsigned char SIOTime_RXAvail(void) {
return HowManyRX;
}
unsigned char SIOTime_GetChar(void) {
unsigned char v;
di();
v = QueueRX[EndRX++];
EndRX &= MASK_RX;
HowManyRX--;
ei();
return v;
}
void SIOTime_PutChar(unsigned char value) {
di();
QueueTX[StartTX++] = value;
StartTX &= MASK_TX;
HowManyTX++;
ei();
}
Timer Tick State Machine
The SIOTime_TimerTick() function must be called periodically from a timer interrupt at a rate of RES ticks per bit period. It runs two independent state machines: one for reception and one for transmission.
void SIOTime_TimerTick(void) {
/* Receive state machine */
switch (rxState) {
case 0:
/* Polling: detect start bit when INT0 is not used */
if (RX_PIN == 0) {
rxState = 1;
rxTickCnt = rxBitCnt = rxShift = 0;
}
break;
case 1:
if (++rxTickCnt == RES) {
if (RX_PIN) {
rxState = 0;
INTCONbits.INT0IF = 0;
INTCONbits.INT0IE = 1;
} else {
rxState = 2;
rxTickCnt = 0;
}
}
break;
case 2:
if (++rxTickCnt == RES) {
rxTickCnt = 0;
if (RX_PIN)
rxShift |= (unsigned char)(1 << rxBitCnt);
if (++rxBitCnt == 8)
rxState = 3;
}
break;
case 3:
if (++rxTickCnt == RES) {
if (RX_PIN && HowManyRX < MAX_RX) {
QueueRX[StartRX++] = rxShift;
StartRX &= MASK_RX;
HowManyRX++;
}
rxState = 0;
INTCONbits.INT0IF = 0;
INTCONbits.INT0IE = 1;
}
break;
}
/* Transmit state machine */
switch (txState) {
case 0:
if (HowManyTX) {
txShift = QueueTX[EndTX++];
EndTX &= MASK_TX;
HowManyTX--;
TX_PIN = 0;
txTickCnt = txBitCnt = 0;
txState = 1;
}
break;
case 1:
if (++txTickCnt == RES) {
TX_PIN = txShift & 1;
txShift >>= 1;
txBitCnt = 1;
txTickCnt = 0;
txState = 2;
}
break;
case 2:
if (++txTickCnt == RES) {
txTickCnt = 0;
if (txBitCnt == 8) {
TX_PIN = 1;
txState = 3;
} else {
TX_PIN = txShift & 1;
txShift >>= 1;
txBitCnt++;
}
}
break;
case 3:
if (++txTickCnt == RES) {
txTickCnt = 0;
txState = 0;
}
break;
}
}