Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed/Added SPI DMA features in SPI library. #234

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions libraries/Adafruit_ZeroDMA/Adafruit_ZeroDMA.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ bool Adafruit_ZeroDMA::isActive(void) const {
DmacDescriptor *Adafruit_ZeroDMA::addDescriptor(
void *src,
void *dst,
uint32_t count,
uint16_t count,
dma_beat_size size,
bool srcInc,
bool dstInc,
Expand Down Expand Up @@ -537,7 +537,7 @@ DmacDescriptor *Adafruit_ZeroDMA::addDescriptor(
// etc.) are unchanged. Mostly for changing the data being pushed to a
// peripheral (DAC, SPI, whatev.)
void Adafruit_ZeroDMA::changeDescriptor(DmacDescriptor *desc,
void *src, void *dst, uint32_t count) {
void *src, void *dst, uint16_t count) {

uint8_t bytesPerBeat; // Beat transfer size IN BYTES
switch(desc->BTCTRL.bit.BEATSIZE) {
Expand Down
4 changes: 2 additions & 2 deletions libraries/Adafruit_ZeroDMA/Adafruit_ZeroDMA.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ class Adafruit_ZeroDMA {
uint8_t getChannel(void) const { return channel; }

// DMA descriptor functions
DmacDescriptor *addDescriptor(void *src, void *dst, uint32_t count = 0,
DmacDescriptor *addDescriptor(void *src, void *dst, uint16_t count = 0,
dma_beat_size size = DMA_BEAT_SIZE_BYTE,
bool srcInc = true, bool dstInc = true,
uint32_t stepSize = DMA_ADDRESS_INCREMENT_STEP_SIZE_1,
bool stepSel = DMA_STEPSEL_DST);
void changeDescriptor(DmacDescriptor *d, void *src = NULL,
void *dst = NULL, uint32_t count = 0);
void *dst = NULL, uint16_t count = 0);
bool isActive(void) const;

void _IRQhandler(uint8_t flags); // DO NOT TOUCH
Expand Down
322 changes: 200 additions & 122 deletions libraries/SPI/SPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -235,155 +235,233 @@ void SPIClass::transfer(void *buf, size_t count)
}
}

/*
// Non-DMA transfer function.
// this was removed from the dma function and made its own
void SPIClass::transfer(void* txbuf, void* rxbuf, size_t count)
{
uint8_t *txbuf8 = (uint8_t *)txbuf,
*rxbuf8 = (uint8_t *)rxbuf;
if(rxbuf8) {
if(txbuf8) {
// Writing and reading simultaneously
while(count--) {
*rxbuf8++ = _p_sercom->transferDataSPI(*txbuf8++);
}
} else {
// Reading only
while(count--) {
*rxbuf8++ = _p_sercom->transferDataSPI(0xFF);
}
}
} else if(txbuf) {
// Writing only
while(count--) {
(void)_p_sercom->transferDataSPI(*txbuf8++);
}
}
}
*/

// Pointer to SPIClass object, one per DMA channel.
static SPIClass *spiPtr[DMAC_CH_NUM] = { 0 }; // Legit inits list to NULL

void SPIClass::dmaCallback(Adafruit_ZeroDMA *dma) {
// dmaCallback() receives an Adafruit_ZeroDMA object. From this we can get
// a channel number (0 to DMAC_CH_NUM-1, always unique per ZeroDMA object),
// then locate the originating SPIClass object using array lookup, setting
// the dma_busy element 'false' to indicate end of transfer.
spiPtr[dma->getChannel()]->dma_busy = false;
// dma callback when the read part is completed
void SPIClass::dmaCallback_read(Adafruit_ZeroDMA *dma)
{
// dmaCallback() receives an Adafruit_ZeroDMA object. From this we can get
// a channel number (0 to DMAC_CH_NUM-1, always unique per ZeroDMA object),
// then locate the originating SPIClass object using array lookup, setting
uint8_t channel = dma->getChannel();

// flag this part of the dma done
spiPtr[channel]->dma_read_done = true;

// see if the entire transaction is completed
checkDmaComplete(channel);
}

void SPIClass::transfer(const void* txbuf, void* rxbuf, size_t count,
bool block) {
// dma callback when the write part is completed
void SPIClass::dmaCallback_write(Adafruit_ZeroDMA *dma)
{
// dmaCallback() receives an Adafruit_ZeroDMA object. From this we can get
// a channel number (0 to DMAC_CH_NUM-1, always unique per ZeroDMA object),
// then locate the originating SPIClass object using array lookup, setting
uint8_t channel = dma->getChannel();

// flag this part of the dma done
spiPtr[channel]->dma_write_done = true;

// see if the entire transaction is completed
checkDmaComplete(channel);

}

// see if the entire dma transaction is completed
// will automatically initiate another dma if we have bytes remaining to transfer
void SPIClass::checkDmaComplete(uint8_t channel)
{
// read and write dmas are both done
if(spiPtr[channel]->dma_read_done && spiPtr[channel]->dma_write_done)
{
// are more bytes that need to be transfered
// fire another dma transaction
if( (spiPtr[channel]->dma_bytes_remaining) > 0)
{
// initiate another transfer for the next section of bytes
// update buffer pointers offsets
// use the same user callback as last time if any
// (uint8_t*) typecast needed as c does not like pointer math of void*, but we know we are doing byte math
void* txbuf = (uint8_t*)spiPtr[channel]->txbuf_last + DMA_MAX_TRANSFER_SIZE;
void* rxbuf = (uint8_t*)spiPtr[channel]->rxbuf_last + DMA_MAX_TRANSFER_SIZE;
spiPtr[channel]->transfer(txbuf, rxbuf, spiPtr[channel]->dma_bytes_remaining, spiPtr[channel]->userDmaCallback );
}
// the transfer is completed, no bytes remaining
else
{
// flag as completed for anything poling for completion
spiPtr[channel]->dma_complete = true;

// call the callback function the user specified if any
if(spiPtr[channel]->userDmaCallback != NULL)
{
spiPtr[channel]->userDmaCallback();
}

}

}
}

// dma transfer function for spi with poll for completion
void SPIClass::transfer(void* txbuf, void* rxbuf, uint32_t count, bool block)
{
// start the dma transfer, but do not specify a user callback function, will poll for completion instead
transfer(txbuf, rxbuf, count, (void (*)(void))NULL );

// if this function should automatically wait for completion, otherwise user must do manually
if(block)
{
waitForTransfer();
}
}

// If receiving data and the RX DMA channel is not yet allocated...
if(rxbuf && (readChannel.getChannel() >= DMAC_CH_NUM)) {
if(readChannel.allocate() == DMA_STATUS_OK) {
readDescriptor =
readChannel.addDescriptor(
// Waits for a prior in-background DMA transfer to complete.
void SPIClass::waitForTransfer(void)
{
while( !dma_complete )
{
// do nothing, wait for transfer completion
}
}

// dma transfer function for spi
// this function does not block, and dma will transfer in the background
// the callback parameter should be passed in by the user, it is called when the dma is done
void SPIClass::transfer(void* txbuf, void* rxbuf, uint32_t count, void (*functionToCallWhenComplete)(void) )
{
// remember these buffer pointers
// will reuse if we have to do multiple dma transactions and pointer math
txbuf_last = txbuf;
rxbuf_last = rxbuf;

// save this function to call when the entire dma is done
userDmaCallback = functionToCallWhenComplete;

// Maximum bytes per DMA descriptor is 65,535 (NOT 65,536).
// We could set up a descriptor chain, but that gets more
// complex. For now, instead, break up long transfers into
// chunks of 65,535 bytes max...these transfers are all
// blocking, regardless of the "block" argument, except
// for the last one which will observe the background request.
// The fractional part is done first, so for any "partially
// blocking" transfers like these at least it's the largest
// single-descriptor transfer possible that occurs in the
// background, rather than the tail end.
uint16_t bytesThisPass;

if(count > DMA_MAX_TRANSFER_SIZE)
{
// Too big for 1 descriptor
// will need to do multiple dma transfers
bytesThisPass = DMA_MAX_TRANSFER_SIZE;

// remember bytes remaining for future transfers
dma_bytes_remaining = count - DMA_MAX_TRANSFER_SIZE;
}
else
{
// can do everything in one dma transfer
bytesThisPass = count;
dma_bytes_remaining = 0;
}

//******************************
// If the RX DMA channel is not yet allocated...
if(readChannel.getChannel() >= DMAC_CH_NUM)
{
if(readChannel.allocate() == DMA_STATUS_OK)
{
readDescriptor = readChannel.addDescriptor(
(void *)getDataRegister(), // Source address (SPI data reg)
NULL, // Dest address (set later)
0, // Count (set later)
rxbuf, // Dest address
bytesThisPass, // bytes to transfer
DMA_BEAT_SIZE_BYTE, // Bytes/hwords/words
false, // Don't increment source address
true); // Increment dest address
readChannel.setTrigger(getDMAC_ID_RX());
readChannel.setAction(DMA_TRIGGER_ACTON_BEAT);
readChannel.setCallback(dmaCallback_read, DMA_CALLBACK_TRANSFER_DONE);
spiPtr[readChannel.getChannel()] = this;
// Since all RX transfers involve a TX, a
// separate callback here is not necessary.
}
}
else
{
// update to use the currently passed buffers
readChannel.changeDescriptor(
readDescriptor,
(void *)getDataRegister(), // Source address (SPI data reg)
rxbuf, // Dest address
bytesThisPass); // bytes to transfer
}

// Unlike the rxbuf check above, where a RX DMA channel is allocated
// only if receiving data (and channel not previously alloc'd), the
// TX DMA channel is always needed, because even RX-only SPI requires
// writing dummy bytes to the peripheral.
if(writeChannel.getChannel() >= DMAC_CH_NUM) {
if(writeChannel.allocate() == DMA_STATUS_OK) {
writeDescriptor =
writeChannel.addDescriptor(
NULL, // Source address (set later)
// If the TX DMA channel is not yet allocated...
if(writeChannel.getChannel() >= DMAC_CH_NUM)
{
if(writeChannel.allocate() == DMA_STATUS_OK)
{
writeDescriptor = writeChannel.addDescriptor(
txbuf, // Source address
(void *)getDataRegister(), // Dest (SPI data register)
0, // Count (set later)
bytesThisPass, // bytes to transfer
DMA_BEAT_SIZE_BYTE, // Bytes/hwords/words
true, // Increment source address
false); // Don't increment dest address
writeChannel.setTrigger(getDMAC_ID_TX());
writeChannel.setAction(DMA_TRIGGER_ACTON_BEAT);
writeChannel.setCallback(dmaCallback);
writeChannel.setCallback(dmaCallback_write, DMA_CALLBACK_TRANSFER_DONE);
spiPtr[writeChannel.getChannel()] = this;
}
}

if(writeDescriptor && (readDescriptor || !rxbuf)) {
static const uint8_t dum = 0xFF; // Dummy byte for read-only xfers

// Initialize read descriptor dest address to rxbuf
if(rxbuf) readDescriptor->DSTADDR.reg = (uint32_t)rxbuf;

// If reading only, set up writeDescriptor to issue dummy bytes
// (set SRCADDR to &dum and SRCINC to 0). Otherwise, set SRCADDR
// to txbuf and SRCINC to 1. Only needed once at start.
if(rxbuf && !txbuf) {
writeDescriptor->SRCADDR.reg = (uint32_t)&dum;
writeDescriptor->BTCTRL.bit.SRCINC = 0;
} else {
writeDescriptor->SRCADDR.reg = (uint32_t)txbuf;
writeDescriptor->BTCTRL.bit.SRCINC = 1;
}

while(count > 0) {
// Maximum bytes per DMA descriptor is 65,535 (NOT 65,536).
// We could set up a descriptor chain, but that gets more
// complex. For now, instead, break up long transfers into
// chunks of 65,535 bytes max...these transfers are all
// blocking, regardless of the "block" argument, except
// for the last one which will observe the background request.
// The fractional part is done first, so for any "partially
// blocking" transfers like these at least it's the largest
// single-descriptor transfer possible that occurs in the
// background, rather than the tail end.
int bytesThisPass;
bool blockThisPass;
if(count > 65535) { // Too big for 1 descriptor
blockThisPass = true;
bytesThisPass = count % 65535; // Fractional part
if(!bytesThisPass) bytesThisPass = 65535;
} else {
blockThisPass = block;
bytesThisPass = count;
}

// Issue 'bytesThisPass' bytes...
if(rxbuf) {
// Reading, or reading + writing.
// Set up read descriptor.
// Src address doesn't change, only dest & count.
// DMA needs address set to END of buffer, so
// increment the address now, before the transfer.
readDescriptor->DSTADDR.reg += bytesThisPass;
readDescriptor->BTCNT.reg = bytesThisPass;
// Start the RX job BEFORE the TX job!
// That's the whole secret sauce to the two-channel transfer.
// Nothing will actually happen until the write channel job
// is also started.
readChannel.startJob();
}
if(txbuf) {
// DMA needs address set to END of buffer, so
// increment the address now, before the transfer.
writeDescriptor->SRCADDR.reg += bytesThisPass;
}
writeDescriptor->BTCNT.reg = bytesThisPass;
dma_busy = true;
writeChannel.startJob();
count -= bytesThisPass;
if(blockThisPass) {
while(dma_busy);
}
}
} else {
// Non-DMA fallback.
uint8_t *txbuf8 = (uint8_t *)txbuf,
*rxbuf8 = (uint8_t *)rxbuf;
if(rxbuf8) {
if(txbuf8) {
// Writing and reading simultaneously
while(count--) {
*rxbuf8++ = _p_sercom->transferDataSPI(*txbuf8++);
}
} else {
// Reading only
while(count--) {
*rxbuf8++ = _p_sercom->transferDataSPI(0xFF);
}
}
} else if(txbuf) {
// Writing only
while(count--) {
(void)_p_sercom->transferDataSPI(*txbuf8++);
}
}
else
{
// update to use the currently passed buffers
writeChannel.changeDescriptor(
writeDescriptor,
txbuf, // Source address
(void *)getDataRegister(), // Dest (SPI data register)
bytesThisPass); // bytes to transfer
}
}

// Waits for a prior in-background DMA transfer to complete.
void SPIClass::waitForTransfer(void) {
while(dma_busy);
//******************************
// clear the flags
dma_read_done = false;
dma_write_done = false;
dma_complete = false;

// fire the dma transactions
readChannel.startJob();
writeChannel.startJob();
}

void SPIClass::attachInterrupt() {
Expand Down
Loading