Chained-DMA signal generator thru SPI DAC on RP2040 (Raspberry Pi Pico)

V. Hunter Adams (vha3@cornell.edu)

Objective and page organization

This project was meant to provide an objective thru which to build understanding of DMA and SPI channels on the RP2040. For this project, I chained two DMA channels. One of those channels is triggered by a timer which is configured to overflow at audio-rate ($\approx 44$ kHz). This channel moves data from a sine table to the SPI transmit buffer. The SPI channel is configured to automatically transmit any new data which appears in its transmit buffer.

The other DMA channel is chained to the first. When the first DMA channel finishes traversing the sine table, it triggers the second channel. This channel writes to the control registers of the first DMA channel. In particular, it writes to the AL3_TRANS_COUNT_TRIG register associated with the other DMA channel. This control register sets the number of transactions that the first DMA channel should execute (the length of the sine table). Because it is a trigger register, writing any non-zero value to this register automatically triggers the DMA channel to start. So, these two channels ping-pong off one another. The first finishes and triggers the second, the second writes to the control/trigger register of the first. The consequence is a persistent sine wave output from the DAC, with no code executing.

All of the code is provided in a listing in the first section of this page. The rest of the page walks through the C source file from top to bottom, explaining each line of code. Lastly, I've included some plots output plots from the oscilloscope and compared the measured output frequency to the expected output frequency. The two agree.

Documentation

The resources for this project include the RP2040 C SDK user's guide and the RP2040 datasheet.

All the code

/**
 *  V. Hunter Adams (vha3)
    Code based on examples from Raspberry Pi Co

    Sets up two DMA channels. One sends samples at audio rate to the DAC,
    (data_chan), and the other writes to the data_chan DMA control registers (ctrl_chan).

    The control channel writes to the data channel, sending one period of
    a sine wave thru the DAC. The control channel is chained to the data
    channel, so it is re-triggered after the data channel is finished. The control
    channel then rewrites and restarts the data channel, etc.

    NOTE: in order to configure the data channel read addresses to 
    wrap properly, the DAC data buffer must be naturally aligned in memory.
    My hacky solution was to generate a few extra periods of the sine wave,
    and then find an index which was naturally aligned.

    The better solution, implemented next, was to use the ((aligned())) attribute

    Configuring the data DMA channel to the DAC to be triggered off an audio-rate
    timer required a function that changes the value of a DMA control register.
    The SDK didn't have any functions that changed this particular register, but
    it contained others upon which I could model the one below. The documentation
    for the RP2040 is written well enough that this wasn't too troublesome.

 */

#include <stdio.h>
#include <math.h>
#include "pico/stdlib.h"
#include "hardware/dma.h"
#include "hardware/spi.h"

// Number of samples per period in sine table
#define sine_table_size 256

// Align DAC data
int raw_sin[sine_table_size] ;
unsigned short DAC_data[sine_table_size] __attribute__ ((aligned(2048))) ;

// A-channel, 1x, active
#define DAC_config_chan_A 0b0011000000000000

//SPI configurations
#define PIN_MISO 4
#define PIN_CS   5
#define PIN_SCK  6
#define PIN_MOSI 7
#define SPI_PORT spi0

// Number of DMA transfers per event
const uint32_t transfer_count = sine_table_size ;

/*! Added by Hunter
    Modifies the TIMER0 register of the dma channel
 */
static inline void dma_channel_set_timer0(uint32_t timerval) {
    dma_hw->timer[0] = timerval;
}


