DMA-based SPI on SHARC ADSP-21489

If you have resorted to Google after having uselessly poured through AD’s reference manuals, hoping to find at least a proper hint about how to approach this, if not a bit of code, it’s ok, you can stop now. It’s right here below. Your quest is complete.

It is my firm belief that the Hardware Reference Manual and the Programmer’s Manual for the ADSP-21489 are byproducts of an experiment ran by the US starting from the 1950s. In the deep vaults of Area 51, eighty monkeys are frantically typing up random letters; they have not produced Shakespeare yet, but clearly they produced a lot of technical documentation for Analog Devices. In the hope that no poor soul will have to bang his head against the desk trying to piece together the countless pieces of this puzzle, I did a quick write-up to show you how to do SPI communication with DMA on ADSP-21489. This should probably work (with obvious adaptation) on any DSP in the same family, and it should be easy to adapt if your DSP is the SPI slave, not the master, as below.

Programmer’s view of the SPI peripheral

Ignore the mumbo-jumbo about MOSI, MISO and all that stuff hardware freaks mumble about, because we will not care about it here. To the programmer, the SPI interface basically looks like this:

Various parameters about how this happens can be adjusted through the SPICTL register.

If you’re using DMA, the programmer’s view of the transfer process is slightly complicated by the fact that there is a single DMA channel. While this makes sense (because the SPI protocol is half-duplex — you never transmit and receive simultaneously), — you need to write up a little more boilerplate because the setup is different for DMA receive and DMA transmit.

The DMA system works by independently walking over a user-provided buffer; it keeps an index that starts at 0 (i.e. the beginning of the buffer), and it increases it by a user supplied-value after each SPI transfer. It looks very much like this:

    for (dma_idx = l; dma_idx < dma_count; dma_idx += dma_increment)
    {
        dma_transfer(&user_buffer[dma_idx]);
    }

You have control ofer user_buffer, dma_count and dma_increment above. You also have some control over how much is transferred (received or transmitted) in dma_transfer, but that depends on the peripheral you’re using. For SPI, you can set the length of one transfer to be 8, 16 or 32 bits. Note 2

Overall, the whole process looks like this:

  1. General setup: set baud rate, automatic/manual slave select, flush TX, RX buffers and DMA FIFO, set transmission parameters (i.e. who’s master and who’s slave, what interrupts to get, how long are the words being transferred and so on. We’ll get to a more detailed example in a minute).
  2. Setup DMA:

    1. Tell the DMA module where to get the data from, by setting IISPI to point at your TX buffer
    2. Tell the DMA module how much to increment its index by, by setting IMSPI. Do note that IMSPI is given in words, not bytes: the next transfer will be done from address <IISP> + <current index> (i.e. make sure your buffers are word-aligned!).
    3. Tell the DMA how many transfers to make by setting CSPI to the desired upper limit: when the DMA index reaches CSPI, the transfer stops and you get notified through an interrupt.
  3. Send command from master: enable the SPI peripheral, then enable DMA in TX mode (i.e. leaving the SPIRCV bit of the SPIDMAC register unset) to send the data.

  4. Wait for the transfer to complete: you can either be notified of that through an interrupt, or just poll the SPIFE bit of the SPISTAT register. Your choice.
  5. Transfer answer from device: disable the SPI peripheral Note 3 , flush the DMA FIFO and setup DMA again:

    1. Tell the DMA module where to put the data, by setting IISPI to point at your RX buffer
    2. Tell the DMA module how much to increment its index by, by setting IMSPI.
    3. Tell the DMA how many transfers to make by setting CSPI to the desired upper limit
  6. Enable the SPI peripheral, the enable DMA in RX mode (i.e. set the SPIRCV bit of the SPIDMAC register)

  7. Wait for the transfer to complete. This is done in the same manner as above.

That’s it.

Put your code where your mouth is

There is no finished technical documentation that does not have code. All those articles about how to write this, or how to do that? If they don’t have code, they’re bullshit. They probably don’t even work.

The code below has no portability layer. If you want to use it in a real-life project, you will probably want to:

We’ll start by writing a type definition of a SHARC SPI port

    struct sharc_spi_port
    {
        uint32_t* spictl;
        uint32_t* spiflg;
        uint32_t* spibaud;
        uint32_t* spidmac;
        uint32_t* spistat;
        uint32_t* iispi;
        uint32_t* imspi;
        uint32_t* cspi;
        uint32_t* cpspi;
        uint32_t* txspi;
        uint32_t* rxspi;
    };

