This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

MSP430x2xx ADC12 -> DMA -> RAM

Hello,

I'm working on a small test program testing ADC sampling on the MSP4F2618.

The intended program flow is that Timer B periodically triggers the ADC12 module to sample A2 and A3. When conversion of this sequence finishes, the DMA module transfers both samples into a double buffer in RAM. The user guide, example code, and two threads below have gotten me most of the way there.

The unwanted behavior I'm encountering is that the DMA transfer overwrites the destination every time, so only the first two slots of the buffer are used. The DMA interrupt is triggered at the end of each block transfer. The needed behavior is that the buffer is filled (two values per Timer B trigger) over time and then triggers an interrupt.

I believe that the problem is in how I'm defining the block size. I currently have it set to 2 words as I only want to transfer the ADC12MEM0 and ADC12MEM1 values each trigger. But because the DMA interrupt triggers and the destination address resets at the end of each block transfer I'm not getting the desired behavior.  I've tried increasing the block size to 4 and 8, but that hasn't seemed to work. I know I could change the DMA0DA at the end of each transfer in the interrupt, but I really want to avoid interrupts where possible to reduce power consumption and simplify the execution paths.

Many thanks for any help.


#include <msp430x26x.h>
#include <stdint.h>

#define Num_Samples 16
volatile uint16_t DMAresults[Num_Samples*2];

void main(void)
{
  WDTCTL = WDTPW+WDTHOLD; // Stop watchdog timer
  P6SEL = BIT2 + BIT3; // Enable A/D channel inputs

  /* Power external ICs */
  P2DIR = BIT0; P2OUT = BIT0;
  P5DIR = BIT5; P5OUT = BIT5;
  P6DIR = BIT4; P6OUT = BIT4;

  /* Configure TimerB to produce the triggers for the ADC */
  TBCTL = TBSSEL_1 | MC_1 | TBCLR;            // Use ACLK, count up, reset counter and direction
  TBCCR0 = 4096;                              // Init TBCCR0 w/ sample period
  TBCCR1 = 4036;                              // Trigger for ADC12 Sample Conversion
  TBCCTL1 = OUTMOD_7;                         // Reset OUT1 on EQU1, set on EQU0

  /* Configure ADC for repeated sampling of A2 and A3 */
  ADC12CTL0 = ADC12ON | MSC | SHT0_8;         // Turn on ADC12, extend sampling time
  ADC12CTL1 = SHS_3 | CONSEQ_3 | ADC12SSEL_1; // Use ACLK timer, repeated sequence

  ADC12MCTL0 = SREF_2 | INCH_2;               // ref+=VeREF+, channel = A2
  ADC12MCTL1 = SREF_2 | INCH_3 | EOS;         // ref+=VeREF+, channel = A3

  ADC12IFG = 0;
  ADC12CTL0 |= ENC;                           // Enable conversions
  
  /* Configure DMA to transfer A2 and A3 values from the ADC to RAM */
  DMACTL0 = DMA0TSEL_6;                       // ADC12IFGx triggers DMA0
  DMA0SA = ADC12MEM0_;                        // Source address
  DMA0DA = (unsigned short) &ADMAresults[0];  // Destination address
  DMA0SZ = 2;                                 // Transfer 2 16 bit words per trigger

  DMA0CTL = DMADT_5 | DMADSTINCR_3 | DMASRCINCR_3 | DMAEN | DMAIE; // Repeated block transfer, increment dest addr, enable DMA & interrupt

  _BIS_SR(GIE);                               // Enable interrupts

  /* Spin forever, ideally would be in lowest power mode possible */
  while(1);
}

/* DMA interrupt, triggers at the end of a sample being transfered to RAM */
#pragma vector=DMA_VECTOR
__interrupt void DMA0_ISR (void)
{
  switch(__even_in_range(DMAIV,16))
  {
    case 0: break; // No interrupt
    case 2:        // DMA0IFG
      _NOP();
      break;
    case 4: break; // DMA1IFG
    case 6: break; // DMA2IFG
    case 8: break; // Reserved
    case 10: break; // Reserved
    case 12: break; // Reserved
    case 14: break; // Reserved
    case 16: break; // Reserved
    default: break;
  }
}

