Serial Data Transfer

The remaining methods to be implemented from the IOSerialDriverSync interface are the data transfer methods. The serial driver will be provided with data to be transmitted over the serial port through the method enqueueData(), and the data that the driver has received from the serial port is provided to clients through the dequeueData() method.

When a user space process writes data to a serial port, it is first handled in the kernel by the IOSerialBSDClient class, which is responsible for passing the data on to the serial port driver. The IOSerialBSDClient will provide the data to the serial driver by calling its enqueueData() method, which has the following signature:

IOReturn  enqueueData(UInt8 *buffer, UInt32 size, UInt32 *count, bool sleep, void *refCon);

The data bytes to be sent are held in the buffer parameter, and the number of bytes to be sent is described by the size parameter. The typical design of a serial driver is to copy the data that has been provided into an internal buffer that it has allocated (known as the transmit buffer) and then return to the caller immediately. The driver will then continue handling the write request by transferring data from its transmit buffer to the hardware serial port asynchronously. Before returning from the enqueueData() method, the driver will return, through the count parameter, the number of bytes that it accepted; note that this is simply the number of bytes that the driver was able to copy to its transmit buffer, not the number of bytes that have been written over the hardware serial port. The sleep parameter allows the caller to request that, if the driver cannot accept all of the bytes that it has been provided, the driver should block and not return to the caller until all bytes have been copied to the driver's internal transmit buffer.

The current implementation of the IOSerialBSDClient will never request that the serial driver sleep if it cannot accept all of the data bytes that have been provided. Rather, it will make sure that it doesn't provide the serial driver with more data than it can accept, which is done by calling the driver's requestEvent() method with the event PD_E_TXQ_AVAILABLE. The IOSerialBSDClient will watch various states of the driver's transmit buffer to determine when the driver is able to accept more data, including the states PD_S_TXQ_LOW_WATER, PD_S_TXQ_EMPTY, and PD_S_TX_BUSY.

A sample implementation of the enqueueData() method is provided in Listing 11-6. Note that this implementation copies the data bytes to a transmit buffer that has been allocated by the serial driver, and then checks whether the hardware is currently writing data out on the serial port. If not, a hypothetical function named StartHardwareTransmit() is called which, although implementation specific, has the purpose of telling the hardware to begin sending data bytes from the driver's transmit buffer out over the serial port.

Listing 11-6. A Sample Implementation of the enqueueData() Method. The Method Is Assumed to Have Been Called Through an IOCommandGate.

IOReturn           MySerialDriver::enqueueDataGated(UInt8* buffer, UInt32 size, UInt32* count,
                                          bool sleep, void* refCon)
{
       // Abort if the serial port has not been acquired
       *count = 0;
       if ((m_currentState & PD_S_ACTIVE) == 0)
              return kIOReturnNotOpen;
       
       // Copy the provided data to the driver's transmit buffer
       *count = AddToTransmitQueue(buffer, size);
       // Regenerate the status bits for the transmit buffer
       CheckQueues(refCon);
       
       // If no hardware transmission is in progress, begin outputting bytes from the driver's
       // buffer
       if ((m_currentState & PD_S_TX_BUSY) == 0)
              StartHardwareTransmit();

       // Block if the caller has requested we send all bytes before returning
       while ((*count < size) && sleep)
       {
              UInt32      state;
              IOReturn    ret;
              
              // Wait until the driver's transmit buffer falls below the low waterlevel,  
              // and try again
              state = PD_S_TXQ_LOW_WATER;
              ret = watchState(&state, PD_S_TXQ_LOW_WATER, refCon);
              if (ret != kIOReturnSuccess)
                     return ret;
              
              // Copy further bytes to the driver's transmit buffer
              *count += AddToTransmitQueue(buffer + *count, size - *count);
              CheckQueues(refCon);
              if ((m_currentState & PD_S_TX_BUSY) == 0)
                     StartHardwareTransmit();
       }
       
       return kIOReturnSuccess;
}

The other part of data transfer is reading bytes that have been received from the hardware serial port. A serial driver will obtain data that has been received from its hardware device and copy it into its internal receive buffer. The exact means by which the hardware will notify the serial driver that data has been received will be implementation-specific, but may be signaled by a PCI interrupt or the completion of a USB transaction. The driver now needs to pass the received data on to the IOSerialBSDClient, which in turn will provide the data to a user space process.

The I/O Kit uses a pull model to return data from the serial driver to the IOSerialBSDClient class. The IOSerialBSDClient will call the driver's dequeueData() method to obtain data that has been received on the hardware serial port; the signature for this method is as follows:

IOReturn    dequeueData(UInt8* buffer, UInt32 size, UInt32* count, UInt32 min, void* refCon);

Upon receiving this method, the serial driver should copy data from its internal receive buffer to the provided parameter buffer. The parameter size describes the maximum number of bytes that the provided buffer can hold. The parameter count is used to return the actual number of bytes that were written to the provided buffer. The caller can request that the dequeueData() method block and not return to the caller until a minimum number of bytes are available; this is done by specifying a non-zero value in the min parameter, which provides the minimum number of bytes that the caller should return.

