KL25Z ADC hardware triggering with DMA

cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

KL25Z ADC hardware triggering with DMA

Jump to solution
3,135 Views
aloblaw
Contributor II

Hi,

I am a bit of a newcommer to Kinetis devices. Been playing with this project for a while and have had relatively little success.

Here are my basic requirements:

-sample 4 ADC inputs at >= 2kHz each (so 8kHz sampling speed total)

-send those values over USB for data logging (I am grabbing the values and writing them values to a .csv file in a Python script)

I found an interesting example online for PIT triggering the ADC, which stores the value in a buffer:

PIT- ADC- DMA Example for FRDM-KL25z, FRDM-K64F, TWR-K60D100 and TWR-K70

This example is great except when I crank up the speed of the PIT to 1us interrupts (i.e. PIT_LDVAL0 = 0x14), the program spends all of it's time in the interrupt routines. I am worried the interrupt is going to eat up all of the clock cycles and leave no time to do anything else.

What I need is a way to set up the ADC to convert values as fast as possible, and transfer at KNOWN periodic intervals (via DMA) to the UART (or a buffer). All of this should take place in the background so the CPU is not used at all. So far I cannot find a way to do that, as an interrupt seems to be required in both the PIT and the DMA.

Any recommendations are appreciated.

Tags (4)
0 Kudos
1 Solution
1,499 Views
aloblaw
Contributor II

Ok I finally figured this all out. For anybody who is curious, here is the solution.

The project is fairly simple:

Turns on the ADC for continuous (as fast as possible) conversion. The ADC conversions trigger the DMA to transfer data to the DAC. At the maximum clock frequency I can get ~600kHz sampling rate out. This is less than the 825kHz specified for the ADC but I'm sure I could tweak something to go faster if needed.

Configuring the DAC is simple:

Turn on the DAC clock and enable the DAC.

Configuring the ADC is also fairly simple.

Turn on the ADC clock and then set the following:

CFG1 register