References
  • The DMA cannot transfer a programmable number of blocks.  It transfers one block and then resets everything (as you noted), optionally staying armed for another block transfer (to the same location, as you noted).  So changing the block size will not help.

    If I understand what you are trying to do, then the MSP430 DMA cannot do what you want.

    But based on your ADC12 configuration, I am a little confused.  Do you want the ADC12 to sample both channels as close to the same time as possible?  If so, you should set SHP to activate the sampling timer inside the ADC12.  With SHP=0, the timer OUT signal controls the sampling, so the timer OUT signal can only produce one conversion per cycle.  With SHP=1, a single edge on the timer OUT signal can produce multiple conversions (the entire sequence).  I'm guessing you wanted SHP=1 so that your two channels are sampled at the same time (well, really really close to the same time).

    If you keep SHP=0, then you can take advantage of the deeper memory inside the ADC12.  Set up your sequence for all 16 conversion registers.  Then use either an ISR or DMA to get all the samples and process them.

    If you need SHP=1, then the DMA controller cannot do what you want.  You'll have to use the CPU to manage the buffer.

    Unfortunately, in either case, the DMA isn't all that useful.  You need it only if you might have long interrupt latency for an ISR that would get the data directly from the ADC12.  Otherwise, the DMA doesn't really buy you anything.  (It would be great if it could do what you wanted though!)

    Jeff

  • Hi Jeff.

    I do want the two channels to be sampled as close to the same time as possible. I'll set the SHP flag. Thanks!

    Ah! I think I've found the flaw in my thinking. Jens-Michael Gross's comment in the second post referenced above implied that the DMA controller could loop N times, pulling  a sample from the ADC to RAM each trigger, then requesting an interrupt at the end of N transfers. Looking back this is clearly when it is in Repeated Single Transfer mode. In order to make this flow function for me I'll have to use both DMA channels. Hopefully this isn't considered too much of a hack.

    For 16 samples the effort probably isn't worth it, but I'm hoping to support up to 1024 samples per second and have some critical sections which I'm leery of having interrupted, even if it would probably function alright. The timing of the samples is of utmost importance in the system, but there are a few other timing critical events.

    I'll try to get this working and return with a summary of the results.

    Thanks again.

  • Hi Derek,

    Keep in mind that when the ADC12 is in "sequence-of-channels" mode, the DMA is triggered only when the sequence is finished.  That makes the DMA Single Transfer modes useless for transferring the results of your sequence.

    However, your "sequence" is only two channels.  So here's one idea that should work.

    Let the ADC12 trigger DMA0, and let DMA0 trigger DMA1.  Configure both DMA channels for "single transfer" for the number of conversions you want.  DMA0 then fills a buffer with data from one ADC channel, and DMA1 fills a parallel buffer with data from the other ADC channel. (Source unchanged, Destination incremented)

    If you can tolerate not sampling the ADC inputs while you are processing the buffers, then "single transfer" (not repeated) is easiest.  Use the interrupt from DMA1 to invoke the processing, and then re-arm the DMA channels after you are done processing. (Manually clear the ADC12IFGx bit that triggers DMA0 after arming it; it needs to see an edge on ADC12IFGx.)

    However, if you need continuous sampling, then "repeated single transfer" is best.  Make your buffers slightly larger than the size of data you want to process together, and treat them like queues (circular buffers).  Wake up every processing interval (using a timer) and process the most-recent N samples in your buffer of size N-plus-safety-factor.  No need for DMA interrupts in this case.

    Jeff

  • That's pretty close to what I'm poking at right now. Continuous sampling is necessary. The sampling needs to occur without fail, on time, from configuration till the end of days. :)

    Question: "let DMA0 trigger DMA1"

    If DMA0IFG is used as the trigger for DMA1 to start its transfers, doesn't that only happen after DMA0SZ samples have been transferred? So I'd get 16 samples of A2 followed by 16 samples of A3, not 1 of each per trigger?

    I believe I should be able to have both trigger off of ADC12IFGx because although the DMA controller isn't signaled until the last of the sequence, the ADC12IFGx flag is only cleared when ADC12MEMx is accessed. i.e 0 for 0, 1 for 1. I have that configuration now and am seeing samples come into the buffers!

    However its taking twice as long as expected. (32 seconds for 16 two channel samples at 1Hz) This is the behavior without SHP set. With SHP set it fills up virtually instantaneously. I'm currently debugging this behavior.

    Attached is my current code with SHP set.

    6175.ADC12_DMA_Example_V01.c
    #include  <msp430x26x.h>
    #include  <stdint.h>
    
    #define   Num_Samples   16
    
    volatile uint16_t A2results[Num_Samples];  
    volatile uint16_t A3results[Num_Samples];  
    
    void main(void)
    {
      WDTCTL = WDTPW+WDTHOLD;                   // Stop watchdog timer
      P6SEL = BIT2 + BIT3;                      // Enable A/D channel inputs
      
      P6DIR = BIT4;  P6OUT = BIT4;
      P5DIR = BIT5;  P5OUT = BIT5;
      P2DIR = BIT0;  P2OUT = BIT0;
      
      /* Configure TimerB to produce the triggers for the ADC */
      TBCTL = TBSSEL_1 | MC_1 | TBCLR;              // Use ACLK, count up, reset counter and direction
      TBCCR0 = 32768;                               // Init TBCCR0 w/ sample period
      TBCCR1 = 32738;                               // Trigger for ADC12 Sample Conversion
      TBCCTL1 = OUTMOD_7;                           // Reset OUT1 on EQU1, set on EQU0
      
      /* Configure ADC for repeated sampling of A2 and A3 */
      ADC12CTL0 = ADC12ON | MSC | SHT0_8;           // Turn on ADC12, extend sampling time to avoid overflow of results
      ADC12CTL1 = SHP + SHS_3 | CONSEQ_3 | ADC12SSEL_1;  // Use ACLK timer, repeated sequence
      
      ADC12MCTL0 = SREF_2 | INCH_2;                 // ref+=VeREF+, channel = A2
      ADC12MCTL1 = SREF_2 | INCH_3 | EOS;           // ref+=VeREF+, channel = A3
      
      ADC12IFG = 0;
      ADC12CTL0 |= ENC;                             // Enable conversions
    
      /* Configure DMA to transfer A2 and A3 values from the ADC to RAM */
      DMACTL0 = DMA0TSEL_6 + DMA1TSEL_6;            // ADC12IFG0 triggers DMA0, ADC12IFG1 triggers DMA1
      
      DMA0SA = ADC12MEM0_;                          // Source address
      DMA0DA = (unsigned short) &A2results[0];      // Destination address
      DMA0SZ = 16;                                  // Transfer Num_Samples words
      DMA0CTL = DMADT_4 | DMADSTINCR_3 | DMAEN;     // Repeated block transfer, increment dest addr, enable DMA
     
      DMA1SA = ADC12MEM1_;                          // Source
      DMA1DA = (unsigned short) &A3results[0];      // Destination
      DMA1SZ = 16;                                  // Transfer Num_Samples words
      DMA1CTL = DMADT_4 | DMADSTINCR_3 | DMAEN | DMAIE;  // Repeated block transfer, increment dest addr, enable DMA & interrupt
      
      _BIS_SR(GIE);                                 // Enable interrupts
      
      while(1);
    }
    
    /* DMA interrupt, triggers at the end of DMA1's 16th sample transfer */
    #pragma vector=DMA_VECTOR
    __interrupt void DMA0_ISR (void)
    {
        switch(__even_in_range(DMAIV,16))
        {
            case 0: break;  // No interrupt
            case 2:         // DMA0IFG
                _NOP();     // Shouldn't be here
                break;
            case 4:         // DMA1IFG
                _NOP();     // Change double buffer
                break;
            case 6: break;  // DMA2IFG
            case 8: break;  // Reserved
            case 10: break; // Reserved
            case 12: break; // Reserved
            case 14: break; // Reserved
            case 16: break; // Reserved
            default: break;
        }
    }

  • The issue here is the interaction of of the Multiple Sample and Convert (MSC) bit and the Sample and Hold Pulse-mode (SHP) bit. When both are set the ADC12 will sample (in sequence if that's configured) continuously after the first trigger until ENC is removed. So it was looping as fast as the ADC could go. See 23.2.6.4-5 in the Family User Guide for details.

    I believe my options are to toggle the trigger twice in quick succession right at the sample period (sigh), or to find a way of toggling on and off ENC just before the trigger.

  • Derek Kozel said:
    doesn't that only happen after DMA0SZ samples have been transferred?

    Oh yeah, you're right.  I didn't think that one all the way through.  Your solution is perfect.  Trigger both channels off ADC12IFGx.

    Derek Kozel said:
    I believe my options are to toggle the trigger twice in quick succession right at the sample period (sigh), or to find a way of toggling on and off ENC just before the trigger.

    You're right again.  Maybe time to employ that third DMA channel.  If you configure the ADC12 for SHS=0 (still MSC=1 and SHP=1), then ADC12SC starts the sequence.  (Use CONSEQ = 1, not 3, for sequence of channels, not repeat sequence of channels.)  You could use TBCCR0 to trigger the DMA to set ADC12SC and start the sequence.

    Now it's getting pretty sticky.  It should work, but there ought to be an easier way!

    Jeff

  • Reviving a thread from the dead...

    I am not 100% sure what the OP was trying to do. But I think he was trying to monitor two external ADC channels and DMA transfer them into a double buffer scheme without interrupting the CPU. 

    AKA: Measure A0 and A1 50 times each then have a DMA interrupt where you switch the buffer and evaluate the data, while continuing to to monitor A0 and A1. I am using a F2819 so the code should be identical.

    If this is the case I was doing something similar and it turned out to be pretty easy.

    I set up the ADC so that it was triggered off TimerA-OUT1 sequentially ending at A5.

    /*Set up the ADC channel 4,5 to both trigger off of TIMERA*/
    ADC12CTL0 |= ADC12ON +SHT0_12; //Turn on ADC12, Sample and hold at 16ADC12CLK Cycles
    ADC12CTL1|= SHS_1 + ADC12SSEL_0 + CONSEQ_3; //Set sample and hold source as TimerA OUT1, ADC Clock ADC12OSC, Repeat-sequence of Channels
    ADC12MCTL0 |= SREF_0 + INCH_4; //Set reference to Vcc, select Channel 4
    ADC12MCTL1 |= SREF_0 + INCH_5+EOS; //Set reference to VCC, select channel 5
    ADC12IFG=0;
    //ADC12IE|= BIT1; //Enable interrupts for channel 4,5
    ADC12CTL0|= ENC; //Enable Conversions

    Then I set up two seperate DMA channels with one pointing at MEM0 and the other pointing at MEM1

    //Have DMA0 transfer from ADCMEM1 to bufferA4 one word at a time
    DMA0SA = (__SFR_FARPTR)(unsigned long)&ADC12MEM1; // Start block address
    DMA0DA = (__SFR_FARPTR)(unsigned long)&dataBuff1.bufferA4[0]; // Destination block address
    DMA0SZ = 10; // Block size
    DMACTL0|=DMA0TSEL_6; //ADC12IFGx CCIFG is trigger for transfer
    DMA0CTL |= DMADT_4 + DMADSTINCR_3+DMAEN+DMAIE; // Rpt, increment both source and destination, Word to word transfer
    //Have DMA1 transfer from ADCMEM0 to bufferA4 one word at a time
    DMA1SA = (__SFR_FARPTR)(unsigned long)&ADC12MEM0; // Start block address
    DMA1DA = (__SFR_FARPTR)(unsigned long)&dataBuff1.bufferA5[0]; // Destination block address
    DMA1SZ = 10; // Block size
    DMACTL0|=DMA1TSEL_6; //ADC12IFGx is trigger for transfer
    DMA1CTL |= DMADT_4 + DMADSTINCR_3+DMAEN+DMAIE; // Rpt, increment both source and destination, Word to word transfer

    I was checking and if you only check one of the memory locations then only one ADC12IFG flag gets reset. So I tried this and it works fine. 

    Set you block size to whatever you want and the DMA will interrupt when DMAxSZ=0 (Or how ever many samples you want to collect).

  • I should mention one more thing. Since we are using DMAxADT=4, there will be a one cycle delay before the buffers change. 

    To get around that disable the DMA(s) in the intterupt and re-enable them on the way out. 

    Here is my ISR:

    // DMA interrupt service routine
    #pragma vector = DMA_VECTOR
    __interrupt void DMA_ISR(void)
    {
    static char pingPong=1; //index for PinPong/double buffer
    //Disable Both DMAs
    DMA0CTL&=~DMAEN;
    DMA1CTL&=~DMAEN;

    //Depending on Index Change destination address of DMAs
    if(pingPong==1)
    {
    DMA0DA = (__SFR_FARPTR)(unsigned long)&dataBuff2.bufferA4[0];
    DMA1DA = (__SFR_FARPTR)(unsigned long)&dataBuff2.bufferA5[0];
    }
    else
    {
    DMA0DA = (__SFR_FARPTR)(unsigned long)&dataBuff1.bufferA4[0];
    DMA1DA = (__SFR_FARPTR)(unsigned long)&dataBuff1.bufferA5[0];

    }

    pingPong^=1;
    //Clear IFGs
    DMA0CTL &= ~DMAIFG; // Clear DMA0 interrupt flag
    DMA1CTL &= ~DMAIFG;
    //Re-enable DMAs
    DMA0CTL|=DMAEN;
    DMA1CTL|=DMAEN;
    _bic_SR_register_on_exit(LPM0_bits);
    }

  • Hi Derek,

    I'll have to look over what you're doing tomorrow, maybe load it onto a dev board if I have a compatible one. I stopped looking for answers in July since I'd devoted too much time to it, but you're spot on about what I was trying to do. If your code does this I'll be thrilled! I know some people are against reviving dead threads but I'd much rather have an answer way late than never. Thanks for posting!

    If you don't have confidential code in the rest of your project, could you post a complete file? It always is useful to see complete context. If it a bother don't worry, I'll put one together and post it if I encounter issues.

    -Derek

  • Derek Kozel said:
    I know some people are against reviving dead threads

    There's nothing wrong with reviving dead threads if the new post is 1) really directly related to the thread topic and 2) you are contributing to it. Asking an additional question to someone in this thread who has long left the forum isn't very useful - and asking a question that is only remotely related (or not at all) is a bad thing even on living threads.

    In this case, I'm happy that the thread was revived. The new answer has brought some details to my attention that I didn't think of before. And maybe I can use them for an upcoming project.

  • The problem is still the ADC12 configuration.  When an entire "sequence of channels" is triggered by a single Timer edge, the ADC12 requires ADC12ENC to be toggled before the next sequence.  That virtually requires CPU intervention after every sequence, which makes DMA almost useless here.

    The DMA solution posted last doesn't really address the underlying ADC12 configuration problem.  Also, I don't think it should use DMADT_4 since that tells the DMA to keep filling the same buffer.  As posted, the CPU has to come in when one buffer becomes full and quickly disable the DMAs (before another ADC12 conversion sequence) and then change the buffer pointer.  You should use DMADT_0 instead, and then the ISR could eliminate the "disable" step but otherwise be the same.

    Still, the ADC12 configuration is the missing link.

    Jeff

  • Hi Jeff, 

    Hmmm, I am not sure about the entire sequence. What about using multiple block bursts?

    My application was to monitor two ADC channels then write the results to an SDCard in 512Byte blocks. I did not want to have the write interrupted by the DMA or CPU, thus I collect 128 samples of the two channels then have the DMA trigger and write the block of memory to the SD card. I also wanted the data to be collected by the DMA into seperate arrays (X-Y Axis on an accelerometer).

    To be honest the ADC12 module and DMA module on the MSP430 is pretty confusing when compared to the C2000 and C6000 processors. 

    I can post up the code later but it is in a messy sandbox that I was playing with. 

**Attention** This is a public forum