Rather than continually polling the dequeueData() method until data is available, the IOSerialBSDClient class will specify a minimum read size of 1 byte. The effect of this is to block in the call to dequeueData() but have the method return immediately as soon as the serial port has received data. The AppleUSBCDCDMM serial port driver implements this method by calling through to the watchState() method, and waiting until the PD_S_RXQ_EMPTY state is clear, indicating that data is available in the driver's receive buffer. An advantage of this design is that it ensures that the driver will unblock a wait in the dequeueData() method when the serial port is closed, since the watchState() method will abort if the PD_S_ACTIVE flag is ever cleared (which happens when the user process closes the serial port).

A sample implementation of the dequeueData() method is given in Listing 11-7. This implementation copies data out of the driver's internal receive buffer and into a buffer that has been provided by the caller of the method.

Listing 11-7. A Sample Implementation of the dequeueData() Method. The Method Is Assumed to Have Been Called Through an IOCommandGate.

IOReturn           MySerialDriver::dequeueDataGated(UInt8* buffer, UInt32 size, UInt32* count,
                                                    UInt32 min, void* refCon)
{
       // Abort if the serial port has not been acquired
       *count = 0;
       if ((m_currentState & PD_S_ACTIVE) == 0)
              return kIOReturnNotOpen;
       
       // Copy data from the driver's receive buffer
       *count = RemovefromReceiveQueue(buffer, size);
       // Regenerate the status bits for the receive buffer
       CheckQueues(refCon);
       
       // Block if the caller has requested a minimum number of bytes
      while ((min > 0) && (*count < min))
       {
              UInt32          state;
              IOReturn        ret;
              
              // Wait until the driver's receive buffer is not empty, and try again
              state = 0;
              ret = watchState(&state, PD_S_RXQ_EMPTY, refCon);
              if (ret != kIOReturnSuccess)
                     return ret;
              
              // Copy further bytes from the driver's receive buffer
              *count += RemovefromReceiveQueue(buffer + *count, size - *count);
              CheckQueues(refCon);
       }
       
       return kIOReturnSuccess;
}

The sample implementations of enqueueData() in Listing 11-6 and dequeueData() in Listing 11-7 both call a hypothetical function named CheckQueues() after reading or writing to the internal transmit buffer or receive buffer. Although CheckQueues() is a hypothetical function, its role is one that is needed by any serial port driver. Its purpose is to examine the number of bytes held in the driver's internal transmit buffer or receive buffer, and to update the state flags that describe the driver's queues. These flags describe whether the transmit or receive queue is empty, full, contains fewer bytes than the low water level, or contains more bytes than the high water level. Since there may be threads that are waiting for the transmit buffer or receive buffer to reach a certain level, it is important that the serial port driver updates these status flags whenever it reads or writes to its internal buffers.

As well as being called from the methods enqueueData() and dequeueData(), as shown in Listing 11-6 and Listing 11-7, a serial port driver would also call the CheckQueues() function when data from the transmit buffer is removed and written over the hardware serial port, and when the hardware adds data that is has read from the serial port to the receive buffer. A sample implementation of the CheckQueues() method is provided in Listing 11-8.

Listing 11-8. A Sample Method to Update the Status Flags for the Driver's Internal Transmit Buffer. The Method Is Assumed to Have Been Called Through an IOCommandGate.

void MySerialDriver::CheckQueues(void* refCon)
{
       UInt32        usedSpace;
       UInt32        freeSpace;
       UInt32        newState;
       UInt32        deltaState;
       
       // Initialize newState with the state at function entry.
       newState = m_currentState;
       
       // Check the number of bytes used and free in the transmit buffer
       usedSpace = GetUsedSpaceInTransmitQueue();
       freeSpace = GetFreeSpaceInTransmitQueue();
       
       // Set the full/empty state for the transmit buffer
       if (freeSpace == 0)
       {
              newState |=  PD_S_TXQ_FULL;
              newState &= ~PD_S_TXQ_EMPTY;
       }
       else if (usedSpace == 0)
       {
              newState &= ~PD_S_TXQ_FULL;
              newState |=  PD_S_TXQ_EMPTY;
       }
       else
       {
              newState &= ~PD_S_TXQ_FULL;
              newState &= ~PD_S_TXQ_EMPTY;
       }
       
       // Set the low/high waterlevel state for the transmit buffer
       if (usedSpace < m_txLowWaterlevel)
              newState |=  PD_S_TXQ_LOW_WATER;
       else
              newState &= ~PD_S_TXQ_LOW_WATER;
       
       if (usedSpace > m_txHighWaterlevel)
              newState |= PD_S_TXQ_HIGH_WATER;
       else
              newState &= ~PD_S_TXQ_HIGH_WATER;
       
       // Perform the same checks on the receive buffer
       …      

       // Update any changed state bits
       deltaState = newState ^ m_currentState;
       setState(newState, deltaState, refCon);
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.237.194