summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke T. Shumaker <lukeshu@lukeshu.com>2024-10-13 21:22:45 -0600
committerLuke T. Shumaker <lukeshu@lukeshu.com>2024-10-13 21:22:45 -0600
commit7efdc721db9220642778a1183ec24ee2762b8ee8 (patch)
treeb8ce3842da4d256710f15a98929452b576b6dd5a
parenta249fae4d0757e305a5a27758f1d1dfb0df6eda9 (diff)
wip w5500
-rw-r--r--cmd/sbc_harness/hw/rp2040_hwspi.c115
-rw-r--r--cmd/sbc_harness/hw/rp2040_hwspi.h50
-rw-r--r--cmd/sbc_harness/hw/spi.h44
-rw-r--r--libnetio/w5500_spiframe.c144
-rw-r--r--notes.md56
5 files changed, 409 insertions, 0 deletions
diff --git a/cmd/sbc_harness/hw/rp2040_hwspi.c b/cmd/sbc_harness/hw/rp2040_hwspi.c
new file mode 100644
index 0000000..2f0bec4
--- /dev/null
+++ b/cmd/sbc_harness/hw/rp2040_hwspi.c
@@ -0,0 +1,115 @@
+/* rp2040_hwspi.c - `struct spi` implementation for the RP2040's ARM Primecell SSP (PL022)
+ */
+
+#include <assert.h>
+
+#include <hardware/spi.h> /* pico-sdk:hardware_spi */
+
+#include "hw/rp2040_hwspi.h"
+
+static void rp2040_hwspi_readwritev(struct spi *_self, const struct bidi_iovec *iov, int iovcnt);
+
+struct spi_vtable rp2040_hwspi_vtable = {
+ .readwritev = rp2040_hwspi_readwritev,
+};
+
+static inline void _rp2040_hwspi_init(struct rp2040_hwspi *self,
+ enum rp2040_hwspi_instance inst_num,
+ uint baudrate_hz;
+ uint pin_miso,
+ uint pin_mosi,
+ uint pin_clk,
+ uint pin_cs) {
+ /* Be not weary: This is but 12 lines of actual code; and many
+ * lines of comments and assert()s. */
+ spi_inst_t *inst;
+
+ assert(self);
+ assert(baudrate_hz);
+ assert(pin_miso != pin_mosi);
+ assert(pin_miso != pin_clk);
+ assert(pin_miso != pin_cs);
+ assert(pin_mosi != pin_clk);
+ assert(pin_mosi != pin_cs);
+ assert(pin_clk != pin_cs);
+
+ /* I know we called this "hwspi", but we're actually going to
+ * disconnect the CS pin from the PL022 SSP and manually drive
+ * it from software. This is because the PL022 has a maximum
+ * of 16-bit frames, while we need to be able to do *at least*
+ * 32-bit frames (and ideally, much larger). By managing it
+ * ourselves, we can just keep CS pulled low extra-long,
+ * making the frame extra-long. */
+
+ /* Regarding the constraints on pin assignments: see the
+ * RP2040 datasheet, table 2, in §1.4.3 "GPIO Functions". */
+ switch (inst_num) {
+ case RP2040_HWSPI_0:
+ inst = spi0;
+ assert(pin_miso == 0 || pin_miso == 4 || pin_miso == 16 || pin_miso == 20);
+ /*assert(pin_cs == 1 || pin_cs == 5 || pin_cs == 17 || pin_cs == 21);*/
+ assert(pin_clk == 2 || pin_clk == 6 || pin_clk == 18 || pin_clk == 22);
+ assert(pin_mosi == 3 || pin_mosi == 7 || pin_mosi == 19 || pin_mosi == 23);
+ break;
+ case RP2040_HWSPI_1:
+ inst = spi1;
+ assert(pin_miso == 8 || pin_miso == 12 || pin_miso == 24 || pin_miso == 28);
+ /*assert(pin_cs == 9 || pin_cs == 13 || pin_cs == 25 || pin_cs == 29);*/
+ assert(pin_clk == 10 || pin_clk == 14 || pin_clk == 26);
+ assert(pin_mosi == 11 || pin_mosi == 15 || pin_mosi == 27);
+ break;
+ default:
+ assert(false);
+ }
+
+ spi_init(inst, baudrate_hz);
+ spi_set_format(inst, 8,
+ (mode & 0b10) ? SPI_CPOL_1 : SPI_CPOL_0,
+ (mode & 0b01) ? SPI_CPHA_1 : SPI_CPHA_0,
+ SPI_MSB_FIRST);
+
+ /* Connect the pins to the PL022; set them each to "function
+ * 1" (again, see the RP2040 datasheet, table 2, in §1.4.3
+ * "GPIO Functions").
+ *
+ * ("GPIO_FUNC_SPI" is how the pico-sdk spells "function 1",
+ * since on the RP2040 all of the "function 1" functions are
+ * some part of SPI.) */
+ gpio_set_function(pin_clk, GPIO_FUNC_SPI);
+ gpio_set_function(pin_mosi, GPIO_FUNC_SPI);
+ gpio_set_function(pin_miso, GPIO_FUNC_SPI);
+
+ /* Initialize the CS pin for software control. */
+ gpio_init(pin_cs);
+ gpio_set_dir(pin_cs, GPIO_OUT);
+ gpio_put(pin_cs, 1);
+
+ /* Return. */
+ self->vtable = &rp2040_hwspi_vtable;
+ self->inst = inst;
+ self->pin_cs = pin_cs;
+}
+
+static void rp2040_hwspi_readwritev(struct spi *_self, const struct bidi_iovec *iov, int iovcnt); {
+ struct rp2040_hwspi* self = _self;
+ assert(self);
+ spi_inst_t *inst = self->inst;
+
+ assert(inst);
+ assert(iov);
+ assert(iovcnt);
+
+ gpio_put(self->pin_cs, 0);
+ /* TODO: Replace blocking reads+writes with DMA. */
+ for (int i = 0; i < iovcnt; i++) {
+ if (iov[i].iov_write_src && iov[i].iov_read_dst)
+ spi_write_read_blocking(inst, iov[i].iov_write_src, iov[i].iov_read_dst, iov[i].iov_len);
+ else if (iov[i].iov_write_src)
+ spi_write_blocking(inst, iov[i].iov_write_src, iov[i].iov_len);
+ else if (iov[i].iov_read_dst)
+ spi_read_blocking(inst, iov[i].iov_read_dst, iov[i].iov_len);
+ else
+ assert(false);
+ }
+ gpio_put(self->pin_cs, 1);
+}
diff --git a/cmd/sbc_harness/hw/rp2040_hwspi.h b/cmd/sbc_harness/hw/rp2040_hwspi.h
new file mode 100644
index 0000000..9f4a551
--- /dev/null
+++ b/cmd/sbc_harness/hw/rp2040_hwspi.h
@@ -0,0 +1,50 @@
+#ifndef _RP2040_HWSPI_H_
+#define _RP2040_HWSPI_H_
+
+#include "hw/spi.h"
+
+enum rp2040_hwspi_instance {
+ RP2040_HWSPI_0 = 0,
+ RP2040_HWSPI_1 = 1,
+};
+
+struct rp2040_hwspi {
+ struct spi_vtable *vtable;
+
+ void /*spi_inst_t*/ *inst;
+ uint pin_cs;
+};
+
+/**
+ * Initialize an instance of `struct rp2040_hwspi`.
+ *
+ * @param self : struct rp2040_hwspi : the structure to initialize
+ * @param name : char * : a name for the SPI port; to include in the bininfo
+ * @param inst_num : enum rp2040_hwspi_instance : the PL220 instance number; RP2040_HWSPI_{0,1}
+ * @param mode : enum spi_mode : the SPI mode; SPI_MODE_{0..3}
+ * @param pin_miso : uint : pin number; 0, 4, 16, or 20 for _HWSPI_0; 8, 12, 24, or 28 for _HWSPI_1
+ * @param pin_mosi : uint : pin number; 3, 7, 19, or 23 for _HWSPI_0; 11, 15, or 27 for _HWSPI_1
+ * @param pin_clk : uint : pin number; 2, 6, 18, or 22 for _HWSPI_0; 10, 14, or 26 for _HWSPI_1
+ * @param pin_cs : uint : pin number; any unused GPIO pin
+ *
+ * There is no bit-order argument; the RP2040's hardware SPI always
+ * uses MSB-first bit order.
+ */
+#define rp2040_hwspi_init(self, name, inst_num, mode, baudrate_hz, pin_miso, pin_mosi, pin_clk, pin_cs) do { \
+ bi_decl(bi_4_pins_with_names(pin_miso, name" SPI MISO", \
+ pin_mosi, name" SPI MOSI", \
+ pin_mosi, name" SPI CLK", \
+ pin_mosi, name" SPI CS")); \
+ _rp2040_hwspi_init(self, inst_num, mode, baudrate_hz, pin_miso, pin_mosi, pin_c;k, pin_cs); \
+ } while(0)
+
+void _rp2040_hwspi_init(struct rp2040_hwspi *self,
+ enum rp2040_hwspi_instance inst_num,
+ enum spi_mode mode,
+ uint baudrate_hz;
+ uint pin_miso,
+ uint pin_mosi,
+ uint pin_clk,
+ uint pin_cs);
+
+#endif /* _RP2040_HWSPI_H_ */
diff --git a/cmd/sbc_harness/hw/spi.h b/cmd/sbc_harness/hw/spi.h
new file mode 100644
index 0000000..4a160ca
--- /dev/null
+++ b/cmd/sbc_harness/hw/spi.h
@@ -0,0 +1,44 @@
+#ifndef _HW_SPI_H_
+#define _HW_SPI_H_
+
+#include <stddef.h> /* for size_t */
+
+enum spi_mode {
+ SPI_MODE_0 = 0, /* clk_polarity=0 (idle low), clk_phase=0 (sample on rise) */
+ SPI_MODE_1 = 1, /* clk_polarity=0 (idle low), clk_phase=1 (sample on fall) */
+ SPI_MODE_2 = 2, /* clk_polarity=1 (idle high), clk_phase=0 (sample on rise) */
+ SPI_MODE_3 = 3, /* clk_polarity=1 (idle high), clk_phase=1 (sample on fall) */
+};
+
+struct bidi_iovec {
+ void *iov_read_dst;
+ void *iov_write_src;
+ size_t iov_len;
+};
+
+struct spi;
+
+/* This API assumes that an SPI frame is a multiple of 8-bits.
+ *
+ * It is my understanding that this is a common constraint of SPI
+ * hardware, and that the RP2040 is somewhat unusual in that it allows
+ * frames of any length 4-16 bits (we disconnect the CS pin from the
+ * PL022 SSP and manually GPIO it from the CPU in order to achieve
+ * longer frames).
+ *
+ * But, more relevantly: The W5500's protocol uses frames that are 4-N
+ * octets; so we have no need for an API that allows a
+ * non-multiple-of-8 number of bits.
+ */
+struct spi_vtable {
+ void (*readwritev)(struct spi *, const struct bidi_iovec *iov, int iovcnt)
+};
+
+struct spi {
+ struct spi_vtable *vtable;
+
+ /* This is where your implementation data goes. */
+ char data[0];
+};
+
+#endif /* _HW_SPI_H_ */
diff --git a/libnetio/w5500_spiframe.c b/libnetio/w5500_spiframe.c
new file mode 100644
index 0000000..d300999
--- /dev/null
+++ b/libnetio/w5500_spiframe.c
@@ -0,0 +1,144 @@
+/* A u8 control byte has 3 parts: block-ID, R/W, and
+ * operating-mode. */
+
+/* Part 1: Block ID. */
+#define CTL_MASK_BLOCK 0b11111'000
+#define _CTL_BLOCK_RES (0b00'000)
+#define _CTL_BLOCK_REG (0b01'000)
+#define _CTL_BLOCK_TX (0b10'000)
+#define _CTL_BLOCK_RX (0b11'000)
+#define CTL_BLOCK_SOCK(n,part) (((n)<<5)|(_CTL_BLOCK_##part))
+#define CTL_BLOCK_COMMON_REG CTL_BLOCK_SOCK(0,RES)
+
+/* Part 2: R/W. */
+#define CTL_MASK_RW 0b1'00
+#define CTL_R 0b0'00
+#define CTL_W 0b1'00
+
+/* Part 3: Operating mode. */
+#define CTL_MASK_OM 0b11
+#define CTL_OM_VDM 0b00
+#define CTL_OM_FDM1 0b01
+#define CTL_OM_FDM2 0b10
+#define CTL_OM_FDM4 0b11
+
+/* The W5500 has 2 channels of communication with the MCU:
+ *
+ * - An SPI-based RPC protocol:
+ * + mode: mode 0 or mode 3
+ * + bit-order: MSB-first
+ * + clock frequency: 33.3MHz - 80MHz
+ * - An interrupt pin that it pulls low when an event happens (to let
+ * the MCU know that it should do an SPI RPC "get" to see what
+ * happened.)
+ *
+ * Even though SPI is a full-duplex protocol, the W5500's RPC protocol
+ * on top of it is only half-duplex. Lame.
+ */
+
+void w5500_spiframe_write(struct spi *spidev, uint16_t addr, uint8_t block, void *data, size_t data_len) {
+ assert(spidev);
+ assert((block & ~CTL_MASK_BLOCK) == 0);
+ assert(data);
+ assert(data_len);
+
+ uint8_t header[3] = {
+ (uint8_t)((addr >> 8) & 0xFF),
+ (uint8_t)(addr & 0xFF),
+ (block & CTL_MASK_BLOCK) | CTL_W | CTL_OM_VDM,
+ };
+ struct bidi_iovec iov[] = {
+ {.iov_read_dst = NULL, .iov_write_src = header, .iov_len = sizeof(header)},
+ {.iov_read_dst = NULL, .iov_write_src = data, .iov_len = data_len},
+ };
+ spidev->vtable->readwritev(spidev, iov, 2);
+}
+
+void w5500_spiframe_read(uint16_t addr, uint8_t ctl, void *data, size_t data_len) {
+ assert(spidev);
+ assert((block & ~CTL_MASK_BLOCK) == 0);
+ assert(data);
+ assert(data_len);
+
+ uint8_t header[3] = {
+ (uint8_t)((addr >> 8) & 0xFF),
+ (uint8_t)(addr & 0xFF),
+ (block & CTL_MASK_BLOCK) | CTL_R | CTL_OM_VDM,
+ };
+ struct bidi_iovec iov[] = {
+ {.iov_read_dst = NULL, .iov_write_src = header, .iov_len = sizeof(header)},
+ {.iov_read_dst = data, .iov_write_src = NULL, .iov_len = data_len},
+ };
+ spidev->vtable->readwritev(spidev, iov, 2);
+}
+
+struct w5500_block_common_reg {
+ uint8_t mode; /* MR */
+ uint8_t ip_gateway_addr[4]; /* GAR0 ... GAR3 */
+ uint8_t ip_subnet_mask[4]; /* SUBR0 ... SUBR3 */
+ uint8_t eth_addr[6]; /* SHAR0 ... SHAR5 */
+ uint8_t ip_addr[4]; /* SIPR0 ... SIPR3 */
+
+ uint8_t intlevel_0; /* INTLEVEL0 */
+ uint8_t intlevel_1; /* INTLEVEL1 */
+ uint8_t interrupt; /* IR */
+ uint8_t interrupt_mask; /* IMR */
+ uint8_t sock_interrupt; /* SIR */
+ uint8_t sock_interrupt_mask; /* SIMR */
+ uint8_t retry_time_0; /* RTR0 */
+ uint8_t retry_time_1; /* RTR0 */
+ uint8_t retry_count; /* RCR */
+
+ uint8_t ppp_lcp_request_timer; /* PTIMER */
+ uint8_t ppp_lcp_magic_bumber; /* PMAGIC */
+ uint8_t ppp_dst_eth_addr[6]; /* PHAR0 ... PHAR5 */
+ uint8_t ppp_sess_id[2]; /* PSID0 ... PSID1 */
+ uint8_t ppp_max_seg_size[2]; /* PMRU0 ... PMRU1 */
+
+ uint8_t unreachable_ip_addr[4]; /* UIPR0 ... UIPR3 */
+ uint8_t unreachable_port[2]; /* UPORTR0, UPROTR1 */
+
+ uint8_t phy_cfg; /* PHYCFGR */
+
+ uint8_t _reserved[10];
+
+ uint8_t chip_version; /* VERSIONR */
+};
+static_assert(sizeof(struct w5500_block_common_reg) == 0x3A);
+
+struct w5500_block_sock_reg {
+ uint8_t mode; /* Sn_MR */
+ uint8_t command; /* Sn_CR */
+ uint8_t interrupt; /* Sn_IR */
+ uint8_t status; /* Sn_SR */
+ uint8_t src_port[2]; /* Sn_PORT0, Sn_PORT1 */
+ uint8_t dst_eth_addr[6]; /* Sn_DHAR0 ... SnDHAR5 */
+ uint8_t dst_ip_addr[4]; /* Sn_DIPR0 ... Sn_DIP3 */
+ uint8_t dst_port[2]; /* Sn_DPORT0 ... Sn_DPORT1 */
+
+ uint8_t max_seg_size[2]; /* Sn_MSSR0, Sn_MSSR1 */
+ uint8_t _reserved0[1];
+ uint8_t ip_tos; /* Sn_TOS */
+ uint8_t ip_ttl; /* Sn_TTL */
+ uint8_t _reserved1[7];
+
+ uint8_t rx_buf_size; /* Sn_RXBUF_SIZE */
+ uint8_t tx_buf_size; /* Sn_TXBUF_SIZE */
+ uint8_t tx_free_size[2]; /* Sn_TX_FSR0, Sn_TX_FSR1 */
+ uint8_t tx_read_pointer[2]; /* Sn_TX_RD0, Sn_TX_RD1 */
+ uint8_t tx_write_pointer[2]; /* Sn_TX_WR0, Sn_TX_WR1 */
+ uint8_t rx_size[2]; /* Sn_RX_RSR0, Sn_RX_RSR1 */
+ uint8_t rx_read_pointer[2]; /* Sn_RX_RD0, Sn_RX_RD1 */
+ uint8_t rx_write_pointer[2]; /* Sn_RX_WR0, Sn_RX_WR1 */
+
+ uint8_t interrupt_mask; /* Sn_IMR */
+ uint8_t fragment_offset[2]; /* Sn_FRAG0, Sn_FRAG1 */
+ uint8_t keepalive_timer; /* Sn_KPALVTR */
+};
+static_assert(sizeof(struct w5500_block_sock_reg) == 0x30);
+
+
+struct w5500_socket {
+ struct spi *spidev;
+ uint8_t socknum; /* 0-7 */
+};
diff --git a/notes.md b/notes.md
index c1427ce..b9d7875 100644
--- a/notes.md
+++ b/notes.md
@@ -87,3 +87,59 @@ these first two, but not the @50Hz one.
https://forums.parallax.com/discussion/download/128730/Hdmi-1.4-1000008562-6364143185282736974850538.pdf
https://ia803002.us.archive.org/1/items/CEA-861-D/CEA-861-D.pdf
+
+The RP2040 has several clocks:
+
+Sources:
+
+- GPCLK0 (GPIO-based clock 0)
+- GPCLK1 (GPIO-based clock 1)
+- XOSC (External (Crystal) Oscillator)
+ + System PLL
+ + USB PLL
+- ROSC (Ring Oscillator)
+
+These can be muxed onto several clocks which each have dividers (and
+most of them enable/disable too):
+
+- clk_gpout0 (GPIO Muxing)
+- clk_gpout1 (GPIO Muxing)
+- clk_gpout2 (GPIO Muxing)
+- clk_gpout3 (GPIO Muxing)
+- clk_adc (ADC)
+- clk_usb (USB)
+- clk_RTC (RTC)
+- clk_peri (UART and SPI)
+- clk_sys (CPU, bus, RAM)
+- clk_ref (watchdog and timers)
+
+```
+SSP = ARM Primecell Synchronous Serial Port
+ ^ ^ ^
+```
+
+- SPI (Serial Peripheral Interface - Motorola)
+- SSI (Synchronous Serial Interface - Texas Instruments)
+- Microwire (National Semiconductor)
+
+| `sclk` | `SSPCLKOUT` | `SSP_CLK_OUT` | SSP clock output |
+| `ss_n` | `SSPFSSOUT` | `SSP_FSS_OUT` | SSP frame/slave select output |
+| `tx` | `SSPTXD` | `SSP_TX_D` | SSP transmit data |
+| `rd` | `SSPRXD` | `SSP_RX_D` | SSP receive data |
+
+"The SPI uses `clk_peri` as its reference clock for SPI timing, and is
+referred to as `SSPCLK` in the following sections. `clk_sys` is used
+as the bus clock, and is referred to as `PCLK` in the following
+sections" wut does that mean
+
+8 16-bit values in both the TX buffer and the RX buffer
+
+
+----
+
+The theoretical max rate of the the W5500 is just shy of 80 Mb/s = 10 MB/s
+
+IDK about HDMI compression yet, but naively uncompressed we're looking
+at wanting to shove ~60 MB/s (480 Mb/s).
+
+Compression is an optional feature introduced in HDMI 2.1 :(