Programming the USB module of PIC32MZ without Harmony (7) – Put USBDMA to work

The missing recipe from the Microchip’s datasheet

Since Microchip doesn’t provide enough details with regard to the correct configuration of the USBDMA module, I had to piece together information from all sorts of sources. The lack of detailed explanation of each USBDMA register is posing significant challenges to make the USB DMA work without relying on the harmony library. Some of the discoveries were obtained after a lot of trial and errors on the actual device. The following codes have been verified to work for my project, and explanation of each register is inserted in line for your reference.

As I explained in my 3rd post of this series, the USBDMA initialization code needs to happen during the CPU start-up phase.

void initUSBDMA()
{
    
    IEC4bits.USBDMAIE = 0;  // Disable USB DMA interrupt.
    IFS4CLR = _IFS4_USB_DMA_EVENT_MASK;
    
    USBDMAINT = 0x00;       // Clear Channels 1-8 USB DMA Interrupt Flags
    
    // Set the USB DMA Interrupt Priority and Sub-Priority Levels
    IPC33bits.USBDMAIP = 5;
    IPC33bits.USBDMAIS = 3;
    
    // USBDPBFD: USB Double Packet Buffer Disable Register
    USBDPBFD = 0x00FF00FF;
    
    // DMABRSTM<1:0>: DMA Burst Mode Selection bit
    // 11 = Burst Mode 3: INCR16, INCR8, INCR4 or unspecified length 
    // 10 = Burst Mode 2: INCR8, INCR4 or unspecified length
    // 01 = Burst Mode 1: INCR4 or unspecified length
    // 00 = Burst Mode 0: Bursts of unspecified length
    USBDMA1Cbits.DMABRSTM = 3;
    USBDMA2Cbits.DMABRSTM = 3;
    USBDMA3Cbits.DMABRSTM = 3;
    
    // DMAEP<3:0>: DMA Endpoint Assignment bits
    // These bits hold the endpoint that the DMA channel is assigned to. 
    // Valid values are 0-7.
    USBDMA1Cbits.DMAEP = 1; // Assign DMA 1 to Endpoint 1 (RX)
    USBDMA2Cbits.DMAEP = 2; // Assign DMA 2 to Endpoint 2 (RX)
    USBDMA3Cbits.DMAEP = 3; // Assign DMA 3 to Endpoint 3 (TX)
    
    // DMAREQEN: Endpoint DMA Request Enable bit
    // 1 = DMA requests are enabled for this endpoint 
    // 0 = DMA requests are disabled for this endpoint
    // USBIENCSR0 for TX endpoint, USBIENCSR1 for RX endpoint
    USBE1CSR1bits.DMAREQEN = 1; // CSR1 for RX
    USBE1CSR1bits.DMAREQMD = 0; // CSR1 for RX
    
    USBE2CSR1bits.DMAREQEN = 1; // CSR1 for RX
    USBE2CSR1bits.DMAREQMD = 0; // CSR1 for RX
    
    USBE3CSR0bits.DMAREQEN = 1; // CSR0 for TX
    USBE3CSR0bits.DMAREQMD = 0; // CSR0 for TX
    //**************************************************************************
    // DMAREQMD: DMA Request Mode Selection bit 
    // 1 = DMA Request Mode 1
    // 0 = DMA Request Mode 0
    // USBIENCSR0 for TX endpoint, USBIENCSR1 for RX endpoint
    
    
    //USBE3CSR1bits.DMAREQMD = 0;
    
    // DMAMODE: DMA Transfer Mode bit 
    // 1 = DMA Mode1 Transfers
    // 0 = DMA Mode0 Transfers
    USBDMA1Cbits.DMAMODE = 0;
    USBDMA2Cbits.DMAMODE = 0;
    USBDMA3Cbits.DMAMODE = 0;
    //**************************************************************************
    
    // DMADIR: DMA Transfer Direction bit
    // 1 = DMA Read (TX endpoint)
    // 0 = DMA Write (RX endpoint)
    USBDMA1Cbits.DMADIR = 0;
    USBDMA2Cbits.DMADIR = 0;
    USBDMA3Cbits.DMADIR = 1; // RX
    
    // DMAADDR<31:0>: DMA Memory Address bits
    // This register identifies the current memory address of the corresponding 
    // DMA channel. The initial memory address written to this register during 
    // initialization must have a value such that its modulo 4 value is equal 
    // to '0'. The lower two bits of this register are read only and cannot be 
    // set by software. As the DMA transfer progresses, the memory address will 
    // increment as bytes are transferred.
    USBDMA1Abits.DMAADDR = KVA_TO_PA(ep1rx_buffer);   // USB DMA1 memory addr
    USBDMA2Abits.DMAADDR = KVA_TO_PA(ep2rx_buffer_a);   // USB DMA2 memory addr
    USBDMA3Abits.DMAADDR = KVA_TO_PA(ep3tx_buffer);     // USB DMA3 memory addr
    
    // TXEDMA: TX Endpoint DMA Assertion Control bit
    // 1 = DMA_REQ signal for all IN endpoints will be deasserted when MAXP-8 bytes 
    // have been written to an endpoint. This is Early mode.
    // 0 = DMA_REQ signal for all IN endpoints will be deasserted when MAXP bytes 
    // have been written to an endpoint. This is Late mode.
    USBOTGbits.TXEDMA = 0;
    
    // RXEDMA: RX Endpoint DMA Assertion Control bit
    // 1 = DMA_REQ signal for all OUT endpoints will be deasserted when MAXP-8 bytes 
    // have been written to an endpoint. This is Early mode.
    // 0 = DMA_REQ signal for all OUT endpoints will be deasserted when MAXP bytes 
    // have been written to an endpoint. This is Late mode.
    USBOTGbits.RXEDMA = 0;
    
    // DMAIE: USB DMA 1 Interrupt Enable bit
    // 1 = Interrupt is enabled for this channel
    // 0 = Interrupt is disabled for this channel
    USBDMA1Cbits.DMAIE = 1;
    USBDMA2Cbits.DMAIE = 1;
    USBDMA3Cbits.DMAIE = 1;   
    
    IEC4bits.USBDMAIE = 1; // Enable USB DMA interrupt.
}

