Share

High Speed Serial Port Design Pattern

Embedded processors now-a-days are connected with each other via high speed serial links. There links can range from 1 Mbps to 100 Mbps. At these high link speeds, the byte by byte transfers described in the serial port design pattern prove inadequate. High speed transfers require devices that can directly transfer data to and from memory without interrupting the processor.

Intent

This design pattern covers interfacing techniques with high speed serial communication devices. The main objective is to encapsulate the interface with the device and provide a hardware independent interface to the high speed serial port.

Also Known As

  • LAN Interface
  • Intelligent Device Interface
  • DMA 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. High speed serial port design pattern encapsulates DMA configuration, register interfacing 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 DMA (direct memory access) transfers to and from the device. In such devices serial transmission and reception are completely handled by the serial device. The device operates as a bus master and transfers data to and from the memory without main processor intervention.

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.

As mentioned earlier, the serial port is intelligent. The device needs to be programmed with the addresses of the transmit and receive message queues. Once these addresses have been programmed, the device automatically reads the buffer header to determine the current state of the buffer. For example, when the device finishes transmission of a buffer, it reads the buffer header for the next buffer to determine there is a new message ready for transmission. If a new message is found, the device initiates transmission immediately, without involving the processor.

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 DMA 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 a transmission buffer and the "ready for transmission" flag is set.
  6. The device periodically becomes a bus master, polling for the "ready for transmission" flag in the first transmission buffer.
  7. The flag is set, so the device begins transmission of the buffer.
  8. When all bytes of the message have been transmitted, the device set the "finished transmission" bit in the buffer header.
  9. The device checks the next buffer to determine if it is ready for transmission.
  10. In this scenario, no other buffer is ready for transmission. Device raises the transmission complete interrupt. (If more messages were enqueued, the device would have automatically started transmitting the buffer).
  11. The InterruptServiceRoutine() is invoked.
  12. The ISR polls the SerialPorts to select the interrupting device.
  13. The HandleInterrupt() method of the SerialPort is invoked.
  14. SerialPort checks the interrupt status register to determine the source of the interrupt.
  15. This is a transmit interrupt, so the HandeTxInterrupt() method is invoked.
  16. A transmission complete event is sent to the task.
  17. This event is routed by the SerialPortManager to the SerialPort.
  18. SerialPort checks if the transmit queue has any more messages.
  19. If a message is found, message transmission of the new message is initiated.

Receiving a Message

  1. When the device detects the start of a new message, it accesses the receive buffers and checks the "free buffer" bit in the buffer header.
  2. The device finds a free buffer, so it starts DMA operations to copy all the received bytes into the designated buffer.
  3. The device raises an interrupt when message reception is completed. It also sets the "received message" bit in the buffer header. (If another message reception starts, the device will automatically start receiving that message in the next buffer)
  4. At this point a message receive complete event is dispatched to the task.
  5. The Serial Port's event handler allocates memory for the received message and writes the new message into the receive queue.
  6. Then it cleans up the receive buffer by setting the "free buffer" bit in the buffer header. 

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 buffer_header;
       int length;
       char data[BUFFER_SIZE];
   };
   
   // Buffers used for store data when the ISR is receiving or transmitting data
   Buffer m_receiveBuffer[MAX_RECEIVE_BUFFERS];
   Buffer m_transmitBuffer[MAX_TRANSMIT_BUFFERS];
   
   // Addresses for device registers
   const long m_interruptStatusRegister;    // Register to manage interrupts
   const long m_transmitBufferRegister;     // Register to copy transmit buffer address
   const long m_receiveBufferRegister;      // Register to copy receive buffer address
   int m_transmitWriteIndex;                // Next buffer to be written
   int m_receiveReadIndex;                  // Next buffer to be read
   
   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_transmitBufferRegister(baseAddr+1),
                               m_receiveBufferRegister(baseAddr+2),
                               m_portId(portId)
   {
      // Set the indices used to keep track of buffers being used
      m_transmitWriteIndex = 0;
      m_receiveReadIndex = 0;
      
      // Mark all receive buffers as available for receiving a message
      for (i=0; i < MAX_RECEIVE_BUFFERS; i++)
      { 
         m_receiveBuffer[i].buffer_header = BUFFER_FREE;
      }
      
      // Mark all transmit buffers free
      for (i=0; i < MAX_TRANSMIT_BUFFERS; i++)
      {
         m_transmitBuffer[i].buffer_header = BUFFER_EMPTY;
      }
      
      // Inform the device about transmit and receive buffer addresses.
      // Once these addresses have been setup, the device will access
      // the buffers by DMA operations
      io_write(m_receiveBufferRegister, &m_receiveBuffer[0]);
      io_write(m_transmitBufferRegister, &m_transmitBuffer[0]);     
          
      // Initialize pointers to associated queues
      m_pTransmitQueue = pTxQueue;
      m_pReceiveQueue = pRxQueue;
   }
   
   ~SerialPort()
   {
      // Reset the transmit and receive buffer addresses to NULL
      io_write(m_receiveBufferRegister, NULL);
      io_write(m_transmitBufferRegister, NULL);
   }

   // 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 the message can be added to the device queue. 
      // This is Accomplished by calling HandleTransmissionComplete(), The
      // handling at this point is the same as handling to scan the queue
      // when a message transmission has been completed.
      HandleTransmissionComplete();         
   
   }
   
   // 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_INTERRUPT_BIT)
      {
         HandleRxInterrupt();
      }
      else if (interruptStatus & TRANSMITTER_INTERRUPT_BIT)
      {
         HandleTxInterrupt();
      }
      else
      {
         m_spuriousInterruptCounter++;
      }      
   } 
};

