PSL Implementation

Describes how to create the PSL implementation.

To create a port of the DMA Framework, you must create an implementation of the PSL for the DMA controller hardware.

Define and implement PSL-specific channel classes

The choice of class to be used for implementing DMA channels is governed by the mode of operation to be supported:

These three classes are defined in dma.h.

In addition, the PSL may need to store channel-specific data. One way of achieving this is by deriving a PSL-specific class from one of the generic ones. For example, the template scatter-gather implementation defines a TTemplateSgChannel class derived from TDmaSgChannel for storing data when appending new descriptors to a running channel:

class TTemplateSgChannel : public TDmaSgChannel
    {
public:
    TDmaDesc* iTmpDes;
    TPhysAddr iTmpDesPhysAddr;
    };

      

Define and implement the controller classes

A controller can either be a physical controller, or a logical one depending on which DMA mode you intend to support.

The PSL (platform-specific layer) must define one concrete controller class per supported controller. A controller class is derived from TDmac. The concrete class must implement all the pure virtual functions defined in TDmac, and, optionally the functions used to manipulate hardware descriptors.

Taking the template port as an example, the class is defined as:

class TTemplateDmac : public TDmac
    {
public:
    TTemplateDmac();
    TInt Create();
private:
    // from TDmac (PIL pure virtual)
    virtual void Transfer(const TDmaChannel& aChannel, const SDmaDesHdr& aHdr);
    virtual void StopTransfer(const TDmaChannel& aChannel);
    virtual TBool IsIdle(const TDmaChannel& aChannel);
    virtual TInt MaxTransferSize(TDmaChannel& aChannel, TUint aFlags, TUint32 aPslInfo);
    virtual TUint MemAlignMask(TDmaChannel& aChannel, TUint aFlags, TUint32 aPslInfo);
    // from TDmac (PIL virtual)
    virtual void InitHwDes(const SDmaDesHdr& aHdr, TUint32 aSrc, TUint32 aDest, TInt aCount,
                            TUint aFlags, TUint32 aPslInfo, TUint32 aCookie);
    virtual void ChainHwDes(const SDmaDesHdr& aHdr, const SDmaDesHdr& aNextHdr);
    virtual void AppendHwDes(const TDmaChannel& aChannel, const SDmaDesHdr& aLastHdr,
                             const SDmaDesHdr& aNewHdr);
    virtual void UnlinkHwDes(const TDmaChannel& aChannel, SDmaDesHdr& aHdr);
    // other
    static void Isr(TAny* aThis);
    inline TDmaDesc* HdrToHwDes(const SDmaDesHdr& aHdr);
private:
    static const SCreateInfo KInfo;
public:
    TTemplateSgChannel iChannels[KChannelCount];
    };

Notes:

  • The array of channels must be defined in the PSL controller class because only the PSL knows what kind of channels are used.

  • The PSL controller class must be allocated in the BSS section of the DMA kernel extension. The second phase constructor must be called from the extension entry point.

  • The C++ constructor must pass a TDmac::SCreateInfo structure to the TDmac constructor. This structure defines what the PIL (platform-independent layer) needs to know about the underlying DMA controller. The template TTemplateDmac constructor just passes a TDmac::ScreateInfo structure, which defines the layout of the (base class managed) descriptor pool to the base class constructor through a ctor list:

    TTemplateDmac::TTemplateDmac()
    //
    // Constructor.
    //
        : TDmac(KInfo)
        {}
    
              

    where KInfo is a TDmac::ScreateInfo type.

  • The PSL controller class needs a second phase constructor. This is the Create() function, which must:

    1. call the second phase constructor of the base class: TDmac::Create().

    2. perform any hardware-specific initialisation.

    3. bind and enable the DMA interrupt(s).

    In the template port, Create() calls the base class TDmac::Create() function and then initialises the template specific channel object members (the temporary descriptor fields used for appending requests to live channels), and performs platform specific initialisation for the interrupt dispatch of DMA interrupt events:

    TInt TTemplateDmac::Create()
    //
    // Second phase construction.
    //
        {
        TInt r = TDmac::Create(KInfo);                            // Base class Create()
        if (r == KErrNone)
            {
            __DMA_ASSERTA(ReserveSetOfDes(KChannelCount) == KErrNone);
            for (TInt i=0; i < KChannelCount; ++i)
                {
                TDmaDesc* pD = HdrToHwDes(*iFreeHdr);
                iChannels[i].iTmpDes = pD;
                iChannels[i].iTmpDesPhysAddr = DesLinToPhys(pD);
                iFreeHdr = iFreeHdr->iNext;
                }
            r = Interrupt::Bind(EAsspIntIdDma, Isr, this);
            if (r == KErrNone)
                {
                // TO DO: Map DMA clients (requests) to DMA channels here.
    
                r = Interrupt::Enable(EAsspIntIdDma);
                }
            }
        return r;
        }
              