MODE = 1 (12 bit mode - the DAC is only 12 bits so be sure to set this or you'll get wrap-around),

ADICLK = 0 (input bus clock = 24MHz),

ADIV = 1 (divide by 2)

SC2 register

DMAEN = 1 (enable DMA)

SC3 register

ADCO = 1 (enable continuous conversions)

SC1A register

Set input channel you are using, I set ADCH = 8

Configuring the DMA is the only tricky part. I think there are some bugs that Kinetis needs to address.

1. Enable the DMAMUX and DMA clocks

2. Turn off the DMAMUX (so you can set up the DMA and DMAMUX)

3. Clear any pending errors and the done mask (DMA_DSR_BCR0 |= DMA_DSR_BCR_DONE_MASK;)

4. Set the source and destination for the DMA:

     Source (SAR0) = ADC0_RA          Destination (DAR0) = DAC0_DATA0L

5. Set the # bytes you want to transfer before interrupting (yes, you HAVE to interrupt, no way around it. I think this is a bug... explained below)

     DMA_DSR_BCR0 = 1048574 (My dma transfers are 2 bytes, so this # must be a multiple of 2)

6. Set up the DCR with the following:

Enable External interrupts (EINT) (Need to reset the BCR and DONE bit to allow continuous operation (bug explained below))

Enable Peripheral Request (ERQ) (Allow the ADC to trigger the DMA)

Force a single read/write transfer per request (CS) (this makes sure the DMA only transfers on ADC trigger)

Reset the ERQ bit when BCR is exhausted (D_REQ) (this makes sure the ADC does not try to trigger an interrupt if BCR is empty, it should not be needed but it is)

Set source and destination size to 2 bytes (SSIZE, DSIZE = 2)

7. Configure the DMAMUX to use the ADC0 channel as the trigger and enable the DMA (ENBL = 1, SOURCE = 40)

There is only one interrupt routine required, the DMA interrupt. It is configured as follows:

1. Clear the done bit (DMA_DSR_BCR0 |= DMA_DSR_BCR_DONE_MASK)

2. reset the BCR count register (I set it back up to 1048574 to have minimal interrupt time) (DMA_DSR_BCR0 |= DMA_DSR_BCR_BCR(1048574))

3. Re-enable peripheral requests to allow ADC to trigger DMA again (DMA_DCR0 |= DMA_DCR_ERQ_MASK)

That's it. It will work and automatically send ADC data to the DAC. The speed at which it does this is dependent on how you set up your clocks in the CPU, but I was able to transfer at a rate of ~600kHz (ADC-DAC).

Bug in the DMA?

To my mind, the CS bit (cycle steal) should allow the DMA to run whether the BCR register is 0 or not. It SHOULD force a transfer of data from the source to the destination (ADC-DAC in my case). But if the BCR register is 0, it does not. This is the only reason why we need an interrupt. To reset the BCR register and clear the DONE bit.

Also, if the D_REQ is not set (i.e. if peripheral requests are not disabled when the BCR is exhausted), then when the ADC tries to trigger the DMA (on an empty BCR register) it causes an error (the CE bit goes high) on the BCR. I don't really understand why, but I've accepted it, and as long as you re-enable peripheral requests (ERQ) it will work continously.

Summing up

I believe this is the most efficient way to grab data from the ADC and pipe it to some peripheral or location in memory. It requires very little processor time (the interrupt occurs very infrequently, less than 1 interrupt per second) and the interrupt itself has minimal instructions (although you can add a few if you want to toggle a peripheral GPIO or transfer some data elsewhere).

I hope this helps other people. Code is attached.

View solution in original post

0 Kudos
4 Replies
1,499 Views
mjbcswitzerland
Specialist V

Hello

When the PIT is used to trigger an ADC conversion it will not require an interrupt.

The DMA triggered on ADC coversion completion saves the vaue to a RAM buffer, which also requires no interrupt

The DMA can be configured to allow this to repeat a number of times before it interrupts, or else to free-run in a circuar buffer without any interrupts ever.

Therefore you should have low (depending on the size of the RAM buffer used) or zero interrupt overhead.

If you don't have the expected situation check that the configuration is not for an interrupt for every ADC conversion.

See also chapter 3 of the following: http://www.utasker.com/docs/uTasker/uTaskerADC.pdf

Regards

Mark

0 Kudos
1,499 Views
aloblaw
Contributor II

Thank you Mark,

I must be missing something though. The PIT works fine for triggering the ADC, except that the PIT itself requires an interrupt routine to restart the timer. I want the PIT to count down until 0, generate the trigger for ADC conversion, then restart it's own timer and countdown again. What register configuration am I missing?

Am I correct in assuming that I need to use DMA cycle steal (DMA_DCR0->CS = 1) to allow continuous conversions without the need to reset the BCR and clear the DONE flag?

Thanks,

Andrew

0 Kudos
1,498 Views
aloblaw
Contributor II

Ok, I've been playing with it a bit more. I set it up as a very simple ADC->DAC push program so I can probe the input and the output on an oscilloscope and verify that it's working (and measure delay).

I've gotten to the point where the ADC to starts and continuously samples (I check using EmbSys Registers debugging tool), and triggers a DMA event. The problem is, the DMA is not pushing the values from the ADC register to the DAC output. My DMA setup is shown below:

DMAMUX0_CHCFG0 = 0x00;

DMA_SAR0 = (uint32_t)&ADC0_RA;

DMA_DAR0 = (uint32_t)&DAC0_DAT0L; //(uint32_t)&value;

DMA_DSR_BCR0 = DMA_DSR_BCR_BCR(2); // 2 bytes (16 bits) per transfer

DMA_DCR0 = DMA_DCR_ERQ_MASK | DMA_DCR_CS_MASK | DMA_DCR_SSIZE(2) | DMA_DCR_DSIZE(2);

DMAMUX0_CHCFG0 = DMAMUX_CHCFG_ENBL_MASK | DMAMUX_CHCFG_SOURCE(40);

I set the DMA_DCR_CS_MASK in the DMA setup but it does not cause a transfer. I found that if I set the DMA_DSR_BCR0 to a large number, I can get that many samples until it decides to stop. For example, I used:

DMA_DSR_BCR0 = DMA_DSR_BCR_BCR(1048574); // 1048574 bytes per transfer

and I was able to get a little over a second of data to which I could freeze the oscilloscope to view performance.

But I need it to be able to work this well without setting the BCR value..What am I missing?

I've attached the code I've been playing with. Any suggestions are greatly appreciated. Thank you!

0 Kudos
1,500 Views
aloblaw
Contributor II

Ok I finally figured this all out. For anybody who is curious, here is the solution.

The project is fairly simple:

Turns on the ADC for continuous (as fast as possible) conversion. The ADC conversions trigger the DMA to transfer data to the DAC. At the maximum clock frequency I can get ~600kHz sampling rate out. This is less than the 825kHz specified for the ADC but I'm sure I could tweak something to go faster if needed.

Configuring the DAC is simple:

Turn on the DAC clock and enable the DAC.

Configuring the ADC is also fairly simple.

Turn on the ADC clock and then set the following:

CFG1 register

MODE = 1 (12 bit mode - the DAC is only 12 bits so be sure to set this or you'll get wrap-around),

ADICLK = 0 (input bus clock = 24MHz),

ADIV = 1 (divide by 2)

SC2 register

DMAEN = 1 (enable DMA)

SC3 register

ADCO = 1 (enable continuous conversions)

SC1A register

Set input channel you are using, I set ADCH = 8

Configuring the DMA is the only tricky part. I think there are some bugs that Kinetis needs to address.

1. Enable the DMAMUX and DMA clocks

2. Turn off the DMAMUX (so you can set up the DMA and DMAMUX)

3. Clear any pending errors and the done mask (DMA_DSR_BCR0 |= DMA_DSR_BCR_DONE_MASK;)

4. Set the source and destination for the DMA:

     Source (SAR0) = ADC0_RA          Destination (DAR0) = DAC0_DATA0L

5. Set the # bytes you want to transfer before interrupting (yes, you HAVE to interrupt, no way around it. I think this is a bug... explained below)

     DMA_DSR_BCR0 = 1048574 (My dma transfers are 2 bytes, so this # must be a multiple of 2)

6. Set up the DCR with the following:

Enable External interrupts (EINT) (Need to reset the BCR and DONE bit to allow continuous operation (bug explained below))

Enable Peripheral Request (ERQ) (Allow the ADC to trigger the DMA)

Force a single read/write transfer per request (CS) (this makes sure the DMA only transfers on ADC trigger)

Reset the ERQ bit when BCR is exhausted (D_REQ) (this makes sure the ADC does not try to trigger an interrupt if BCR is empty, it should not be needed but it is)

Set source and destination size to 2 bytes (SSIZE, DSIZE = 2)

7. Configure the DMAMUX to use the ADC0 channel as the trigger and enable the DMA (ENBL = 1, SOURCE = 40)

There is only one interrupt routine required, the DMA interrupt. It is configured as follows:

1. Clear the done bit (DMA_DSR_BCR0 |= DMA_DSR_BCR_DONE_MASK)

2. reset the BCR count register (I set it back up to 1048574 to have minimal interrupt time) (DMA_DSR_BCR0 |= DMA_DSR_BCR_BCR(1048574))

3. Re-enable peripheral requests to allow ADC to trigger DMA again (DMA_DCR0 |= DMA_DCR_ERQ_MASK)

That's it. It will work and automatically send ADC data to the DAC. The speed at which it does this is dependent on how you set up your clocks in the CPU, but I was able to transfer at a rate of ~600kHz (ADC-DAC).

Bug in the DMA?

To my mind, the CS bit (cycle steal) should allow the DMA to run whether the BCR register is 0 or not. It SHOULD force a transfer of data from the source to the destination (ADC-DAC in my case). But if the BCR register is 0, it does not. This is the only reason why we need an interrupt. To reset the BCR register and clear the DONE bit.

Also, if the D_REQ is not set (i.e. if peripheral requests are not disabled when the BCR is exhausted), then when the ADC tries to trigger the DMA (on an empty BCR register) it causes an error (the CE bit goes high) on the BCR. I don't really understand why, but I've accepted it, and as long as you re-enable peripheral requests (ERQ) it will work continously.

Summing up

I believe this is the most efficient way to grab data from the ADC and pipe it to some peripheral or location in memory. It requires very little processor time (the interrupt occurs very infrequently, less than 1 interrupt per second) and the interrupt itself has minimal instructions (although you can add a few if you want to toggle a peripheral GPIO or transfer some data elsewhere).

I hope this helps other people. Code is attached.

0 Kudos