Share

Hardware/Peripheral Device Design Pattern

We have already looked at classes that provide a high level interface to the underlying hardware (e.g. Serial Port). Here we will look at the design of classes corresponding to the individual devices. The main objective is to keep all register programming information at one place.

Intent

The Hardware Device Design Pattern encapsulates the actual hardware device being programmed. The main idea is to encapsulate device register programming and bit manipulation into a single class dealing with the device.

Also Known As

  • Device
  • Hardware Interface
  • Peripheral Interface
  • Peripheral

Motivation

Very often the lowest level of code that interfaces with the hardware is difficult to understand and maintain. One of the main reasons for this is the idiosyncrasies of register level programming model of hardware devices.  Very often devices require registers to be accessed in a certain sequence. Defining a class to represent the device can go a long way in simplifying the code by decoupling the low level code and register manipulation.

Another motivation for this design pattern is skill sets. Often details about intricacies of register programming in devices are understood only by the persons familiar with the hardware design. Many times other low level code might be written by software engineers with just basic understanding of hardware.

Also note that separating the device programming and logic simplifies porting of the code to a different hardware platform.

Applicability

This pattern can be used to represent any hardware device.

Structure

The structure of class in this design pattern largely depends upon the register programming model of the device being programmed. In most cases, this design pattern would be implemented as a single class representing the device. In case of complex devices, the device might be modeled as a main device class and other subclasses modeling different parts of the device.

Participants

This design pattern generally interfaces with other classes that need to access hardware registers.

Collaboration

Collaboration between the device class and other classes would largely depend upon the purpose for which the device is being used. For example, a hardware device that provides timers, serial ports and a DMA controller might be modeled just as a serial device if other device functionality is not being used.

Consequences

The device involved in low level hardware programming is simplified as details about register manipulation have been hidden within the class. Thus the code accessing the hardware device can focus on the logic of the operation being performed. As noted earlier, porting of hardware dependent software is also simplified.

Implementation

We will study the implementation of this pattern by working with an imaginary serial device with the following register set:

  • Status Register (STAT): This read only register contains the following status bits:
    • Bit 0: Transmit Buffer Has Empty Space
    • Bit 1: Receive Buffer Has Data
    • Bit 2: Transmit under run
    • Bit 3: Receive overrun
  • Action Register (ACT): Bits in this write only register correspond to the bits in the status register. A condition in the status register can be cleared by writing the corresponding bit as 1. Note that bit 0 automatically gets cleared when writes are performed to the transmit buffer. Bit 1 is cleared automatically when reads are performed from the receive buffer. Bit 2 and 3 however need to be cleared explicitly.
  • Transmit Buffer (TXBUF): Write only buffer in which bytes meant for transmission should be written.
  • Receive Buffer (RXBUF): Read only buffer in which received bytes are stored.

Sample Code and Usage

Here is the code for the Serial_Device class:

Serial_Device

class Serial_Device
{
   enum Register_Offsets
   {
       STAT_REG_OFFSET = 0,
       ACT_REG_OFFSET = 0,
       TXBUF_OFFSET = 1,
       RXBUF_OFFSET = 2
   };
   
   enum Status_Register_Bits
   {
     TX_EMPTY,
     RX_DATA,
     TX_UNDERRUN,
     RX_OVERRUN 
   };
   const long m_status_Register;
   const long m_action_Register;
   const long m_transmit_Register;
   const long m_receive_Register;
   
public:
   Serial_Device(long baseAddress) : m_status_Register(baseAddress + STAT_REG_OFFSET),
                                    m_action_Register(baseAddress + ACT_REG_OFFSET),
                                    m_transmit_Register(baseAddress + TXBUF_OFFSET),
                                    m_receive_Register(baseAddress + RXBUF_OFFSET)
   {
   }
   
   // Use this method to determine if a transmit interrupt might be
   // pending.
   bool Transmitter_Has_Space() const
   {  return ((io_read(m_status_Register) & TX_EMPTY) == TX_EMPTY); }
   
   // This method returns true if a receive interrupt is pending
   bool Receiver_Has_Data() const
   {  return ((io_read(m_status_Register) & RX_DATA) == RX_DATA); }
   
   // Returns true if transmit error interrupt is active
   bool Transmitter_Has_Error() const
   {  return ((io_read(m_status_Register) & TX_UNDERRUN) == TX_UNDERRUN); }
   
   // Returns true if receive error interrupt is active
   bool Receiver_Has_Error() const
   {  return ((io_read(m_status_Register) & RX_OVERRUN) == RX_OVERRUN); }
  
  // Clear all the error conditions
   void Clear_Errors() const
   {  io_write(m_action_Register, TX_UNDERRUN | TX_OVERRUN); } 
   
   // Write_Data transmits the specified number of bytes. All bytes
   // may not be transmitted due to transmit buffer space. The total
   // number of transmitted bytes is returned.
   int Write_Data(const char *pData, int byteCount) const
   {
      // Keep writing transmit bytes until all the bytes
      // have been transmitted or transmit buffer has no space
      for (int txCount=0; i < byteCount; txCount++)
      {
         if (!Transmitter_Has_Space())
         {
            break;
         }
         // Write the byte to the transmit buffer for transmission
         io_write(m_transmit_Register, pData[txCount]);
      }
      
      // Return the count of transmitted bytes
      return txCount;
   }
   
   // Read_Data reads the bytes from the device. The maximum number
   // of bytes to be read can be specified. The actual number
   // of received bytes is returned.
   int Read_Data(char *pData, int byteLimit) const
   {
      // Keep reading received bytes until all the received bytes
      // have been copied or specified limit has been reached
      for (int rxCount=0; i < byteLimit; rxCount++)
      {
         if (!Receiver_Has_Data())
         {
            break;
         }               
         pData[rxCount] = io_read(m_receive_Register);
      }
      // Return count of received bytes
      return rxCount;
   }    
};

Known Uses

This pattern is used to decouple the logical device handling and device register manipulation.

Related Patterns