Implement the channel allocator

Channel allocation is implemented in the PSL (platform-specific layer) because this is a hardware-specific operation. There are two basic options:

  • Preallocate, at design time, one channel per DMA-aware peripheral. This is the simplest approach, and it should be acceptable for most Symbian platform devices because the set of supported peripherals is closed. In this case, cookies passed by client device drivers map uniquely to DMA channels.

  • Full dynamic allocation. This is a simple approach too, but DMA channels are, in general, not completely identical. For example, some channels may have greater priorities than others.

Mixed schemes are also possible, for example, client device driver cookies could be used to select a subset of channels, and dynamic allocation used inside this subset.

Whatever option is chosen, the PSL must provide an implementation for the function DmaChannelMgr::Open(). The template implementation is:

TDmaChannel* DmaChannelMgr::Open(TUint32 aOpenId)
//
//
//
    {
    __KTRACE_OPT(KDMA, Kern::Printf(">DmaChannelMgr::Open aOpenId=%d", aOpenId));

    __DMA_ASSERTA(aOpenId < static_cast<TUint32>(KChannelCount));

    TDmaChannel* pC = Controller.iChannels + aOpenId;
    if (pC->IsOpened())
        pC = NULL;
    else
        {
        pC->iController = &Controller;
        pC->iPslId = aOpenId;
        }

    return pC;
    }
      

The template DMA channel manager returns a pointer to a DMA channel if the channel has not been previously allocated. Note that since channels possess preset priorities, the device drivers must be aware of which channel(s) they require DMA service from and configure the DMA controller to route sources to allocated channels accordingly.

The platform-specific cookies passed by client device drivers to the PSL must be defined somewhere so that client device drivers can access them.

[Optionally] implement channel cleanup

In the template PSL (platform-specific layer), the function DmaChannelMgr::Close() is a no-operation. This function is called by the PIL (platform-independent layer) when client device drivers call TDmaChannel::Close(). If the PSL needs to perform any hardware-specific operation when the channel is closed, then the implementation of DmaChannelMgr::Close() should be updated to reflect that.

Implement the mandatory controller virtual functions

The TDmac class contains several pure virtual functions that must be implemented by the PSL (platform-specific layer):

These functions start and stop transfers on a DMA channel and are the main interface between the PIL (platform-independent layer) and the PSL. The implementation of these functions depends on the hardware available for performing DMA, and on the characteristics used to specify a DMA transfer:

  • the source and destination addresses

  • the burst size

  • the maximum transfer size

  • the transfer width, i.e. number of bits per memory access

  • the memory alignment and endianness.

The DMA Framework manages the transfer descriptors according to the descriptor parameter passed into the TDmac constructor by the derived class constructor; the descriptor parameter is a TDmac::SCreateInfo structure. The per-request transfer parameters are passed into the descriptors for each request issued by a driver.

The transfer function: Transfer()

This function initiates a previously constructed request on a specific channel. This is the template implementation:

void TTemplateDmac::Transfer(const TDmaChannel& aChannel, const SDmaDesHdr& aHdr)
//
// Initiates a (previously constructed) request on a specific channel.
//
    {
    const TUint8 i = static_cast<TUint8>(aChannel.PslId());
    TDmaDesc* pD = HdrToHwDes(aHdr);

    __KTRACE_OPT(KDMA, Kern::Printf(">TTemplateDmac::Transfer channel=%d des=0x%08X", i, pD));

    // TO DO (for instance): Load the first descriptor address into the DMAC and start it
    // by setting the RUN bit.
    (void) *pD, (void) i;

    }
        

