I am trying to understand how the AHB DMA controller works, but am having quite a bit of trouble.
I understand on a general level that the DMA controller enables transfer of data between memory and peripherals. It can be set up to transfer single or multiple buffers of data. Transfers are controlled by handshakes consisting of requests from the source and destination, which can be triggered via software or hardware.
But that's about as far as I can understand. The SAM3X datasheet, for me at least, does not really explain some things very well. It mentions that transfers may consist of multiple buffers, which are sometimes (?) broken down into "chunks", but I cannot figure out how these concepts actually work and are interfaced with.
For example, I tried a very simple case of transferring one 32-bit word of data from memory to a PWM register using software handshaking. The code I have is as follows:
void setup()
{
Serial.begin(115200);
// Enable PWM clock supply (otherwise writes to PWM registers won't happen).
pmc_enable_periph_clk(ID_PWM);
uint32_t src = 1234;
// Dummy destination register - PWM channel 0 duty cycle.
uint32_t volatile& dst = REG_PWM_CDTY0;
dst = 0;
Serial.print("Destination register: ");
Serial.println(dst);
// Enable DMAC clock supply.
pmc_enable_periph_clk(ID_DMAC);
// Enable DMA controller.
REG_DMAC_EN = DMAC_EN_ENABLE;
// Disable channel while we modify settings.
REG_DMAC_CHDR = DMAC_CHDR_DIS5;
// Set DMAC to fixed priority.
REG_DMAC_GCFG = DMAC_GCFG_ARB_CFG_FIXED;
// Disable DMAC interrupts.
REG_DMAC_EBCIDR = 0x3F3F3F;
// Set the source address.
REG_DMAC_SADDR5 = reinterpret_cast<uint32_t>(&src);
// Set the destination address.
REG_DMAC_DADDR5 = reinterpret_cast<uint32_t>(&dst);
// Set the buffer descriptor to 0 (last buffer?).
REG_DMAC_DSCR5 = 0;
// Set transfer size to desired, source and destination widths to 32 bits.
REG_DMAC_CTRLA5 = 1u | DMAC_CTRLA_SRC_WIDTH_WORD | DMAC_CTRLA_DST_WIDTH_WORD;
// Set flow controller to memory-to-peripheral controller, transfer source address fixed, transfer destination address fixed.
REG_DMAC_CTRLB5 = DMAC_CTRLB_FC_MEM2PER_DMA_FC | DMAC_CTRLB_SRC_INCR_FIXED | DMAC_CTRLB_DST_INCR_FIXED;
// Set destination handshaking to software.
REG_DMAC_CFG5 = DMAC_CFG_DST_H2SEL_SW;
// Enable channel 5.
REG_DMAC_CHER = DMAC_CHER_ENA5;
Serial.println("Initiating transfer");
// Trigger transfer.
REG_DMAC_SREQ = DMAC_SREQ_DSREQ5;
Serial.println("Waiting for transfer to complete");
// Wait for channel to be disabled, signalling end of transfer.
while (REG_DMAC_CHSR & DMAC_CHSR_ENA5) {}
Serial.println("Transfer complete");
Serial.print("Destination register: ");
Serial.println(dst);
}
void loop()
{}
As I understand this should work fine, yet apparently the transfer doesn't occur because execution never gets past "Waiting for transfer to complete". What makes it work is if I change REG_DMAC_SREQ = DMAC_SREQ_DSREQ5 to REG_DMAC_CREQ = DMAC_CREQ_DCREQ5. I.e., changing from making a destination single transfer request to making a destination chunk transfer request. (As I understand a source request is not required here because transfer requests are not required if the peripheral is main memory.)
However I do not understand why this would be the case, as on page 342 the datasheet states:
The length of a single transaction is always 1 and is converted to a single AMBA access
Which I take to refer to the BTSIZE entry of the DMAC_CTRLA register, which I have specified as 1. But even for a transaction of size of 1, apparently this is still considered a "chunk" transaction - what then actually is a single transaction and what is the difference?
Furthermore, consider the following code, a slight modification of the previous example:
void setup()
{
Serial.begin(115200);
// Enable PWM clock supply (otherwise writes to PWM registers won't happen).
pmc_enable_periph_clk(ID_PWM);
uint32_t src[] = {1234, 5678, 9012, 3456};
// Dummy destination register - PWM channel 0 duty cycle.
uint32_t volatile& dst = REG_PWM_CDTY0;
dst = 0;
Serial.print("Destination register: ");
Serial.println(dst);
// Enable DMAC clock supply.
pmc_enable_periph_clk(ID_DMAC);
// Enable DMA controller.
REG_DMAC_EN = DMAC_EN_ENABLE;
// Disable channel while we modify settings.
REG_DMAC_CHDR = DMAC_CHDR_DIS5;
// Set DMAC to fixed priority.
REG_DMAC_GCFG = DMAC_GCFG_ARB_CFG_FIXED;
// Disable DMAC interrupts.
REG_DMAC_EBCIDR = 0x3F3F3F;
// Set the source address.
REG_DMAC_SADDR5 = reinterpret_cast<uint32_t>(src + 0);
// Set the destination address.
REG_DMAC_DADDR5 = reinterpret_cast<uint32_t>(&dst);
// Set the buffer descriptor to 0 (last buffer?).
REG_DMAC_DSCR5 = 0;
// Set transfer size to desired, source and destination widths to 32 bits.
REG_DMAC_CTRLA5 = 4u | DMAC_CTRLA_SRC_WIDTH_WORD | DMAC_CTRLA_DST_WIDTH_WORD;
// Set flow controller to memory-to-peripheral controller, transfer source address fixed, transfer destination address fixed.
REG_DMAC_CTRLB5 = DMAC_CTRLB_FC_MEM2PER_DMA_FC | DMAC_CTRLB_SRC_INCR_INCREMENTING | DMAC_CTRLB_DST_INCR_FIXED;
// Set destination handshaking to software.
REG_DMAC_CFG5 = DMAC_CFG_DST_H2SEL_SW;
// Enable channel 5.
REG_DMAC_CHER = DMAC_CHER_ENA5;
Serial.println("Initiating transfer");
// Trigger transfer.
REG_DMAC_CREQ = DMAC_CREQ_DCREQ5;
Serial.println("Waiting for transfer to complete");
// Wait for channel to be disabled, signalling end of transfer.
while (REG_DMAC_CHSR & DMAC_CHSR_ENA5) {}
Serial.println("Transfer complete");
Serial.print("Destination register: ");
Serial.println(dst);
}
void loop()
{}
The only changes are that I am now trying to transfer 4 words to the destination. The expected result is that each transfer overwrites the previous, and hence the destination ends up with the value 3456. However, the execution never gets past "Waiting for transfer to complete" once again. In fact, I need to change the code to write a destination request to DMAC_CREQ 4 times in order for it to work (each time waiting for the destination request bit to be cleared). Only after 4 requests does the DMA channel become disabled, indicating the full transfer is complete.
In this case I would think that it is a single buffer transaction, yet it is actually being broken into multiple transfers that each require a request/handshake. Is a "buffer" considered as only a single byte/half-word/word of data? Or is this a single buffer broken into 4 "chunks"? Changing the SCSIZE and DCSIZE entries of the DMAC_CTRLA register, which somehow control the size of said "chunks", seems to have no effect, though.
I am thoroughly confused as to the relationships between handshakes, transactions, transfers, buffers and chunks and how you actually interact with these concepts via registers. If anyone could enlighten me on any of these topics that would be amazing.
Thank you.