Buffer multiple ADC channel results using DMA?

Tip / Sign in to post questions, reply, level up, and achieve exciting badges. Know more

cross mob
lock attach
Attachments are accessible only for community members.
KyTr_1955226
Level 6
Level 6
250 sign-ins 10 likes given 50 solutions authored

Hi all,

I've got some questions regarding a specific ADC+DMA setup I'd like to accomplish, but I'm not sure how to make it happen (I'm rather new to using DMA).

I'd like to, through DMA, have the SAR ADC cycle between 3 channels and maintain 3 separate ADC results buffers.  The idea being that I could at any time read the last 8 (or really any arbitrary number) readings for any of the inputs and quickly generate an average.

My schematic looks like this:

pastedImage_0.png

The idea is that the PWM module provides a short (about 1.2ms) pulse from the PWM module.  The rising edge starts the ADC Conversion, the EOC signal triggers the DMA to transfer the reading to SRAM, and the interrupt on the falling edge of the PWM increments the mux(es).

The PWM Period is 40ms, so there should be more than enough time to switch the analog mux for the next reading (I plan on tightening this time up once I get the general system working the way I want).  The Timer and UART are just for serial reporting.  I've confirmed the EOC signal is coming out of the ADC well before the falling edge of my PWM.

What's getting me stuck is configuring the DMA to properly increment the destination address and reset to the first address after 8 DMA transactions.  My idea was to count the number of transfers in their respective ISRs and reset the DMA when it reaches the end of it's respective buffer.  I.E.

void DMA0_Reset(void){

    ADC_CH0_DMA_DmaRelease();

    DMA_Ch0_Init();

}

void ISR_DMA0_DONE_Interrupt_InterruptCallback(void){

    static uint8_t count = 0;

    ISR_DMA0_DONE_ClearPending();

    /*DMA is done transferring reading to SRAM*/

    if (count++ >= ADC_BUFF_SIZE-1){

        /*End of buffer, reset DMA to initial state*/

        DMA0_Reset();

        count = 0;

    }

    DMADone = true;

}

I am initializing the DMA though the autogenerated code of the DMA wizard.  An init function like this one is being called for all 3 DMA channels before the main loop, as well as being used to reinitialize the channel in the Reset function:

void DMA_Ch0_Init(void){

    /* DMA Configuration for ADC_CH0_DMA */

    ADC_CH0_DMA_Chan = ADC_CH0_DMA_DmaInitialize(ADC_CH0_DMA_BYTES_PER_BURST, ADC_CH0_DMA_REQUEST_PER_BURST,

        HI16(ADC_CH0_DMA_SRC_BASE), HI16(ADC_CH0_DMA_DST_BASE));

    ADC_CH0_DMA_TD[0] = CyDmaTdAllocate();

    CyDmaTdSetConfiguration(ADC_CH0_DMA_TD[0], 2, CY_DMA_DISABLE_TD, ADC_CH0_DMA__TD_TERMOUT_EN | CY_DMA_TD_INC_DST_ADR );

    CyDmaTdSetAddress(ADC_CH0_DMA_TD[0], LO16((uint32)ADC_SAR_SAR_WRK0_PTR), LO16((uint32)&ADC_Data1[0]));

    CyDmaChSetInitialTd(ADC_CH0_DMA_Chan, ADC_CH0_DMA_TD[0]);

}

My PWM TC (falling edge) ISR is as follows:

void ISR_PWM_Interrupt_InterruptCallback(void){

    /*Falling edge of PWM, increment the mux input*/

    ADC_PWM_ReadStatusRegister();

    ISR_PWM_ClearPending();

   

    if (DMADone){

        DMADone = false;

       

        switch (ADChannel){

            case 0:

                CyDmaChEnable(ADC_CH1_DMA_Chan, 0);

                ADChannel++;

            break;

           

            case 1:

                CyDmaChEnable(ADC_CH2_DMA_Chan, 0);

                ADChannel++;

            break;

           

            case 2:

                CyDmaChEnable(ADC_CH0_DMA_Chan, 0);

                ADChannel = 0;

            break;

           

        }

        MuxCtrl_Write(ADChannel);

    }

}

What I'm seeing with this project is that the first ADC input readings just seem to overrun into all 3 of my ADC buffers, incrementing the destination address, but never incrementing the muxes and switching to the next channel.  I have attached the project, any suggestions to where I'm going wrong would be appreciated (I'm sure there's probably a better way to accomplish this).

So as far as specific questions go:

1) How should I go about reinitializing a DMA channel back to the starting address?

2) Would I maybe be better off using a single DMA component to dump all my readings into a single buffer, then pulling out the values I want for my average with some pointer math?