This can be use to statically initialize two variables, struct sharc_spi_port spi_a, spi_b, so that we don’t end up writing port-specific code.

Based on the above, the init function could look as follows:

    static void sharc_spi_init(struct sharc_spi_port *p)
    {
        (p->spibaud) = 4;
    
        /* SPI Flag 0 is select output */
        *(p->spiflg) = DS0EN;
    
        /* Clear TX and RX buffers */
        *(p->spictl) = TXFLSH | RXFLSH;
    
        /* Clear SPI status reg */
        *(p->spidmac) = FIFOFLSH;
    
        /* Clear DMA FIFO */
        *(p->spistat) = 0xFF; 
    
        /* Not sure what it does, but it's needed needed.
        * Something something DMA chain. */
        *(p->cpspi) = 0;
    
        /**
        * - Initialize transfer by DMA
        * - If RX buffer is full, get more data, overwrite previous
        *   contents
        * - No delay between word transfers
        * - When TX buffer is empty, send 0
        * - SPI word length is 16 bits
        * - Send words MSB first
        * - DSP is SPI Master */
    
        *(p->spictl) = TIMOD2 | GM | SENDZ | WL16 | MSBF | SPIMS;
    }

We’re going to use a single buffer called spi_samples for both TX and RX. You can use separate buffers if you need to; it not only simplifies things a lot, but it also means that you can process a buffer while transmitting the next command.

    static void spi_dma_send(struct sharc_spi_port *p, uint32_t *data,
    unsigned int len)
    {
        memcpy(&spi_samples[0], data, len);
    
        crt_spi_count = len;
    
        *(p->iispi) = &spi_samples[0];
        *(p->imspi) = 1; /* Each read increments DMA index reg by 1 */
        *(p->cspi) = crt_spi_count; /* Transfer this many words */
    
        /* Enable SPI */
        *(p->spictl) |= SPIEN; 
    
        /* Enable DMA with interrupt on transfer, transfer from internal memory */
        *(p->spidmac) = SPIDEN | INTEN;
    }

The corresponding RX function would be:

    static void spi_dma_recv(struct sharc_spi_port *p, unsigned int len)
    {
        memset(&spi_samples[0], 0, sizeof(rhd_spi_samples));
    
        crt_spi_count = len;
    
        *(p->iispi) = &samples[0];
    
        /* Each read increments DMA index reg by 1 */
        *(p->imspi) = 1;
    
        /* Transfer this many words */
        *(p->cspi) = crt_spi_count;
    
        /* Enable SPI. Needs to be done before enabling DMA */
        *(p->spictl) |= SPIEN;
    
        /* Enable DMA with interrupt on transfer, transfer from internal
        * memory */
        *(p->spidmac) = SPIDEN | INTEN | SPIRCV;
    }

And we can piece this up together in a simple echo loop for a fictitious SPI device that echoes all it gets back when you send it 0xCAFE:

    static void rhd_query(struct device_drv* drv)
    {
        uint32_t cmd = 0xCAFE;

        while (1) {
            spi_init(drv->spi_port);
            spi_dma_send(drv->spi_port, &cmd, 1);

            /* Change from TX to RX DMA. */
            /* You will want to integrate this in a separate function, or in
            * spi_dma_{send|recv} */

            while (!*(drv->spi_port->spistat) & SPIFE)) {}

            *(drv->spi_port->spictl) = 0x00;
            *(drv->spi_port->spidmac) = 0x00;
            *(drv->spi_port->spistat) = 0xFF;

            spi_init(&spi_a); spi_dma_recv(&spi_a, 1);

            while (! (*(drv->spi_port->spistat) & SPIFE)) {}

            *(drv->spi_port->spictl) = 0x00;
            *(drv->spi_port->spidmac) = 0x00;
            *(drv->spi_port->spistat) = 0xFF;
        }
    }
  

That’s about it. I trust the astute reader will be able to work out more serious real-life requirements (i.e. doing something useful while the DMA transfer is happening) on his own.


  1. If you need more control or you’re dealing with some weird peripheral, you can always drive the CS line manually, but we won’t cover that here. ^
  2. This suggests that any value other than 1 for dma_increment is meaningless, but it’s not always true. E.g. if your SPI peripheral is a 4-channel ADC, you can use dma_increment = 4 to get the samples interleaved in a single buffer. \^
  3. AD’s documentation points out a way to do this without disabling the SPI peripheral. It’s probably the only part of their SPI-related documentation that makes sense.\^