SAMD21: Reset DMAC using ASF

Go To Last Post
10 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Hi,

I have a DMA job set up, which consists of four resources. Each resource transfers a buffer of a couple of hundred bytes over to SERCOM1. I use software triggering for the transfer. The first time I want to transfer (the first buffer) I call dma_start_transfer_job() and the data gets transferred. Following I call dma_resume_job() to transfer the next buffer. This works perfectly. However, there is the case that the controller needs to resets is complete state and start all over again, starting with the first buffer. This can happen at any time, but the DMA will always be in SUSPEND state (so there is currently no transfer going on). To reset the DMA I tried calling dma_abort_job() and dma_start_transfer_job() once I want to send the fist buffer. This does not seem to work. The data is not transferred to the SERCOM1 data register and the state of the DMA is always BUSY after that. dma_start_transfer_job() however returns OK.

 

Below is a simple test snipped that shows the problem.

stateUSARTDMA = dma_get_job_status(&usart_dma_resource_tx);  // OK
stateUSARTDMA = dma_start_transfer_job(&usart_dma_resource_tx);  // OK, data transferred
delay_ms(10);
stateUSARTDMA = dma_get_job_status(&usart_dma_resource_tx);  // SUSPEND
dma_resume_job(&usart_dma_resource_tx);  // data transferred
delay_ms(10);
stateUSARTDMA = dma_get_job_status(&usart_dma_resource_tx);  // SUSPEND
dma_abort_job(&usart_dma_resource_tx);
delay_ms(10);
stateUSARTDMA = dma_get_job_status(&usart_dma_resource_tx);  // ABORDTED
stateUSARTDMA = dma_start_transfer_job(&usart_dma_resource_tx);  // OK, no data transferred
delay_ms(10);
stateUSARTDMA = dma_get_job_status(&usart_dma_resource_tx);  // BUSY

Below is the code for the initialization of the DMA:

// USART: Set up DMA resource for transmitting
struct dma_resource_config config_usart_dma_resource;
dma_get_config_defaults(&config_usart_dma_resource);
config_usart_dma_resource.peripheral_trigger = SERCOM1_DMAC_ID_TX;
config_usart_dma_resource.trigger_action = DMA_TRIGGER_ACTION_BEAT;
dma_allocate(&usart_dma_resource_tx, &config_usart_dma_resource);

// USART: Set up USART DMA descriptors for each buffer
struct dma_descriptor_config usart_dma_descriptor_config;
dma_descriptor_get_config_defaults(&usart_dma_descriptor_config);
usart_dma_descriptor_config.beat_size = DMA_BEAT_SIZE_BYTE;
usart_dma_descriptor_config.dst_increment_enable = false;
usart_dma_descriptor_config.block_transfer_count = sizeof(packetADC_t);
usart_dma_descriptor_config.step_size = DMA_ADDRESS_INCREMENT_STEP_SIZE_1;
usart_dma_descriptor_config.destination_address = (uint32_t)(&REG_SERCOM1_USART_DATA);
usart_dma_descriptor_config.block_action = DMA_BLOCK_ACTION_SUSPEND;
for(int i = 0; i < ADC_BLOCK_COUNT; i++)
{
	usart_dma_descriptor_config.source_address = ((uint32_t)(&(vibBlockList[i])))+sizeof(packetADC_t);
	if(i+1 != ADC_BLOCK_COUNT)
		usart_dma_descriptor_config.next_descriptor_address = (uint32_t)&(usart_dma_descriptor_list[i+1]);
	else
		usart_dma_descriptor_config.next_descriptor_address = (uint32_t)&(usart_dma_descriptor_list[0]);
	dma_descriptor_create(&(usart_dma_descriptor_list[i]), &usart_dma_descriptor_config);
}
dma_add_descriptor(&usart_dma_resource_tx, &(usart_dma_descriptor_list[0]));
dma_register_callback(&usart_dma_resource_tx, usart_tx_done, DMA_CALLBACK_CHANNEL_SUSPEND);
dma_enable_callback(&usart_dma_resource_tx, DMA_CALLBACK_CHANNEL_SUSPEND);

Any thoughts?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Using the I/O Debug window I was able to tell the difference between the two points right after calling dma_start_transfer_job(). In the second case the ACTIVE register is not initialized. It is still 0, while it has some other value for the first time, including the remaining length of the block to transfer. However, I was not able to find the code in ASF manipulating this register. It is not touched during dma_start_transfer_job().

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The ACTIVE register is read-only so you will not find any writes.

Seems what you want here is not covered by the driver, looks like it handles the case where an ongoing transfer is aborted better than when it's done.

I did some testing and came up with a replacement abort function, no guarantees but it seems to work for a case close to what you have.

