UNO Q: Simultaneous ADC1 + ADC4 timer-triggered DMA capture at up to 2.5 MS/s

Hello,

I am evaluating the STM32U585 on the Arduino UNO Q for a I/Q acquisition application. I need to acquire two analog signals, I and Q, during an autonomous PLL frequency ramp.

The required acquisition is:

  • ADC1 on one analog pin and ADC4 on a different analog pin;
  • both ADCs triggered by the same timer event;
  • one GPDMA channel per ADC;
  • 512 or 1024 samples per ADC;
  • target sampling rates of 1.0, 1.8 and ideally 2.5 MS/s;
  • samples stored as two 16-bit buffers, for a total of 2048 or 4096 bytes;
  • no CPU processing is required while the capture is running.

From the STM32U585 pinctrl definitions, the UNO Q analog pins are physically connected to both ADC instances. For example, PA4/A0 can be ADC1_IN9 or ADC4_IN9, and PA5/A1 can be ADC1_IN10 or ADC4_IN10. I would use different pins, for example A0 for ADC1 and A1 for ADC4.

In the current ArduinoCore-zephyr UNO Q variant I found that:

  • only adc1 is enabled in the board overlay;
  • A0-A5 are currently assigned to ADC1;
  • CONFIG_DMA=y is enabled;
  • CONFIG_ADC_STM32_DMA is not enabled;
  • IQRead() performs a blocking one-sample adc_read();
  • the Arduino sketch is loaded as an LLEXT into a precompiled Zephyr loader.

I therefore assume that this cannot be implemented by a normal sketch using analogRead(), but should be possible with a custom UNO Q loader.

My questions are:

  1. Is rebuilding the UNO Q loader the supported way to enable ADC4, CONFIG_ADC_STM32_DMA, two DMA channels and a timer-triggered capture?
  2. Are TIM1, ADC4 or specific GPDMA channels already reserved by the UNO Q loader, RouterBridge or another board service?
  3. Can a function implemented in the custom loader, such as IQCapture(bufferI, bufferQ, 1024, sampleRate), be exported to and called safely from an Arduino LLEXT sketch?
  4. Is there an existing UNO Q branch, example or experimental implementation using ADC DMA or more than one ADC instance?
  5. Is there a recommended procedure to install and recover a custom loader while keeping the standard Linux/MCU Bridge functions?

I found an earlier discussion where approximately 2 MS/s was requested but polling remained limited to about 160 kS/s: UNO Q: ADC sampling rates > 1MHz

Relevant core files:

Has anyone already tested ADC1 or ADC4 with DMA on a real UNO Q?

1.

Are you sure about ADC4? Searching STM32U585 MCU datasheet, I have found:

ADC1_IN1 to ADC1_IN15
ADC2_IN1 to ADC2_IN13

No ADC3
No ADC4

2.


Figure-1:

(1) Fig-1 is taken from UNO Q User Manual.
2) Ae per datasheet of MCU:

A0 to A3 are : ADC1_IN7 to ADC1_IN10
A4 and A5 are: ADC1_IN2 and ADC1_IN1</s> Edit 

@GolamMostafa - The processor appears to have both ADC1 and ADC4 as mentioned.
If you look at the Reference manual rm0456-stm32u5...
You will find Chapter 33 on ADC12 which if for ADC1 and a few other boards ADC2 as well.
And if you look at Chapter 34 for ADC4, which is lower power 12 bits instead of 14...

And if you look at the UNOQ Documention page:
UNO Q | Arduino Documentation

And download the datasheet for the STM32 processor:
https://www.st.com/resource/en/datasheet/stm32u585ai.pdf

You will find the tables like:


Note the highlight where ADC1 and ADC4 or shown, with many of the overlapping.

I Should probably add another column to my Excel document with this information...

@azatta - As you mentioned, I don't think Arduino has defined any of the ADC4's into the device tree of zephyr.

As you mentioned, I also don't believe they have setup to do DMA and the like.

Options: You might be able to add some of this to zephyr...

You might also be able to create your own stuff outside of zephyr, that is have your sketch or some library modify the appropriate registers and the like. As for setting up the DMA, that might be slightly more difficult, without maybe updating the zephyr side.

Not sure you would get it into the analogRead like API, but could be something like what was done for Teensy boards, where someone created an external library that does all of the advanced stuff.

pedvide/ADC: Teensy 4/3.x/LC ADC implementation
Obviously the ADC library itself may not be much help here as different architectures.

More details about the capabilities of that library are in the PJRC forum post:
ADC library, with support for Teensy 4, 3.x, and LC | Teensy Forum

Good luck

Thank you @KurtE and @GolamMostafa.

I checked the exact MCU fitted to the UNO Q, the STM32U585AII6TR. It contains
ADC1 and ADC4, not ADC1 and ADC2. ST DS13086 Rev. 10, Table 27, confirms:

PA4/A0: ADC1_IN9 / ADC4_IN9
PA5/A1: ADC1_IN10 / ADC4_IN10
PA6/A2: ADC1_IN11 / ADC4_IN11
PA7/A3: ADC1_IN12 / ADC4_IN20
PC1/A4: ADC1_IN2 / ADC4_IN2
PC0/A5: ADC1_IN1 / ADC4_IN1

The current UNO Q overlay simply enables ADC1 and assigns A0-A5 to it.

The complete application also requires an autonomous DAC ramp. Since A0 and
A1 are DAC1 outputs, a possible assignment is:

A0/PA4: DAC1_CH1, PLL ramp
A2/PA6: ADC1_IN11, I signal
A3/PA7: ADC4_IN20, Q signal

The STM32U585 provides separate GPDMA requests for ADC1, ADC4 and DAC1_CH1.
TIM2_TRGO, TIM6_TRGO and TIM15_TRGO are common trigger options for all three
peripherals. TIM6 may be a good candidate because it is not currently enabled
in the UNO Q overlay.

However, CONFIG_ADC_STM32_DMA alone does not appear to implement the required
autonomous 1024-sample acquisition. The current Zephyr STM32 driver configures
a one-sequence DMA transfer, restarts extra samples through software and
initializes software triggering. A dedicated LL/CMSIS implementation therefore
seems necessary, either inside an LLEXT library or as a native service in a
custom loader.

Could someone familiar with the UNO Q loader clarify:

  1. Can an Arduino LLEXT directly configure ADC1, ADC4, DAC1, TIM6 and GPDMA
    through CMSIS/LL registers?
  2. Can an LLEXT install GPDMA interrupt handlers, or must these be compiled
    into the loader?
  3. Which GPDMA channels, timers or ADC resources are reserved by RouterBridge
    or other UNO Q services?
  4. How should DMA buffers be allocated with the data cache enabled:
    non-cacheable memory, explicit cache maintenance or loader-owned buffers?
  5. Can a native loader function such as IQCaptureStart() be exported safely
    to an LLEXT while retaining the standard Linux/MCU Bridge?
  6. Is there a documented build, installation and recovery procedure for a
    custom UNO Q loader?
  7. Does an example already exist for LLEXT register access, DMA interrupts,
    ADC DMA or native loader exports?

One additional concern is STM32U585 erratum ES0499 Rev. 11 section 2.10.1.
During simultaneous ADC1/ADC4 operation, ST requires identical ADC clocks,
same frequency and phase, /1 prescalers, a maximum 55 MHz ADC clock and an
ADC4 duty cycle between 45% and 55%.

The 1.0 and 1.8 MS/s targets appear realistic. For 2.5 MS/s, I would use
12-bit mode on both ADCs and validate simultaneous accuracy on real hardware.

Any existing code, measurements or guidance on the preferred LLEXT versus
custom-loader approach would be very useful.

1

Thank you.

I have got it.

Then what was my problem? I am extremely sorry to say that I have been consulting datasheet of STM32U385xx and not STM32U585.

The error occured as datasheets for boh MCUs are under the same folder named STM32U585.

Sorry, for many of you questions, probably need someone from Arduino to answer.

For what it is worth, the next Zephyr core, that they are working toward releasing ArduinoCore-zephyr using is: V4.4 the current releases are against a modified version of V4.2

In it on the Zephyr side, they are referencing ADC4:
zephyr/boards/arduino/uno_q/arduino_uno_q-common.dtsi at zephyr-arduino-v4.4.0 · arduino/zephyr

&adc4 {
	/* needed by vbat */
	st,adc-clock-source = "ASYNC";
	st,adc-prescaler = <4>;
	status = "okay";
};

I am also wondering if they are also exposing a ADC read for the current Temp...

Yes you can create modified Zephyr installs ( "Bootloader") for all of the zephyr boards.
Some of the instructions you will find in the Readme file for ArduinoCore-zephyr:

arduino/ArduinoCore-zephyr: Arduino Core based on Zephyr+llext

I have built for the Q - on my Windows Machine to use with IDE 2.x, I have also built it directly on a Q4. Q2 can be problematic for space, but you can build it on another machine and copy it to the Q...

A lot of the Q stuff for building it for APPLAB setup can be found in the thread:

Install sources built zephyr on UNO Q for applab - Development Tools / App Lab - Arduino Forum

I´m also very interested in this topic. Does anybody know, if we can use ADC1 with DMA? And if not...will it be available in upcoming releases?

In arduino_uno_q_stm32u585xx.overlay I found this, but it looks like commented out:

/*
&dcmi {
	status = "okay";
	pinctrl-0 = <&dcmi_hsync_pd8 &dcmi_pixclk_pd9 &dcmi_vsync_pi5
				&dcmi_d0_pc6 &dcmi_d1_pc7 &dcmi_d2_pc8 &dcmi_d3_pc9
				&dcmi_d4_pe4 &dcmi_d5_pi4 &dcmi_d6_pi6 &dcmi_d7_pi7>;
	pinctrl-names = "default";
	dmas = <&gpdma1 12 86 (STM32_DMA_PERIPH_TO_MEMORY | STM32_DMA_PERIPH_NO_INC |
			STM32_DMA_MEM_INC | STM32_DMA_PERIPH_32BITS | STM32_DMA_MEM_32BITS |
			STM32_DMA_PRIORITY_HIGH) STM32_DMA_FIFO_1_4>;

	port {
		dcmi_ep_in: endpoint {
			remote-endpoint-label = "gc2145_ep_out";
			bus-width = <8>;
			hsync-active = <0>;
			vsync-active = <0>;
			pclk-sample = <0>;
		};
	};
};
*/

Check if the following sketch, dveloped with the help of AI, serves your purposes.

//#include <zephyr/kernel.h>
//#include <zephyr/drivers/adc.h>

/* Obtain ADC1 node from Devicetree */
#define ADC_NODE DT_NODELABEL(adc1)

#if !DT_NODE_EXISTS(ADC_NODE)
#error "ADC1 devicetree node is not defined!"
#endif

#define ADC_CHANNEL_ID      9        // ADC1_IN9 (PA4 / A0)
#define ADC_RESOLUTION      12       // 12-bit resolution
#define ADC_GAIN            ADC_GAIN_1
#define ADC_REFERENCE       ADC_REF_INTERNAL

/* Number of samples to capture via DMA per batch */
#define DMA_BUFFER_SIZE     16

static const struct device *adc_dev = DEVICE_DT_GET(ADC_NODE);

/* Buffer array to hold 16 sequential DMA values */
static int16_t sample_buffer[DMA_BUFFER_SIZE];

/* 
 * Sequence Options:
 * Instructs the driver to perform 15 extra samplings (16 total) 
 * in rapid succession into the array buffer.
 */
static const struct adc_sequence_options sequence_opts = {
    .interval_us     = 0,                        // Continuous sampling
    .extra_samplings = DMA_BUFFER_SIZE - 1,      // 1 initial + 15 extra = 16 samples
};

/* Primary ADC sequence structure */
static struct adc_sequence sequence = {
    .options     = &sequence_opts,
    .channels    = BIT(ADC_CHANNEL_ID),
    .buffer      = sample_buffer,
    .buffer_size = sizeof(sample_buffer),
    .resolution  = ADC_RESOLUTION,
};

void setup() {
    /* Initialize Serial Communication */
    Serial.begin(115200);
    while (!Serial) {
        ; // Wait for Serial Monitor connection
    }

    Serial.println("Initializing ADC1_IN9 for DMA transfer...");

    /* 1. Ensure ADC driver hardware is initialized */
    if (!device_is_ready(adc_dev)) {
        int err = device_init(adc_dev);
        if (err < 0) {
            Serial.print("Error: ADC device driver not ready! Code: ");
            Serial.println(err);
            return;
        }
    }

    /* 2. Setup Channel 9 Configuration */
    struct adc_channel_cfg channel_cfg = {
        .gain             = ADC_GAIN,
        .reference        = ADC_REFERENCE,
        .acquisition_time = ADC_ACQ_TIME_DEFAULT,
        .channel_id       = ADC_CHANNEL_ID,
        .differential     = 0,
    };

    int err = adc_channel_setup(adc_dev, &channel_cfg);
    if (err < 0) {
        Serial.print("Error: Failed to setup ADC channel 9. Code: ");
        Serial.println(err);
        return;
    }

    Serial.println("ADC Channel 9 DMA Setup Complete.");
}

void loop() {
    if (!device_is_ready(adc_dev)) {
        Serial.println("ADC device not ready!");
        delay(1000);
        return;
    }

    /* Clear array buffer before triggering transfer */
    for (int i = 0; i < DMA_BUFFER_SIZE; i++) {
        sample_buffer[i] = 0;
    }

    /* 
     * Trigger ADC read via DMA. 
     * GPDMA engine populates all 16 slots in sample_buffer.
     */
    int err = adc_read(adc_dev, &sequence);
    if (err < 0) {
        Serial.print("ADC DMA read failed with error: ");
        Serial.println(err);
    } else {
        Serial.println("--- DMA 16-Sample Batch Dump ---");
        for (int i = 0; i < DMA_BUFFER_SIZE; i++) {
            Serial.print("Sample [");
            Serial.print(i);
            Serial.print("]: ");
            Serial.println(sample_buffer[i]);
        }
    }

    /* Delay 1 second between batch captures */
    delay(1000);
}

Output:

--- DMA 16-Sample Batch Dump ---
Sample [0]: 4091      //A0-is tied to 3.3V
Sample [1]: 4093
Sample [2]: 4095
Sample [3]: 4093