Once the USB DMA has been initiated, the CPU doesn’t need to pay attention to the USB transaction at all until the _USB_DMA_VECTOR flag is set, which is signaling that at least one of the USB DMA channels has finished transaction. Let’s take a look at the ISR code below:

volatile _Bool is_usb_dma3_ep3_tx_busy = false;
volatile _Bool is_usb_dma2_ep2_rx_done = false;
volatile _Bool is_usb_dma1_ep1_rx_done = false;

void __attribute__((interrupt(ipl5srs), at_vector(_USB_DMA_VECTOR), no_fpu, nomips16)) USBDMA_Handler(void){
    uint32_t usbdmaint = USBDMAINT; // All bits are cleared on a read of this
                                    // No need to clear individual flag
    
    _Bool dma1if = (usbdmaint & (1<<0)) ? true : false;
    _Bool dma2if = (usbdmaint & (1<<1)) ? true : false;
    _Bool dma3if = (usbdmaint & (1<<2)) ? true : false;

    if(dma1if == 1){ 
        is_usb_dma1_ep1_rx_done = true;
        
        USBE1CSR1bits.RXPKTRDY = 0;
        USBDMA1Cbits.DMAEN = 0;
    }
    
    if(dma2if == 1){
        /* do something */
        is_usb_dma2_ep2_rx_done = true;

        /* !IMPORTANT! */
        /*
         * RXPKTRDY should only be cleared after data has finished offloading
         * from RX FIFO. Otherwise data will be lost.
         * Clearing RXPKTRDY also affects RXCNT
         */

        USBE2CSR1bits.RXPKTRDY = 0;
        USBDMA2Cbits.DMAEN = 0;
    }
    
    if(dma3if)
    {
        /*
         * When it reaches here, the data has been successfully transferred from
         * ep3tx_buffer to endpoint3's FIFO. 
         * 
         * Next step is to start the transfer from EP3 FIFO to host
         */
        USBE3CSR0bits.TXPKTRDY = 1; // Start the data transfer
        USBDMA3Cbits.DMAEN = 0; // Disable DMA to avoid false-trigging
        // Wait till data has been transmitted out of FIFO
    }
    
    if(USBDMA2Cbits.DMAERR){
        // Clear the error
        USBDMA2Cbits.DMAERR = 0;
    }
    
    IFS4CLR = _IFS4_USB_DMA_EVENT_MASK;
}

First of all, make sure the register “USBDMAINT” is read only once because it will be cleared upon read. In this example, USB endpoint 1 and 2 are configured as “OUT/TX” (host➡device) whereas the USB endpoint 3 is configured as “IN/RX” (host⬅device). When “dma2if” flag is set, all the data has been successfully transmitted from the USB FIFO to “ep2rx_buffer_a”, the CPU can immediately start accessing the data. The data can be very large, may require several packets to finish the transaction, but you don’t need to worry about anything because the DMA handles everything for you automatically. The only thing you need to do is to set “USBDMA2Cbits.DMAEN” again so that the DMA will be re-armed for the next transaction.

The USB endpoint 3 (IN/RX) proceeds slightly differently but with the similar DMA concepts. Once your program loads all the application data into a buffer (ep3tx_buffer in this case), set USBDMA3Cbits.DMAEN to trigger the DMA to start uploading data from the buffer to the USB FIFO. A USBDMA event will be triggered once this upload is completed, it’s the “dma3if” branch in this case. Setting USBE3CSR0bits.TXPKTRDY will trigger further transmission of the data from the FIFO to the host, but it’s all the DMA’s jobs, your CPU can now spend time on other stuffs. Clear the USBDMA2Cbits.DMAEN so that it can be used again to set the start of next IN/RX transaction.

More notes:

  1. RXPKTRDY should only be cleared after data has finished offloading off FIFO, because clearing RXPKTRDY seems to zero out the FIFO. Clearing RXPKTRDY also affects RXCNT.
  2. If the RXPKTRDY is not cleared, it will prevent the device from receiving the next data packet. Host program (PyUSB in my case) will report “timeout error” after sending the data.
  3. When AUTOSET is set to 0, TXPKTRDY must be set manually. Setting TXPKTRDY to 1 will initiate the data transfer from FIFO to the host.
  4. USB events trigger the flags only after a transaction is completed, e.g. when EP1RXIF is flagged, the data is already in the FIFO of the corresponding endpoint. Same for the USB DMA event.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.