This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

EK-TM4C123GXL: External Serial Nor Flash + SSI + uDMA

Part Number: EK-TM4C123GXL

Hi,

I already have a working interrupt driven SSI to communicate with my Winbond flash and recently I have dived into the realm of DMA. I thought it would be great to incorporate it with my Winbond code to make data transfer faster and to reduce CPU load. Has anyone done it before? I can't seem to find anything about this in the forum.

Currently, I have created an spi_transfer function which, when called, will setup and transfer the command / address bytes first. Then after the DMA transfer has completed, the SSI ISR will be triggered. Once triggered, the ISR will setup another DMA transfer in order to write / read data bytes that we are interested in.

For example, if I want to read the device id and manufacturer id of the winbond flash. The flash requires the master to send WB_READ_ID followed by 3 bytes of 0x00, then the flash will spit out the device id + manufacturer id. Below is a diagram to illustrate the transfer:

XX = don't care

// Transmitting 4 data bytes and expects two bytes of device id + manufacturer id

TX                                RX

WB_READ_ID            XX ----
0x00                             XX      |______ Transferred during the first DMA transfer
0x00                             XX      |
0x00                             XX ----

// Master needs to drive clock to allow man_id and dev_id to be shifted out

0x00                             man_id ----_____ Transferred during the second DMA transfer
0x00                             dev_id   __|

My code is as follows:

// Initiate transfer by transferring cmd / addr bytes first

