Title: AD7606-F4 on HW-AD7606-F4 module: BUSY never asserts, no SPI data despite correct VDD/VDRIVE and CONVST
Body:
Hardware
-
ADC: AD7606-F4 mounted on a “HW-AD7606-F4” breakout module (20-pin header). Not the bare IC.
-
MCU: Raspberry Pi Pico 2 (RP2350)
-
Level shifting: tested two ways
A) Direct 3.3 V I/O with VDRIVE=3.3 V (preferred)
B) Through a bidirectional BSS138 “HW-221” level shifter with pull-ups (LV=3.3 V, HV=5 V) -
Supplies: VDD=5.0 V, VDRIVE as noted above, common AGND/DGND with MCU.
Module straps (forced)
-
PAR/SER=1, RANGE=1, REF_SELECT=1, STBY=1 (all tied to 3.3 V).
-
CONVST_B=0, OS2=OS1=OS0=0, BYTE_SEL(DB15)=0, HBEN(DB14)=0, DB13..0=0.
Connections (current direct 3.3 V setup)
-
DOUTA → RP2350 GP0 (MISO, SPI0).
-
CS → GP1.
-
SCLK → GP5.
-
MOSI (unused) → GP2.
-
CONVST_A → GP3.
-
BUSY → GP4.
-
RESET → GP7 (held high; also tried low 10 ms → high).
Analog test input
-
CH1+: tied to GND or +5 V for sanity tests.
-
CH1−: GND.
Symptoms
-
With a simple blink test, I confirmed on the module pin that CONVST_A toggles 0
3.3 V at 1 Hz.
-
RESET on the module measures 3.3 V (idle high).
-
BUSY stays low in my main “BUSY probe” test (up=0, ended=1).
-
If I route signals through the HW-221 (BSS138) and add 10 kΩ pull-ups on both sides, CONVST_A propagates, but BUSY still does not assert.
-
SPI reads return 0xFFFF or 0x0000 for all four 16-bit words after a CONVST pulse, even when I delay 10–20 µs in place of waiting for BUSY.
What I already checked
-
VDD=5.0 V on module VDD pin.
-
VDRIVE=3.3 V on module VDRIVE pin (for direct 3.3 V case).
-
CONVST_A pin on the module really sees 0
3.3 V from the MCU.
-
RESET on the module is high (3.3 V).
-
PAR/SER, RANGE, REF_SELECT, STBY are physically strapped to 3.3 V.
-
CONVST_B, OS2, OS1, OS0 are physically strapped to GND.
-
BYTE_SEL and HBEN tied to GND; DB13..0 tied to GND.
-
Tried lowering SCLK to 500 kHz.
-
Tried both waiting for BUSY and fixed delays.
-
Tried reading only the first 16-bit word after CS↓.
Open questions
-
On the HW-AD7606-F4 module, is BUSY available on the header as BUSY_A or BUSY_B, and is it active-high push-pull on the module? Any variants where that header pin is not actually routed to AD7606 pin 56?
-
With PAR/SER=1, BYTE_SEL=0, HBEN=0, what exact SPI mode and timing should I meet for the first 16-bit word (V1) after conversion? I use mode 1, 1 MHz to start.
-
Are there any module revisions where CONVST_A on the silkscreen maps to CONVST_B on the IC, causing BUSY behavior to look stuck?
-
Any additional required straps for using the internal reference on the module? The board appears to have the REFCAP parts populated.
-
Is there any condition where BUSY would remain low even with valid CONVST due to STBY/REF/RANGE timing after reset? Recommended reset and startup sequence for the module?
What would help me debug
-
A verified pinout of the HW-AD7606-F4 header: BUSY, CONVST_A, RESET, CS, SCLK, DOUTA, VDD, VDRIVE, grounds.
-
A minimal known-good SPI timing diagram for serial mode (first 16-bit word) and BUSY timing for OS[2:0]=000.
-
Any known module errata regarding silkscreen labels vs actual nets.
Thank you for any guidance. I can provide scope captures of CONVST, BUSY, CS, SCLK, and DOUTA if needed.
#include <stdio.h> #include <string.h> #include <math.h> #include <stdlib.h> #include "pico/stdlib.h" #include "pico/double.h" #include "hardware/pio.h" #include "hardware/dma.h" #include "hardware/gpio.h" #include "hardware/clocks.h" #include "hardware/sync.h" #include "ad7606_spi.pio.h" // ============================================================================ // GPIO PIN DEFINITIONS // ============================================================================ #define PIN_SCK 2 // SPI Clock (SCLK/RD) - MUST be PIN_CS + 1 #define PIN_MISO 0 // SPI Data Out (DOUTA / DB7) #define PIN_CS 1 // Chip Select (active LOW) #define PIN_BUSY 4 // Busy signal (BUSY) #define PIN_CONVST 3 // Conversion start pulse (CONVST_A) #define PIN_CONVST_B 3 // Optional: Conversion start for CAN B (CONVST_B) #define PIN_RESET 5 // Hardware Reset (RST) #define PIN_RANGE 0 // (Não conectado diretamente; selecione com GND/3.3V) #define PIN_OS0 0 // (Fixe ou não usado) #define PIN_OS1 0 // (Fixe ou não usado) #define PIN_OS2 0 // (Fixe ou não usado) // ============================================================================ // AD7606 CONFIGURATION // ============================================================================ #define CHANNELS 8 #define BYTES_PER_SAMPLE 16 // 8 channels × 2 bytes each #define WORDS_PER_SAMPLE 4 // 8 channels × 2 bytes = 16 bytes = 4×32-bit words // For ±1V range, we need to use ±5V hardware range and scale appropriately // AD7606 only supports ±5V or ±10V ranges via hardware // We'll use ±5V and note that actual sensor range should be ±1V #define REF_VOLTAGE 5.0 // Hardware reference (±5V) #define ACTUAL_RANGE 1.0 // Actual measurement range (±1V) #define SCALE_FACTOR (2.0 * REF_VOLTAGE / 65536.0) // ADC to voltage conversion // ============================================================================ // RING BUFFER CONFIGURATION // ============================================================================ #define RING_BUFFER_LEN 64 // Reduced for simpler processing #define WINDOW_SIZE 32 // Samples to accumulate for RMS // ============================================================================ // PIO AND DMA CONFIGURATION // ============================================================================ #define PIO_FREQ 1000000.0 // 1 MHz PIO clock (slower, more reliable) // ============================================================================ // DATA STRUCTURES // ============================================================================ typedef struct { uint16_t data[CHANNELS]; absolute_time_t timestamp; uint32_t sample_index; } RingBuffer; typedef struct { bool is_voltage; double conversion_factor; int paired_channel; // -1 if no pairing } ChannelConfig; // ============================================================================ // GLOBAL VARIABLES // ============================================================================ RingBuffer ring_buffer[RING_BUFFER_LEN]; volatile uint ring_buffer_head = 0; volatile uint ring_buffer_tail = 0; volatile uint32_t current_sample_index = 0; volatile uint32_t dma_transfer_count = 0; PIO pio = pio0; uint sm = 0; int dma_chan = -1; // Channel configuration: all voltage channels for simplicity ChannelConfig channel_config[CHANNELS] = { {true, 1.0, -1}, // Channel 0 - Voltage {true, 1.0, -1}, // Channel 1 - Voltage {true, 1.0, -1}, // Channel 2 - Voltage {true, 1.0, -1}, // Channel 3 - Voltage {true, 1.0, -1}, // Channel 4 - Voltage {true, 1.0, -1}, // Channel 5 - Voltage {true, 1.0, -1}, // Channel 6 - Voltage {true, 1.0, -1}, // Channel 7 - Voltage }; // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ /** * @brief Get number of samples in ring buffer (thread-safe) */ static inline uint get_buffer_count(void) { uint32_t irq_status = save_and_disable_interrupts(); uint head = ring_buffer_head; uint tail = ring_buffer_tail; restore_interrupts(irq_status); return (head >= tail) ? (head - tail) : (RING_BUFFER_LEN - tail + head); } /** * @brief Convert ADC value to voltage * @param adc_value Raw 16-bit ADC value * @return Voltage in volts (bipolar: -REF_VOLTAGE to +REF_VOLTAGE) */ static inline double adc_to_voltage(uint16_t adc_value) { // AD7606 bipolar mode: 0x0000 = -Vref, 0x8000 = 0V, 0xFFFF = +Vref return ((double)adc_value - 32768.0) * SCALE_FACTOR; } // ============================================================================ // GPIO INITIALIZATION // ============================================================================ /** * @brief Initialize all GPIO pins for AD7606 control */ void setup_gpio(void) { printf("Initializing GPIO pins...\n"); // RESET pin - active LOW gpio_init(PIN_RESET); gpio_set_dir(PIN_RESET, GPIO_OUT); gpio_put(PIN_RESET, 1); // Keep out of reset // CONVST pin - conversion trigger gpio_init(PIN_CONVST); gpio_set_dir(PIN_CONVST, GPIO_OUT); gpio_put(PIN_CONVST, 1); // Idle HIGH // BUSY pin - input from AD7606 gpio_init(PIN_BUSY); gpio_set_dir(PIN_BUSY, GPIO_IN); // The following pins (RANGE, OS) are defined on GPIO 0, which conflicts with PIN_MISO. // They should be hardwired to the desired levels (GND/VCC) on the PCB, not controlled by the MCU. /* // RANGE pin - LOW = ±5V, HIGH = ±10V gpio_init(PIN_RANGE); gpio_set_dir(PIN_RANGE, GPIO_OUT); gpio_put(PIN_RANGE, 0); // Select ±5V range // Oversampling pins - all LOW for no oversampling (OS=000) gpio_init(PIN_OS0); gpio_set_dir(PIN_OS0, GPIO_OUT); gpio_put(PIN_OS0, 0); gpio_init(PIN_OS1); gpio_set_dir(PIN_OS1, GPIO_OUT); gpio_put(PIN_OS1, 0); gpio_init(PIN_OS2); gpio_set_dir(PIN_OS2, GPIO_OUT); gpio_put(PIN_OS2, 0); */ printf("GPIO configured:\n"); printf(" RESET=%d (HIGH), CONVST=%d (HIGH)\n", PIN_RESET, PIN_CONVST); printf(" RANGE=%d (LOW=±5V)\n", PIN_RANGE); printf(" OS[2:0]=%d,%d,%d (000=No oversampling)\n", PIN_OS2, PIN_OS1, PIN_OS0); } /** * @brief Reset AD7606 */ void reset_ad7606(void) { printf("Resetting AD7606...\n"); gpio_put(PIN_RESET, 0); sleep_us(10); // Minimum 50ns, use 10us for safety gpio_put(PIN_RESET, 1); sleep_us(10); printf("Reset complete\n"); } // ============================================================================ // PIO SPI INITIALIZATION // ============================================================================ /** * @brief Initialize PIO state machine for SPI communication */ void init_pio_spi(void) { printf("Initializing PIO SPI...\n"); // Load PIO program uint offset = pio_add_program(pio, &ad7606_spi_128bit_read_program); printf(" PIO program loaded at offset: %d\n", offset); // Calculate clock divider uint32_t sys_clock = clock_get_hz(clk_sys); float clkdiv = (float)sys_clock / PIO_FREQ; printf(" System clock: %lu Hz\n", sys_clock); printf(" PIO clock divider: %.2f (target: %.0f Hz)\n", clkdiv, PIO_FREQ); // Initialize state machine ad7606_spi_128bit_init(pio, sm, offset, clkdiv, PIN_SCK, PIN_MISO, PIN_CS, PIN_BUSY); // Clear FIFO and enable state machine pio_sm_clear_fifos(pio, sm); pio_sm_set_enabled(pio, sm, true); printf("PIO SPI initialized successfully\n"); } // ============================================================================ // DMA SETUP // ============================================================================ /** * @brief DMA interrupt handler - called when transfer completes */ void dma_irq_handler(void) { // Clear interrupt dma_hw->ints0 = 1u << dma_chan; // Record metadata ring_buffer[ring_buffer_head].timestamp = get_absolute_time(); ring_buffer[ring_buffer_head].sample_index = current_sample_index++; dma_transfer_count++; // Debug output every 100 samples if (dma_transfer_count % 100 == 0) { printf("Sample %lu: ", dma_transfer_count); for (int i = 0; i < CHANNELS; i++) { double voltage = adc_to_voltage(ring_buffer[ring_buffer_head].data[i]); printf("CH%d=%.4fV ", i, voltage); } printf("\n"); } // Advance head pointer uint next_head = (ring_buffer_head + 1) % RING_BUFFER_LEN; if (next_head == ring_buffer_tail) { printf("WARNING: Ring buffer overflow!\n"); } ring_buffer_head = next_head; // Setup next DMA transfer dma_channel_set_write_addr(dma_chan, ring_buffer[ring_buffer_head].data, false); } /** * @brief Initialize DMA channel for PIO to memory transfers */ void setup_dma(void) { printf("Initializing DMA...\n"); dma_chan = dma_claim_unused_channel(true); dma_channel_config config = dma_channel_get_default_config(dma_chan); // Configure transfer: 32-bit words from PIO RX FIFO channel_config_set_transfer_data_size(&config, DMA_SIZE_32); channel_config_set_read_increment(&config, false); // Read from same address (FIFO) channel_config_set_write_increment(&config, true); // Write to sequential memory channel_config_set_dreq(&config, pio_get_dreq(pio, sm, false)); // Triggered by PIO RX // Configure DMA channel dma_channel_configure( dma_chan, &config, ring_buffer[ring_buffer_head].data, // Destination &pio->rxf[sm], // Source (PIO RX FIFO) WORDS_PER_SAMPLE, // Number of 32-bit transfers false // Don't start yet ); // Enable interrupt dma_channel_set_irq0_enabled(dma_chan, true); irq_set_exclusive_handler(DMA_IRQ_0, dma_irq_handler); irq_set_enabled(DMA_IRQ_0, true); printf("DMA channel %d configured\n", dma_chan); } // ============================================================================ // AD7606 CONVERSION CONTROL // ============================================================================ /** * @brief Trigger a single conversion on AD7606 * Similar to Arduino's conversionPulse() function */ void trigger_conversion(void) { // Generate falling edge pulse on CONVST (min 25ns wide) gpio_put(PIN_CONVST, 0); sleep_us(1); // 1us pulse width (way more than 25ns minimum) gpio_put(PIN_CONVST, 1); // The PIO program will wait for BUSY to go HIGH then LOW // Then it will read the data automatically via SPI } /** * @brief Start DMA transfer and trigger conversion */ void start_conversion_and_dma(void) { // Start DMA transfer (it will wait for PIO data) dma_channel_start(dma_chan); // Trigger AD7606 conversion // PIO will wait for BUSY signal and read data when ready trigger_conversion(); } // ============================================================================ // DATA PROCESSING // ============================================================================ /** * @brief Process accumulated window of samples for RMS calculation */ void process_accumulated_data(void) { absolute_time_t start_time = get_absolute_time(); double sum_square[CHANNELS] = {0}; uint samples_processed = 0; // Get safe copy of tail uint32_t irq_status = save_and_disable_interrupts(); uint local_tail = ring_buffer_tail; restore_interrupts(irq_status); uint buffer_index = local_tail; // Accumulate squared values for (uint i = 0; i < WINDOW_SIZE; i++) { for (int ch = 0; ch < CHANNELS; ch++) { double voltage = adc_to_voltage(ring_buffer[buffer_index].data[ch]); voltage *= channel_config[ch].conversion_factor; sum_square[ch] += voltage * voltage; } buffer_index = (buffer_index + 1) % RING_BUFFER_LEN; samples_processed++; } // Calculate and print RMS values printf("\n=== RMS Results (Window: %u samples) ===\n", samples_processed); for (int i = 0; i < CHANNELS; i++) { double rms = sqrt(sum_square[i] / samples_processed); printf("CH%d: Vrms=%.4f V\n", i, rms); } printf("=========================================\n\n"); // Update tail pointer irq_status = save_and_disable_interrupts(); ring_buffer_tail = (ring_buffer_tail + WINDOW_SIZE) % RING_BUFFER_LEN; restore_interrupts(irq_status); // Processing time int64_t processing_time_us = absolute_time_diff_us(start_time, get_absolute_time()); printf("Processing time: %lld us\n\n", processing_time_us); } /** * @brief Check if enough data is available and process it */ void check_and_process_data(void) { uint buffer_count = get_buffer_count(); if (buffer_count >= WINDOW_SIZE) { process_accumulated_data(); } } // ============================================================================ // MAIN SETUP AND LOOP // ============================================================================ /** * @brief Initialize all peripherals */ void setup(void) { printf("\n=================================\n"); printf("AD7606 Setup for Pico2\n"); printf("=================================\n"); setup_gpio(); sleep_ms(100); reset_ad7606(); sleep_ms(100); init_pio_spi(); sleep_ms(100); setup_dma(); sleep_ms(100); printf("\nConfiguration Summary:\n"); printf(" Range: ±5V (hardware) / ±1V (recommended sensor range)\n"); printf(" Oversampling: None (OS=000)\n"); printf(" Channels: 8\n"); printf(" PIO Frequency: %.0f Hz\n", PIO_FREQ); printf(" Scale Factor: %.10f V/LSB\n", SCALE_FACTOR); printf("\nSetup complete!\n"); } /** * @brief Main program entry point */ int main(void) { // Initialize USB serial stdio_init_all(); sleep_ms(2000); // Wait for USB connection printf("\n\n"); printf("=====================================\n"); printf(" AD7606 Data Acquisition System\n"); printf(" Raspberry Pi Pico2\n"); printf("=====================================\n\n"); setup(); printf("\nStarting data acquisition...\n"); printf("Triggering conversions at software-controlled rate\n\n"); absolute_time_t last_conversion = get_absolute_time(); absolute_time_t last_stats = get_absolute_time(); const uint64_t conversion_interval_us = 1000; // 1ms = 1kHz sampling rate while (true) { // Trigger conversion at regular intervals int64_t time_since_last = absolute_time_diff_us(last_conversion, get_absolute_time()); if (time_since_last >= (int64_t)conversion_interval_us) { // By removing the check for dma_channel_is_busy, we prevent a deadlock. // The conversion is triggered periodically, and the hardware (PIO/DMA) // ensures the data is read correctly without race conditions. start_conversion_and_dma(); last_conversion = get_absolute_time(); } // Process accumulated data check_and_process_data(); // Print statistics every second int64_t time_since_stats = absolute_time_diff_us(last_stats, get_absolute_time()); if (time_since_stats >= 1000000) { uint buffer_count = get_buffer_count(); printf("--- Statistics ---\n"); printf("Samples: %lu (%.1f Hz)\n", dma_transfer_count, (float)dma_transfer_count / (time_since_stats / 1000000.0)); printf("Buffer: %u / %u\n", buffer_count, RING_BUFFER_LEN); printf("------------------\n\n"); dma_transfer_count = 0; last_stats = get_absolute_time(); } // Small delay to prevent busy-waiting sleep_us(100); } return 0; } -