The stop transfer function: StopTransfer()

This function requires that the RUN mode is cleared. This is the template implementation:

void TTemplateDmac::StopTransfer(const TDmaChannel& aChannel)
//
// Stops a running channel.
//
    {
    const TUint8 i = static_cast<TUint8>(aChannel.PslId());

    __KTRACE_OPT(KDMA, Kern::Printf(">TTemplateDmac::StopTransfer channel=%d", i));

    // TO DO (for instance): Clear the RUN bit of the channel.
    (void) i;

    }
        

The function: IsIdle()

IsIdle() returns the state of a given channel. This is the template implementation:

TBool TTemplateDmac::IsIdle(const TDmaChannel& aChannel)
//
// Returns the state of a given channel.
//
    {
    const TUint8 i = static_cast<TUint8>(aChannel.PslId());

    __KTRACE_OPT(KDMA, Kern::Printf(">TTemplateDmac::IsIdle channel=%d", i));

    // TO DO (for instance): Return the state of the RUN bit of the channel.
    // The return value should reflect the actual state.
    (void) i;

    return ETrue;
    }
        

Implement the non-mandatory controller virtual functions

The following auxiliary functions are used to implement the scatter-gather transfer mode behaviour by creating and manipulating the linked list of transfer fragment headers that describe a given scatter-gather transaction. They are called by the TDmac base class functions when the instance of the TDmac derived class reports itself as being capable of scatter-gather operations.

First scatter-gather support function: InitHwDes()

This is a function for creating a scatter-gather list. From the information in the passed-in request, the function sets up the descriptor with that fragment's:

  • source and destination address

  • size

  • driver/DMA controller specific transfer parameters: memory/peripheral, burst size, transfer width.

This is the template implementation:

void TTemplateDmac::InitHwDes(const SDmaDesHdr& aHdr, TUint32 aSrc, TUint32 aDest, TInt aCount,
                              TUint aFlags, TUint32 aPslInfo, TUint32 /*aCookie*/)
//
// Sets up (from a passed in request) the descriptor with that fragment's source and destination address,
// the fragment size, and the (driver/DMA controller) specific transfer parameters (mem/peripheral,
// burst size, transfer width).
//
    {
    TDmaDesc* pD = HdrToHwDes(aHdr);

    __KTRACE_OPT(KDMA, Kern::Printf("TTemplateDmac::InitHwDes 0x%08X", pD));

    // Unaligned descriptor? Error in generic layer!
    __DMA_ASSERTD(IsHwDesAligned(pD));

    pD->iSrcAddr = (aFlags & KDmaPhysAddrSrc) ? aSrc : Epoc::LinearToPhysical(aSrc);
    pD->iDestAddr = (aFlags & KDmaPhysAddrDest) ? aDest : Epoc::LinearToPhysical(aDest);
    pD->iCmd = DcmdReg(aCount, aFlags, aPslInfo);
    pD->iDescAddr = TDmaDesc::KStopBitMask;
    }

          

Second scatter-gather support function: ChainHwDes()

If the framework needs to fragment the client’s request, for transfer size or memory discontiguousness reasons, then the framework calls this function. It chains hardware descriptors together by setting the next pointer of the original descriptor to the physical address of the descriptor to be chained. It assumes that the DMAC channel is quiescent when called.

This is the template implementation:

void TTemplateDmac::ChainHwDes(const SDmaDesHdr& aHdr, const SDmaDesHdr& aNextHdr)
//
// Chains hardware descriptors together by setting the next pointer of the original descriptor
// to the physical address of the descriptor to be chained.
//
    {
    TDmaDesc* pD = HdrToHwDes(aHdr);
    TDmaDesc* pN = HdrToHwDes(aNextHdr);

    __KTRACE_OPT(KDMA, Kern::Printf("TTemplateDmac::ChainHwDes des=0x%08X next des=0x%08X", pD, pN));

    // Unaligned descriptor? Error in generic layer!
    __DMA_ASSERTD(IsHwDesAligned(pD) && IsHwDesAligned(pN));

    // TO DO: Modify pD->iCmd so that no end-of-transfer interrupt gets raised any longer.

    pD->iDescAddr = DesLinToPhys(pN);
    }