3) I notice that when I set preserveTD to 1 when I enable a DMA channel, my mux switching logic and switching of buffers seems to work, but I never fill anything other than the first index of the buffer for each channel.  When preserveTD is 0, the first channel just overruns all the buffers.  Which of these settings should I actually be using in this case?

Thanks in advance for any assistance!

I have attached the project if someone wants to have a deeper look.  I'm just trying to prototype this on a CY8CKIT-050.

0 Likes
1 Solution

I have had the exact same issue before with a design where I needed multiplexed inputs to the ADC but the sequencing SAR was too complex in terms of UDB.

My solution was to essentially create my own smaller sequencing SAR ADC and it worked quite well.

An overview of what I did that might help you:

I didn't service anything in interrupt routines. I handled the start of conversion, end of conversion, dma transaction chaining and channel switching logic in hardware.

The general process here is you use a digital clock to make your sample requests, at your desired sample rate. When you get a clock edge, or an eoc occurs from the previous channel, you switch to your next channel and then trigger a delay to occur (about 2us or so) that will then send a signal to your soc of the ADC. Essentially this provides a small buffer of time between the actual channel switching and the start of conversions, which is needed for the switch to be settled otherwise your results are noisy. I do the delay with PWM setup for a hardware trigger (from the mux switch signal) that has about a 2us period and the cmp being such that a small pulse is generated at the end of the period.

Then you just need your logic for the switching channels. I handled it by creating two custom components, one that managed the sequencing for my mux (can be lookup table based, or digital logic, or verilog), the other to manage the timings between channel switching, start of conversions, and end of conversions.

View solution in original post

12 Replies
lock attach
Attachments are accessible only for community members.
KyTr_1955226
Level 6
Level 6
250 sign-ins 10 likes given 50 solutions authored

So I took a crack at implementing #2 on my list of questions and it seems to work pretty well (for a bit at least).

pastedImage_0.png

I am moving the readings from all channels into a single buffer via DMA and generating an average.  The DMA is set to perform 32 sets of 2 bytes each.  I'm using an interrupt for the PWM Falling Edge, ADC EoC, and DMA nrq.  The flow being as follows:

  1. ADC Starts Convert on selected channel on PWM rising edge
  2. EoC ISR fires - Select next mux input.
  3. DMA nrq ISR fires - We have the specified number of readings in our buffer, disable DMA Channel to reset address to beginning of buffer.  Set DMA_Done flag.
  4. PWM Falling Edge: If DMA_Done is set, Re-Enable DMA Channel to take the next batch of readings.
  5. Averages are generated every few seconds by picking out the desired readings based on which channel is specified:

uint16_t ADC_GenerateAvg(uint8_t ch){

    uint32_t sum = 0;

    uint16_t offset;

    uint16_t * base;

    uint16_t * dataptr;

   

    base = (ADC_Data + ch);

   

    for (offset = 0 ; offset < DMA_BUFF_SIZE ; offset += ADC_NUM_CHANNELS){

        dataptr = base + offset;

        sum += *dataptr;

    }

   

    return (uint16_t)(sum >> 3); //8 samples per channel, div by 8

   

}

I am having an issue though,  I'm finding that after a bit of time the DMA nrq ISR stops firing, and my values stop updating and I can't find why.  I'm also sometimes losing the UART (it stops transmitting, the flag to print is being set and it's entering the code block with UART_WriteString() though.)

Project is attached.  This is an alright solution otherwise (for as long as it works), and only uses a single DMA channel but I'd be curious to see if it's possible to accomplish my original goal, which is to maintain multiple separate buffers for each ADC channel with no (or minimal, to do things like switch analog channel) CPU intervention.

0 Likes

Still working on the project in the 2nd post - the single DMA channel.  Something's going VERY wrong, but I can't seem to track a cause down.

