Okay, I have figured out how to get DMA working with SPI1 - the primary reason why I was having with getting SPI DMA transfers to work was that I was using SPI1 (which only has one DMA trigger) when all the examples out there use SPI0 (which has two triggers). Along with that, I figured out how to use the SPI DMAs for single byte commands to the OLED controller that I am using.
Thank you to Gerardo Rodriguez who put the answer in here: SPI1 with DMA on K22 - BUG - it just took me a while to figure out enough to understand what was being explained. The issue is that SPI1 only has one trigger while SPI0 has two.
Here is the SPI code that I am now using - the basis is from the "dspi_edma_b2b_transfer_master" example project with a few modifcations the primary reasons for:
- Having code that will work for both SPI0 as well as SPI1
- Handling datablocks larger than the "limited_size" value outlined in "fsl_dspi_edma.c"
- Handling only data writes - if you want to do data reads as well as writes or use half-duplex mode, you will have to do some work on your own.
The SPI Defines/Constants are identical to what I was using above. I'm including them here so somebody researching (ie copying the code) doesn't have to search for the values.
enum { SPI_TRANSFER_BAUDRATE = 10000000U
};
#define OLED_DSPI_MASTER_DMA_BASE DMA_BASE
#define OLED_DSPI_MASTER_DMA_BASEADDR ((DMA_Type *)(OLED_DSPI_MASTER_DMA_BASE))
#define OLED_DSPI_MASTER_DMA_MUX_BASE DMAMUX_BASE
#define OLED_DSPI_MASTER_DMA_MUX_BASEADDR ((DMAMUX_Type *) \
(OLED_DSPI_MASTER_DMA_MUX_BASE))
#define OLED_DSPI_MASTER_DMA_TX_REQUEST_SOURCE kDmaRequestMux0SPI1
#define OLED_DSPI_MASTER_DMA_RX_REQUEST_SOURCE kDmaRequestMux0SPI1
#define OLED_DSPI_MASTER_BASE (SPI1_BASE)
#define OLED_DSPI_MASTER_BASEADDR ((SPI_Type *)OLED_DSPI_MASTER_BASE)
#define OLED_DSPI_MASTER_IRQN (SPI1_IRQn)
#define OLED_DSPI_MASTER_CLK_SRC (DSPI1_CLK_SRC)
#define OLED_DSPI_MASTER_CLK_FREQ CLOCK_GetFreq(DSPI1_CLK_SRC)
The Global Variables have been updated to include a second dspi_transfer_t object which is actually used for the EDMA APIs as well as variables to keep track of how much data has been sent.
const dspi_master_config_t oledSPIConfig =
{ .whichCtar = kDSPI_Ctar0
, .ctarConfig.baudRate = SPI_TRANSFER_BAUDRATE
, .ctarConfig.bitsPerFrame = 8
, .ctarConfig.cpol = kDSPI_ClockPolarityActiveHigh
, .ctarConfig.cpha = kDSPI_ClockPhaseFirstEdge
, .ctarConfig.direction = kDSPI_MsbFirst
, .ctarConfig.pcsToSckDelayInNanoSec = 1000000000U / SPI_TRANSFER_BAUDRATE
, .ctarConfig.lastSckToPcsDelayInNanoSec = 1000000000U / SPI_TRANSFER_BAUDRATE
, .ctarConfig.betweenTransferDelayInNanoSec = 500000000U / SPI_TRANSFER_BAUDRATE
, .whichPcs = kDSPI_Pcs0
, .pcsActiveHighOrLow = kDSPI_PcsActiveLow
, .enableContinuousSCK = FALSE
, .enableRxFifoOverWrite = FALSE
, .enableModifiedTimingFormat = FALSE
, .samplePoint = kDSPI_SckToSin0Clock
};
enum { SPIBUFFERSIZE = 8192
};
QueueHandle_t spiQUEUE_Handle;
QueueHandle_t spiRXQUEUE_Handle;
uint8_t spiCmdBuffer[16];
uint8_t spiTXBuffer[SPIBUFFERSIZE];
dspi_master_handle_t oledSPIHandle;
dspi_transfer_t spiXfer = { .rxData = NULL
, .configFlags = kDSPI_MasterCtar0 |
kDSPI_MasterPcs0 |
kDSPI_MasterPcsContinuous
};
dspi_transfer_t actualSPIXfer = { .rxData = NULL
, .configFlags = kDSPI_MasterCtar0 |
kDSPI_MasterPcs0 |
kDSPI_MasterPcsContinuous
};
edma_handle_t dspiEdmaMasterRxRegToRxDataHandle;
edma_handle_t dspiEdmaMasterTxDataToIntermediaryHandle;
edma_handle_t dspiEdmaMasterIntermediaryToTxRegHandle;
dspi_master_edma_handle_t g_dspi_edma_m_handle;
volatile uint32_t spiTransferCompleted = TRUE;
uint32_t spiBlockSize = 510; // Should be 511, but keeping it even
uint32_t spiSentCurrently;
DMA/SPI initialization. Note that I check to see whether or not the SPI has one or two trigger sources - this was the issue with the orginal code. SPI0 has two trigger sources while SPI1 only has one (and has to use the RX trigger). I put in the check used by "fsl_dspi_edma.c" to only set the TX source if it is appropriate as well as change the size of the maximum DMA block size that can be sent for this case.
DSPI_MasterInit(OLED_DSPI_MASTER_BASEADDR
, &oledSPIConfig
, CLOCK_GetFreq(DSPI1_CLK_SRC));
/* DMA MUX init */
DMAMUX_Init(OLED_DSPI_MASTER_DMA_MUX_BASEADDR);
DMAMUX_SetSource(OLED_DSPI_MASTER_DMA_MUX_BASEADDR
, masterRxChannel
, OLED_DSPI_MASTER_DMA_RX_REQUEST_SOURCE);
DMAMUX_EnableChannel(OLED_DSPI_MASTER_DMA_MUX_BASEADDR
, masterRxChannel);
/* If DMA has two triggers, add the "TX" one */
if (1 == FSL_FEATURE_DSPI_HAS_SEPARATE_DMA_RX_TX_REQn(OLED_DSPI_MASTER_BASEADDR)) {
DMAMUX_SetSource(OLED_DSPI_MASTER_DMA_MUX_BASEADDR
, masterTxChannel
, OLED_DSPI_MASTER_DMA_TX_REQUEST_SOURCE);
DMAMUX_EnableChannel(OLED_DSPI_MASTER_DMA_MUX_BASEADDR
, masterTxChannel);
spiBlockSize = 32766; // Should be 32,767, but keeping it even
}
EDMA_GetDefaultConfig(&userConfig);
EDMA_Init(OLED_DSPI_MASTER_DMA_BASEADDR
, &userConfig);
/* Set up dspi master */
memset(&dspiEdmaMasterRxRegToRxDataHandle
, 0
, sizeof(dspiEdmaMasterRxRegToRxDataHandle));
memset(&dspiEdmaMasterTxDataToIntermediaryHandle
, 0
, sizeof(dspiEdmaMasterTxDataToIntermediaryHandle));
memset(&dspiEdmaMasterIntermediaryToTxRegHandle
, 0
, sizeof(dspiEdmaMasterIntermediaryToTxRegHandle));
EDMA_CreateHandle(&dspiEdmaMasterRxRegToRxDataHandle
, OLED_DSPI_MASTER_DMA_BASEADDR
, masterRxChannel);
EDMA_CreateHandle(&dspiEdmaMasterTxDataToIntermediaryHandle
, OLED_DSPI_MASTER_DMA_BASEADDR
, masterIntermediaryChannel);
EDMA_CreateHandle(&dspiEdmaMasterIntermediaryToTxRegHandle
, OLED_DSPI_MASTER_DMA_BASEADDR
, masterTxChannel);
DSPI_MasterTransferCreateHandleEDMA(OLED_DSPI_MASTER_BASEADDR
, &g_dspi_edma_m_handle
, DSPI_MasterEDMACallback
, NULL
, &dspiEdmaMasterRxRegToRxDataHandle
, &dspiEdmaMasterTxDataToIntermediaryHandle
, &dspiEdmaMasterIntermediaryToTxRegHandle);
Before I send data, I check the status of "spiTransferCompleted" and reset it. In my case, I have to do this because I may have to change the value of a Command/Data line, but it's always good practice to to this as this way you don't waste processor cycles after starting the DMA cycle waiting for the transfer to complete - instead you're allowing basic programming to take place while the DMA is executing and hopefully it will complete before you need it again. I'm mentioning it because in the examples, I see the following code after the call to "DSPI_MasterTransferEDMA".
for (; !spiTransferCompleted;) { }
spiTransferCompleted = FALSE;
In my application, I have to send up to 8,192 bytes but if you look at the code for "DSPI_MasterTransferEDMA" you'll see that the transfer limit for devices which only have one trigger (ie SPI1) are limited to a data size of 511 bytes. To handle this situation, I break up the DMA initiation calls to the size of the block the DMA can send with larger blocks being at least 256 bytes so the call to "DSPI_MasterTransferEDMA" that is located within the Callback can complete before the DMA interrupt is requested again and you end up in a situation where you get nested Callbacks with data that has not yet been sent being overwritten. If the user is sending less than a full block, the DMA exeutes normally.
void SPI_BulkTransfer() {
#if (2 != SDK_DEBUGCONSOLE)
int32_t status;
#endif
spiSentCurrently = 0;
actualSPIXfer.txData = spiXfer.txData;
if ((spiBlockSize + 0) >= spiXfer.dataSize) {
actualSPIXfer.dataSize = spiXfer.dataSize;
}
else if ((spiBlockSize + 256) > spiXfer.dataSize) {
actualSPIXfer.dataSize = 256;
}
else {
actualSPIXfer.dataSize = spiBlockSize;
}
DSPI_MasterTransferEDMA(OLED_DSPI_MASTER_BASEADDR
, &g_dspi_edma_m_handle
, &actualSPIXfer);
}
As noted above, the SPI DMA Callback will request additional transfers if they are required with the minimum size of additional transfers being 256 bytes.
void DSPI_MasterEDMACallback(SPI_Type *base
, dspi_master_edma_handle_t *handle
, status_t status
, void *userData) {
if (status == kStatus_Success) {
spiSentCurrently += actualSPIXfer.dataSize;
actualSPIXfer.txData = &spiXfer.txData[spiSentCurrently];
if ((spiXfer.dataSize + 0) == spiSentCurrently) {
spiTransferCompleted = TRUE;
}
else {
if (256 >= (spiXfer.dataSize - spiSentCurrently)) {
actualSPIXfer.dataSize = spiXfer.dataSize - spiSentCurrently;
}
else if ((spiBlockSize + 0) > ((spiXfer.dataSize - spiSentCurrently))) {
actualSPIXfer.dataSize = (spiXfer.dataSize - spiSentCurrently) - 256;
}
else {
actualSPIXfer.dataSize = spiBlockSize;
}
DSPI_MasterTransferEDMA(OLED_DSPI_MASTER_BASEADDR
, &g_dspi_edma_m_handle
, &actualSPIXfer);
}
}
}
In answer to the questions in the original post:
1. Is there a way of sending single bytes using SPI DMA? Block Data writes to the OLED seem to be limited to two bytes or more. There is no "NOP" bytes for the OLED that could be teamed up with the commands.
[MAP] I discovered that if I sent an invalid command bye to the OLED before the actual one, the OLED ignored it and everything was okay. Similarly, with a single data byte as part of the command, if I sent a single 0x00 byte after it, it was ignored.
1.1. Can the single byte SPI writes co-exist with the multi-byte SPI writes?
[MAP] As I have everything working with a DMA, I haven't pursued this. I do have the question out to the OLED controller manufacturer (Solomon Systech) and I'm waiting for a response.
2. Can I use a single callback for the single byte and DMA transfers (Assuming that the solution to the problems allows both approaches to be used)? This would require type casting one or the other paremeters in the Callback methods.
[MAP] This is no longer relevant due to the use of only DMA.
3. Once I have the code working, how do I remove the "spiRXBuffer" and associated read? The SPI port to the OLED is only one way, so the buffer is SRAM that I would like to find a better use for.
[MAP] In going through the SPI/EDMA code I discovered that if you load rxData or txData of the dspi_transfer_t structure passed to the DMA request then it is ignored.
3.1. Is it an issue that I'm doing bi-directional send/receives on the single "kDmaRequestMux0SPI1" channel? Ideally, I just want to do transmits.
[MAP] This is basically the issue that caused the problems with getting DMA operations to work correctly and is addressed in the code above.
4. Looking at the EDMA code, it looks like the maximum size that can be transferred is 511 bytes - the OLED requires 8,192 bytes. When I attempt to send more than 511 bytes, I get an error 603 ("kStatus_DSPI_OutOfRange") from DSPI_MasterTransferEDMA. After sending a block of (say 256) bytes, can I call the DSPI_MasterTransferEDMA from within the EDMA callback?
[MAP] This is addressed above.
So, was all this worth it? In terms of performance, I see a pretty substantial increase in full OLED write speed changing from single byte writes to DMA:
- At 60MHz, Single Byte OLED write is 93ms and with DMA it dropped down to 17ms
- At 120MHz, Single Byte OLED write is 57ms and with DMA it dropped down to 12ms
At 17ms or 12ms, the screen update appears to be instantaneous but at the original speeds, the update was easily observed.
This was an investigation that took two weeks of work (including finding the right wiring and initialization/image load code for hte OLED) but the results are quite impressive.