Third scatter-gather support function: AppendHwDes()

This function is called by the TDmac base class when a driver request is called for a channel that is still active, i.e. where the intent is to provide data streaming so that the target device is constantly provided with data; for example, an audio device playing a track. In this case, the function provided by the derived class must:

  • stop the DMAC to prevent any corruption of the scatter-gather list while appending the new fragment descriptor

  • append the new descriptor

  • re-enable the channel, ideally before the target has detected the gap in service.

This is the template implementation:

void TTemplateDmac::AppendHwDes(const TDmaChannel& aChannel, const SDmaDesHdr& aLastHdr,
                                const SDmaDesHdr& aNewHdr)
//
// Appends a descriptor to the chain while the channel is running.
//
    {
    const TUint8 i = static_cast<TUint8>(aChannel.PslId());

    TDmaDesc* pL = HdrToHwDes(aLastHdr);
    TDmaDesc* pN = HdrToHwDes(aNewHdr);

    __KTRACE_OPT(KDMA, Kern::Printf(">TTemplateDmac::AppendHwDes channel=%d last des=0x%08X new des=0x%08X",
                                    i, pL, pN));
    // Unaligned descriptor? Error in generic layer!
    __DMA_ASSERTD(IsHwDesAligned(pL) && IsHwDesAligned(pN));

    TPhysAddr newPhys = DesLinToPhys(pN);

    const TInt irq = NKern::DisableAllInterrupts();
    StopTransfer(aChannel);

    pL->iDescAddr = newPhys;
    const TTemplateSgChannel& channel = static_cast<const TTemplateSgChannel&>(aChannel);
    TDmaDesc* pD = channel.iTmpDes;

    // TO DO: Implement the appropriate algorithm for appending a descriptor here.
    (void) *pD, (void) i;

    NKern::RestoreInterrupts(irq);

    __KTRACE_OPT(KDMA, Kern::Printf("<TTemplateDmac::AppendHwDes"));
    }
    

Implement an interrupt service routine

The interrupt service routine needs to do the following:

  • identify the channel that raised the interrupt

  • decide whether the interrupt was raised because of a successful data transfer or because of an error

  • call the base class function TDmac::HandleIsr(), which queues a DFC, or increments the number of pending interrupts if a DFC is already queued.

This is the template implementation:

void TTemplateDmac::Isr(TAny* aThis)
//
// This ISR reads the interrupt identification and calls back into the base class
// interrupt service handler with the channel identifier and an indication whether the
// transfer completed correctly or with an error.
//
    {
    TTemplateDmac& me = *static_cast<TTemplateDmac*>(aThis);

    // TO DO: Implement the behaviour described above, call HandleIsr().

    HandleIsr(me.iChannels[5], 0);                            // Example

    }
     

Implement the test support table and function

The DMA Framework comes with a test harness that can be used to validate the port if the underlying DMA controller supports memory to memory transfers.

The test harness needs to know about the capabilities of the port being tested. The PSL provides the global function DmaTestInfo() that returns a TDmaTestInfo object that contains this information. In the template PSL, this structure is initialised to binary zeroes. Before using the test harness, it must be initialised with valid values.

See TDmaTestInfo, DmaTestInfo() and Validation.

Optimise the performance of critical functions

You can optionally optimise critical functions by writing them in assembler. The two candidates for an assembler rewrite are:

  • The interrupt service routine

  • In scatter-gather mode, the TDmac::AppendHwDes() function if it needs to suspend a transfer when appending a new descriptor chain to an existing one.

Extend the framework with platform-specific functionality

There are two ways of extending the DMA Framework:

  1. to provide platform-specific functionality on a per-channel basis.

  2. to provide platform-specific functionality on a channel independent basis.

In the first case, the PSL provides an implementation of the virtual function TDmac::Extension(). The default implementation just returns KErrNotSupported.

TDmaChannel::Extension() calls TDmac::Extension().

In the second case, the PSL provides an implementation of the static function DmaChannelMgr::StaticExtension(). The template PSL implementation just returns KErrNotSupported.

TDmaChannel::StaticExtension() calls DmaChannelMgr::StaticExtension().