int main() {
    stdio_init_all();

    // Initialize SPI channel (channel, baud rate set to 20MHz)
    spi_init(SPI_PORT, 20000000) ;
    // Format (channel, data bits per transfer, polarity, phase, order)
    spi_set_format(SPI_PORT, 16, 0, 0, 0);

    // Map SPI signals to GPIO ports, acts like framed SPI with this CS mapping
    gpio_set_function(PIN_MISO, GPIO_FUNC_SPI);
    gpio_set_function(PIN_CS, GPIO_FUNC_SPI) ;
    gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
    gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);

    // Build sine table
    int i ;
    for (i=0; i<(sine_table_size); i++){
        raw_sin[i] = (int)(2047 * sin((float)i*6.283/(float)sine_table_size) + 2047); //12 bit
        DAC_data[i] = DAC_config_chan_A | (raw_sin[i] & 0x0fff) ;
    }


    // Get a free channel, panic() if there are none
    int data_chan = dma_claim_unused_channel(true);
    int ctrl_chan = dma_claim_unused_channel(true);

    // Setup the control channel
    dma_channel_config c = dma_channel_get_default_config(ctrl_chan); // default configs
    channel_config_set_transfer_data_size(&c, DMA_SIZE_32); // 32-bit txfers
    channel_config_set_read_increment(&c, false); // no read incrementing
    channel_config_set_write_increment(&c, false); // no write incrementing

    dma_channel_configure(
        ctrl_chan,
        &c,
        &dma_hw->ch[data_chan].al1_transfer_count_trig, // txfer to transfer count trigger
        &transfer_count,
        1,
        false
    );

    // Confirm memory alignment
    printf("\n\nBeginning: %x", &DAC_data[0]);
    printf("\nFirst: %x", &DAC_data[1]);
    printf("\nSecond: %x", &DAC_data[2]);

    // 16 bit transfers. Read address increments after each transfer.
    // DREQ to Timer 0 is selected, so the DMA is throttled to audio rate
    dma_channel_config c2 = dma_channel_get_default_config(data_chan);
    // 16 bit transfers
    channel_config_set_transfer_data_size(&c2, DMA_SIZE_16);
    // increment the read adddress, don't increment write address
    channel_config_set_read_increment(&c2, true);
    channel_config_set_write_increment(&c2, false);
    // (X/Y)*sys_clk, where X is the first 16 bytes and Y is the second
    // sys_clk is 125 MHz unless changed in code
    dma_channel_set_timer0(0x0017ffff) ;
    // 0x3b means timer0 (see SDK manual)
    channel_config_set_dreq(&c2, 0x3b);
    // chain to the controller DMA channel
    channel_config_set_chain_to(&c2, ctrl_chan);
    // set wrap boundary. This is why we needed alignment!
    channel_config_set_ring(&c2, false, 9); // 1 << 9 byte boundary on read ptr


    dma_channel_configure(
        data_chan,          // Channel to be configured
        &c2,            // The configuration we just created
        &spi_get_hw(SPI_PORT)->dr, // write address
        DAC_data, // The initial read address (AT NATURAL ALIGNMENT POINT)
        sine_table_size, // Number of transfers; in this case each is 2 byte.
        false           // Don't start immediately.
    );


    // start the control channel
    dma_start_channel_mask(1u << ctrl_chan) ;

    // Exit main.
    // No code executing!!

}

Stepping thru the code

Includes

The first lines of code in the C source file include some header files. Two of these are standard C headers (stdio.h and math.h) and the others are headers which come from the C SDK for the Raspberry Pi Pico. The first of these, pico/stdlib.h is what the SDK calls a "High-Level API." These high-level API's "provide higher level functionality that isn’t hardware related or provides a richer set of functionality above the basic hardware interfaces." The architecture of this SDK is described at length in the SDK manual. All libraries within the SDK are INTERFACE libraries. pico/stdlib.h in particular pulls in a number of lower-level hardware libraries, listed on page 196 of the C SDK guide.

The next two includes pull in hardware API's which are not already brought in by pico/stdlib.h. These include hardware/dma.h and hardware/spi.h. As the names suggest, these two interface libraries give us access to the API's associated with the DMA and SPI peripherals on the RP2040. Don't forget to link these in the CMakeLists.txt file!

#include <stdio.h>
#include <math.h>
#include "pico/stdlib.h"
#include "hardware/dma.h"
#include "hardware/spi.h"

Global declarations and defines

// Number of samples per period in sine table
#define sine_table_size 256

// Align DAC data
int raw_sin[sine_table_size] ;
unsigned short DAC_data[sine_table_size] __attribute__ ((aligned(2048))) ;

// A-channel, 1x, active
#define DAC_config_chan_A 0b0011000000000000

//SPI configurations
#define PIN_MISO 4
#define PIN_CS   5
#define PIN_SCK  6
#define PIN_MOSI 7
#define SPI_PORT spi0

// Number of DMA transfers per event
const uint32_t transfer_count = sine_table_size ;

