add external_dualchan_adc for AD9238 boards

This commit is contained in:
Steve Markgraf 2025-11-15 21:21:37 +01:00
parent 1a9131277a
commit a187e84865
13 changed files with 379 additions and 11 deletions

View file

@ -43,7 +43,7 @@ The data from the internal ADC is streamed out via USB. Default configuration is
### external_adc
This app contains a PIO program that reads the data from a 12-bit ADC connected to GP0-GP11, outputs the ADC clock on GP20, and packs the 12 bit samples to 16-bit words to achieve maximum throughput.
This app contains a PIO program that reads the data from a 12-bit ADC connected to GP0-GP11, outputs the ADC clock on GP20, and packs the 12-bit samples to 16-bit words to achieve maximum throughput.
It is meant to be used with cheap AD9226 ADC boards. The default setting is overclocking the RP2350 to 160 MHz and driving the ADC with a 40 MHz clock. With higher overclocking up to 96 MHz ADC clock can be used.
This can be used for sampling the IF of a tuner/downcoverter, as a direct-sampling HF SDR, or for capturing a video signal e.g. with [vhsdecode](https://github.com/oyvindln/vhs-decode).
@ -51,9 +51,13 @@ For the vhsdecode use-case, there is also an [adapter PCB](https://github.com/Se
![Pico2 with AD9226 ADC board](https://steve-m.de/projects/hsdaoh/rp2350_external_adc_rot.jpg)
### external_dualchan_adc
Similar to the external_adc app, but samples a AD9238 dual channel 12-bit ADC connected to GP0-GP11, with clock for both channels on GP20. The benefit of using the AD9238 is that only 12 data lines are required to transfer the data DDR-style, so it works with a regular RP2350A/Pico2 board.
### dual_external_adc
Similar to the external_adc app, but samples two 12 bit ADCs connected to a RP2350B, as well as two PCM1802 modules. Intended for use with vhs-decode, see [this PCB](https://github.com/Sev5000/RP2350B_DualADC_DualPCM) for the matching hardware.
Also similar to the external_adc app, but samples two 12-bit ADCs connected to a RP2350B, as well as two PCM1802 modules. Intended for use with vhs-decode, see [this PCB](https://github.com/Sev5000/RP2350B_DualADC_DualPCM) for the matching hardware.
This example needs to be built with an RP2350B-board in order to work correctly:
cmake -DPICO_PLATFORM=rp2350 -DPICO_BOARD=solderparty_rp2350_stamp_xl ../

View file

@ -1,6 +1,7 @@
add_subdirectory(counter)
add_subdirectory(external_adc)
add_subdirectory(dual_external_adc)
add_subdirectory(external_dualchan_adc)
add_subdirectory(sdr)
add_subdirectory(internal_adc)
add_subdirectory(logic_analyzer)

View file

@ -117,7 +117,7 @@ void init_pio_input(void)
int main()
{
vreg_set_voltage(VREG_VOLTAGE_MAX);
sleep_ms(1);
sleep_us(SYS_CLK_VREG_VOLTAGE_AUTO_ADJUST_DELAY_US);
hsdaoh_set_sys_clock_khz(SYS_CLK);
/* set HSTX clock to sysclk/1 */

View file

@ -3,7 +3,7 @@
;
; SPDX-License-Identifier: BSD-3-Clause
;
; Sample 2x12 bit parallel ADC (AD9226) every 4 PIO cycles on rising clock edge,
; Sample 2x12 bit parallel ADC (AD9226) every 2 PIO cycles on rising clock edge,
; pack four 24 bit samples in three 32 bit words
; ADC clock output as side-set
;
@ -64,7 +64,7 @@ static inline void adc_24bit_input_program_init(PIO pio, uint sm, uint offset, u
32 // Autopush threshold = 32
);
// required in order to set shift-to-right to true (for the out, null ops)
// required in order to set shift-to-right to true (for the out ops)
sm_config_set_out_shift(
&c,
true, // Shift-to-right = true

View file

@ -3,7 +3,7 @@
;
; SPDX-License-Identifier: BSD-3-Clause
;
; Sample 12 bit parallel ADC (AD9226) every 4 PIO cycles on rising clock edge,
; Sample 12 bit parallel ADC (AD9226) every 2 PIO cycles on rising clock edge,
; pack four 12 bit samples in three 16 bit words
; ADC clock output as side-set
;
@ -63,7 +63,7 @@ static inline void adc_12bit_input_program_init(PIO pio, uint sm, uint offset, u
16 // Autopush threshold = 16
);
// required in order to set shift-to-right to true (for the out, null ops)
// required in order to set shift-to-right to true (for the out ops)
sm_config_set_out_shift(
&c,
true, // Shift-to-right = true

View file

@ -208,7 +208,7 @@ int main()
#ifdef OVERVOLT
/* set maximum 'allowed' voltage without voiding warranty */
vreg_set_voltage(VREG_VOLTAGE_MAX);
sleep_ms(1);
sleep_us(SYS_CLK_VREG_VOLTAGE_AUTO_ADJUST_DELAY_US);
#endif
hsdaoh_set_sys_clock_khz(SYS_CLK);

View file

@ -0,0 +1,21 @@
add_executable(external_dualchan_adc
external_dualchan_adc
)
target_compile_options(external_dualchan_adc PRIVATE -Wall)
target_link_libraries(external_dualchan_adc
pico_stdlib
pico_util
hardware_pio
hardware_dma
libpicohsdaoh
)
pico_generate_pio_header(external_dualchan_adc ${CMAKE_CURRENT_LIST_DIR}/dualchan_adc_12bit_input.pio)
pico_generate_pio_header(external_dualchan_adc ${CMAKE_CURRENT_LIST_DIR}/../external_adc/pcm1802_fmt00.pio)
# enable usb output, disable uart output
pico_enable_stdio_usb(external_dualchan_adc 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(external_dualchan_adc)

View file

@ -0,0 +1,83 @@
;
; Copyright (c) 2024-2025 Steve Markgraf <steve@steve-m.de>
;
; SPDX-License-Identifier: BSD-3-Clause
;
; Sample 12 bit dualchannel parallel ADC (AD9238) every 2 PIO cycles on both
; clock edges (DDR), pack four 12 bit samples in three 16 bit words
; ADC clock output as side-set
;
; Data being pushed to the FIFO, four 12 bit samples A-D
; First word: A03 A02 A01 A00 B11 B10 B09 B08 B07 B06 B05 B04 B03 B02 B01 B00
; Second word: A07 A06 A05 A04 C11 C10 C09 C08 C07 C06 C05 C04 C03 C02 C01 C00
; Third word: A11 A10 A09 A08 D11 D10 D09 D08 D07 D06 D05 D04 D03 D02 D01 D00
.pio_version 1
.program dualchan_adc_12bit_input
.side_set 2
public entry_point:
.wrap_target
mov osr, pins side 3 ; sample A
out isr, 4 side 0
in pins, 12 side 0 ; sample B, autopush
out isr, 4 side 3
in pins, 12 side 3 ; sample C, autopush
out isr, 4 side 0
in pins, 12 side 0 ; sample D, autopush
nop side 3
.wrap
% c-sdk {
static inline void dualchan_adc_12bit_input_program_init(PIO pio, uint sm, uint offset, uint pin, uint clk_pin)
{
pio_sm_config c = dualchan_adc_12bit_input_program_get_default_config(offset);
// Set the IN base pin to the provided `pin` parameter.
sm_config_set_in_pins(&c, pin);
// configure CLK pin for side-set
sm_config_set_sideset_pins(&c, clk_pin);
sm_config_set_sideset(&c, 2, false, false);
pio_sm_set_consecutive_pindirs(pio, sm, clk_pin, 2, true);
pio_gpio_init(pio, clk_pin);
pio_gpio_init(pio, clk_pin+1);
// Set the pin directions to input at the PIO
// Set D0-D11 of the ADC as input
pio_sm_set_consecutive_pindirs(pio, sm, pin, 12, false);
// Connect these GPIOs to this PIO block
for (int i = pin; i < (pin+12); i++)
pio_gpio_init(pio, i);
sm_config_set_in_shift(
&c,
false, // Shift-to-right = false (i.e. shift to left)
true, // Autopush enabled
16 // Autopush threshold = 16
);
// required in order to set shift-to-right to true (for the out, null ops)
sm_config_set_out_shift(
&c,
true, // Shift-to-right = true
false, // Autopush disabled
1 // Autopush threshold: ignored
);
// We only receive, so disable the TX FIFO to make the RX FIFO deeper.
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_RX);
sm_config_set_clkdiv(&c, 2.f);
// Load our configuration, and start the program from the beginning
pio_sm_init(pio, sm, offset, &c);
// pio_sm_set_enabled(pio, sm, true);
}
%}

View file

@ -0,0 +1,236 @@
/*
* hsdaoh - High Speed Data Acquisition over MS213x USB3 HDMI capture sticks
* Implementation for the Raspberry Pi RP2350 HSTX peripheral
*
* External dualchannel 12-bit ADC example, connected to the PIO
*
* Copyright (c) 2024-2025 by Steve Markgraf <steve@steve-m.de>
*
* SPDX-License-Identifier: BSD-3-Clause
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the author nor the names of its contributors may
* be used to endorse or promote products derived from this software
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#include "pico/stdlib.h"
#include "hardware/clocks.h"
#include "hardware/irq.h"
#include "hardware/sync.h"
#include "hardware/vreg.h"
#include "hardware/pll.h"
#include "hardware/dma.h"
#include "hardware/pio.h"
#include "picohsdaoh.h"
#include "dualchan_adc_12bit_input.pio.h"
#include "pcm1802_fmt00.pio.h"
/* The PIO is running with sys_clk/1, and needs 4 cycles per sample,
* so the ADC clock is sys_clk/4 */
#define SYS_CLK 320000 // 40 MHz ADC clock
// For alignment of 3x16 bit words in the payload, so that every line starts with word 0
#define ADC_DATA_LEN (RBUF_SLICE_LEN - 3)
// Same here for 2x32 bit words
#define AUDIO_DATA_LEN (RBUF_SLICE_LEN - 4)
#define AUDIO_RBUF_SLICES 8
// ADC is attached to GP0 - GP11 with clock on GP20
#define PIO_INPUT_PIN_BASE 0
#define PIO_OUTPUT_CLK_PIN 20
#define DMACH_PIO_PING 0
#define DMACH_PIO_PONG 1
static bool pio_dma_pong = false;
uint16_t ringbuffer[RBUF_DEFAULT_TOTAL_LEN];
int ringbuf_head = 2;
void __scratch_y("") pio_dma_irq_handler()
{
uint ch_num = pio_dma_pong ? DMACH_PIO_PONG : DMACH_PIO_PING;
dma_channel_hw_t *ch = &dma_hw->ch[ch_num];
dma_hw->intr = 1u << ch_num;
pio_dma_pong = !pio_dma_pong;
ringbuf_head = (ringbuf_head + 1) % RBUF_DEFAULT_SLICES;
ch->write_addr = (uintptr_t)&ringbuffer[ringbuf_head * RBUF_SLICE_LEN];
ch->transfer_count = ADC_DATA_LEN;
hsdaoh_update_head(0, ringbuf_head);
}
void init_pio_input(void)
{
PIO pio = pio0;
uint offset = pio_add_program(pio, &dualchan_adc_12bit_input_program);
uint sm_data = pio_claim_unused_sm(pio, true);
dualchan_adc_12bit_input_program_init(pio, sm_data, offset, PIO_INPUT_PIN_BASE, PIO_OUTPUT_CLK_PIN);
dma_channel_config c;
c = dma_channel_get_default_config(DMACH_PIO_PING);
channel_config_set_chain_to(&c, DMACH_PIO_PONG);
channel_config_set_dreq(&c, pio_get_dreq(pio, sm_data, false));
channel_config_set_read_increment(&c, false);
channel_config_set_write_increment(&c, true);
channel_config_set_transfer_data_size(&c, DMA_SIZE_16);
dma_channel_configure(
DMACH_PIO_PING,
&c,
&ringbuffer[0 * RBUF_SLICE_LEN],
&pio->rxf[sm_data],
ADC_DATA_LEN,
false
);
c = dma_channel_get_default_config(DMACH_PIO_PONG);
channel_config_set_chain_to(&c, DMACH_PIO_PING);
channel_config_set_dreq(&c, pio_get_dreq(pio, sm_data, false));
channel_config_set_read_increment(&c, false);
channel_config_set_write_increment(&c, true);
channel_config_set_transfer_data_size(&c, DMA_SIZE_16);
dma_channel_configure(
DMACH_PIO_PONG,
&c,
&ringbuffer[1 * RBUF_SLICE_LEN],
&pio->rxf[sm_data],
ADC_DATA_LEN,
false
);
dma_hw->ints0 |= (1u << DMACH_PIO_PING) | (1u << DMACH_PIO_PONG);
dma_hw->inte0 |= (1u << DMACH_PIO_PING) | (1u << DMACH_PIO_PONG);
irq_set_exclusive_handler(DMA_IRQ_0, pio_dma_irq_handler);
irq_set_enabled(DMA_IRQ_0, true);
dma_channel_start(DMACH_PIO_PING);
}
#define PCM1802_DATA_PIN 22
#define DMACH_AUDIO_PIO_PING 2
#define DMACH_AUDIO_PIO_PONG 3
static bool audio_pio_dma_pong = false;
uint16_t audio_ringbuffer[AUDIO_RBUF_SLICES * RBUF_SLICE_LEN];
int audio_ringbuf_head = 2;
void __scratch_y("") audio_pio_dma_irq_handler()
{
uint ch_num = audio_pio_dma_pong ? DMACH_AUDIO_PIO_PONG : DMACH_AUDIO_PIO_PING;
dma_channel_hw_t *ch = &dma_hw->ch[ch_num];
dma_hw->intr = 1u << ch_num;
audio_pio_dma_pong = !audio_pio_dma_pong;
audio_ringbuf_head = (audio_ringbuf_head + 1) % AUDIO_RBUF_SLICES;
ch->write_addr = (uintptr_t)&audio_ringbuffer[audio_ringbuf_head * RBUF_SLICE_LEN];
ch->transfer_count = AUDIO_DATA_LEN/2;
hsdaoh_update_head(2, audio_ringbuf_head);
}
void init_audio_pio_input(void)
{
PIO pio = pio0;
uint offset = pio_add_program(pio, &pcm1802_fmt00_program);
uint sm_data = pio_claim_unused_sm(pio, true);
pcm1802_fmt00_program_init(pio, sm_data, offset, PCM1802_DATA_PIN);
dma_channel_config c;
c = dma_channel_get_default_config(DMACH_AUDIO_PIO_PING);
channel_config_set_chain_to(&c, DMACH_AUDIO_PIO_PONG);
channel_config_set_dreq(&c, pio_get_dreq(pio, sm_data, false));
channel_config_set_read_increment(&c, false);
channel_config_set_write_increment(&c, true);
channel_config_set_transfer_data_size(&c, DMA_SIZE_32);
dma_channel_configure(
DMACH_AUDIO_PIO_PING,
&c,
&audio_ringbuffer[0 * RBUF_SLICE_LEN],
&pio->rxf[sm_data],
AUDIO_DATA_LEN/2,
false
);
c = dma_channel_get_default_config(DMACH_AUDIO_PIO_PONG);
channel_config_set_chain_to(&c, DMACH_AUDIO_PIO_PING);
channel_config_set_dreq(&c, pio_get_dreq(pio, sm_data, false));
channel_config_set_read_increment(&c, false);
channel_config_set_write_increment(&c, true);
channel_config_set_transfer_data_size(&c, DMA_SIZE_32);
dma_channel_configure(
DMACH_AUDIO_PIO_PONG,
&c,
&audio_ringbuffer[1 * RBUF_SLICE_LEN],
&pio->rxf[sm_data],
AUDIO_DATA_LEN/2,
false
);
dma_hw->ints1 |= (1u << DMACH_AUDIO_PIO_PING) | (1u << DMACH_AUDIO_PIO_PONG);
dma_hw->inte1 |= (1u << DMACH_AUDIO_PIO_PING) | (1u << DMACH_AUDIO_PIO_PONG);
irq_set_exclusive_handler(DMA_IRQ_1, audio_pio_dma_irq_handler);
irq_set_enabled(DMA_IRQ_1, true);
dma_channel_start(DMACH_AUDIO_PIO_PING);
}
#define OVERVOLT 1
int main()
{
#ifdef OVERVOLT
/* set maximum 'allowed' voltage without voiding warranty */
vreg_set_voltage(VREG_VOLTAGE_MAX);
sleep_us(SYS_CLK_VREG_VOLTAGE_AUTO_ADJUST_DELAY_US);
#endif
hsdaoh_set_sys_clock_khz(SYS_CLK);
/* set HSTX clock to sysclk/1 */
hw_write_masked(
&clocks_hw->clk[clk_hstx].div,
1 << CLOCKS_CLK_HSTX_DIV_INT_LSB,
CLOCKS_CLK_HSTX_DIV_INT_BITS
);
stdio_init_all();
hsdaoh_init(GPIO_DRIVE_STRENGTH_12MA, GPIO_SLEW_RATE_FAST);
hsdaoh_add_stream(0, PIO_DUALCHAN_12BIT, (SYS_CLK/4) * 1000, ADC_DATA_LEN, RBUF_DEFAULT_SLICES, ringbuffer);
hsdaoh_add_stream(2, PIO_PCM1802_AUDIO, 78125, AUDIO_DATA_LEN, AUDIO_RBUF_SLICES, audio_ringbuffer);
hsdaoh_start();
init_pio_input();
init_audio_pio_input();
/* synchronously start data input */
pio_set_sm_mask_enabled(pio0, 3, true);
while (1)
__wfi();
}

View file

@ -132,7 +132,7 @@ int main()
{
/* set maximum 'allowed' voltage without voiding warranty */
vreg_set_voltage(VREG_VOLTAGE_MAX);
sleep_ms(1);
sleep_us(SYS_CLK_VREG_VOLTAGE_AUTO_ADJUST_DELAY_US);
hsdaoh_set_sys_clock_khz(SYS_CLK);

View file

@ -122,7 +122,7 @@ int main()
{
/* set maximum 'allowed' voltage without voiding warranty */
vreg_set_voltage(VREG_VOLTAGE_MAX);
sleep_ms(1);
sleep_us(SYS_CLK_VREG_VOLTAGE_AUTO_ADJUST_DELAY_US);
hsdaoh_set_sys_clock_khz(SYS_CLK);

View file

@ -247,7 +247,7 @@ int main()
//vreg_set_voltage(VREG_VOLTAGE_MAX);
vreg_disable_voltage_limit();
vreg_set_voltage(VREG_VOLTAGE_1_65);
sleep_ms(1);
sleep_us(SYS_CLK_VREG_VOLTAGE_AUTO_ADJUST_DELAY_US);
#endif
hsdaoh_set_sys_clock_khz(enable_8bit_mode ? SYS_CLK_8BIT : SYS_CLK_10BIT);
int usbdiv = HSTX_CLK_MHZ / 48;

View file

@ -104,6 +104,29 @@ enum
PIO_32BIT,
PIO_32BIT_IQ,
PIO_PCM1802_AUDIO,
PIO_AUDIO_PLACEHOLDER1,
PIO_AUDIO_PLACEHOLDER2,
PIO_AUDIO_PLACEHOLDER3,
PIO_DUALCHAN_1BIT,
PIO_DUALCHAN_1BIT_IQ,
PIO_DUALCHAN_2BIT,
PIO_DUALCHAN_2BIT_IQ,
PIO_DUALCHAN_4BIT,
PIO_DUALCHAN_4BIT_IQ,
PIO_DUALCHAN_8BIT,
PIO_DUALCHAN_8BIT_IQ,
PIO_DUALCHAN_10BIT,
PIO_DUALCHAN_10BIT_IQ,
PIO_DUALCHAN_12BIT,
PIO_DUALCHAN_12BIT_IQ,
PIO_DUALCHAN_14BIT,
PIO_DUALCHAN_14BIT_IQ,
PIO_DUALCHAN_16BIT,
PIO_DUALCHAN_16BIT_IQ,
PIO_DUALCHAN_24BIT,
PIO_DUALCHAN_24BIT_IQ,
PIO_DUALCHAN_32BIT,
PIO_DUALCHAN_32BIT_IQ,
};
void hsdaoh_start(void);