// Called when the ISR has finished processing the
// current message and it is ready to process another one. This method
// checks for free transmit buffers and replenishes them with data from
// the message queue
void SerialPort::HandleTransmissionComplete()
{
   Message *pMsg;
      
   // Look for free space in the device transmit queue
   for (i=0; i < MAX_TRANSMIT_BUFFERS; i++)
   {       
       if (m_transmitBuffer[m_transmitWriteIndex].buffer_header & BUFFER_EMPTY)
       {
          // A free buffer found
          // Check for more messages to transmit
          pMsg = m_pTransmitQueue->Read();
          
          if (pMsg)
          {
             // Message found for transmission, set up the transmit
             // buffer
             // Copying data for transmission
             memcpy(m_transmitBuffer[m_transmitWriteIndex].data, pMsg, pMsg->length);
             
             // Copy the message length of the length field in the device
             // buffer
             m_transmitBuffer[m_transmitWriteIndex].length = pMsg->length;

             // Clear the buffer empty bit, this signals to the device
             // that this buffer is available for transmission             
             m_transmitBuffer[m_transmitWriteIndex].buffer_header &= ~BUFFER_EMPTY;
             
             // Module increment the transmit write index
             if (++m_transmitWriteIndex == MAX_TRANSMIT_INDEX)
             {
                m_transmitWriteIndex = 0; 
             }                      
         }
      }
   }       
}

// Called when ISR has received a complete message. The device has
// been configured to interrupt after every message (this minimizes
// delay in message processing. This does increase the total amount
// of processing has every message has to be processed)
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[m_receiveReadIndex].data, 
                      m_receiveBuffer[m_receiveIndex].length);
   // Copy the length of the message
   pMsg->length = m_receiveBuffer.length;  
   // Pass the message to the higher layers
   m_pReceiveQueue->Write(pMsg);
   
   // Modulo increment the receive read index
   if (++m_receiveReadIndex == MAX_RECEIVE_INDEX)
   {
      m_receiveReadIndex = 0; 
   }            
}

// Receive interrupt handler
void SerialPort::HandleRxInterrupt()
{   
    // Receive ISR is invoked only when message receive
    // has been completed. So send the receive complete
    // event right away
    send_event(RECEIVE_COMPLETE, m_portId);    
}

// Transmit Interrupt Handler
void SerialPort::HandleTxInterrupt()
{
   // Transmit ISR is invoked only when message transmission
   // is completed. So send the transmit complete
   // event right away
   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 is used to design serial port interfaces with intelligent devices which are capable of performing DMA operations to manage buffers in the memory. These devices only interrupt the processor when complete messages have been received/transmitted.

Related Patterns