diff --git a/CMakeLists.txt b/CMakeLists.txt index f952409..12f2799 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,11 @@ project(pico_serprog) pico_sdk_init() add_executable(pico_serprog) -target_sources(pico_serprog PRIVATE main.c usb_descriptors.c) -target_link_libraries(pico_serprog PRIVATE pico_stdlib hardware_spi tinyusb_device) +pico_generate_pio_header(pico_serprog ${CMAKE_CURRENT_LIST_DIR}/spi.pio) +target_sources(pico_serprog PRIVATE main.c usb_descriptors.c pio_spi.c) +target_link_libraries(pico_serprog PRIVATE pico_stdlib hardware_spi hardware_pio tinyusb_device) pico_add_extra_outputs(pico_serprog) + +# enable usb output, disable uart output +pico_enable_stdio_usb(pico_serprog 1) +pico_enable_stdio_uart(pico_serprog 0) diff --git a/main.c b/main.c index 1a0657c..cfb740d 100644 --- a/main.c +++ b/main.c @@ -9,28 +9,67 @@ * */ +#include +#include +#include "pico/stdlib.h" +#include "pico/binary_info.h" +#include "hardware/clocks.h" #include "pico/stdlib.h" #include "pico/time.h" #include "hardware/spi.h" #include "tusb.h" #include "serprog.h" +#include "pio_spi.h" + + #define CDC_ITF 0 // USB CDC interface no +#define PIN_LED PICO_DEFAULT_LED_PIN #define SPI_IF spi0 // Which PL022 to use -#define SPI_BAUD 4000000 // Default baudrate (4 MHz - SPI default) +#define SPI_BAUD 115200 // Default baudrate (4 MHz - SPI default) #define SPI_CS 5 #define SPI_MISO 4 #define SPI_MOSI 3 #define SPI_SCK 2 -#define MAX_BUFFER_SIZE 1024 -#define MAX_OPBUF_SIZE 1024 -#define SERIAL_BUFFER_SIZE 1024 +#define MAX_BUFFER_SIZE 512 +#define MAX_OPBUF_SIZE 512 +#define SERIAL_BUFFER_SIZE 512 +#define FREQ 1000000 // Define a global operation buffer and a pointer to track the current position uint8_t opbuf[MAX_OPBUF_SIZE]; uint32_t opbuf_pos = 0; +static const pio_spi_inst_t spi = { + .pio = pio0, + .sm = 0, + .cs_pin = SPI_CS +}; +static uint spi_offset; + +static void wait_for_write(void) +{ + do { + tud_task(); + } while (!tud_cdc_n_write_available(CDC_ITF)); +} + +static inline float freq_to_clkdiv(uint32_t freq) { + float div = clock_get_hz(clk_sys) * 1.0 / (freq * pio_spi_cycles_per_bit); + + if (div < 1.0) + div = 1.0; + if (div > 65536.0) + div = 65536.0; + + return div; +} + +static inline uint32_t clkdiv_to_freq(float div) { + return clock_get_hz(clk_sys) / (div * pio_spi_cycles_per_bit); +} + static void enable_spi(uint baud) { // Setup chip select GPIO @@ -38,8 +77,13 @@ static void enable_spi(uint baud) gpio_put(SPI_CS, 1); gpio_set_dir(SPI_CS, GPIO_OUT); - // Setup PL022 + spi_offset = pio_add_program(pio0, &spi_cpha0_program); spi_init(SPI_IF, baud); + float clkdiv = freq_to_clkdiv(FREQ); + pio_spi_init(pio0, 0, pio_add_program(pio0, &spi_cpha0_program), 8, clkdiv, false, false, SPI_SCK, SPI_MOSI, SPI_MISO); + + + // Setup PL022 gpio_set_function(SPI_MISO, GPIO_FUNC_SPI); gpio_set_function(SPI_MOSI, GPIO_FUNC_SPI); gpio_set_function(SPI_SCK, GPIO_FUNC_SPI); @@ -100,29 +144,42 @@ static inline uint8_t readbyte_blocking(void) return b; } -static void wait_for_write(void) -{ - do { - tud_task(); - } while (!tud_cdc_n_write_available(CDC_ITF)); -} static inline void sendbytes_blocking(const void *b, uint32_t len) { while (len) { - // wait_for_write(); + wait_for_write(); uint32_t w = tud_cdc_n_write(CDC_ITF, b, len); b += w; len -= w; } } + +void read_spi_and_send_via_usb(const pio_spi_inst_t *spi, const uint32_t rlen) { + static uint8_t rxbuf[MAX_BUFFER_SIZE]; + memset(rxbuf, 0, MAX_BUFFER_SIZE); // Clear the rx buffer + + // Ensure we send rlen bytes + uint32_t remaining = rlen; + while (remaining) { + uint32_t chunk_size = (remaining < MAX_BUFFER_SIZE) ? remaining : MAX_BUFFER_SIZE; + pio_spi_read8_blocking(spi, rxbuf, chunk_size); + remaining -= chunk_size; + + // Transfer data via USB + sendbytes_blocking(rxbuf, chunk_size); + } +} + + static inline void sendbyte_blocking(uint8_t b) { wait_for_write(); tud_cdc_n_write(CDC_ITF, &b, 1); } + static void command_loop(void) { uint baud = spi_get_baudrate(SPI_IF); @@ -202,46 +259,24 @@ static void command_loop(void) uint8_t tx_buffer[MAX_BUFFER_SIZE]; // Buffer for transmit data uint8_t rx_buffer[MAX_BUFFER_SIZE]; // Buffer for receive data - // Read data to be sent (if slen > 0) - if (slen > 0) { - readbytes_blocking(tx_buffer, slen); - } - - // Perform SPI operation cs_select(SPI_CS); - if (slen > 0) { - spi_write_blocking(SPI_IF, tx_buffer, slen); - } - if (rlen > 0 && rlen < MAX_BUFFER_SIZE ) { - spi_read_blocking(SPI_IF, 0, rx_buffer, rlen); - // Send ACK followed by received data - sendbyte_blocking(S_ACK); - if (rlen > 0) { - sendbytes_blocking(rx_buffer, rlen); - } + fread(tx_buffer, 1, slen, stdin); + pio_spi_write8_blocking(&spi, tx_buffer, slen); - cs_deselect(SPI_CS); - break; - } - - // Send ACK after handling slen (before reading) - sendbyte_blocking(S_ACK); - - // Handle receive operation in chunks for large rlen + putchar(S_ACK); uint32_t chunk; - char buf[128]; for(uint32_t i = 0; i < rlen; i += chunk) { - chunk = MIN(rlen - i, sizeof(buf)); - spi_read_blocking(SPI_IF, 0, buf, chunk); - // Send ACK followed by received data - sendbyte_blocking(S_ACK); - sendbytes_blocking(buf, rlen); + chunk = MIN(rlen - i, sizeof(rx_buffer)); + pio_spi_read8_blocking(&spi, rx_buffer, chunk); + fwrite(rx_buffer, 1, chunk, stdout); + fflush(stdout); } + cs_deselect(SPI_CS); break; } - case S_CMD_S_SPI_FREQ: + case S_CMD_S_SPI_FREQ: { uint32_t want_baud; readbytes_blocking(&want_baud, 4); @@ -342,9 +377,21 @@ static void command_loop(void) int main() { // Setup USB + stdio_init_all(); + + stdio_set_translate_crlf(&stdio_usb, false); + tusb_init(); - // Setup PL022 SPI - enable_spi(SPI_BAUD); + // Initialize CS + gpio_init(SPI_CS); + gpio_put(SPI_CS, 1); + gpio_set_dir(SPI_CS, GPIO_OUT); + + spi_offset = pio_add_program(spi.pio, &spi_cpha0_program); + enable_spi(SPI_BAUD); + + gpio_init(PIN_LED); + gpio_set_dir(PIN_LED, GPIO_OUT); command_loop(); } diff --git a/pio_spi.c b/pio_spi.c new file mode 100644 index 0000000..6306b7d --- /dev/null +++ b/pio_spi.c @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "pio_spi.h" + +// Just 8 bit functions provided here. The PIO program supports any frame size +// 1...32, but the software to do the necessary FIFO shuffling is left as an +// exercise for the reader :) +// +// Likewise we only provide MSB-first here. To do LSB-first, you need to +// - Do shifts when reading from the FIFO, for general case n != 8, 16, 32 +// - Do a narrow read at a one halfword or 3 byte offset for n == 16, 8 +// in order to get the read data correctly justified. + +void __time_critical_func(pio_spi_write8_blocking)(const pio_spi_inst_t *spi, const uint8_t *src, size_t len) { + size_t tx_remain = len, rx_remain = len; + // Do 8 bit accesses on FIFO, so that write data is byte-replicated. This + // gets us the left-justification for free (for MSB-first shift-out) + io_rw_8 *txfifo = (io_rw_8 *) &spi->pio->txf[spi->sm]; + io_rw_8 *rxfifo = (io_rw_8 *) &spi->pio->rxf[spi->sm]; + while (tx_remain || rx_remain) { + if (tx_remain && !pio_sm_is_tx_fifo_full(spi->pio, spi->sm)) { + *txfifo = *src++; + --tx_remain; + } + if (rx_remain && !pio_sm_is_rx_fifo_empty(spi->pio, spi->sm)) { + (void) *rxfifo; + --rx_remain; + } + } +} + +void __time_critical_func(pio_spi_read8_blocking)(const pio_spi_inst_t *spi, uint8_t *dst, size_t len) { + size_t tx_remain = len, rx_remain = len; + io_rw_8 *txfifo = (io_rw_8 *) &spi->pio->txf[spi->sm]; + io_rw_8 *rxfifo = (io_rw_8 *) &spi->pio->rxf[spi->sm]; + while (tx_remain || rx_remain) { + if (tx_remain && !pio_sm_is_tx_fifo_full(spi->pio, spi->sm)) { + *txfifo = 0; + --tx_remain; + } + if (rx_remain && !pio_sm_is_rx_fifo_empty(spi->pio, spi->sm)) { + *dst++ = *rxfifo; + --rx_remain; + } + } +} + +void __time_critical_func(pio_spi_write8_read8_blocking)(const pio_spi_inst_t *spi, uint8_t *src, uint8_t *dst, + size_t len) { + size_t tx_remain = len, rx_remain = len; + io_rw_8 *txfifo = (io_rw_8 *) &spi->pio->txf[spi->sm]; + io_rw_8 *rxfifo = (io_rw_8 *) &spi->pio->rxf[spi->sm]; + while (tx_remain || rx_remain) { + if (tx_remain && !pio_sm_is_tx_fifo_full(spi->pio, spi->sm)) { + *txfifo = *src++; + --tx_remain; + } + if (rx_remain && !pio_sm_is_rx_fifo_empty(spi->pio, spi->sm)) { + *dst++ = *rxfifo; + --rx_remain; + } + } +} + diff --git a/pio_spi.h b/pio_spi.h new file mode 100644 index 0000000..dfa929d --- /dev/null +++ b/pio_spi.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef _PIO_SPI_H +#define _PIO_SPI_H + +#include "hardware/pio.h" +#include "spi.pio.h" + +typedef struct pio_spi_inst { + PIO pio; + uint sm; + uint cs_pin; +} pio_spi_inst_t; + +void pio_spi_write8_blocking(const pio_spi_inst_t *spi, const uint8_t *src, size_t len); + +void pio_spi_read8_blocking(const pio_spi_inst_t *spi, uint8_t *dst, size_t len); + +void pio_spi_write8_read8_blocking(const pio_spi_inst_t *spi, uint8_t *src, uint8_t *dst, size_t len); + +#endif diff --git a/spi.pio b/spi.pio new file mode 100644 index 0000000..84d4be4 --- /dev/null +++ b/spi.pio @@ -0,0 +1,171 @@ +; +; Copyright (c) 2020 Raspberry Pi (Trading) Ltd. +; +; SPDX-License-Identifier: BSD-3-Clause +; + +; These programs implement full-duplex SPI, with a SCK period of 4 clock +; cycles. A different program is provided for each value of CPHA, and CPOL is +; achieved using the hardware GPIO inversion available in the IO controls. +; +; Transmit-only SPI can go twice as fast -- see the ST7789 example! + + +.program spi_cpha0 +.side_set 1 + +; Pin assignments: +; - SCK is side-set pin 0 +; - MOSI is OUT pin 0 +; - MISO is IN pin 0 +; +; Autopush and autopull must be enabled, and the serial frame size is set by +; configuring the push/pull threshold. Shift left/right is fine, but you must +; justify the data yourself. This is done most conveniently for frame sizes of +; 8 or 16 bits by using the narrow store replication and narrow load byte +; picking behaviour of RP2040's IO fabric. + +; Clock phase = 0: data is captured on the leading edge of each SCK pulse, and +; transitions on the trailing edge, or some time before the first leading edge. + + out pins, 1 side 0 [1] ; Stall here on empty (sideset proceeds even if + in pins, 1 side 1 [1] ; instruction stalls, so we stall with SCK low) + +.program spi_cpha1 +.side_set 1 + +; Clock phase = 1: data transitions on the leading edge of each SCK pulse, and +; is captured on the trailing edge. + + out x, 1 side 0 ; Stall here on empty (keep SCK deasserted) + mov pins, x side 1 [1] ; Output data, assert SCK (mov pins uses OUT mapping) + in pins, 1 side 0 ; Input data, deassert SCK + +% c-sdk { +#include "hardware/gpio.h" +static inline void pio_spi_init(PIO pio, uint sm, uint prog_offs, uint n_bits, + float clkdiv, bool cpha, bool cpol, uint pin_sck, uint pin_mosi, uint pin_miso) { + pio_sm_config c = cpha ? spi_cpha1_program_get_default_config(prog_offs) : spi_cpha0_program_get_default_config(prog_offs); + sm_config_set_out_pins(&c, pin_mosi, 1); + sm_config_set_in_pins(&c, pin_miso); + sm_config_set_sideset_pins(&c, pin_sck); + // Only support MSB-first in this example code (shift to left, auto push/pull, threshold=nbits) + sm_config_set_out_shift(&c, false, true, n_bits); + sm_config_set_in_shift(&c, false, true, n_bits); + sm_config_set_clkdiv(&c, clkdiv); + + // MOSI, SCK output are low, MISO is input + pio_sm_set_pins_with_mask(pio, sm, 0, (1u << pin_sck) | (1u << pin_mosi)); + pio_sm_set_pindirs_with_mask(pio, sm, (1u << pin_sck) | (1u << pin_mosi), (1u << pin_sck) | (1u << pin_mosi) | (1u << pin_miso)); + pio_gpio_init(pio, pin_mosi); + pio_gpio_init(pio, pin_miso); + pio_gpio_init(pio, pin_sck); + + // The pin muxes can be configured to invert the output (among other things + // and this is a cheesy way to get CPOL=1 + gpio_set_outover(pin_sck, cpol ? GPIO_OVERRIDE_INVERT : GPIO_OVERRIDE_NORMAL); + // SPI is synchronous, so bypass input synchroniser to reduce input delay. + hw_set_bits(&pio->input_sync_bypass, 1u << pin_miso); + + pio_sm_init(pio, sm, prog_offs, &c); + pio_sm_set_enabled(pio, sm, true); +} +%} + +; SPI with Chip Select +; ----------------------------------------------------------------------------- +; +; For your amusement, here are some SPI programs with an automatic chip select +; (asserted once data appears in TX FIFO, deasserts when FIFO bottoms out, has +; a nice front/back porch). +; +; The number of bits per FIFO entry is configured via the Y register +; and the autopush/pull threshold. From 2 to 32 bits. +; +; 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) +; +; This program only supports one chip select -- use GPIO if more are needed +; +; Provide a variation for each possibility of CPHA; for CPOL we can just +; invert SCK in the IO muxing controls (downstream from PIO) + + +; CPHA=0: data is captured on the leading edge of each SCK pulse (including +; the first pulse), and transitions on the trailing edge + +.program spi_cpha0_cs +.side_set 2 + +.wrap_target +bitloop: + out pins, 1 side 0x0 [1] + in pins, 1 side 0x1 + jmp x-- bitloop side 0x1 + + out pins, 1 side 0x0 + mov x, y side 0x0 ; Reload bit counter from Y + in pins, 1 side 0x1 + jmp !osre bitloop side 0x1 ; 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 + +; CPHA=1: data transitions on the leading edge of each SCK pulse, and is +; captured on the trailing edge + +.program spi_cpha1_cs +.side_set 2 + +.wrap_target +bitloop: + out pins, 1 side 0x1 [1] + in pins, 1 side 0x0 + jmp x-- bitloop side 0x0 + + out pins, 1 side 0x1 + mov x, y side 0x1 + in pins, 1 side 0x0 + jmp !osre bitloop side 0x0 + +public entry_point: ; Must set X,Y to n-2 before starting! + pull ifempty side 0x2 [1] ; Block with CSn high (minimum 2 cycles) + nop side 0x0 [1]; CSn front porch +.wrap + +% c-sdk { +#include "hardware/gpio.h" + +static const int pio_spi_cycles_per_bit = 4; + +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, uint pin_miso) { + pio_sm_config c = cpha ? spi_cpha1_cs_program_get_default_config(prog_offs) : spi_cpha0_cs_program_get_default_config(prog_offs); + sm_config_set_out_pins(&c, pin_mosi, 1); + sm_config_set_in_pins(&c, pin_miso); + sm_config_set_sideset_pins(&c, pin_sck); + sm_config_set_out_shift(&c, false, true, n_bits); + sm_config_set_in_shift(&c, false, true, n_bits); + sm_config_set_clkdiv(&c, clkdiv); + + pio_sm_set_pins_with_mask(pio, sm, (2u << pin_sck), (3u << pin_sck) | (1u << pin_mosi)); + pio_sm_set_pindirs_with_mask(pio, sm, (3u << pin_sck) | (1u << pin_mosi), (3u << pin_sck) | (1u << pin_mosi) | (1u << pin_miso)); + pio_gpio_init(pio, pin_mosi); + pio_gpio_init(pio, pin_miso); + pio_gpio_init(pio, pin_sck); + pio_gpio_init(pio, pin_sck + 1); + gpio_set_outover(pin_sck, cpol ? GPIO_OVERRIDE_INVERT : GPIO_OVERRIDE_NORMAL); + hw_set_bits(&pio->input_sync_bypass, 1u << pin_miso); + + uint entry_point = prog_offs + (cpha ? spi_cpha1_cs_offset_entry_point : spi_cpha0_cs_offset_entry_point); + pio_sm_init(pio, sm, entry_point, &c); + pio_sm_exec(pio, sm, pio_encode_set(pio_x, n_bits - 2)); + pio_sm_exec(pio, sm, pio_encode_set(pio_y, n_bits - 2)); + pio_sm_set_enabled(pio, sm, true); +} +%} \ No newline at end of file diff --git a/spi_loopback.c b/spi_loopback.c new file mode 100644 index 0000000..ac5897f --- /dev/null +++ b/spi_loopback.c @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include + +#include "pico/stdlib.h" +#include "pio_spi.h" + +// This program instantiates a PIO SPI with each of the four possible +// CPOL/CPHA combinations, with the serial input and output pin mapped to the +// same GPIO. Any data written into the state machine's TX FIFO should then be +// serialised, deserialised, and reappear in the state machine's RX FIFO. + +#define PIN_SCK 18 +#define PIN_MOSI 16 +#define PIN_MISO 16 // same as MOSI, so we get loopback + +#define BUF_SIZE 20 + +void test(const pio_spi_inst_t *spi) { + static uint8_t txbuf[BUF_SIZE]; + static uint8_t rxbuf[BUF_SIZE]; + printf("TX:"); + for (int i = 0; i < BUF_SIZE; ++i) { + txbuf[i] = rand() >> 16; + rxbuf[i] = 0; + printf(" %02x", (int) txbuf[i]); + } + printf("\n"); + + pio_spi_write8_read8_blocking(spi, txbuf, rxbuf, BUF_SIZE); + + printf("RX:"); + bool mismatch = false; + for (int i = 0; i < BUF_SIZE; ++i) { + printf(" %02x", (int) rxbuf[i]); + mismatch = mismatch || rxbuf[i] != txbuf[i]; + } + if (mismatch) + printf("\nNope\n"); + else + printf("\nOK\n"); +} + +int main() { + stdio_init_all(); + + pio_spi_inst_t spi = { + .pio = pio0, + .sm = 0 + }; + float clkdiv = 31.25f; // 1 MHz @ 125 clk_sys + uint cpha0_prog_offs = pio_add_program(spi.pio, &spi_cpha0_program); + uint cpha1_prog_offs = pio_add_program(spi.pio, &spi_cpha1_program); + + for (int cpha = 0; cpha <= 1; ++cpha) { + for (int cpol = 0; cpol <= 1; ++cpol) { + printf("CPHA = %d, CPOL = %d\n", cpha, cpol); + pio_spi_init(spi.pio, spi.sm, + cpha ? cpha1_prog_offs : cpha0_prog_offs, + 8, // 8 bits per SPI frame + clkdiv, + cpha, + cpol, + PIN_SCK, + PIN_MOSI, + PIN_MISO + ); + test(&spi); + sleep_ms(10); + } + } +}