The next section of code #define's some parameter values, and declares some variables and arrays in global space. sine_table_size represents the number of elements in our sine table. The sine table will contain amplitudes for a single period of a sine wave, sine_table_size sets the number of samples that we will store. Note that, for reasons that will become apparent farther down in the code, it is convenient to make the length of this table a power of two.

We next declare two arrays with lengths equal to sine_table_size. We will populate these arrays in main(), but note that raw_sin is an array of ints and DAC_data is an array of unsigned shorts. As the names suggest, we will store raw sine wave amplitudes in raw_sin, and we will store the formatted bits that will be send to the DAC through the SPI channel in DAC_data. As described in the DAC datasheet, the DAC expects 16-bit packets through the SPI channel. The most significant 4 of these bits are configuration and control bits, and the least significant 12 are data (i.e. the 12-bit number in the range [0, 4096] that we want for the DAC to convert to a voltage). So, each element in the DAC_data array will contain a formatted version of the corresponding element in the raw_sin array. In particular, the sine wave amplitudes will be truncated to 12 bits, and the DAC control bits will be masked to the top 4 bits of each element in the array. The result will be that each element in the DAC_data array will be an unsigned short, the top 4 bits of which are DAC control bits, and the bottom 12 bits of which are data.

Furthermore, note that we have aligned the DAC_data array in memory. The reason for this will become obvious later in the code, but this is so that we can use the channel_config_set_ring option for the DMA channel, so that it will wrap its read address at the end of the array and start reading again at the beginning. For this to work, we need a naturally aligned array.

The next line of code sets the top 4 DAC configuration bits. You can read about these configuration bits in the DAC datasheet, but note that the bottom 12 bits of DAC_config_chan_A are all 0's. In main(), we will mask the DAC data into these bottom 12 bits, maintaining the top 4 control bits.

The next chunk of code gives some names to a handful of GPIO ports for later association with the SPI channel. Note that the numbers in these lines of code correspond to GPIO port number and not to pin numbers. See the GPIO port numbers in the image below. Note also that these pins are not chosen arbitrarily. We've chosen a particular set of GPIO ports which are all associated with the same SPI channel (SPI0), and named each according to its available function on that SPI channel (MISO/RX, MOSI/TX, CS, SCK). We could have chosen different GPIO ports for each of these functions, but not arbitrary ports, only those with the same signals mapped to them. The final line in this chunk specifies the SPI channel which we are using, which is spi0. spi0 is declared in the spi.h header file (pico-sdk\src\rp2_common\hardware_spi\include\hardware\spi.h).

Lastly, we declare a const unsigned int in which we store the length of the array. This will be necessary for configuring the DMA channel.

missing
GPIO port names and functions

Function for manipulating DMA timer register

The SDK function channel_config_set_dreq(dma_channel_config *c, uint dreq) allows the programmer to select a transfer request signal for a particular DMA channel. This is described on page 98 of the SDK guide and summarized here.

The first argument is a pointer to channel configuration data, and the second is the dreq source. To quote the SDK guide: "Sources for TREQ signals are internal (TIMERS) or external (DREQ, a Data Request from the system). 0x0 to 0x3a → select DREQ n as TREQ 0x3b → Select Timer 0 as TREQ 0x3c → Select Timer 1 as TREQ 0x3d → Select Timer 2 as TREQ (Optional) 0x3e → Select Timer 3 as TREQ (Optional) 0x3f → Permanent request, for unpaced transfers."

At present, there is no SDK function for manipulating the TIMER0, TIMER1, TIMER2, or TIMER3 registers. However, these registers are described on page 109 of the RP2040 datasheet, and they are mapped to the dma_hw_t struct in pico-sdk\src\rp2040\hardware_structs\include\hardware\structs\dma.h. The struct which organizes the DMA control registers is copied below from that document:

typedef struct {
    dma_channel_hw_t ch[NUM_DMA_CHANNELS];
    uint32_t _pad0[16 * (16 - NUM_DMA_CHANNELS)];
    io_ro_32 intr;
    io_rw_32 inte0;
    io_rw_32 intf0;
    io_rw_32 ints0;
    uint32_t _pad1[1];
    io_rw_32 inte1;
    io_rw_32 intf1;
    io_rw_32 ints1;
    io_rw_32 timer[4];
    io_wo_32 multi_channel_trigger;
    io_rw_32 sniff_ctrl;
    io_rw_32 sniff_data;
    uint32_t _pad2[1];
    io_ro_32 fifo_levels;
    io_wo_32 abort;
} dma_hw_t;

