This program was an implementation of the SPI communication protocol using PIO on the Raspberry Pi Pico. RP2040 has two identical SPI controllers, both based on an ARM Primecell Synchronous Serial Port (SSP). I used an MCP4822 DAC to generate a sine wave of a given frequency. The Pico transmits data to the DAC using SPI. The resources for the project include the C SDK User Guide, the RP2040 Datasheet and Prof. Hunter's website.
#include <stdio.h> //The standard C library
#include "pico/stdlib.h" //Standard library for Pico
#include <math.h> //The standard math library
#include "hardware/gpio.h" //The hardware GPIO library
#include "hardware/adc.h" //The hardware ADC library
#include "pico/time.h" //The pico time library
#include "hardware/irq.h" //The hardware interrupt library
#include "hardware/pwm.h" //The hardware PWM library
#include "hardware/pio.h" //The hardware PIO library
#include "PIODDS.pio.h" //The pio header created after compilation
#define CS 18 //The chip-select pin
#define MOSI 19 //The MOSI pin
#define SCK 17 //The clock pin
#define SIZE 256 //The sine table size
#define Fs 44000 //The sampling frequency
#define two32divFs 97612.8930909 //(4294967296 / 44000)
#define DAC_config_chan_A 0b0011000000000000 //The DAC configuration bits
uint32_t phaseAccum = 0, phaseInc = 0; //The phase accumulator and the phase incrementer
uint16_t adcIn = 0, dacData; //The ADC input and data to be sent to DAC
int sinTable[SIZE]; //The sin table
PIO pio = pio0; //Identifier for the first (PIO 0) hardware PIO instance
uint sm = 0; //The state machine
typedef struct pio_spi_inst{ //PIO SPI struct
PIO pio; //The PIO
uint sm; //State machine
uint cs_pin; //Chip select
} pio_spi_inst_t;
void pio_spi_write16_blocking(const pio_spi_inst_t *spi, const uint16_t *src, size_t len); //Function prototype to write 16 bits to the SPI channel
void __time_critical_func(pio_spi_write16_blocking)(const pio_spi_inst_t *spi, const uint16_t *src, size_t len){ //Decorates a function name, such that the function will execute from RAM
size_t tx_remain = len; //The length of transmission
io_rw_16 *txfifo = (io_rw_16 *) &spi->pio->txf[spi->sm]; //Address to the transmit buffer
while (tx_remain) { //As long as there is data to transmit
if (tx_remain && !pio_sm_is_tx_fifo_full(spi->pio, spi->sm)) { //If FIFO not full
*txfifo = *src++; //Copy the number in the FIFO and increment the pointer
--tx_remain; //Reduce the remaining transmit length
}
}
}
pio_spi_inst_t spi = { //The SPI instance
.pio = pio0,
.sm = 0,
.cs_pin = CS
};
static bool repeating_timer_callback(struct repeating_timer *t) { //Callback for a repeating timer
adcIn = adc_read(); //Read the ADC value
phaseInc = adcIn * two32divFs; //Calculate the phase increment
phaseAccum += phaseInc; //Add the incrementer to the accumulator
dacData = sinTable[phaseAccum >> 24]; //We only care about the 8 most significant bits
dacData = DAC_config_chan_A | (dacData & 0x0fff); //Configure DAC data using bitmasks on the data
pio_spi_write16_blocking(&spi, &dacData, 1); //Send the data to the SPI channel
return true;
}
int main() {//The program running on core 0
stdio_init_all(); //Initialize all of the present standard stdio types that are linked into the binary
struct repeating_timer timer; //The repeating timer object for interrupt
int i; //The counter i
for(i = 0; i < SIZE; i++){ //For the sine table size
sinTable[i] = (int)(2047 * sin((float) i * 6.283 / (float) SIZE) + 2047); //Create the sine table
}
adc_init(); //Initialise the ADC hardware
adc_gpio_init(26); //Initialise the gpio for use as an ADC pin
adc_select_input(0); //Select an ADC input. 0...3 are GPIOs 26...29 respectively
gpio_init(CS); //Initialize the CS pin as GPIO
gpio_set_dir(CS, GPIO_OUT); //Set the pin direction as output
gpio_put(CS, 1); //Drive CS high
uint offset = pio_add_program(spi.pio, &spi_cpha0_cs_program); //Attempt to load the program
pio_spi_cs_init(spi.pio, spi.sm, offset, 16, 15.625f, false, false, SCK, MOSI); //Initialize the SPI program
add_repeating_timer_us(-23, repeating_timer_callback, NULL, &timer); //Add a repeating timer that is called repeatedly at the specified interval in microseconds. Negative time means it is the time difference between the start times of 2 consecutive callbacks
while(1){
}
}
assembly
.program spi_cpha0_cs ;Program name
.side_set 2 ;Set 2 pins for sideset
;Drive SPI
; Pin assignments:
; - SCK is side-set bit 0
; - CSn is side-set bit 1
; - MOSI is OUT bit 0 (host-to-device)
; - MISO is IN bit 0 (device-to-host)
.wrap_target ;Free 0 cycle unconditional jump
bitloop: ;Bitloop label
out pins, 1 side 0x0 [1] ;Output the bit on pin, sideset the clock
jmp x-- bitloop side 0x1 [1] ;Jump to bitloop if bit counter still available
out pins, 1 side 0x0 ;Output the bit on pin, sideset the clock
mov x, y side 0x0 ; Reload bit counter from Y
jmp !osre bitloop side 0x1 [1] ; Fall-through if TXF empties
nop side 0x0 [1] ; CSn back porch
public entry_point: ; Must set X,Y to n-2 before starting!
pull ifempty side 0x2 [1] ; Block with CSn high (minimum 2 cycles)
.wrap ; Note ifempty to avoid time-of-check race
;Helper function
% c-sdk {
#include "hardware/gpio.h" //The hardware GPIO library
static inline void pio_spi_cs_init(PIO pio, uint sm, uint prog_offs, uint n_bits, float clkdiv, bool cpha, bool cpol, uint pin_sck, uint pin_mosi){ //The PIO SPI initialize functions
pio_sm_config c = spi_cpha0_cs_program_get_default_config(prog_offs); //Get default configurations for the PIO state machine
sm_config_set_out_pins(&c, pin_mosi, 1); //Set the 'out' pins in a state machine configuration
sm_config_set_sideset_pins(&c, pin_sck); //Set the 'sideset' pins in a state machine configuration
sm_config_set_out_shift(&c, false, true, n_bits); //Setup 'out' shifting parameters in a state machine configuration
sm_config_set_clkdiv(&c, clkdiv); //Set the state machine clock divider
pio_sm_set_pins_with_mask(pio, sm, (2u << pin_sck), (3u << pin_sck) | (1u << pin_mosi)); //Use a state machine to set a value on multiple pins for the PIO instance
pio_sm_set_pindirs_with_mask(pio, sm, (3u << pin_sck) | (1u << pin_mosi), (3u << pin_sck) | (1u << pin_mosi)); //Use a state machine to set the pin directions for multiple pins for the PIO instance
pio_gpio_init(pio, pin_mosi); //Setup the function select for a GPIO to use output from the given PIO instance
pio_gpio_init(pio, pin_sck); //Setup the function select for a GPIO to use output from the given PIO instance
pio_gpio_init(pio, pin_sck + 1); //Setup the function select for a GPIO to use output from the given PIO instance
gpio_set_outover(pin_sck, cpol ? GPIO_OVERRIDE_INVERT : GPIO_OVERRIDE_NORMAL); //Set GPIO output override
uint entry_point = prog_offs + spi_cpha0_cs_offset_entry_point; //The offset entry point
pio_sm_init(pio, sm, entry_point, &c); //Resets the state machine to a consistent state, and configures it
pio_sm_exec(pio, sm, pio_encode_set(pio_x, n_bits - 2)); //Put 14 in pio_x
pio_sm_exec(pio, sm, pio_encode_set(pio_y, n_bits - 2)); //Put 14 in pio_y
pio_sm_set_enabled(pio, sm, true); //Enable or disable a PIO state machine
}
%}
The observation upon which the direct digital synthesis algorithm is based is that a variable overflowing is isomorphic to one rotation of a phasor. A sine wave is generated by projecting a rotating phasor onto the imaginary axis, as shown below. We see the rotating phasor and it's associated angle in red, the projection onto the imaginary axis in green, and the generated sine wave streaming off in orange. Note that, after the phasor has rotated 360 degrees, we have completed one period of the sine wave and the whole thing repeats.
Now, suppose that we represented the angle of that phasor, from the positive x-axis, with a 32-bit number that we'll call the accumulator. A scaled version of the phase angle will be stored in the accumulator. An angle of 0 degrees from the positive x-axis will correspond to an accumulator value of 0 (a 0 in each of the 32 bits). An angle of 360 degrees from the positive x-axis will correspond to an accumulator value of $2^{32}−1$ (a 1 in each of the 32 bits). Then, overflowing the accumulator corresponds to completing a full rotation of the phasor.
We'll see why this is useful in the next section. For now, all that we've done is scaled the range of phasor angles from 0→2π radians to instead 0→$2^{32}−1$ units, and stored that rescaled value in a 32-bit variable that we are calling accumulator.
anim