Post Go back to editing

Title: AD7606-F4 on HW-AD7606-F4 module: BUSY never asserts, no SPI data despite correct VDD/VDRIVE and CONVST

Category: Hardware
Product Number: hw-ad7606-f4

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

  1. With a simple blink test, I confirmed on the module pin that CONVST_A toggles 0 Left right arrow 3.3 V at 1 Hz.

  2. RESET on the module measures 3.3 V (idle high).

  3. BUSY stays low in my main “BUSY probe” test (up=0, ended=1).

  4. 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.

  5. 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 Left right arrow 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

    1. 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?

    2. 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.

    3. Are there any module revisions where CONVST_A on the silkscreen maps to CONVST_B on the IC, causing BUSY behavior to look stuck?

    4. Any additional required straps for using the internal reference on the module? The board appears to have the REFCAP parts populated.

    5. 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;
    }