We can manipulate each of the TIMER registers by touching the timer[4] array in this struct. The next chunk of code creates a function for doing this, and is listed below:

/*! Added by Hunter
    Modifies the TIMER0 register of the dma channel
 */
static inline void dma_channel_set_timer0(uint32_t timerval) {
    dma_hw->timer[0] = timerval;
}

This modifies TIMER0 in particular. So, we'll configure the transfer request signal for the DMA channel to be TIMER0 by setting the dreq source to 0x3b, as described in the SDK manual and the text above. Page 108 of the RP2040 datasheet describes the TIMER0 registers as follows:

"Pacing (X/Y) Fractional Timer. The pacing timer produces TREQ assertions at a rate set by ((X/Y) * sys_clk). This equation is evaluated every sys_clk cycles and therefore can only generate TREQs at a rate of 1 per sys_clk (i.e. permanent TREQ) or less."

Note that, by default, the sys_clk for the RP2040 is 125 MHz.

Dropping into main()

Initializing UART

The first line in main() is a call to stdio_init_all(). This function initializes stdio to communicate thru either UART or USB, depending on the configurations in the CMakeLists.txt file.

SPI initialization and configuration

The next two lines are copied below. The first of these is a call to spi_init(), which takes two arguments. The first is the name of the spi channel (which we'd previously defined to spi0, and the second is the baud rate for the channel. Per the DAC datasheet, this is configured to 20MHz. Note that an SPI channel must be initialized before it is configured, which is what happens in the second line of code.

spi_set_format configures the SPI channel to whichever mode the device with which the RP2040 is communicating requires. static void spi_set_format (spi_inst_t *spi, uint data_bits, spi_cpol_t cpol, spi_cpha_t cpha, __unused spi_order_t order) takes 4 arguments. The first is the SPI instance specifier. The second is the number of data bits per transfer. Since the DAC expects 16 bit transfers, this is configured to 16. CPOL and CPHA set the SPI clock polarity and phase (i.e. the "mode"). Finally order is not presently configurable, but sets the endianness of the transfer. Sends MSB first.

// Initialize SPI channel (channel, baud rate set to 20MHz)
    spi_init(SPI_PORT, 20000000) ;
    // Format (channel, data bits per transfer, polarity, phase, order)
    spi_set_format(SPI_PORT, 16, 0, 0, 0);

GPIO mapping

// Map SPI signals to GPIO ports, acts like framed SPI with this CS mapping
    gpio_set_function(PIN_MISO, GPIO_FUNC_SPI);
    gpio_set_function(PIN_CS, GPIO_FUNC_SPI) ;
    gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
    gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);

