Confusion on the workings of DMA

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.

Working with the AHB DMA for the PWM peripheral is a bit tricky, whereas the PDC DMA for the PWM is relatively easier.

The DMA with the PWM works with synchro channels. Here you will find an example sketch with a PDC DMA(reply #6):

https://forum.arduino.cc/index.php?topic=537904.0

The AHB DMA does more or less the same, BUT a special register is missing in the header file (I don't know why) even though an AHB DMA is provided page 339 of SAM3x datasheet. Note that this register, namely PWM_DMAR, is not missing either in datsheet or in header files of SAM4E or SAMS70 uc. Go to page 1204 of SAMS70 datasheet to understand how works PWM_DMAR.

Conclusion: you have to declare this missing register in the header file. Good news, once PWM_DMAR is declared, AHB DMA for the PWM peripheral works nicely !

Does the use of the special register really make a difference? As shown in my example, it is possible to get it working by using the PWM_CDTY register directly, it's just that I don't understand how/why it works, as I explained. Especially since this special register is not documented, I would presume one can use DMA to transfer to any peripheral register and it makes no difference. As I understand, the only "peripheral" part is the use of the peripheral to request a transfer automatically.

In any case, my question is not specific to PWM, I just used it as an example. In the project I am actually working on I am interested in DMA transfers to the SPI module (if my testing is correct you can substitute PWM_CDTY for, say, SPI_TDR in my example code and the result is exactly the same). My question is more so how DMA transfers themselves are supposed to work.

AHB DMA transfers are supported ONLY for the ones listed page 339 of Sam3x datasheet.

You have a good example of AHB DMA SPI in TurboSpi library for Sam3x.

For the PWM peripheral, PDC DMA or AHB DMA doesn't make a big difference but with AHB DMA, providing a transfer is supported, transfers are possible Mem2Per or Per2Per.

In all cases I have been using, DMAC is the Flow Controller which monitores periperal requests when a given peripheral is listed page 339.

I appreciate your input, but I think you are misunderstanding my question. I am not so much interested in just getting some code that works, but more wanting to understand how the DMA controller actually works and how to meaningfully use it.

None of the resources I've seen so far properly explain what exactly is going on with DMA past a "here is some code that does DMA things, it works" level. The TurboSPI library is pretty much the same as what I stumbled upon myself, but I have only a surface level understanding of what it's really doing. If I needed to change any of its intricate DMA functionality then I couldn't because I don't even know how what I started with actually works. The TurboSPI library is little help in this regard as there are almost no comments in any section that deals with DMA.

For example, some of the things I'd like to be able to answer:

  • What is a transaction? (e.g. How does it relate to a transfer? How does it relate to a handshake?)
  • What is a transfer? (e.g. How does it relate to a transaction? How does it relate to a handshake?)
  • What is a buffer? (e.g. Is it just one byte/half-word/word? Can it be an array of bytes/half-words/words?)
  • What is a chunk? (e.g. How does it relate to a buffer? How does it relate to a transaction/transfer?)
  • What does a handshake actually control? (e.g. How many bytes/half-words/words/chunks/buffers transfers/transactions does it initiate?)
  • What are single and chunk transfers?
  • What are single and multi- buffer transfers?

Hi ard_newbie,

I was just wondering if you perhaps have any example code for the AHB DMAC with the PWM controller?

I've been attempting to configure the AHB DMAC for hardware handshaking with the PWM controller, but the PWM controller doesn't respond. There simply isn't enough information in the SAM3X8E datasheet about the arcane relationship between these two peripherals, or indeed how either peripheral should be configured.

I've successfully configured the AHB DMAC for memory to memory operation and I've seen both the TurboSPI and your excellent PDC/AHB DMAC USART code, but haven't been able to find any examples for the PWM controller itself.

Here's my code for AHB DMAC with the PWM controller, I'm following the single buffer set-up procedure, perhaps I'm missing something?:

// Enable the AHB DMAC with the PWM Controller to transfer duty-cycles from memory to output
//#define REG_PWM_DMAR      (*(__I uint32_t*)0x40094024U) /**< brief (PWM) PWM DMA Register */

uint16_t data[] = { 9999, 4999, 14999, 4999, 4999, 14999, 9999, 4999, 14999, 14999, 4999, 14999, 19999, 4999, 14999, 9999 };

void setup() {
  // Set-up a PWM output pin D34
  PMC->PMC_PCER1 |= PMC_PCER1_PID39;                        // Enable the DMAC
  DMAC->DMAC_EN = DMAC_EN_ENABLE;                           
  PMC->PMC_PCER1 |= PMC_PCER1_PID36;                        // Enable the PWM controller   
  
  DMAC->DMAC_CHDR = DMAC_CHDR_DIS3;                                               // Disble DMAC channel 3
  DMAC->DMAC_EBCISR;                                                              // Clear any pending interrupts
  DMAC->DMAC_CH_NUM[3].DMAC_SADDR = (uint32_t)data;                               // Set source address to data array in memory
  DMAC->DMAC_CH_NUM[3].DMAC_DADDR = (uint32_t)&PWM->PWM_CH_NUM[0].PWM_CDTYUPD;    // Set destination address to the PWM channel 0 duty-cycle update register
  //DMAC->DMAC_CH_NUM[3].DMAC_DADDR = (uint32_t)&PWM->Reserved1[0];                 // Set destination address to the PWM DMAR register
  DMAC->DMAC_CH_NUM[3].DMAC_DSCR = 0;                                             // Set the descriptor to 0
  DMAC->DMAC_CH_NUM[3].DMAC_CTRLA = DMAC_CTRLA_BTSIZE(8) |                        // Set the beat size to 8
                                    DMAC_CTRLA_SRC_WIDTH_HALF_WORD |              // Set the source data size to half word (16-bits)
                                    DMAC_CTRLA_DST_WIDTH_HALF_WORD;               // Set the destination data size to half word (16-bits) 
  DMAC->DMAC_CH_NUM[3].DMAC_CTRLB = DMAC_CTRLB_SRC_DSCR_FETCH_DISABLE |           // Use DMAC registers for the source descriptor
                                    DMAC_CTRLB_DST_DSCR_FETCH_DISABLE |           // Use DMAC registers for the destination descriptor
                                    DMAC_CTRLB_FC_MEM2PER_DMA_FC |                // Set-up the transfer from memory to peripheral
                                    DMAC_CTRLB_SRC_INCR_INCREMENTING |            // Increment the source address for each beat
                                    DMAC_CTRLB_DST_INCR_FIXED;                    // Keep the destination address fixed for each beat
  DMAC->DMAC_CH_NUM[3].DMAC_CFG = DMAC_CFG_DST_PER(15) |                          // Set the destination trigger to the PWM Controller
                                  DMAC_CFG_DST_H2SEL |                            // Activate the destination hardware handshaking interface 
                                  DMAC_CFG_SOD |                                  // Enable Stop On Done (SOD)
                                  //DMAC_CFG_AHB_PROT(1) |                          // Set the AHB bus protection
                                  DMAC_CFG_FIFOCFG_ASAP_CFG;                      // Send AHB bus transfer to destination ASAP
                                  //DMAC_CFG_FIFOCFG_ALAP_CFG;                      // Send largest AHB bus transfer to destination
  DMAC->DMAC_CHER = DMAC_CHER_ENA3;                                               // Enable the AHB DMA Controller
 
  PIOC->PIO_ABSR |= PIO_ABSR_P2;                            // Set the port C PWM pins to peripheral type B                
  PIOC->PIO_PDR |= PIO_PDR_P2;                              // Set the port C PWM pins to outputs                 
  PWM->PWM_CLK = PWM_CLK_PREA(0) | PWM_CLK_DIVA(84);        // Set the PWM clock A rate to 1MHz (84MHz/84)
  PWM->PWM_SCM |= PWM_SCM_UPDM_MODE2 | PWM_SCM_SYNC0;       // Automatically load the duty-cycle register in sync mode                                  
  PWM->PWM_CH_NUM[0].PWM_CMR = PWM_CMR_CPRE_CLKA;           // Enable single slope PWM and set the clock source as CLKA for all synchronous channels
  PWM->PWM_CH_NUM[0].PWM_CPRD = 19999;                      // Set the PWM frequency 1MHz/(19999 + 1) = 50Hz for all synchronous channels
  PWM->PWM_CH_NUM[0].PWM_CDTY = 4999;                       // Set the PWM duty cycle to 25%   
  PWM->PWM_ENA = PWM_ENA_CHID0;                             // Enable PWM channel 0                                
}

void loop() 
{  
  DMAC->DMAC_CH_NUM[3].DMAC_CTRLA |= DMAC_CTRLA_BTSIZE(8);        // Prime the AHB DMA Controller
  DMAC->DMAC_CHER |= DMAC_CHER_ENA3;                              // Enable the AHB DMA Controller 
  //while (DMAC->DMAC_CHSR & DMAC_CHSR_ENA3);                     // Wait for the AHB DMA Controller to complete
  delay(1000);                                                    // Wait for 1 second
}

Hi MartinL,

The AHB DMA for PWM is a bit tricky as mentionned in my reply #1, BECAUSE a register is missing (PWM_DMAR) in your PWM header file.

PWM_DMAR is a special register needed to "spread" the values sent to the DMAC Destination (DMAC_DADDR) over all synchro channels. This register exist in the other Sam uc header file where ther is a PWM AHB DMA (SamS70, etc...)

Stepwise, 2 possibilities to ressucite this register:

1/ Without modifying the header file

Before setup(), declare:

volatile uint32_t REG_PWM_DMAR = (uint32_t)0x40094024;

In setup();

The destination is now REG_PWM_DMAR.

DMAC->DMAC_CH_NUM[DMAC_CH].DMAC_DADDR = (uint32_t)REG_PWM_DMAR;

2/ With a modification of the PWM header file

Go to : File>Preferences, Then follow the path: */
packages/arduino/hardware/sam/1.6.x/system/CMSIS/Device/Atmel/Sam3xa/include/component/component_pwm and place PWM_DMAR at offset 0x24:

typedef struct {
.....
RwReg PWM_SCM; /< \brief (Pwm Offset: 0x20) PWM Sync Channels Mode Register */
RwReg PWM_DMAR; /
< \brief (Pwm Offset: 0x24) PWM DMAR Register */
RwReg PWM_SCUC; /< \brief (Pwm Offset: 0x28) PWM Sync Channels Update Control Register /
.....
#define PWM_SCM_PTRCS(value) ((PWM_SCM_PTRCS_Msk & ((value) << PWM_SCM_PTRCS_Pos)))
/
-------- PWM_DMAR : (PWM Offset: 0x24) PWM DMA Register -------- */
#define PWM_DMAR_DMADUTY_Pos 0
#define PWM_DMAR_DMADUTY_Msk (0xffffffu << PWM_DMAR_DMADUTY_Pos) /
< \brief (PWM_DMAR) Duty-Cycle Holding Register for DMA Access /
#define PWM_DMAR_DMADUTY(value) ((PWM_DMAR_DMADUTY_Msk & ((value) << PWM_DMAR_DMADUTY_Pos)))
/
-------- PWM_SCUC : (PWM Offset: 0x28) PWM Sync Channels Update Control Register -------- */

Then in setup():

DMAC->DMAC_CH_NUM[DMAC_CH].DMAC_DADDR = (uint32_t)&PWM->PWM_DMAR;

And you are done !

If I may, MartinL:

  • Try without setting DMAC_CFG_SOD. I'm not sure exactly what it does, but it's possible it's stopping the DMA transfer prematurely.
  • A BTSIZE of 8 will only do 8 DMA transfers, looks like your source array is 16 elements long though. You'll need to set it to 16 if you want to transfer the whole array.
  • Try FIFOCFG_ALAP_CFG for the DMAC. I don't know exactly what the other options do but I can tell you that FIFOCFG_ALAP_CFG does work.
  • Try making your source data array volatile. I've had times when weird things have happened and data doesn't get transferred correctly if it isn't.
  • Doesn't look like you're re-setting DMAC_SADDR each transfer. When the source is set to increment mode, I'm pretty sure the value in the register itself gets incremented - i.e. after one set of transfers are done, the register is now pointing to whatever's after your source data array.

Also definitely try some dry runs where you DMAC transfer to a normal memory location instead so you can validate that DMAC is actually working. Leave the PWM all set up, so it triggers the DMAC as usual, but set DMAC_DADDR to some other location. Then you can check to see if the values really are getting written there as you expect.

I'd recommend doing the same for just the PWM, if you haven't already. Just to make sure it is working as it should be.

Thank you to you both for your suggestions, much appreciated.

So far, I've managed to get memory to memory transfers working, for example transfering data from one array to another:

// Enable the AHB DMAC for memory to memory transfer
uint16_t data[] = { 9999, 4999, 14999, 4999, 4999, 14999, 9999, 4999, 14999, 14999, 4999, 14999, 19999, 4999, 14999, 9999 };
uint16_t destData[16];

void setup() {
  SerialUSB.begin(115200);                                                        // Initialise the native USB port
  while(!SerialUSB);                                                              // Wait for the console to be ready
  
  PMC->PMC_PCER1 |= PMC_PCER1_PID39;                                              // Enable the DMAC
  DMAC->DMAC_EN = DMAC_EN_ENABLE;                           
  DMAC->DMAC_CHDR = DMAC_CHDR_DIS3;                                               // Disble DMAC channel 3
  DMAC->DMAC_EBCISR;                                                              // Clear any pending interrupts
  DMAC->DMAC_CH_NUM[3].DMAC_SADDR = (uint32_t)data;                               // Set source address to data array in memory
  DMAC->DMAC_CH_NUM[3].DMAC_DADDR = (uint32_t)destData;                           // Set destination address to data array in memory
  DMAC->DMAC_CH_NUM[3].DMAC_DSCR = 0;                                             // Set the descriptor to 0
  DMAC->DMAC_CH_NUM[3].DMAC_CTRLA = DMAC_CTRLA_BTSIZE(16) |                       // Set the beat size to 16
                                    DMAC_CTRLA_SRC_WIDTH_HALF_WORD |              // Set the source data size to half word (16-bits)
                                    DMAC_CTRLA_DST_WIDTH_HALF_WORD;               // Set the destination data size to half word (16-bits) 
  DMAC->DMAC_CH_NUM[3].DMAC_CTRLB = DMAC_CTRLB_SRC_DSCR_FETCH_DISABLE |           // Use DMAC registers for the source descriptor
                                    DMAC_CTRLB_DST_DSCR_FETCH_DISABLE |           // Use DMAC registers for the destination descriptor
                                    DMAC_CTRLB_FC_MEM2MEM_DMA_FC |                // Set-up the transfer from memory to peripheral
                                    DMAC_CTRLB_SRC_INCR_INCREMENTING |            // Increment the source address for each beat
                                    DMAC_CTRLB_DST_INCR_INCREMENTING;             // Keep the destination address fixed for each beat
  DMAC->DMAC_CH_NUM[3].DMAC_CFG = //DMAC_CFG_DST_PER(15) |                          // Set the destination trigger to the PWM Controller
                                  //DMAC_CFG_DST_H2SEL |                            // Activate the destination hardware handshaking interface 
                                  DMAC_CFG_SOD |                                  // Enable Stop On Done (SOD)
                                  //DMAC_CFG_AHB_PROT(1) |                          // Set the AHB bus protection
                                  //DMAC_CFG_FIFOCFG_ASAP_CFG;                      // Send AHB bus transfer to destination ASAP
                                  DMAC_CFG_FIFOCFG_ALAP_CFG;                      // Send largest AHB bus transfer to destination
  DMAC->DMAC_CHER = DMAC_CHER_ENA3;                                               // Enable the AHB DMA Controller
  while (DMAC->DMAC_CHSR & DMAC_CHSR_ENA3);                                       // Wait for the AHB DMA Controller to complete
  for (uint8_t i = 0; i < 16; i++)
  {
    SerialUSB.println(destData[i]);                                               // Display the results in the destintation array
  }
}

void loop() {}

I'm still working on the PWM Controller. I'll let you know, if I finally find a solution.

Thanks again.

Kind regards,
Martin