First of all, let me share good news. I finally figured this out, thanks for your posts. the STM32F103 can actually perform “regular simultaneous dual conversion with DMA transfer”. It took quite a lot of time to figure out. Read on if you like a mystery novel… and like any mystery novel, once explained it all makes sense. I’m taking the time to explain it fully, because as far as I know there’s no explanation of any of this in either the ST docs nor online (at least, not breaking down every single step)
It turns out that using both continuous conversion and DMA creates a few tricky timing conditions that have to be met. It doesn’t matter when used like in the O-Scope, since you are scanning only one channel per ADC. When scanning more than one channel per ADC, timing matters a lot.
The moment you enable the ADC in continuous mode (any dual mode), the ADC keeps running, and writing the converted value for ADC1 and ADC2 in the DR register of ADC1. At the end of each conversion, it sets the EOC bit in register SR, and that in turns tells the DMA to read the content of register ADC1->regs->DR and transfer it to memory. In the case of the O-Scope, the ADC is always running and sampling, and when DMA is enabled in the takeSamples() function, it starts transferring right away. Since DR always contains the same 2 channels (one for ADC1, one for AC2), it works.
When you have a sequence of channels to be sampled, if you use the same approach, the DMA transfer starts as soon as you enable the DMA. Problem is, the ADC is always running, and you don’t know what channels are in the DR register at that time. If like me you have 4 channels (IN8 and IN6 on ADC1, IN7 and IN5 on ADC2), you can start the memory transfer either when IN8 and IN7 are there, or when IN6 and IN5 are there. That’s what was happening to me.
The trick is to make sure that the ADC is not running, enable DMA, then start the ADC only when needed. The DMA transfer will wait patiently until the ADC starts, then upon the first EOC will start transferring from the right set of channels (IN8 and IN7 in my case).
It turns out, though, it’s tricky to set the ADC in continuous mode and ensure that the ADC is not running (probably due to the Maple libraries already having enabled the ADC).
Doing something like:
ADC1->regs->CR2 = ( ADC_CR2_ADON | ADC_CR2_CONT | ADC_CR2_DMA | ADC_CR2_EXTSEL | ADC_CR2_EXTTRIG);
Turns out to be a bad idea, while
ADC1->regs->CR2 = ( ADC_CR2_CONT | ADC_CR2_DMA | ADC_CR2_EXTSEL | ADC_CR2_EXTTRIG);
ADC1->regs->CR2 |= ADC_CR2_ADON;
Works fine. There’s a cryptic note in the reference manual, 11.12.3. It says “If any other bit in this register apart from ADON is changed at the same time, then conversion is not triggered. This is to prevent triggering an erroneous conversion”. Well, it turns out that changing a bunch of bits together while changing ADC_CR2_ADON doesn’t stop a conversion either (and this was a real puzzler for the longest time). When I enabled ADC_CR2_CONT, the ADC was already enabled and even if the ADC_CR2_SWSTART bit was not set, started running. When later on I thought I was starting conversion using ADC_CR2_SWSTART, made no difference (since it was already running), and that’s why the DMA transfer started happening randomly (seemingly randomly, I can now say).
So the trick is to set the CR2 register while explicitly disabling the ADC. That stops everything. Then enable just ADC_CR2_CONT. At this point the ADC is “armed” and waiting for ADC_CR2_SWSTART to actually start the continuous conversion.
So, assuming the ADC1 and ADC2 registers are set in your init code, the right order to start a new conversion is:
- Disable the ADC, and set the CR2 register as needed
Enable the ADC
Set the DMA
(do whatever you need to do to decide when the conversion is ready to start, trigger or otherwise)
Set ADC_CR2_SWSTART to finally get the conversion going
In my case it’s critical to start converting all 4 channels as close as possible to the trigger, because in order to cross correlate various sound waves, the initial slope is pretty critical (and likely the strongest part of the sound impulse for impulsive sounds). It might be critical for an oscilloscope in order to capture a single high speed signal (but, then again, you can run quite a few ARM instructions in a CPU running at 72MHz, maybe I worry too much)
Here’s the working code:
/* MapleDualADC - A sample to perform dual ADC conversions using DMA */
//
// As written it performs a total of NUM_SAMPLES dual conversions (ADC1 and ADC2) on Maple Mini
// continuosly scanning the defined channels (D3->IN8 and D5->IN7 in the first conversion
// followed by D4->IN6 and D6->IN5 on the second conversion). In the example, repeated 4 times
// NUM_SAMPLES=(number INx channels converted/2) * number of times each channel is scanned
/*
Copyright (c) 2015 Roberto Cazzaro. All rights reserved.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
*/
#include <libmaple/dma.h>
#define CHANNELS_PER_ADC 2 // number of channels for each ADC. Must match values in ADCx_Sequence array below
#define NUM_SAMPLES 8 // number of samples for each ADCx. Each channel will be sampled NUM_SAMPLES/CHANNELS_PER_ADC
#define SAMPLE_RATE ADC_SMPR_71_5 // when using dual mode, each pair of channels must have same rate. Here all channels have the same
uint32 adcbuf[NUM_SAMPLES+1]; // buffer to hold samples, ADC1 16bit, ADC2 16 bit
uint8 ADC1_Sequence[]={8,6,0,0,0,0}; // ADC1 channels sequence, left to right. Unused values must be 0. Note that these are ADC channels, not pins
uint8 ADC2_Sequence[]={7,5,0,0,0,0}; // ADC2 channels sequence, left to right. Unused values must be 0
// calculate values for SQR3. Function could be extended to also work for SQR2 and SQR1. As is, you can sequence only 6 sequences per ADC
uint32 calc_adc_SQR3(uint8 adc_sequence[6]){
int SQR3=0;
for (int i=0;i<6;i++) // There are 6 available sequences in SQR3 (SQR2 also 6, and 4 in SQR1).
{
//each sequence is 5 bits
SQR3 |= adc_sequence[i] << ((i*5));
}
return SQR3;
}
// initialize DMA1, Channel1 (ADC)
void set_dma() {
dma_init(DMA1);
dma_setup_transfer(DMA1, DMA_CH1, &ADC1->regs->DR, DMA_SIZE_32BITS,
adcbuf, DMA_SIZE_32BITS, DMA_MINC_MODE);
dma_set_num_transfers(DMA1, DMA_CH1,NUM_SAMPLES);
dma_set_priority(DMA1, DMA_CH1, DMA_PRIORITY_VERY_HIGH);
dma_enable(DMA1, DMA_CH1);
}
// calibrate ADC1 and ADC2, then set all registers for regular simultaneous dual ADC conversion with DMA transfer
void set_adc() {
adc_set_sample_rate(ADC1, SAMPLE_RATE); // sets sample rate for all channels, all identical (critical in dual mode, even if only pairs sampled together need to be identical)
adc_set_sample_rate(ADC2, SAMPLE_RATE);
adc_calibrate(ADC1);
adc_calibrate(ADC2);
ADC1->regs->CR1 = 1 << 8; // Set scan mode
ADC1->regs->CR1 |= 6 << 16; //Regular simultaneous mode. Required for ADC1 only, ADC2 is slave
ADC1->regs->CR2 = ( ADC_CR2_CONT | ADC_CR2_DMA | ADC_CR2_EXTSEL | ADC_CR2_EXTTRIG); //0xe0102; cont conversion, DMA, right aligned, disable external; EXTSEL, exttrig=0,jswstart=0
adc_set_reg_seqlen(ADC1, CHANNELS_PER_ADC); // how many channels per ADC
//ADCx->regs->SQR3-1 holds the sequence of channels to convert. A conversion function is provided calc_adc_sequence(ADC1_Sequence) only for SQR3.
//If more than 6 channels are needed, repeat the same for SQR2 and SQR1 (SQR1 only holds 4 sequences)
ADC1->regs->SQR3 |= calc_adc_SQR3(ADC1_Sequence); //200; for IN8 and IN6, on Maple Mini these are D3, D5
ADC2->regs->CR1 = 1 << 8; // Set scan mode
ADC2->regs->CR2 = ( ADC_CR2_CONT | ADC_CR2_EXTSEL | ADC_CR2_EXTTRIG); //0xe0003;
adc_set_reg_seqlen(ADC2, CHANNELS_PER_ADC);
ADC2->regs->SQR3 |= calc_adc_SQR3(ADC2_Sequence); //= 167 forIN7 and IN5, on Maple Mini these are D4, D6 respectively
ADC1->regs->CR2 |= ADC_CR2_ADON; // it is critical to enable ADC (bit 0=1) independently of all other changes to the CR2 register
ADC2->regs->CR2 |= ADC_CR2_ADON; // enabling all at once (i.e. ADC_CR2_ADON | ADC_CR2_CONT | ADC_CR2_EXTSEL ) will cause problems when used with continuous mode
}
// set only the registers that need to be reset before each conversion
void adc_to_ready() {
ADC1->regs->CR2 = ( ADC_CR2_CONT | ADC_CR2_DMA | ADC_CR2_EXTSEL | ADC_CR2_EXTTRIG); //0xe0102; cont conversion, DMA, right aligned, disable external; EXTSEL, exttrig=0,jswstart=0
ADC2->regs->CR2 = ( ADC_CR2_CONT | ADC_CR2_EXTSEL | ADC_CR2_EXTTRIG); //0xe0002;
ADC1->regs->CR2 |= ADC_CR2_ADON; // it is critical to enable ADC (bit 0=1) independently of all other changes to the CR2 register
ADC2->regs->CR2 |= ADC_CR2_ADON; // enabling all at once (i.e. ADC_CR2_ADON | ADC_CR2_CONT | ADC_CR2_EXTSEL ) will cause problems when used with continuous mode
}
// check for DMA transfer finished, resetting isr bit
uint8 dma_transfer_finished() {
if(dma_get_isr_bits(DMA1,DMA_CH1)==0x07) {
int result=dma_get_irq_cause(DMA1,DMA_CH1);//<--clears isr bits
return 1;
}
return 0;
}
void setup() {
Serial.begin(115200); // Ignored by Maple. But needed by boards using Hardware serial via a USB to Serial Adaptor
pinMode(3,INPUT_ANALOG); //set all used pins as INPUT_ANALOG, in the example 3,4,5,6
pinMode(4,INPUT_ANALOG);
pinMode(5,INPUT_ANALOG);
pinMode(6,INPUT_ANALOG);
set_adc(); // initial ADCa and ADC2 settings
}
void loop() {
adc_to_ready(); // it is critical to set ADC before DMA, to avoid partial DMA transfers being stored. If the ADC is in continuous mode while DMA is being set
set_dma(); // the DMA transfer will be triggered and the DMA counter will start from the wrong value
ADC1->regs->CR2 |= ADC_CR2_SWSTART; //start conversion, STM32 will reset this bit immediately. Only ADC1 (master) needs to be started
while(!dma_transfer_finished());
dma_disable(DMA1,DMA_CH1); //stop dma transfer
for(int i=0;i<(NUM_SAMPLES);i++) {
Serial.print(i);
Serial.print(" ");
Serial.print(adcbuf[i] & 0xFFFF);
Serial.print(" - ");
Serial.println((adcbuf[i] & 0xFFFF0000) >>16);
}
Serial.println();
delay(750);
}