c++embeddedsd-carddmassp

DMA write to SD card (SSP) doesn't write bytes


I'm currently working on replacing a blocking busy-wait implementation of an SD card driver over SSP with a non-blocking DMA implementation. However, there are no bytes actually written, even though everything seems to go according to plan (no error conditions are ever found).

First some code (C++):

(Disclaimer: I'm still a beginner in embedded programming so code is probably subpar)

namespace SD {
    bool initialize() {
        //Setup SSP and detect SD card
        //... (removed since not relevant for question)

        //Setup DMA
        LPC_SC->PCONP |= (1UL << 29);
        LPC_GPDMA->Config = 0x01;
        //Enable DMA interrupts
        NVIC_EnableIRQ(DMA_IRQn);
        NVIC_SetPriority(DMA_IRQn, 4);
        //enable SSP interrupts
        NVIC_EnableIRQ(SSP2_IRQn);
        NVIC_SetPriority(SSP2_IRQn, 4);
    }

    bool write (size_t block, uint8_t const * data, size_t blocks) {
        //TODO: support more than one block
        ASSERT(blocks == 1);

        printf("Request sd semaphore (write)\n");
        sd_semaphore.take();
        printf("Writing to block " ANSI_BLUE "%d" ANSI_RESET "\n", block);

        memcpy(SD::write_buffer, data, BLOCKSIZE);


        //Start the write
        uint8_t argument[4];
        reset_argument(argument);
        pack_argument(argument, block);
        if (!send_command(CMD::WRITE_BLOCK, CMD_RESPONSE_SIZE::WRITE_BLOCK, response, argument)){
            return fail();
        }

        assert_cs();
        //needs 8 clock cycles
        delay8(1);

        //reset pending interrupts
        LPC_GPDMA->IntTCClear = 0x01 << SD_DMACH_NR;
        LPC_GPDMA->IntErrClr = 0x01 << SD_DMACH_NR;

        LPC_GPDMA->SoftSReq = SD_DMA_REQUEST_LINES;

        //Prepare channel
        SD_DMACH->CSrcAddr = (uint32_t)SD::write_buffer;
        SD_DMACH->CDestAddr = (uint32_t)&SD_SSP->DR;
        SD_DMACH->CLLI = 0;
        SD_DMACH->CControl = (uint32_t)BLOCKSIZE
                                             | 0x01 << 26 //source increment
                                             | 0x01 << 31; //Terminal count interrupt

        SD_SSP->DMACR = 0x02; //Enable ssp write dma

        SD_DMACH->CConfig = 0x1  //enable
                                            | SD_DMA_DEST_PERIPHERAL << 6
                                            | 0x1 << 11 //mem to peripheral
                                            | 0x1 << 14 //enable error interrupt
                                            | 0x1 << 15; //enable terminal count interrupt
        return true;
    }
}
extern "C" __attribute__ ((interrupt)) void DMA_IRQHandler(void) {
    printf("dma irq\n");
    uint8_t channelBit = 1 << SD_DMACH_NR;
    if (LPC_GPDMA->IntStat & channelBit) {
        if (LPC_GPDMA->IntTCStat & channelBit) {
            printf(ANSI_GREEN "terminal count interrupt\n" ANSI_RESET);
            LPC_GPDMA->IntTCClear = channelBit;
        }
        if (LPC_GPDMA->IntErrStat & channelBit) {
            printf(ANSI_RED "error interrupt\n" ANSI_RESET);
            LPC_GPDMA->IntErrClr = channelBit;
        }
        SD_DMACH->CConfig = 0;

        SD_SSP->IMSC = (1 << 3);

    }
}

extern "C" __attribute__ ((interrupt)) void SSP2_IRQHandler(void) {
    if (SD_SSP->MIS & (1 << 3)) {
        SD_SSP->IMSC &= ~(1 << 3);
        printf("waiting until idle\n");
        while(SD_SSP->SR & (1UL << 4));

        //Stop transfer token
        //I'm not sure if the part below up until deassert_cs is necessary.
        //Adding or removing it made no difference.
        SPI::send(0xFD);

        {
            uint8_t response;
            unsigned int timeout = 4096;
            do {
                response = SPI::receive();
            } while(response != 0x00 && --timeout);
            if (timeout == 0){
                deassert_cs();
                printf("fail");
                return;
            }
        }

        //Now wait until the device isn't busy anymore
        {
            uint8_t response;
            unsigned int timeout = 4096;
            do {
                response = SPI::receive();
            } while(response != 0xFF && --timeout);
            if (timeout == 0){
                deassert_cs();
                printf("fail");
                return;
            }
        }
        deassert_cs();
        printf("idle\n");
        SD::sd_semaphore.give_from_isr();
    }
}

A few remarks about the code and setup:

Now I have already tried the following things:

Unfortunately due to lack of hardware tools I haven't been able yet to verify if the bytes are actually send over the data lines.

What is wrong with my code, or where can I look to find the cause of this problem? After spending way more hours on this then I'd like to admit I really have no idea how to get this working and any help is appreciated!

UPDATE: I did a lot more testing, and thus I got some more results. The results below I got by writing 4 blocks of 512 bytes. Each block contains constantly increasing numbers module 256. Thus each block contains 2 sequences going from 0 to 255. Results:

And here is the raw binary dump. Note that this exact pattern is fully reproducible: so far each time I tried this I got this exact same pattern.

Update2: Not sure if it's relevant, but I use sdram for memory.


Solution

  • When I finally got my hands on a logic analyzer I got a lot more information and was able to solve these problems.

    There were a few small bugs in my code, but the bug that caused this behaviour was that I didn't send the "start block" token (0xFE) before the block and I didn't send the 16 bit (dummy) crc after the block. When I added these to the transfer buffer everything was written successfully!

    So this fix was as followed:

    bool write (size_t block, uint8_t const * data, size_t blocks) {
        //TODO: support more than one block
        ASSERT(blocks == 1);
    
        printf("Request sd semaphore (write)\n");
        sd_semaphore.take();
        printf("Writing to block " ANSI_BLUE "%d" ANSI_RESET "\n", block);
    
        SD::write_buffer[0] = 0xFE; //start block
    
        memcpy(&SD::write_buffer[1], data, BLOCKSIZE);
    
        SD::write_buffer[BLOCKSIZE + 1] = 0; //dummy crc
        SD::write_buffer[BLOCKSIZE + 2] = 0;
    
        //...
    }
    

    As a side note, the reason why the first block wasn't written was simply because I didn't wait until the device was ready before sending the first block. Doing so fixed the problem.