//*****************************************************************************
//
// Initiate communication with flash by transferring command / address bytes
//
//*****************************************************************************
void ssi_transfer(uint8_t *cmd_buf, uint32_t cmd_count, uint8_t *tx_buf,
                  uint32_t tx_count, uint8_t *rx_buf, uint32_t rx_count) {

    g_ui8SSITxBuf = tx_buf;
    g_ui8SSIRxBuf = rx_buf;

    g_ui32SSITxCount = tx_count;
    g_ui32SSIRxCount = rx_count;

    //
    // Enable the uDMA interface for both TX and RX channels.
    //
    SSIDMAEnable(SSI0_BASE, SSI_DMA_RX | SSI_DMA_TX);

    //*************************************************************************
    //uDMA SSI0 RX
    //*************************************************************************

    //
    // Put the attributes in a known state for the uDMA SSI0RX channel.  These
    // should already be disabled by default.
    //
    uDMAChannelAttributeDisable(UDMA_CHANNEL_SSI0RX,
                                UDMA_ATTR_USEBURST |
                                UDMA_ATTR_ALTSELECT |
                                (UDMA_ATTR_HIGH_PRIORITY | UDMA_ATTR_REQMASK));

    //
    // Configure the control parameters for the primary control structure for
    // the SSIORX channel.
    //
    uDMAChannelControlSet(UDMA_CHANNEL_SSI0RX | UDMA_PRI_SELECT,
                          UDMA_SIZE_8 | UDMA_SRC_INC_NONE |
                          UDMA_DST_INC_8 | UDMA_ARB_4);
    //
    // Set up the transfer parameters for the SSI0RX Channel
    //
    uDMAChannelTransferSet(UDMA_CHANNEL_SSI0RX | UDMA_PRI_SELECT,
                           UDMA_MODE_BASIC,
                           (void *)(SSI0_BASE + SSI_O_DR),
                           cmd_buf,
                           cmd_count);


    //*************************************************************************
    //uDMA SSI0 TX
    //*************************************************************************

    //
    // Put the attributes in a known state for the uDMA SSI0TX channel.  These
    // should already be disabled by default.
    //
    uDMAChannelAttributeDisable(UDMA_CHANNEL_SSI0TX,
                                UDMA_ATTR_ALTSELECT |
                                UDMA_ATTR_HIGH_PRIORITY |
                                UDMA_ATTR_REQMASK);

    //
    // Configure the control parameters for the SSI0 TX.
    //
    uDMAChannelControlSet(UDMA_CHANNEL_SSI0TX | UDMA_PRI_SELECT,
                          UDMA_SIZE_8 | UDMA_SRC_INC_8 |
                          UDMA_DST_INC_NONE | UDMA_ARB_4);


    //
    // Set up the transfer parameters for the uDMA SSI0 TX channel.  This will
    // configure the transfer source and destination and the transfer size.
    // Basic mode is used because the peripheral is making the uDMA transfer
    // request.  The source is the TX buffer and the destination is the SSI0
    // data register.
    //
    uDMAChannelTransferSet(UDMA_CHANNEL_SSI0TX | UDMA_PRI_SELECT,
                           UDMA_MODE_BASIC,
                           cmd_buf,
                           (void *)(SSI0_BASE + SSI_O_DR),
                           cmd_count);

    // Begin sending command / address bytes and pop out return bytes
    chip_select(~GPIO_PIN_3);

    //
    // Now both the uDMA SSI0 TX and RX channels are primed to start a
    // transfer.  As soon as the channels are enabled, the peripheral will
    // issue a transfer request and the data transfers will begin.
    //
    uDMAChannelEnable(UDMA_CHANNEL_SSI0RX);
    uDMAChannelEnable(UDMA_CHANNEL_SSI0TX);

    //
    // Enable the SSI0 DMA TX/RX interrupts.
// Do I need to include SSI_DMATX as well? // SSIIntEnable(SSI0_BASE, SSI_DMARX); // Wait until the SSI chip select de-asserts, indicating the end of the // transfer. while (!(GPIOPinRead(GPIO_PORTA_BASE, GPIO_PIN_3) & GPIO_PIN_3)); }

// ISR

void SSI0IntHandler(void) {
    GPIOPinWrite(GPIO_PORTB_BASE, GPIO_PIN_2, GPIO_PIN_2); // indicate ISR entry
uint32_t ui32Status; uint32_t ui32Mode; ui32Status = SSIIntStatus(SSI0_BASE, 1); SSIIntClear(SSI0_BASE, ui32Status); if (g_cmd_sent) { chip_select(GPIO_PIN_3); } else { // Enters here meaning command / address bytes were sent // Setup another transfer for the data that we're interested g_cmd_sent = 1; // signal that cmd and addr were sent so next // ISR entry will pull SS line HIGH ui32Mode = uDMAChannelModeGet(UDMA_CHANNEL_SSI0RX | UDMA_PRI_SELECT); // RX DMA complete // If it's a write, g_rx_dst_inc will be UDMA_DST_INC_NONE // if read, g_rx_dst_inc = UDMA_DST_INC_8 if (ui32Mode == UDMA_MODE_STOP) { uDMAChannelControlSet(UDMA_CHANNEL_SSI0RX | UDMA_PRI_SELECT, UDMA_SIZE_8 | UDMA_SRC_INC_NONE | g_rx_dst_inc | UDMA_ARB_4); uDMAChannelTransferSet(UDMA_CHANNEL_SSI0RX | UDMA_PRI_SELECT, UDMA_MODE_BASIC, (void *)(SSI0_BASE + SSI_O_DR), g_ui8SSIRxBuf, g_ui32SSIRxCount); uDMAChannelEnable(UDMA_CHANNEL_SSI0RX); } // TX DMA complete // If it's a write, g_tx_dst_inc will be UDMA_SRC_INC_8 // if read, g_tx_src_inc = UDMA_SRC_INC_NONE if (!uDMAChannelIsEnabled(UDMA_CHANNEL_SSI0TX)) { uDMAChannelControlSet(UDMA_CHANNEL_SSI0TX | UDMA_PRI_SELECT, UDMA_SIZE_8 | g_tx_src_inc | UDMA_DST_INC_NONE | UDMA_ARB_4); uDMAChannelTransferSet(UDMA_CHANNEL_SSI0TX | UDMA_PRI_SELECT, UDMA_MODE_BASIC, g_ui8SSITxBuf, (void *)(SSI0_BASE + SSI_O_DR), g_ui32SSITxCount); uDMAChannelEnable(UDMA_CHANNEL_SSI0TX); } } GPIOPinWrite(GPIO_PORTB_BASE, GPIO_PIN_2, ~GPIO_PIN_2); // indicate ISR exit }

So my problem is, when I step debug the program, I will get the correct man_id and dev_id (0xef and 0x17) but when I do a full run, the IDs are wrong (0x90, 0xff). Here is a screenshot of the full run on the logic analyzer:




As shown in the screenshot, the SS line is pulled HIGH too soon and I am not sure why.

Just to make sure my DMA is working properly, I have also tried to transfer all the data in one DMA transfer instead of two (as described earlier) which gives me the correct result as shown in the screenshot below:

Is there something that I am missing or not understanding correctly about uDMA?

Best Regards and Thanks in Advance

Jacky

  • Just a follow up to the first post, the reason I want to split the cmd/addr and actual data transfer is that I don't want the return data bytes from the cmd / addr bytes to mess with my destination data buffers. For example, in the event of a read operation on the flash, if we transfer cmd/addr and the actual data of interest in a single DMA transfer, the read destination buffer provided by the user will contain 4 extra bytes (return bytes from the cmd/addr) which is kind of inconvenient.

    This is the solution that I came up to make the API more user friendly. Is there a better way to go about this? I really would like to hear from others.

    Thanks,
    Jacky
  • Hi,

    Why are you using cmd_buf and cmd_count for your receive channel? I thought cmd_buf and cmd_count are meant for the TX channel.

    uDMAChannelTransferSet(UDMA_CHANNEL_SSI0RX | UDMA_PRI_SELECT,
    UDMA_MODE_BASIC,
    (void *)(SSI0_BASE + SSI_O_DR),
    cmd_buf,
    cmd_count);

    It seems to me that the uDMA peripheral scatter-gather is well suited for your application requirement even though your current setup should work too if all you want is to read the manufacturing and device id.
  • Hi Charles,

    I use cmd_buf for RX because I don't want to create an extra variable to store the returned bytes from cmd / addr transfer. Also, since we receive the same amount of data bytes as we transmit, therefore both TX and RX share the same transfer size of cmd_count.

    Btw, is there a reason behind why it works out when I put everything in one DMA transfer and fails when I break it down into multiple DMA transfer? (as shown in the original post with screenshots)
    Or I guess I should ask if there is anything in my DMA setup or configuration that causes problems to the transfer.

    In the mean time, I will look into scatter-gather method and keep the post updated.

    Thanks,

    Jacky

  • Charles Tsai said:


    It seems to me that the uDMA peripheral scatter-gather is well suited for your application requirement even though your current setup should work too if all you want is to read the manufacturing and device id.

    Are you saying if I were to read / write large amount of data, then my method will not be as efficient?

    Thanks,

    Jacky

  • Hi,

    I think if all you want is to read the manufacturing and device ID then your original attempt where only one DMA channel is configured to transfer six bytes of data is the easiest. You just need to ignore the first 4 bytes on the MISO from the SPI as the first 4 bytes corresponds to the cmd cycles. The id comes on the fifth and sixth byte. Why do you want to break it into two different DMA transfers as shown in your code?
  • Hi Charles,

    I intend to do read/write/erase operations on the flash as well. The ID reading is just to make sure my basic SSI transfer function works.

    What would you suggest If I plan to do so? Could I have done read/write/erase using the basic mode?


    Thanks,

    Jacky

  • I'm not familiar with your specific NOR flash. For reading the manufacturing/device id, it takes six bytes of cmd and data cycles as you indicated. What about the write operation and erase operations? How are you giving the command and data for these operations? The read/write/erase are three separate operations where I think the peripheral scatter-gather is well suited. For these three operations you will first define three alternate DMA control structures in the memory and upon detecting a request from the peripheral, the μDMA controller uses the primary control structure to copy one entry from the list to the alternate control structure and then performs the transfer. Please refer to the datasheet for details.
  • Charles Tsai said:


    Why do you want to break it into two different DMA transfers as shown in your code?

    So the reason that I want to break it into two different DMA transfers is that I don't want the return data bytes from sending the cmd / addr bytes to mess with my destination data buffers. In my case for example, the flash read function has to be like the following, (takes in three parameters from the user and returns void. The content within the function can be modified just as long as the read function parameter and return type remains)

    void my_spi_read(uint32_t addr, uint32_t num_byte, uint8_t *dst) {
        uint8_t cmd[4];
    
        cmd[0] = WB_READ_DATA;
        cmd[1] = (addr >> 16) & 0xFF;
        cmd[2] = (addr >> 8) & 0xFF;
        cmd[3] = (addr >> 0) & 0xFF;
    
        g_rx_dst_inc = UDMA_DST_INC_8;
        g_tx_src_inc = UDMA_SRC_INC_NONE;
    
        uint8_t tx_dummy = 0x00;
    
        ssi_transfer(cmd, 4, &tx_dummy, num_byte, dst, num_byte);
    
        while (chip_busy() & 1);
    }

    My thought was, if I put the cmd / addr transfer in a separate DMA transfer, then my destination data buffer will not contain the return bytes from sending cmd / addr.

    I want the user of this function to just enter a starting address within the flash, the number of bytes to read and provide a destination data buffer (with buffer size = number of bytes to read) to store the data read.

    Eg. user wants to read 256 bytes starting at addr 0x00, then the destination data buffer will only contain 256 bytes of data (not containing return bytes from sending cmd / addr)

    I hope this makes my problem clearer. Do you think scatter-gather method is better in this case (maybe have cmd/addr as one task and the rest as another task?) or do you have other ways to go about this? If so, how would you approach this problem?

    Thank you very much for your help!

    Regards,

    Jacky

  • Jacky,
    The scatter-gather will work and will be a better solution if you have more complex transfer requirements in the future. For now, you might want to look at the ping-pong mode instead. In ping-pong mode you will still setup both the primary and the alternate control structures. In the primary structure, the goal is to transfer the cmd/address to the flash device. You will setup a destination buffer (let's call it bufferA) for the primary structure but the data stored in the bufferA will be ignored. Once the primary structure is complete the uDMA will trigger the alternate control structure by itself. The alternate control structure is setup to transfer the N bytes of data which will be stored in the bufferB. The bufferA and bufferB are separate and there is no need to worry messing them up together if that is your concern.
  • Hi Charles,

    So I think my code in the earlier post did not work out is due to the DMA being in basic mode which leaves a dead time between reload and next transfer?

    Regards,

    Jacky

  • Hi Charles,

    I have setup two primary structures (one for cmd / addr and one for actual data) and two alternate structures (one for cmd / addr and one for actual data) for the DMA transfer and my communication with the flash worked out fine.

    Assuming my length of data will not exceed 1024, which means I will never reload, what is the best way to properly exit the DMA? So far, I am checking the SSIRX channel of the alternate structure for stop mode signal in the ISR like so:

    void SSI0IntHandler(void) {
        GPIOPinWrite(GPIO_PORTB_BASE, GPIO_PIN_2, GPIO_PIN_2); // indicate ISR entry
    
        uint32_t ui32Status;
        uint32_t ui32Mode;
    
        ui32Status = SSIIntStatus(SSI0_BASE, 1);
    
        SSIIntClear(SSI0_BASE, ui32Status);
    
        ui32Mode = uDMAChannelModeGet(UDMA_CHANNEL_SSI0RX | UDMA_ALT_SELECT);
    
        // Data transfer done, pull CS high
        if (ui32Mode == UDMA_MODE_STOP) {
            IntDisable(INT_UDMAERR);
            IntDisable(INT_SSI0);
            chip_select(GPIO_PIN_3);
        }
    
        GPIOPinWrite(GPIO_PORTB_BASE, GPIO_PIN_2, ~GPIO_PIN_2); // indicate ISR entry
    }

    Is this a safe way to do or should I check if all channels are at stop mode before actually stopping (by pulling the SS line high)?

    Thanks,

    Jacky

  • Hi Jacky,

    As for as ending the DMA transfers with what you have, I don't see a problem. My only question is why do want to setup two different channels? I thought one ping-pong channel is enough where the primary transfers the cmd/addr while the alternate transfers the data. Perhaps you have different requirements than just a simple NOR flash read command. If that is the case, then it should work if you have proven it working.
  • Hi Charles,

    Ok. I think I am not understanding DMA ping-pong mode properly. Here is my current setup:

    TX (UDMA_CHANNEL_SSI0TX)                                 RX (UDMA_CHANNEL_SSI0RX)

    Primary Struct (Sending cmd/addr)    ------------>    Primary Struct (Receiving return bytes from sending cmd/addr)  

    Alternate Struct (Sending N bytes of data) ------>   Alternate Struct (Receiving N bytes of data)

    Here is my code for the DMA setup:

    void ssi_transfer(uint8_t *cmd_buf, uint32_t cmd_count, uint8_t *tx_buf,
                      uint8_t *rx_buf, uint32_t data_count) {
    
        //
        // Enable the uDMA interface for both TX and RX channels.
        //
        SSIDMAEnable(SSI0_BASE, SSI_DMA_RX | SSI_DMA_TX);
    
        //*************************************************************************
        //uDMA SSI0 RX
        //*************************************************************************
    
        //
        // Put the attributes in a known state for the uDMA SSI0RX channel.  These
        // should already be disabled by default.
        //
        uDMAChannelAttributeDisable(UDMA_CHANNEL_SSI0RX,
                                    UDMA_ATTR_USEBURST |
                                    UDMA_ATTR_ALTSELECT |
                                    (UDMA_ATTR_HIGH_PRIORITY | UDMA_ATTR_REQMASK));
    
        //
        // Configure the control parameters for the primary control structure for
        // the SSIORX channel.
        //
        uDMAChannelControlSet(UDMA_CHANNEL_SSI0RX | UDMA_PRI_SELECT,
                              UDMA_SIZE_8 | UDMA_SRC_INC_NONE |
                              UDMA_DST_INC_8 | UDMA_ARB_4);
    
        //
        // Configure the control parameters for the alternate control structure for
        // the SSIORX channel.
        //
        uDMAChannelControlSet(UDMA_CHANNEL_SSI0RX | UDMA_ALT_SELECT,
                              UDMA_SIZE_8 | UDMA_SRC_INC_NONE |
                              g_rx_dst_inc | UDMA_ARB_4);
    
        //
        // Set up the transfer parameters for the SSI0RX Channel (primary)
        //
        uDMAChannelTransferSet(UDMA_CHANNEL_SSI0RX | UDMA_PRI_SELECT,
                               UDMA_MODE_PINGPONG,
                               (void *)(SSI0_BASE + SSI_O_DR),
                               cmd_buf,
                               cmd_count);
    
        //
        // Set up the transfer parameters for the SSI0RX Channel (alternate)
        //
        uDMAChannelTransferSet(UDMA_CHANNEL_SSI0RX | UDMA_ALT_SELECT,
                               UDMA_MODE_PINGPONG,
                               (void *)(SSI0_BASE + SSI_O_DR),
                               rx_buf,
                               data_count);
    
    
        //*************************************************************************
        //uDMA SSI0 TX
        //*************************************************************************
    
        //
        // Put the attributes in a known state for the uDMA SSI0TX channel.  These
        // should already be disabled by default.
        //
        uDMAChannelAttributeDisable(UDMA_CHANNEL_SSI0TX,
                                    UDMA_ATTR_ALTSELECT |
                                    UDMA_ATTR_HIGH_PRIORITY |
                                    UDMA_ATTR_REQMASK);
    
        //
        // Configure the control parameters for the SSI0 TX.
        //
        uDMAChannelControlSet(UDMA_CHANNEL_SSI0TX | UDMA_PRI_SELECT,
                              UDMA_SIZE_8 | UDMA_SRC_INC_8 |
                              UDMA_DST_INC_NONE | UDMA_ARB_4);
    
        //
        // Configure the control parameters for the SSI0 TX.
        //
        uDMAChannelControlSet(UDMA_CHANNEL_SSI0TX | UDMA_ALT_SELECT,
                              UDMA_SIZE_8 | g_tx_src_inc |
                              UDMA_DST_INC_NONE | UDMA_ARB_4);
    
        //
        // Set up the transfer parameters for the uDMA SSI0 TX channel.  This will
        // configure the transfer source and destination and the transfer size.
        // Basic mode is used because the peripheral is making the uDMA transfer
        // request.  The source is the TX buffer and the destination is the SSI0
        // data register.
        //
        uDMAChannelTransferSet(UDMA_CHANNEL_SSI0TX | UDMA_PRI_SELECT,
                               UDMA_MODE_PINGPONG,
                               cmd_buf,
                               (void *)(SSI0_BASE + SSI_O_DR),
                               cmd_count);
    
        uDMAChannelTransferSet(UDMA_CHANNEL_SSI0TX | UDMA_ALT_SELECT,
                               UDMA_MODE_PINGPONG,
                               tx_buf,
                               (void *)(SSI0_BASE + SSI_O_DR),
                               data_count);
    
        // Send command / address bytes and pop out return bytes
        chip_select(~GPIO_PIN_3);
    
        //
        // Now both the uDMA SSI0 TX and RX channels are primed to start a
        // transfer.  As soon as the channels are enabled, the peripheral will
        // issue a transfer request and the data transfers will begin.
        //
        uDMAChannelEnable(UDMA_CHANNEL_SSI0RX);
        uDMAChannelEnable(UDMA_CHANNEL_SSI0TX);
    
        // Wait until the SSI chip select de-asserts, indicating the end of the
        // transfer.
        while (!(GPIOPinRead(GPIO_PORTA_BASE, GPIO_PIN_3) & GPIO_PIN_3));
    }

    Charles Tsai said:


    My only question is why do want to setup two different channels? I thought one ping-pong channel is enough where the primary transfers the cmd/addr while the alternate transfers the data.

    I am not exactly understanding how this is going to work using just one channel. Are you saying one struct is able to do both transmit/receive? Do I re-setup the transfer in the ISR? Please enlighten me!

    Regards,

    Jacky

  • Jacky,
    I see what you are doing and it should work. For the TX side, you just need to send dummy data for the alternate control structure. Is N-byte a fixed length? Earlier you were trying to read just 2-bytes of manufacturing/device id. If you need to read variable bytes then you will need to reconfigure the alternate structure with the correct transfer size. I think you also need to reconfigure the primary structure for the cmd/addr depending what commands you want to give to the flash device.
  • Hi Charles,

    The N bytes is variable. My goal is to make flash read, write, erase ... etc APIs utilizing DMA transfer in order to reduce CPU load. If it works out well, I am even considering using those APIs on a flash file system!

    For example my flash read API would have the user input the starting address for the read (addr), the number of bytes to read (num_bytes) as well as providing a destination buffer to store the data read (dst). 

    void my_spi_read(uint32_t addr, uint32_t num_byte, uint8_t *dst) {
    
        uint8_t cmd[4];
    
        cmd[0] = WB_READ_DATA;
        cmd[1] = (addr >> 16) & 0xFF;
        cmd[2] = (addr >> 8) & 0xFF;
        cmd[3] = (addr >> 0) & 0xFF;
    
        g_rx_dst_inc = UDMA_DST_INC_8;
        g_tx_src_inc = UDMA_SRC_INC_NONE;
    
        uint8_t tx_dummy = 0x00;
    
        ssi_transfer(cmd, 4, &tx_dummy, dst, num_byte);
    
        while (chip_busy() & 1);
    }

    In the case that the number of bytes exceeds 1024, I will simply reload in the ISR.

    So I guess, my DMA setup is fine for what I am trying to do? Do you see potential problems with where I am heading to that I should keep an eye on?

    Regards,

    Jacky

  • Jacky,
    I can't think of a problem with your proposal. If you have tried your proposal and if it is working then I think you are good to go.