Serial Port Design Pattern
Embedded software has to interact with hardware devices of various types. In
this article we will consider a design pattern for handling hardware interfaces
for a serial port. A serial port interface drives serial links like HDLC,
RS-232, RS-422 etc.
Intent
The Serial Port design pattern defines a generic interface with a serial port
device. The main intention here is to completely encapsulate the interface with
the serial port hardware device. All classes interfacing with the serial port
will not be impacted by change in the hardware device.
Also Known As
- Asynchronous Communication Adaptor
- Serial Interface
- Serial Device
Motivation
The main motivation for development of this design pattern is to minimize
dependency on hardware. Very often the hardware team decides to change the
interface devices due to cost, end-of-life or functionality improvements. This involves a
costly software porting exercise. Serial port design pattern encapsulates the
register and interrupt handling specific to a device. Change in the device will
just result in changes to just the classes involved in implementing this design
pattern.
Applicability
This pattern is applicable to all serial devices that involve direct byte
transfers to and from the device using program instructions. In such devices,
serial transmission is implemented by the device interrupting the processor for
data bytes. When data is received on the serial link, the device interrupts the
processor to transfer data.
Structure
Serial Port is implemented with the SerialPort and SerialPortManager classes.
The SerialPortManager maintains an array of SerialPort objects. Each SerialPort
object manages the transmit and receive buffers. The SerialPortManager class
also implements the interrupt service routine.
Participants
The key participants in this pattern are:
- Serial Port Manager: Manages all the
Serial Ports on the board.
- Interrupt Service Routine: Implemented
as a static method in Serial Port Manager.
- Serial Port: Handles the interface
with a single serial port device. This class also contains the transmit and
receive buffers.
- Transmit Queue: This queue contains
messages awaiting transmission on the serial port.
- Receive Queue: Messages received on
the serial link are stored in this queue.
Collaboration
The interactions between the participants are shown in the figure below:
Consequences
Implementing the Serial Port design pattern keeps the hardware dependent code
confined to a few classes in the system. This simplifies the software port to
new hardware.
Implementation
The implementation of this design pattern is explained in terms of handling
of message transmission and reception. The important point to note here is that
the code executing in the context of the ISR is kept to the minimum. All the CPU
intensive operations are carried out at the task level.
Transmitting a Message
- SerialPortManager's constructor installs the InterruptServiceRoutine().
- Serial Port's constructor initializes the interrupts so that the
transmitter empty interrupt is disabled and the receiver ready interrupt is
enabled.
- A message is enqueued to the SerialPort by invoking the HandleTxMessage()
method.
- The method enqueues the message in the Transmit Queue and checks if this
is the first message in the queue.
- Since this is the first message in the queue, the message is removed from
the queue and copied into the transmission buffer.
- Then the transmitter empty interrupt is enabled.
- The device raises an interrupt as soon as it is enabled.
- The InterruptServiceRoutine() is invoked.
- The ISR polls the SerialPorts to select the interrupting device.
- The HandleInterrupt() method of the SerialPort is invoked.
- SerialPort checks the interrupt status register to determine the source of
the interrupt.
- This is a transmit interrupt, so the HandeTxInterrupt() method is invoked.
- The byte to be transmitted is copied into the transmit data register of
the device.
- The interrupt handling sequence presented above is repeated until all
bytes have been transmitted.
- When the message transmission has been completed, a transmission complete
event is sent to the task.
- This event is routed by the SerialPortManager to the SerialPort.
- SerialPort checks if the transmit queue has any more messages.
- If a message is found, message transmission of the new message is
initiated. If no message is found, the transmitter empty interrupt is
disabled.
Receiving a Message
- When the first byte of a message is received, the SerialPort's receive
interrupt handler interprets it as the length of the message.
- The interrupt handler keeps receiving the bytes until the complete message
has been received.
- At this point a message receive complete event is dispatched to the task.
- The Serial Port's event handler allocates memory for the received message
and writes the new message into the receive queue.
- Then it cleans up the receive buffer for the next message.
Sample Code and Usage
Here is the code for a typical implementation of this pattern:
Serial
Port |
class SerialPort
{
// Queues for receiving and transmitting messages
Queue *m_pReceiveQueue;
Queue *m_pTransmitQueue;
// Common Buffer structure for Transmit and Receive Buffers
struct Buffer
{
int currentIndex;
char length;
char data[BUFFER_SIZE];
};
// Buffers used for store data when the ISR is receiving or transmitting data
Buffer m_receiveBuffer;
Buffer m_transmitBuffer;
// Addresses for device registers
const long m_interruptStatusRegister; // Register to manage interrupts
const long m_transmitDataRegister; // Register to copy data to be transmitted
const long m_receiveDataRegister; // Register to obtain received data
const int m_portId; // Id assigned to this serial port
// Interrupt handlers
void HandleRxInterrupt();
void HandleTxInterrupt();
public:
SerialPort(long baseAddr, int portId, Queue *pTxQueue, Queue *pRxQueue)
: m_interruptStatusRegister(baseAddr),
m_transmitDataRegister(baseAddr+1),
m_receiveDataRegister(baseAddr+2),
m_portId(portId)
{
m_receiveBuffer.length = 0;
m_receiveBuffer.currentIndex = 0;
m_transmitBuffer.length = 0;
m_transmitBuffer.currentIndex = 0;
// Note: Receive interrupt is always enabled, as data can be received
// at any time. Transmit interrupt is enabled only when transmitting
// data on the serial link.
io_write(m_interruptStatusRegister, ENABLE_RX_DISABLE_TX_MASK);
// Initialize pointers to associated queues
m_pTransmitQueue = pTxQueue;
m_pReceiveQueue = pRxQueue;
}
~SerialPort()
{
io_write(m_interruptStatusRegister, DISABLE_RX_DISABLE_TX_MASK);
}
// Event Handler that is invoked when the ISR has finished transmitting
// a message
void HandleTransmissionComplete();
// Event Handler that is invoked when the ISR has received a new message
void HandleReceiveComplete();
// This handler is invoked by higher layers when they wish to transmit
// a message over the serial link
void HandleTxMessage(Message *pMsg)
{
// Add the message to the transmit queue
m_pTransmitQueue->Write(pMsg);
// Check if this is the first message. If so start transmission
// for the data by preparing the transmit buffer
// (Accomplished by calling HandleTransmissionComplete())
// Also enable the transmit interrupt as new data is available
// for transmission.
if (m_pTransmitQueue->GetLength() == 1)
{
HandleTransmissionComplete();
io_write(interruptStatusRegister, ENABLE_RX_ENABLE_TX);
}
}
// Check the interrupt status register to determine if some
// interrupt is pending
bool IsInterruptPending()
{
int interruptStatus = io_read(m_interruptStatusRegisterAddress);
return (interruptStatus & PENDING_INTERRUPT_BIT);
}
// This method is executed from the ISR. It checks the interrupt
// status register to determine the exact source of interrupt.
void HandleInterrupt()
{
int interruptStatus = io_read(m_interruptStatusRegisterAddress);
if (interruptStatus & RECEIVED_DATA_BIT)
{
HandleRxInterrupt();
}
else if (interruptStatus & TRANSMITTER_EMPTY_BIT)
{
HandleTxInterrupt();
}
else
{
m_spuriousInterruptCounter++;
}
}
};
// Called when the ISR has finished processing the
// current message and it is ready to process another one.
void SerialPort::HandleTransmissionComplete()
{
Message *pMsg;
// Check for more messages to transmit
pMsg = m_pTransmitQueue->Read();
if (pMsg)
{
// Message found for transmission, set up the transmit
// buffer
m_transmitBuffer.length = pMsg->length;
m_transmitBuffer.currentIndex = 0;
// Copying data for tranmisson
memcpy(m_transmitBuffer.data, pMsg, pMsg->length);
}
else
{
// No more messages pending for transmission, so disable the
// transmit interrupt.
io_write(m_interruptStatusRegister, ENABLE_RX_DISABLE_TX_MASK);
}
}
// Called when ISR has received a complete message
void SerialPort::HandleReceiveComplete()
{
// Allocate a buffer for the message and copy the contents
// from the receive buffer
Message *pMsg = new Message;
memcpy(pMsg, m_receiveBuffer.data, m_receiveBuffer.length);
// Copy the length of the message
pMsg->length = m_receiveBuffer.length;
// Pass the message to the higher layers
m_pReceiveQueue->Write(pMsg);
// Cleanup the message buffer for receiving the next message
m_receiveBuffer.currentIndex = 0;
}
// Receive interrupt handler
void SerialPort::HandleRxInterrupt()
{
int data;
// Read the received byte from the device
data = io_read(m_receiveDataRegister);
// Check if this is the first byte. The first
// byte contains the total length of the message
if (m_receiveBuffer.currentIndex == 0)
{
m_receiveBuffer.length = data;
}
// Copy the bytes into the receive buffer
m_receiveBuffer.data[m_receiveBuffer.currentIndex++] = data;
// Check if the complete message has been received, if so
// raise an event to notify the protocol task.
if (m_receiveBuffer.currentIndex == m_receiveBuffer.length)
{
send_event(RECEIVE_COMPLETE, m_portId);
}
}
// Transmit Interrupt Handler
void SerialPort::HandleTxInterrupt()
{
// Get the byte to be transmitted
char data =
m_transmitBuffer.data[m_transmitBuffer.currentIndex++];
// Write the byte to the transmit register
io_write(m_transmitDataRegister, data);
// Check if the complete message has been transmitted, if so
// raise an event to notify the protocol task
if (m_transmitBuffer.currentIndex == m_transmitBuffer.length)
{
send_event(TRANSMISSION_COMPLETE, m_portId);
}
}
// Manager all the serial ports on the board
class SerialPortManager
{
// Array of serial ports (declared static, as it is accessed
// from the ISR)
static SerialPort m_serialPort[MAX_SERIAL_PORTS];
public:
// Interrupt Handler. (Needs to be static as ISRs should be
// regular functions with C calling conventions. Methods cannot be
// declared ISRs)
static void InterruptServiceRoutine();
SerialPortManager()
{
// Install the interrupt handler on start up
install_interrupt_handler(SERIAL_PORT_ISR, InterruptServiceRoutine);
}
~SerialPortManager()
{
// Deinstall the handler when exiting
deinstall_interrupt_handler(SERIAL_PORT_ISR);
}
// Called when the ISR generates an event. This method dispatches
// the event to the appropriate serial port object
void HandleInterruptEvent(const Event *pEvent)
{
SerialPort *pPort;
pPort = m_serialPort[pEvent->portId];
switch (pEvent->type)
{
case TRANSMISSION_COMPLETE:
pPort->HandleTransmissionComplete();
break;
case RECEIVE_COMPLETE:
pPort->HandleReceiveComplete();
break;
}
}
};
// Static declaration
SerialPort SerialPortManager::m_serialPort[MAX_SERIAL_PORTS];
// Interrupt Service Routine for all interrupts
void SerialPortManager::InterruptServiceRoutine()
{
bool foundInterruptSource = false;
// Loop through all the serial ports to find out which serial device
// generated this interrupt. (Multiple device interrupts might be
// generated at the same time)
for (i=0; i < MAX_SERIAL_PORTS; i++)
{
if (m_serialPort[i].IsInterruptPending())
{
foundInterruptSource = true;
m_serialPort[i].HandleInterrupt();
}
}
// Interrupt was raised but no device was found with a pending
// interrupt. Raise the spurious interrupt counter
if (!foundInterruptSource)
{
m_spuriousInterruptCount++;
}
}
|
Known Uses
This pattern can be used to implement serial interfaces where data handling
is handled in interrupt service routines. It is not suitable for direct memory
access (DMA) based serial devices.
Related Patterns
|