The next chunk of code is a series of calls to gpio_set_function(). Each of these calls maps a particular GPIO port (#define'd above) to a particular function. This takes two arguments. The first is the GPIO number, and the second specifies the function. The function specifier comes from the enum listed below:

enum gpio_function { GPIO_FUNC_XIP = 0, GPIO_FUNC_SPI = 1, GPIO_FUNC_UART = 2, GPIO_FUNC_I2C = 3, GPIO_FUNC_PWM = 4, GPIO_FUNC_SIO = 5, GPIO_FUNC_PIO0 = 6, GPIO_FUNC_PIO1 = 7, GPIO_FUNC_GPCK = 8, GPIO_FUNC_USB = 9, GPIO_FUNC_NULL = 0xf }

Something to note! We are mapping the chip select line here. If instead we made the chip select line a digital output pin, we would need to toggle it in software before and after each SPI transmission. Configured as shown here, the SPI channel is setup in a way that is very similar to "Framed SPI Mode" for the PIC32." That is, the RP2040 will automatically toggle the chip select line if it is mapped using gpio_set_function.

Building the sine table

// Build sine table
    int i ;
    for (i=0; i<(sine_table_size); i++){
        raw_sin[i] = (int)(2047 * sin((float)i*6.283/(float)sine_table_size) + 2047); //12 bit
        DAC_data[i] = DAC_config_chan_A | (raw_sin[i] & 0x0fff) ;
    }

This chunk of code populates the raw_sin and DAC_data arrays. Note that DAC_data is populated with 12 data bits and the 4 DAC configuration bits.

Obtaining DMA channels

// Get a free channel, panic() if there are none
    int data_chan = dma_claim_unused_channel(true);
    int ctrl_chan = dma_claim_unused_channel(true);

We claim two DMA channels by making calls to dma_claim_unused_channel(true). By making the argument to this call true, the function will panic if no DMA channels are available. Each call will return the name of a channel (an int) which we store in data_chan and ctrl_chan, respectively.

Configuring the control DMA channel

// Setup the control channel
    dma_channel_config c = dma_channel_get_default_config(ctrl_chan); // default configs
    channel_config_set_transfer_data_size(&c, DMA_SIZE_32); // 32-bit txfers
    channel_config_set_read_increment(&c, false); // no read incrementing
    channel_config_set_write_increment(&c, false); // no write incrementing

We declare an object of type dma_channel_config and name that object c. This object is a struct and, initially, the fields of that struct are populated with those provided by the call to dma_channel_get_default_config(ctrl_chan). This function is defined in pico-sdk\src\rp2_common\hardware_dma\include\hardware\dma.h, and copied below:

/*! \brief  Get the default channel configuration for a given channel
 *  \ingroup channel_config
 *
 * Setting | Default
 * --------|--------
 * Read Increment | true
 * Write Increment | false
 * DReq | DREQ_FORCE
 * Chain to | self
 * Data size | DMA_SIZE_32
 * Ring | write=false, size=0 (i.e. off)
 * Byte Swap | false
 * Quiet IRQs | false
 * Channel Enable | true
 * Sniff Enable | false
 *
 * \param channel DMA channel
 * \return the default configuration which can then be modified.
 */
static inline dma_channel_config dma_channel_get_default_config(uint channel) {
    dma_channel_config c = {0};
    channel_config_set_read_increment(&c, true);
    channel_config_set_write_increment(&c, false);
    channel_config_set_dreq(&c, DREQ_FORCE);
    channel_config_set_chain_to(&c, channel);
    channel_config_set_transfer_data_size(&c, DMA_SIZE_32);
    channel_config_set_ring(&c, false, 0);
    channel_config_set_bswap(&c, false);
    channel_config_set_irq_quiet(&c, false);
    channel_config_set_enable(&c, true);
    channel_config_set_sniff_enable(&c, false);
    return c;
}

In the remainder of this code chunk, we make calls to a series of SDK functions to change some of these default channel configurations. Not all of these are strictly necessary (since they configure the channel to default settings), but are included for clarity. In particular, we set the transfer data size to 32 bits and turn off read/write incrementing.

Associating the configured channel with the DMA control channel

dma_channel_configure(
        ctrl_chan,
        &c,
        &dma_hw->ch[data_chan].al1_transfer_count_trig, // txfer to transfer count trigger
        &transfer_count,
        1,
        false
    );

We now have a claimed dma channel (ctrl_chan) and we have a configured channel (c). We need to associate the two, and do the remaining DMA configurations for this channel. We do that with a call to dma_channel_configure. The first argument is the channel. The second is a pointer the the dma config structure. The third is the source address (for this channel, we are writing to the AL1_TRANSFER_COUNT_TRIG register of the other DMA channel). The fourth argument is the source address, which is a pointer to the int which contains the length of the sine table. The fifth argument specifies the number of transfers to execute (1), and the last argument being false means don't start the channel right away.

Verify memory alignment of DAC_data array

// Confirm memory alignment
    printf("\n\nBeginning: %x", &DAC_data[0]);
    printf("\nFirst: %x", &DAC_data[1]);
    printf("\nSecond: %x", &DAC_data[2]);

We print out the memory addresses of the first few elements of the DAC_data array, just to confirm that it is indeed naturally aligned. The output from Putty, pictured below, shows these addresses are aligned.

missing
First few memory addresses of DAC_data array

Configure the data DMA channel

// 16 bit transfers. Read address increments after each transfer.
    // DREQ to Timer 0 is selected, so the DMA is throttled to audio rate
    dma_channel_config c2 = dma_channel_get_default_config(data_chan);
    // 16 bit transfers
    channel_config_set_transfer_data_size(&c2, DMA_SIZE_16);
    // increment the read adddress, don't increment write address
    channel_config_set_read_increment(&c2, true);
    channel_config_set_write_increment(&c2, false);
    // (X/Y)*sys_clk, where X is the first 16 bytes and Y is the second
    // sys_clk is 125 MHz unless changed in code
    dma_channel_set_timer0(0x0017ffff) ;
    // 0x3b means timer0 (see SDK manual)
    channel_config_set_dreq(&c2, 0x3b);
    // chain to the controller DMA channel
    channel_config_set_chain_to(&c2, ctrl_chan);
    // set wrap boundary. This is why we needed alignment!
    channel_config_set_ring(&c2, false, 9); // 1 << 9 byte boundary on read ptr

Similarly to before, we declare an object of type dma_channel_config and name this one c2. We start with the default configurations, and then change some of these configurations with subsequent calls to various channel_config functions from the SDK.

We set the transfer data size to 16 bits, since that's what the DAC expects.

We enable read incrementing, and disable write incrementing. This means that the DMA channel will increment from one read address to the next between subsequent transfers, but it will always write to the same write address.

We call the function that we created to configure the DMA timer 0. In this case, we call it with the value 0x0017ffff. This will configure the timer to overflow at (0x0017/0xffff)*sys_clk Hz, or (23/65535)*sys_clk Hz. With a default sys_clk of 125MHz, this gives us (3.51e-4)*(125MHz) $\approx$ 43,870 Hz.

We set the transfer request signal to Timer 0 by making the second argument of channel_config_set_dreq 0x3b (see the SDK guide).

We chain the control channel to the data channel. By calling channel_config_set_chain_to(&c2, ctrl_chan);, the control dma channel will start automatically when the data channel finishes.

Finally, we call channel_config_set_ring(&c2, false, 9);. The first argument is a pointer to the dma config structure c2. The second argument being false means that we are configuring read addresses (true here would instead configure a wrap boundary for write addresses). And finally, we specify a 1<<9 byte boundary on the read pointer. This means that the read address wraps on a (512-byte) boundary, so that the data channel reads the same array from beginning to end when it is next triggered. Why 512 bytes? The sine table is 256 elements long, each element is a 16-bit short, which is 2 bytes.

Associating the configured channel with the DMA data channel

dma_channel_configure(
        data_chan,          // Channel to be configured
        &c2,            // The configuration we just created
        &spi_get_hw(SPI_PORT)->dr, // write address
        DAC_data, // The initial read address (AT NATURAL ALIGNMENT POINT)
        sine_table_size, // Number of transfers; in this case each is 2 byte.
        false           // Don't start immediately.
    );

Very similar to before. The first argument is the channel to be configured. The second is a pointer to the dma config structure c2. The third is the destination address which, for the data channel, is the SPI channel 0 write buffer. The fourth argument is the initial source address, which points to the beginning of the DAC_data array (remember that we've configured this channel to increment thru read addresses, wrapping at the end of the array). The fifth argument is the number of transfers to execute (the size of the array). The final argument prevents the DMA channel from starting instantly.

Start the control channel

// start the control channel
    dma_start_channel_mask(1u << ctrl_chan) ;

We start the control DMA channel. This will write to a triggered control register of the data channel, starting the first transfer of the the sine table out to the DAC. When the data channel completes, it triggers the control channel again (because we've chained the two). This starts the process over, and the two DMA channels ping-pong off of one another indefinitely. Our program exits main, but these DMA channels continue to operate.

Expected and actual results

We have a 256-element sine table array, and the data channel is configured to send a new sample to the DAC at 43,870 Hz. So, one period of the sine wave will take $\frac{1}{43870} \cdot 256 = 5.835\text{ ms}$. Thus, the frequency of this output wave is $\frac{1}{5.835\times 10^{-3}} = 171.36 Hz.$

The scope trace below shows the output from the DAC. In tiny text at the bottom is a measure of the frequency of the wave, which is 171.4 Hz. We've obtained the expected result.

missing
Scope trace of DAC output

CMakeLists.txt

cmake_minimum_required(VERSION 3.13)

include(pico_sdk_import.cmake)

project(dma-testing-project)

pico_sdk_init()

add_executable(dma-test dma-test.c)

target_link_libraries(dma-test pico_stdlib hardware_dma hardware_spi)

# create map/bin/hex file etc.
pico_add_extra_outputs(dma-test)