Share

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:

UML Class Diagram for Serial Port Pattern

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

  1. SerialPortManager's constructor installs the InterruptServiceRoutine().
  2. Serial Port's constructor initializes the interrupts so that the transmitter empty interrupt is disabled and the receiver ready interrupt is enabled.
  3. A message is enqueued to the SerialPort by invoking the HandleTxMessage() method.
  4. The method enqueues the message in the Transmit Queue and checks if this is the first message in the queue.
  5. Since this is the first message in the queue, the message is removed from the queue and copied into the transmission buffer.
  6. Then the transmitter empty interrupt is enabled.
  7. The device raises an interrupt as soon as it is enabled.
  8. The InterruptServiceRoutine() is invoked.
  9. The ISR polls the SerialPorts to select the interrupting device.
  10. The HandleInterrupt() method of the SerialPort is invoked.
  11. SerialPort checks the interrupt status register to determine the source of the interrupt.
  12. This is a transmit interrupt, so the HandeTxInterrupt() method is invoked.
  13. The byte to be transmitted is copied into the transmit data register of the device.
  14. The interrupt handling sequence presented above is repeated until all bytes have been transmitted.
  15. When the message transmission has been completed, a transmission complete event is sent to the task.
  16. This event is routed by the SerialPortManager to the SerialPort.
  17. SerialPort checks if the transmit queue has any more messages.
  18. 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

  1. When the first byte of a message is received, the SerialPort's receive interrupt handler interprets it as the length of the message.
  2. The interrupt handler keeps receiving the bytes until the complete message has been received.
  3. At this point a message receive complete event is dispatched to the task.
  4. The Serial Port's event handler allocates memory for the received message and writes the new message into the receive queue.
  5. 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