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.

How to receive a DMA interrupt only after X number of 32-bit SD24 ADC samples have been transferred?

Other Parts Discussed in Thread: MSP430F5438, MSP430F5529

Using the MSP430's DMA controller and the SD24_B ADC, is there a way to program it to receive an interrupt after X number of 32-bit ADC samples have been collected?

The DMA controller's 'DMA_TRANSFER_REPEATED_SINGLE' mode seems appropriate for this, since the 'transferSize' can be decremented after each sample has been transferred and then the DMA waits for the next trigger to indicate that another sample is ready.  However, in Single mode the DMA controller is limited to only transferring one byte or one word (16 bits since it's a 16-bit processor).  So in order to transfer an entire ADC sample, we would have to use Block mode to transfer 2 words, but we would receive an interrupt after every ADC sample was completed, instead of after X samples.

  • I think you could do something sneaky with two DMA channels to achieve this. None of this has been tested or even thoroughly considered, I'm just throwing it out there for inspiration...

    Idea 1: Use one DMA channel to transfer the high words and another for the low words. Both channels would be triggered from the SD_24 in single-transfer mode with DMAxSZ=X, and each pair of high/low copies would occur in channel priority order. You'd need to use zero source address increment and 2-byte destination increment. Each DMA channel would output to its own memory buffer, so that would require processing of the high and low blocks to re-interleave them correctly. This would take two single transfers per result, plus the extra interleave time at the end of a batch of X results.

    Idea 2: Use one channel in repeated block mode to copy 32 bits from the SD_24 as you've already described, but keep DMAIE clear. Set up another channel triggered by the first that does a dummy transfer between two "spare" locations in single transfer mode, DMAIE set. The second channel effectively just counts the block transfers done by the first, and then fires its ISR when X transfers have completed. That would result in two burst transfer and one single transfer per SD_24 result, but the output memory would have the results in the correct format.


    I've tried the first suggestion and it works fine, at least in a simulated test case. I have no SD_24-equipped MSP430s, so I'm pretending the DMA can only transfer bytes and copying high/low separately from ADC12MEM0.

    Idea 2 is a bit more complicated than I thought. There are two problems; first of all the destination address for DMA0 needs to be updated after each block copy. That's fine, DMA1 is already triggered on the end of the burst so can copy from a destination address table into DMA0DA. The second problem is that DMA0CTL.DMAIFG isn't cleared when DMA1 triggers, so DMA1 only triggers once. I haven't figured out a solution for that yet. DMA2 triggered from DMA1 could fix DMA1 triggering, but then DMA2 would only trigger once... Of course, that's not a problem if you trigger DMA0 and DMA1 from the ADC, and let channel priority handle the ordering.

  • I got both methods working in a test program, included below. For demonstration purposes I'm using ADC12 to convert a sequence of two channels, which gives 32 bits worth of results. If you ignore the fact that I've got two independent 16-bit results and treat each pair as a unit this can be applied to SD_24.

    The first method is a lot easier to implement than the second, but I've left out the code that reinterleaves the high and low words to get the original 32-bit blocks. That's not difficult, but it does cost CPU time and buffer space which may not be available.

    The main inconvenience with the second method is the need to create the array of destination addresses. Perhaps there's some way to generate that array at compile time without needing to specify each element individually.

    // Demonstrate two ways of transferring 32-bit data from a fixed source address to an incrementing
    // destination address.
    //
    // Method 1 uses DMA0 for the low words and DMA1 for the high words. Results are split into a low
    // block and a high block, requiring post-processing to reinterleave them correctly (not shown).
    //
    // Method 2 uses DMA0 to copy the full 32 bits in repeated block mode, while DMA1 updates the
    // destination address for DMA0.
    //
    // The test code uses a 2-channel ADC12 sequence to generate 32 bit blocks of source data.
    // Note: this means it cannot be ported to FR5/FR6 chips that are affected by the ADC43 erratum.
    // Tested on MSP430F5529 in small code, small data model.
    #include <msp430.h>
    
    #define METHOD_1
    
    #if defined(METHOD_1)
    volatile unsigned int analog_result_low[8] = {0};
    volatile unsigned int analog_result_high[8] = {0};
    #elif defined(METHOD_2)
    volatile unsigned long analog_result[8] = {0};
    
    // This array of DMA transfer destination addresses is const, so should end up in flash (check the
    // map file to be certain). The array is rotated left by two places; the first destination address
    // loaded into DMA0DA by DMA1 is that of analog_result[2]. DMA0 completes before DMA1, so DMA1
    // needs to load the "next" destination address for DMA0 to use. That would be analog_result+1, but
    // DMAxDA gets copied into the T_DestAdd internal register at the end of each transfer. This extra
    // buffering means that DMA1 needs to load the next-but-one destination address when it triggers
    // after DMA0 has transferred a block.
    volatile unsigned long * const transfer_addresses[8] =
        {analog_result+2, analog_result+3, analog_result+4, analog_result+5,
         analog_result+6, analog_result+7, analog_result+0, analog_result+1};
    #endif
    
    void ConfigureTest(); // Set up clocks, REF, ADC12 and IO
    
    void main(void)
    {
        WDTCTL = WDTPW | WDTHOLD;
    
        // Configure ADC12 in 12-bit sequence-of-channels conversion mode (two channels,
        // both connected to P6.4), 2.0V reference voltage, 8MHz MCLK/SMCLK
        ConfigureTest();
    
        // Configure DMA0 and DMA1 to both trigger on ADC conversion
        DMACTL0 = DMA0TSEL__ADC12IFG | DMA1TSEL__ADC12IFG;
    
    #if defined(METHOD_1)
        // Configure DMA0 to copy the lower word of each 32 bit block (single transfer mode, 8x)
        DMA0SZ  = 8;
        DMA0SA  = (__SFR_FARPTR)&ADC12MEM0;
        DMA0DA  = (__SFR_FARPTR)&analog_result_low;
        DMA0CTL = DMADT_0 | DMASRCINCR_0 | DMADSTINCR_3 | DMASWDW | DMAEN;
    
        // Configure DMA1 to copy the upper word of each 32 bit block (single transfer mode, 8x)
        DMA1SZ  = 8;
        DMA1SA  = (__SFR_FARPTR)&ADC12MEM1;
        DMA1DA  = (__SFR_FARPTR)&analog_result_high;
        DMA1CTL = DMADT_0 | DMASRCINCR_0 | DMADSTINCR_3 | DMASWDW | DMAEN | DMAIE;
    #elif defined(METHOD_2)
        // Configure DMA0 to copy a block of two words each time it's triggered (repeated block mode)
        DMA0SZ  = 2;
        DMA0SA  = (__SFR_FARPTR)&ADC12MEM0;
        DMA0DA  = (__SFR_FARPTR)transfer_addresses[6];
    
        // DMA0DA is copied into the T_DestAdd internal register as soon as DMAEN is set
        DMA0CTL = DMADT_5 | DMASRCINCR_3 | DMADSTINCR_3 | DMASWDW | DMAEN;
    
        // Preload the next destination address so it will be copied to T_DestAdd at the end of the
        // first transfer (just before DMA1 updates DMA0DA)
        DMA0DA  = (__SFR_FARPTR)transfer_addresses[7];
    
        // Configure DMA1 to update the destination address after each block (single transfer mode, 8x)
        DMA1SZ  = 8;
        DMA1SA  = (__SFR_FARPTR)&transfer_addresses;
        DMA1DA  = (__SFR_FARPTR)&DMA0DA;
        DMA1CTL = DMADT_0 | DMASRCINCR_3 | DMADSTINCR_0 | DMASWDW | DMAEN | DMAIE;
    #endif
    
        while(ADC12CTL1 & ADC12BUSY);
    
        _bis_SR_register(GIE);
    
        ADC12CTL0 |= ADC12ENC;
        int count = 8;
        do
        {
            ADC12CTL0 &= ~ADC12SC; // Breakpoint here to compare results against ADC registers
            ADC12CTL0 |= ADC12SC;
    
            __delay_cycles(10000);
        }
        while(--count > 0);
    
        _bis_SR_register(LPM4_bits);
    }
    
    void ConfigureTest()
    {
        // Configure Clock System (DCO == 8MHz, MCLK == DC0, SMCLK == DC0/4)
        _bis_SR_register(SCG0);
        UCSCTL0 = DCO4 | MOD4;      // Set DCO to middle of its range
        UCSCTL1 = DCORSEL_5;        // Switch to range 5 ([6.0, 23.7] worst case)
        UCSCTL2 = FLLD__2 | 0x0F3;  // DCO @ 16Mhz, DCOCLKDIV @ 8MHz
        UCSCTL4 = SELA_1 | SELS_4 | SELM_4;
        UCSCTL5 = DIVA_0 | DIVS_4 | DIVM_0;
        UCSCTL7 &= ~(DCOFFG | XT1LFOFFG | XT2OFFG);
        _bic_SR_register(SCG0);
    
        // Configure P6.4 for ADC input
        P6OUT = 0;
        P6DIR &= ~BIT4;
        P6SEL |= BIT4;
    
        // Ensure ADC12 is stopped
        ADC12CTL1 &= ~ADC12CONSEQ_3;
        ADC12CTL0 &= ~ADC12ENC;
        while(ADC12CTL1 & ADC12BUSY);
    
        // Ensure REF isn't already busy
        while(REFCTL0 & REFGENBUSY);
    
        // Configure REF for 2.0V
        REFCTL0 = REFVSEL_1 | REFMSTR | REFON;
        __delay_cycles(10000);
    
        // Configure ADC12 in 12-bit sequence-of-channels conversion mode (two channels)
        ADC12CTL0 = ADC12SHT0_0 | ADC12SHT1_0 | ADC12MSC;
        ADC12CTL1 = ADC12CSTARTADD_0 | ADC12SHS_0 | ADC12SHP | ADC12SSEL_3 | ADC12CONSEQ_1;
        ADC12CTL2 = ADC12RES_2;
        ADC12IE = 0;
        ADC12IFG = 0;
        ADC12MCTL0 = ADC12INCH_4 | ADC12SREF_1;
        ADC12MCTL1 = ADC12INCH_4 | ADC12SREF_1 | ADC12EOS;
        ADC12CTL0 |= ADC12ON;
    }
    
    #define __debug_break() __op_code(0x4343)
    
    #pragma vector=DMA_VECTOR
    __interrupt void DMA_ISR(void)
    {
        switch(__even_in_range(DMAIV, DMAIV_DMA2IFG))
        {
            case DMAIV_DMA1IFG:
            {
                __debug_break(); // Insert a software breakpoint for inspection of results
            }
            break;
    
            default: _never_executed();
        }
    }
  • Thanks for the ideas! After discussion, we've opted for "Idea #1". It works in a unit test using the SD24_B and DMA drivers and we're now integrating the solution with the rest of our code.
  • Robert, as far as I know, the DMA registers cannot be source or destination of a DMA transfer. At least I remember reading this in some errata sheets. So updating the destination address by DMA is likely not an option. So idea #1 is the only one.
  • Jens-Michael Gross said:
    Robert, as far as I know, the DMA registers cannot be source or destination of a DMA transfer. At least I remember reading this in some errata sheets.

    I've found no such restriction in practice... it works just fine! Source is included in my previous post.

    If anyone knows of an official statement that this is unsupported, then I'd appreciate a link to the relevant document.

  • DMA6: DMA Module
    Function: DMA cannot write to DMA
    Description: One DMA channel cannot modify the registers of another DMA channel. DMA register
    modifications must be done by JTAG or by the CPU.
    Workaround None

    Found in MSP430F5438 (non-A) errata sheet, that's where I had it from. Other MSPs might not be affected.
  • Aha, that's a new one on me. Thanks for the information.

    At least it's easy to find out which MCUs are affected (eg MSP430F5529 doesn't have this erratum.)

**Attention** This is a public forum