I'm still seeing the DMA crap out (and seemingly taking the UART with it sometimes?  Maybe something is getting clobbered somewhere).  I put a counter that increments inside the ISR for the DMA nrq to count the number of completed transfers and I'm seeing that it dies out every time with the counter at/around 0xB15 (I've also seen 0xB18).  Speeding up the ADC PWM period can make it occur faster, slowing it down pushes it out slower, but every time it goes wrong it's after 0xB15 (2837) DMA transfers.

CyDmacError does not return an error code when this occurs.

I'm kind of stumped.

Thoughts?

[EDIT 1/25]:

After playing with it some more I found the issue (I think). 0xB15 is the value I'm pulling in on one of the ADC channels.  So this must mean the DMA is overrunning into my DMACounter variable (as well as anything else that might be in its path of destruction).

Here's what's going on:

After a full transfer of 48 words (3 channels, 16 words each) the ISR for nrq is firing, and flagging that DMA is done as well as disabling the DMA channel via a call to:

CyDmaChDisable(ADC_DMA_Chan);

The DMA Done flag then gets serviced the in the falling edge ISR of the PWM that drives the ADC SOC signal.  This calls DMA_Init() which should reset the destination address to the first address of my ADC_Data buffer and re-enables the DMA channel:

.

void DMA_Init(void){

/* DMA Configuration for ADC_DMA */

    ADC_DMA_Chan = ADC_DMA_DmaInitialize(ADC_DMA_BYTES_PER_BURST, ADC_DMA_REQUEST_PER_BURST,

    HI16(ADC_DMA_SRC_BASE), HI16(ADC_DMA_DST_BASE));

    ADC_DMA_TD[0] = CyDmaTdAllocate();

    /*Term Out generated after DMA_BUFF_SIZE samples*/

    /*Total Bytes = DMA_BUFF_SIZE *2*/

    CyDmaTdSetConfiguration(ADC_DMA_TD[0], (DMA_BUFF_SIZE*2), CY_DMA_DISABLE_TD, ADC_DMA__TD_TERMOUT_EN | CY_DMA_TD_INC_DST_ADR);

    CyDmaTdSetAddress(ADC_DMA_TD[0], LO16((uint32)ADC_SAR_SAR_WRK0_PTR), LO16((uint32)ADC_Data));

    CyDmaChSetInitialTd(ADC_DMA_Chan, ADC_DMA_TD[0]);

    CyDmaChEnable(ADC_DMA_Chan, 0);

}

What I was missing was that CyDmaChDisable() does NOT seem to free the TD.  I thought resetting the destination address was being handled by my call to CyDmaTdSetConfiguration() inside DMA_Init() but evidently you cannot reconfigure a TD while it is active.  Before calling DMA_Init() to reconfigure the TD, I have to free the TD first!  Adding a call to :

CyDmaTdFree(ADC_DMA_TD[0]);

Prior to disabling/re-enabling the DMA channel looks to have done the trick.

0 Likes
lock attach
Attachments are accessible only for community members.

I am running across a problem still:

I run stable until 1000+ DMA transfers, then suddenly my DMA DONE interrupt no longer fires.

The running time at which the isr stops firing varies.  I've seen go out after 1097 transfers, 1758, and 2419.  1758 seems to be the most common, so there's got to be some logic to it that can't find yet.  I've attached a scope shot of the order of operations when DMA finishes, which should be every 48 pulses of the PWM.  Everything seems to be in the correct order:

EOC ISR increments the mux channel for the next conversion.

DMA DONE ISR increments my DMA transfer count, sets the DMA_Done flag, frees the TD, and disables the DMA Channel.

PWM ISR calls DMA_Init if DMA_Done has been set.

pastedImage_0.jpg

Apologies for the photo of the scope rather than direct capture. Only way to save captures on this scope is via floppy drive!

What could be killing my DMA Done ISR?  The EOC and PWM TC ISRs continue to fire.

I've attached the latest version of this project.

0 Likes
KyTr_1955226
Level 6
Level 6
250 sign-ins 10 likes given 50 solutions authored

Bumping this up.  100 views but nobody has any ideas

I've still got no idea why this is not working reliably.  The ISR just seems to stop firing.

Changing the frequency of the PWM controlling the ADC conversions maybe makes a difference?

1 DMA = 32 bytes transferred and DMA Done ISR fires

Period = 20000 -> (~3100 DMAs before failure)

Period = 10000 -> (~3100 DMAs before failure)

Period = 5000 -> (1433 DMAs before failure)

0 Likes
KyTr_1955226
Level 6
Level 6
250 sign-ins 10 likes given 50 solutions authored

3 weeks, no ideas?

Bumping again, hoping someone maybe has a suggestion...

0 Likes
BrBr_1320441
Level 3
Level 3
25 replies posted 10 replies posted 5 replies posted

Typically when a DMA stops firing, in my experience, there is a timing issue, an allocation issue (be that channel resources, td resources, or overflowing bounds of the source or destination buffers).

My first observation is that I think the way you are stopping and restarting your DMA is a little peculiar. I would not be able to say if it was wrong, but I don't believe it is the recommended action for what you are doing.

Typically, I believe you should stop and restart a DMA as such:

//Stop

CyDmaChDisable(channel)

CyDmaChFree(channel)

//Restart

CyDmaChEnable(channel) //Maybe you need to DMA_Initialize again too? But I don't think so.

Currently you are disabling the channel and then freeing the TD, then reallocating everything including the channel which was never freed. I do not think that would be a problem, but it is hard to say without looking much deeper.

Start with the above to see if it has any effect. Otherwise I didn't see anything clearly wrong. Your setup looked right for what you were doing. I will say I occasionally have signaling issues at higher frequencies, but at 1.6MHz it shouldn't be a big deal. It would not hurt to add a pulse converter to the output of the nrq that takes the nrq pulse and converts to a lower frequency (1.6MHz for instance). I have seen that cause missed nrq events before.

SO, with that said, I would recommend a few other changes that I think would improve the function of this design (once experimenting with the above a little)

First, if I understand correctly, you simply want to have a PWM that triggers ADC conversions on whatever the selected channel is, and then on EOC you want to change the channel, then stop conversions if all channels once all channels have been scanned, and reenable them if it is disabled on the next conversion request?

I think there is a much better way to do this that requires fewer interrupt routines and relies more on the PSoC hardware. I also think there is room to improve your data buffering to not need to do any pointer magic as you called it.  But first try to understand where you are going wrong. DMA debugging is very frustrating, I know.

130  if (ADChannel >2){

.......
uart.png

0 Likes

Thanks for the input!  I'll have a look at your suggestions.  As far as I could tell when I was getting this thing working, until I freed up and re-allocated the TD, I couldn't get the starting address back where it needed to be.  I was overrunning the bounds of my results space in memory because it would never go back to the starting location.  Again though, I'm hardly experienced in DMA so maybe what you suggested is somehow a way of restarting DMA that I didn't try.

I'll give these things a shot.  I think the pulse converter on NRQ is probably a really good idea.

There's almost certainly a better way of doing what I'm trying to do.  Using a Sequencing SAR component would probably help too, but that's too expensive from a UDB/Macrocell standpoint to be feasible in many of the projects I would need this in (I also need 2 UARTs in most projects, so fitting in that and a Sequencing SAR ADC is usually not doable).

Ideally what I'd like to have is 3 separate buffers constantly updating so I can at any point pull out an average, hence the project in post #1 using 3 DMA modules.  Is there a way to configure a single DMA channel to manage 3 output buffers rather than doing the pointer math to pull the average out for a single channel that I'm doing now?

Which version of the project are you looking at?  The most recent is written:

if (ADChannel >= 2){

        ADChannel = 0;  

    } else {

        ADChannel++;  

    }

Because that timing diagram look like it would have the effect of shifting my averages around (Channel 0 would suddenly start dropping results in the channel 1 buffer, and so on).  That was something I was seeing on an older version of the project but have not seen on the latest posted.

0 Likes

sorry - my mistake.

DMA_DONE_TP is lost when UART is running

if  comment   UART_PutString (UARTBuffer);    DMA_DONE_TP no losses.

You might be interested in this:  Terminating DMA  

Thanks for the link!

I'm curious as to why writing to the UART is wiping out my DMA Done ISR.  Wouldn't the interrupt take precedence?  The TX ISR is not enabled in the component...  At the very I would think it would queue up and fire when the UART is done, not wipe it out entirely.

While that would create issues I doubt it's why the DMA interrupts stop entirely since I can see the next DMA Done fires after the UART wipes one out.

Unfortunately I don't have a ton of time to shake this out over next couple days, but I will come back to this and try and get it going next time I get the chance.

Thanks for the suggestions everyone

0 Likes

KTrenholm,

this is rather old link:

Order of bytes in USB ISOC transfer changing

osc.PNG

0 Likes

I have had the exact same issue before with a design where I needed multiplexed inputs to the ADC but the sequencing SAR was too complex in terms of UDB.

My solution was to essentially create my own smaller sequencing SAR ADC and it worked quite well.

An overview of what I did that might help you:

I didn't service anything in interrupt routines. I handled the start of conversion, end of conversion, dma transaction chaining and channel switching logic in hardware.

The general process here is you use a digital clock to make your sample requests, at your desired sample rate. When you get a clock edge, or an eoc occurs from the previous channel, you switch to your next channel and then trigger a delay to occur (about 2us or so) that will then send a signal to your soc of the ADC. Essentially this provides a small buffer of time between the actual channel switching and the start of conversions, which is needed for the switch to be settled otherwise your results are noisy. I do the delay with PWM setup for a hardware trigger (from the mux switch signal) that has about a 2us period and the cmp being such that a small pulse is generated at the end of the period.

Then you just need your logic for the switching channels. I handled it by creating two custom components, one that managed the sequencing for my mux (can be lookup table based, or digital logic, or verilog), the other to manage the timings between channel switching, start of conversions, and end of conversions.