void dma_abort_job(struct dma_resource *resource)
{
    uint32_t write_size;
    uint32_t total_size;

    Assert(resource);
    Assert(resource->channel_id != DMA_INVALID_CHANNEL);

    system_interrupt_enter_critical_section();
    DMAC->CHID.reg = DMAC_CHID_ID(resource->channel_id);
    DMAC->CHCTRLA.reg = 0;
    uint32_t temp = DMAC->CHCTRLB.reg;
    /** Perform a reset for the allocated channel */
    DMAC->CHCTRLA.reg = DMAC_CHCTRLA_SWRST;
    while(DMAC->CHCTRLA.reg & DMAC_CHCTRLA_SWRST);
    /* Write config to CTRLB register */
    DMAC->CHCTRLB.reg = temp;
    system_interrupt_leave_critical_section();

    /* Get transferred size */
    total_size = descriptor_section[resource->channel_id].BTCNT.reg;
    write_size = _write_back_section[resource->channel_id].BTCNT.reg;
    resource->transfered_size = total_size - write_size;

    DmacDescriptor dummy = *resource->descriptor;
    dummy.DESCADDR.reg = (uint32_t)resource->descriptor;
    _write_back_section[resource->channel_id] = dummy;
    resource->job_status = STATUS_ABORTED;
}

/Lars

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Hi,

that seems to work just fine. Have not looked at the data yet, but I guess it is ok.

I have one question: What exactly is _write_back_section? I had to copy the definition

COMPILER_ALIGNED(16)
DmacDescriptor _write_back_section[CONF_MAX_USED_CHANNEL_NUM] SECTION_DMAC_DESCRIPTOR;

to my own code. In my opinion this is wrong as I am creating a new array _write_back_section which is not the same as the "original" one. If I add an "extern" to the definition, I would expect him to use the "original" one, but instead I get an "undefined reference" in the linker stage. This is confusing me. I would like to keep the original ASF code untouched.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0
COMPILER_ALIGNED(16)
static DmacDescriptor _write_back_section[CONF_MAX_USED_CHANNEL_NUM] SECTION_DMAC_DESCRIPTOR;

The write back memory is explained in the datasheet:

The write-back memory section is the section where the DMAC stores the transfer descriptors for the
ongoing block transfers.

It is a static in the driver dma.c so in this case you can't keep the ASF code untouched.

/Lars

 

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Have not looked at the data yet, but I guess it is ok.

I would guess not if you did not write in the actual _write_back_section since this was needed to get it to restart (not continue with the block after the aborted one but with the first block).

/Lars

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

You are right. He was transmitting data but not what I wanted him to transmit. Once I edited the function in ASF it seems to work - but I have not completely tested it yet.

However, I still do not understand why I cannot re-declare _write_back_section as extern and still have the function in my code. I think it is quite ugly to change the ASF code as any update or change in ASF will overwrite my changes.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The problem is that it is static  https://en.wikipedia.org/wiki/Static_(keyword)

/Lars

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Untested fix for that is to read out the address from the DMAC:

void mydma_abort_job(struct dma_resource *resource)
{
    uint32_t write_size;
    uint32_t total_size;

    Assert(resource);
    Assert(resource->channel_id != DMA_INVALID_CHANNEL);

    system_interrupt_enter_critical_section();
    DMAC->CHID.reg = DMAC_CHID_ID(resource->channel_id);
    DMAC->CHCTRLA.reg = 0;
    uint32_t temp = DMAC->CHCTRLB.reg;
    /** Perform a reset for the allocated channel */
    DMAC->CHCTRLA.reg = DMAC_CHCTRLA_SWRST;
    while(DMAC->CHCTRLA.reg & DMAC_CHCTRLA_SWRST);
    /* Write config to CTRLB register */
    DMAC->CHCTRLB.reg = temp;
    system_interrupt_leave_critical_section();
    DmacDescriptor* writeBack = (DmacDescriptor*)DMAC->WRBADDR.reg;
    /* Get transferred size */
    total_size = descriptor_section[resource->channel_id].BTCNT.reg;
    write_size = writeBack[resource->channel_id].BTCNT.reg;
    resource->transfered_size = total_size - write_size;

    DmacDescriptor dummy = *resource->descriptor;
    dummy.DESCADDR.reg = (uint32_t)resource->descriptor;
    writeBack[resource->channel_id] = dummy;
    resource->job_status = STATUS_ABORTED;
}

/Lars

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Wow, haven been programming C since more than 15 years, but have never stumbled across that. You live and learn, I guess :)

Thanks a lot!

I will try the suggested code once I have some free time - the current solutions works and I will stick with it.

Last Edited: Mon. Oct 28, 2019 - 05:25 PM