summaryrefslogtreecommitdiff
path: root/libhw/w5500_ll.h
diff options
context:
space:
mode:
Diffstat (limited to 'libhw/w5500_ll.h')
-rw-r--r--libhw/w5500_ll.h363
1 files changed, 363 insertions, 0 deletions
diff --git a/libhw/w5500_ll.h b/libhw/w5500_ll.h
new file mode 100644
index 0000000..57bfccd
--- /dev/null
+++ b/libhw/w5500_ll.h
@@ -0,0 +1,363 @@
+/* libhw/w5500_ll.h - Low-level header library for the WIZnet W5500 chip
+ *
+ * Based entirely on the W5500 datasheet, v1.1.0.
+ * https://docs.wiznet.io/img/products/w5500/W5500_ds_v110e.pdf
+ *
+ * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-Licence-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _LIBHW_W5500_LL_H_
+#define _LIBHW_W5500_LL_H_
+
+#include <assert.h> /* for assert(), static_assert() */
+#include <stdint.h> /* for uint{n}_t */
+#include <string.h> /* for memcmp() */
+
+#include <libmisc/vcall.h> /* for VCALL() */
+#include <libmisc/endian.h> /* for uint16be_t */
+
+#include <libhw/generic/net.h> /* for struct net_eth_addr, struct net_ip4_addr */
+#include <libhw/generic/spi.h> /* for implements_spi */
+
+
+/* Low-level protocol built on SPI frames. ***********************************/
+
+/* A u8 control byte has 3 parts: block-ID, R/W, and operating-mode. */
+
+/* Part 1: Block ID. */
+#define CTL_MASK_BLOCK 0b11111000
+#define _CTL_BLOCK_RES 0b00000
+#define _CTL_BLOCK_REG 0b01000
+#define _CTL_BLOCK_TX 0b10000
+#define _CTL_BLOCK_RX 0b11000
+#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 0b100
+#define CTL_R 0b000
+#define CTL_W 0b100
+
+/* 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
+
+/* Even though SPI is a full-duplex protocol, the W5500's spiframe on top of it is only half-duplex.
+ * Lame. */
+
+static inline void
+w5500ll_write(implements_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},
+ };
+ VCALL(spidev, readwritev, iov, 2);
+}
+
+static inline void
+w5500ll_read(implements_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_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},
+ };
+ VCALL(spidev, readwritev, iov, 2);
+}
+
+/* Common chip-wide registers. ***********************************************/
+
+struct w5500ll_block_common_reg {
+ uint8_t mode; /* MR; bitfield, see CHIPMODE_{x} below */
+ struct net_ip4_addr ip_gateway_addr; /* GAR0 ... GAR3 */
+ struct net_ip4_addr ip_subnet_mask; /* SUBR0 ... SUBR3 */
+ struct net_eth_addr eth_addr; /* SHAR0 ... SHAR5 */
+ struct net_ip4_addr ip_addr; /* SIPR0 ... SIPR3 */
+
+ uint16be_t intlevel; /* INTLEVEL0, INTLEVEL1; if non-zero,
+ * hysteresis between pin_intr being pulled
+ * low (hysteresis=(intlevel+1)*4/(150MHz)) */
+ uint8_t chip_interrupt; /* IR; bitfield, see CHIPINTR_{x} below */
+ uint8_t chip_interrupt_mask; /* IMR; bitfield, see CHIPINTR_{x} below, 0=disable, 1=enable */
+ uint8_t sock_interrupt; /* SIR; bitfield of which sockets have their .interrupt set */
+ uint8_t sock_interrupt_mask; /* SIMR; bitfield of sockets, 0=disable, 1=enable */
+ uint16be_t retry_time; /* RTR0, RTR0; configures re-transmission period, in units of 100µs */
+ uint8_t retry_count; /* RCR; configures max re-transmission count */
+
+ uint8_t ppp_lcp_request_timer; /* PTIMER */
+ uint8_t ppp_lcp_magic_bumber; /* PMAGIC */
+ struct net_eth_addr ppp_dst_eth_addr; /* PHAR0 ... PHAR5 */
+ uint16be_t ppp_sess_id; /* PSID0 ... PSID1 */
+ uint16be_t ppp_max_seg_size; /* PMRU0 ... PMRU1 */
+
+ struct net_ip4_addr unreachable_ip_addr; /* UIPR0 ... UIPR3 */
+ uint16be_t unreachable_port; /* UPORTR0, UPORTR1 */
+
+ uint8_t phy_cfg; /* PHYCFGR */
+
+ uint8_t _reserved[10];
+
+ uint8_t chip_version; /* VERSIONR */
+};
+static_assert(sizeof(struct w5500ll_block_common_reg) == 0x3A);
+
+/* bitfield */
+#define CHIPMODE_RST ((uint8_t)(1<<7)) /* software reset */
+#define _CHIPMODE_UNUSED6 ((uint8_t)(1<<6))
+#define CHIPMODE_WOL ((uint8_t)(1<<5)) /* wake-on-lan */
+#define CHIPMODE_BLOCK_PING ((uint8_t)(1<<4))
+#define CHIPMODE_PPP ((uint8_t)(1<<3))
+#define _CHIPMODE_UNUSED2 ((uint8_t)(1<<2))
+#define CHIPMODE_FORCE_ARP ((uint8_t)(1<<1))
+#define _CHIPMODE_UNUSED0 ((uint8_t)(1<<0))
+
+#define CHIPINTR_CONFLICT ((uint8_t)(1<<7)) /* ARP says remote IP is self */
+#define CHIPINTR_UNREACH ((uint8_t)(1<<6))
+#define CHIPINTR_PPP_CLOSE ((uint8_t)(1<<6))
+#define CHIPINTR_WOL ((uint8_t)(1<<4)) /* wake-on-LAN */
+#define _CHIPINTR_UNUSED3 ((uint8_t)(1<<3))
+#define _CHIPINTR_UNUSED2 ((uint8_t)(1<<2))
+#define _CHIPINTR_UNUSED1 ((uint8_t)(1<<1))
+#define _CHIPINTR_UNUSED0 ((uint8_t)(1<<0))
+
+#define w5500ll_write_common_reg(spidev, field, val) \
+ w5500ll_write_reg(spidev, \
+ CTL_BLOCK_COMMON_REG, \
+ struct w5500ll_block_common_reg, \
+ field, val)
+
+
+#define w5500ll_read_common_reg(spidev, field) \
+ w5500ll_read_reg(spidev, \
+ CTL_BLOCK_COMMON_REG, \
+ struct w5500ll_block_common_reg, \
+ field)
+
+/* Per-socket registers. *****************************************************/
+
+struct w5500ll_block_sock_reg {
+ uint8_t mode; /* Sn_MR; see SOCKMODE_{x} below */
+ uint8_t command; /* Sn_CR; see CMD_{x} below */
+ uint8_t interrupt; /* Sn_IR; bitfield, see SOCKINTR_{x} below */
+ uint8_t state; /* Sn_SR; see STATE_{x} below */
+ uint16be_t local_port; /* Sn_PORT0, Sn_PORT1 */
+ struct net_eth_addr remote_eth_addr; /* Sn_DHAR0 ... SnDHAR5 */
+ struct net_ip4_addr remote_ip_addr; /* Sn_DIPR0 ... Sn_DIP3 */
+ uint16be_t remote_port; /* Sn_DPORT0 ... Sn_DPORT1 */
+
+ uint16be_t max_seg_size; /* 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; in KiB, power of 2, <= 16 */
+ uint8_t tx_buf_size; /* Sn_TXBUF_SIZE; in KiB, power of 2, <= 16 */
+ uint16be_t tx_free_size; /* Sn_TX_FSR0, Sn_TX_FSR1 */
+ uint16be_t tx_read_pointer; /* Sn_TX_RD0, Sn_TX_RD1 */
+ uint16be_t tx_write_pointer; /* Sn_TX_WR0, Sn_TX_WR1 */
+ uint16be_t rx_size; /* Sn_RX_RSR0, Sn_RX_RSR1 */
+ uint16be_t rx_read_pointer; /* Sn_RX_RD0, Sn_RX_RD1 */
+ uint16be_t rx_write_pointer; /* Sn_RX_WR0, Sn_RX_WR1 */
+
+ uint8_t interrupt_mask; /* Sn_IMR */
+ uint16be_t fragment_offset; /* Sn_FRAG0, Sn_FRAG1 */
+ uint8_t keepalive_timer; /* Sn_KPALVTR */
+};
+static_assert(sizeof(struct w5500ll_block_sock_reg) == 0x30);
+
+/* low 4 bits are the main enum, high 4 bits are flags */
+#define SOCKMODE_CLOSED ((uint8_t)0b0000)
+#define SOCKMODE_TCP ((uint8_t)0b0001)
+#define SOCKMODE_UDP ((uint8_t)0b0010)
+#define SOCKMODE_MACRAW ((uint8_t)0b0100)
+
+#define SOCKMODE_FLAG_TCP_NODELAY_ACK ((uint8_t)(1<<5))
+
+#define SOCKMODE_FLAG_UDP_ENABLE_MULTICAST ((uint8_t)(1<<7))
+#define SOCKMODE_FLAG_UDP_BLOCK_BROADCAST ((uint8_t)(1<<6))
+#define SOCKMODE_FLAG_UDP_MULTICAST_DOWNGRADE ((uint8_t)(1<<5))
+#define SOCKMODE_FLAG_UDP_BLOCK_UNICAST ((uint8_t)(1<<4))
+
+#define SOCKMODE_FLAG_MACRAW_MAC_FILTERING ((uint8_t)(1<<7))
+#define SOCKMODE_FLAG_MACRAW_BLOCK_BROADCAST ((uint8_t)(1<<6))
+#define SOCKMODE_FLAG_MACRAW_BLOCK_MULTICAST ((uint8_t)(1<<5))
+#define SOCKMODE_FLAG_MACRAW_BLOCK_V6 ((uint8_t)(1<<4))
+
+#define CMD_OPEN ((uint8_t)0x01)
+#define CMD_LISTEN ((uint8_t)0x02) /* TCP-only */
+#define CMD_CONNECT ((uint8_t)0x04) /* TCP-only: dial */
+#define CMD_DISCON ((uint8_t)0x08) /* TCP-only: send FIN */
+#define CMD_CLOSE ((uint8_t)0x10)
+#define CMD_SEND ((uint8_t)0x20)
+#define CMD_SEND_MAC ((uint8_t)0x21) /* UDP-only: send to remote_eth_addr without doing ARP on remote_ip_addr */
+#define CMD_SEND_KEEP ((uint8_t)0x22) /* TCP-only: send a keepalive without any data */
+#define CMD_RECV ((uint8_t)0x40)
+
+#define _SOCKINTR_SEND_UNUSED7 ((uint8_t)1<<7)
+#define _SOCKINTR_SEND_UNUSED6 ((uint8_t)1<<6)
+#define _SOCKINTR_SEND_UNUSED5 ((uint8_t)1<<5)
+#define SOCKINTR_SEND_OK ((uint8_t)1<<4)
+#define SOCKINTR_TIMEOUT ((uint8_t)1<<3)
+#define SOCKINTR_RECV ((uint8_t)1<<2)
+#define SOCKINTR_FIN ((uint8_t)1<<1)
+#define SOCKINTR_CONN ((uint8_t)1<<1) /* first for SYN, then when SOCKMODE_ESTABLISHED */
+
+#define STATE_CLOSED ((uint8_t)0x00)
+
+/**
+ * The TCP state diagram is as follows.
+ * - Reading the W5500's "state" register does not distinguish between FIN_WAIT_1 and FIN_WAIT_2;
+ * it just has a single FIN_WAIT.
+ * - At any point the state can jump to "CLOSED" either by CMD_CLOSE or by a timeout.
+ * - Writing data is valid in ESTABLISHED and CLOSE_WAIT.
+ * - Reading data is valid in ESTABLISHED and FIN_WAIT.
+ *
+ * TCP state diagram, showing the flow of │ CLOSED │ ━━ role separator ┌───────┐
+ * SYN, FIN, and their assocaited ACKs. └────────┘ ══ state transition │ state │
+ * V ┈┈ packet flow └───────┘
+ * (CMD_OPEN) ║
+ * V (action/event)
+ * ┌────────┐
+ * ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━│ INIT │━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
+ * ┃ server ┃ └────────┘ ┃ client ┃
+ * ┣━━━━━━━━━━━━━━━━┛ V ┃┃ V ┗━━━━━━━━━━━━━━━━┫
+ * ┃ ╔═══════════╝ ┃┃ ╚═══════════╗ ┃
+ * ┃ (CMD_LISTEN) ┃┃ (CMD_CONNECT) ┃
+ * ┃ V ┌┃┃┈┈┈┈┈┈┈┈<(send SYN) ┃
+ * ┃ ┌────────┐ ┊┃┃ V ┃
+ * ┃ │ LISTEN │ ┊┃┃ ┌─────────┐ ┃
+ * ┃ └────────┘ ┊┃┃ │ SYNSENT │ ┃
+ * ┃ V ┊┃┃ └─────────┘ ┃
+ * ┃ (recv SYN)<┈┈┈┈┈┈┘┃┃ V ┃
+ * ┃ (send SYN+ACK)>┈┈┈┈┐┃┃ ║ ┃
+ * ┃ V └┃┃┈┈┈┈┈┈>(recv SYN+ACK) ┃
+ * ┃ ┌─────────┐ ┌┃┃┈┈┈┈┈┈┈┈<(send ack) ┃
+ * ┃ │ SYNRECV │ ┊┃┃ ║ ┃
+ * ┃ └─────────┘ ┊┃┃ ║ ┃
+ * ┃ V V ┊┃┃ ║ ┃
+ * ┃ ║ (recv ACK)<┈┈┈┘┃┃ ║ ┃
+ * ┃ ║ ╚═════════╗ ┃┃ ╔═══════════╝ ┃
+ * ┃ ╚═══╗ V ┃┃ V ┃
+ * ┃ ║ ┌─────────────┐ ┃
+ * ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━║━━━━━━━│ ESTABLISHED │━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ * ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━║━━━━━━━└─────────────┘━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+ * ┃ ║ V ┃┃ V ┃
+ * ┃ ╠═══════════╝ ┃┃ ╚═══════════╗ ┃
+ * ┃ (CMD_DISCON) ┃┃ ║ ┃
+ * ┃ Both sides sent ┌┈┈┈<(send FIN)>┈┈┈┈┈┈┐┃┃ ║ ┃
+ * ┃ FIN at the "same" ┊ V └┃┃┈┈┈┈┈┈┈┈>(recv FIN) ┃
+ * ┃ time; both are ┊ ┌────────────┐ ┌┃┃┈┈┈┈┈┈┈┈<(send ACK) ┃
+ * ┃ active closers ┊ │ FIN_WAIT_1 │ ┊┃┃ V ┃
+ * ┃ / \ ┊ └────────────┘ ┊┃┃ ┌────────────┐ ┃
+ * ┃ ,------' '------, ┊ V V ┊┃┃ │ CLOSE_WAIT │ ┃
+ * ┃ ╔════════════════╝ ║ ┊┃┃ └────────────┘ ┃
+ * ┃ (recv FIN)<┈┈┈┈┤ ╔══╝ ┊┃┃ V ┃
+ * ┃ ┌┈┈<(send ACK)>┈┈┐ ┊ ║ ┊┃┃ ║ ┃
+ * ┃ ┊ ║ └┈┈┈┈┈>(recv ACK)<┈┈┈┈┈┈┘┃┃ ║ ┃
+ * ┃ ┊ V ┊ V ┃┃ ║ ┃
+ * ┃ ┊ ┌─────────┐ ┊ ┌────────────┐ ┃┃ ║ ┃
+ * ┃ ┊ │ CLOSING │ ┊ │ FIN_WAIT_2 │ ┃┃ ║ ┃
+ * ┃ ┊ └─────────┘ ┊ └────────────┘ ┃┃ (CMD_DISCON) ┃
+ * ┃ ┊ V ┊ V ┌┃┃┈┈┈┈┈┈┈┈<(send FIN) ┃
+ * ┃ ┊ ║ └┈┈┈>(recv FIN)<┈┈┈┈┈┈┘┃┃ ║ ┃
+ * ┃ ┊ ║ ┌┈┈┈┈┈<(send ACK)>┈┈┈┈┈┈┐┃┃ V ┃
+ * ┃ └┈┈>(recv ACK)<┈┈┘ ╚═╗ ┊┃┃ ┌──────────┐ ┃
+ * ┃ ╚════════════════╗ ║ ┊┃┃ │ LAST_ACK │ ┃
+ * ┃ V V ┊┃┃ └──────────┘ ┃
+ * ┃ ┌───────────┐ ┊┃┃ V ┃
+ * ┃ │ TIME_WAIT │ ┊┃┃ ║ ┃
+ * ┃ └───────────┘ └┃┃┈┈┈┈┈┈┈┈>(recv ACK) ┃
+ * ┃ V ┃┃ ║ ┃
+ * ┣━━━━━━━━━━━━━━━━┓ (2*MSL has elapsed) ┃┃ ║ ┏━━━━━━━━━━━━━━━━┫
+ * ┃ active closer ┃ ╚═══════════╗ ┃┃ ╔═══════════╝ ┃ passive closer ┃
+ * ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━V━┛┗━V━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛
+ * ┌────────┐
+ * │ CLOSED │
+ */
+#define STATE_TCP_INIT ((uint8_t)0x13)
+#define STATE_TCP_LISTEN ((uint8_t)0x14) /* server */
+#define STATE_TCP_SYNSENT ((uint8_t)0x15) /* client; during dial */
+#define STATE_TCP_SYNRECV ((uint8_t)0x16) /* server; during accept */
+#define STATE_TCP_ESTABLISHED ((uint8_t)0x17)
+#define STATE_TCP_FIN_WAIT ((uint8_t)0x18) /* during active close */
+#define STATE_TCP_CLOSING ((uint8_t)0x1a) /* during active close */
+#define STATE_TCP_TIME_WAIT ((uint8_t)0x1b) /* during active close */
+#define STATE_TCP_CLOSE_WAIT ((uint8_t)0x1c) /* during passive close */
+#define STATE_TCP_LAST_ACK ((uint8_t)0x1d) /* during passive close */
+
+#define STATE_UDP ((uint8_t)0x22)
+
+#define STATE_MACRAW ((uint8_t)0x42)
+
+#define w5500ll_write_sock_reg(spidev, socknum, field, val) \
+ w5500ll_write_reg(spidev, \
+ CTL_BLOCK_SOCK(socknum, REG), \
+ struct w5500ll_block_sock_reg, \
+ field, val)
+
+#define w5500ll_read_sock_reg(spidev, socknum, field) \
+ w5500ll_read_reg(spidev, \
+ CTL_BLOCK_SOCK(socknum, REG), \
+ struct w5500ll_block_sock_reg, \
+ field)
+
+/******************************************************************************/
+
+#define w5500ll_write_reg(spidev, blockid, blocktyp, field, val) do { \
+ typeof((blocktyp){}.field) lval = val; \
+ w5500ll_write(spidev, \
+ offsetof(blocktyp, field), \
+ blockid, \
+ &lval, \
+ sizeof(lval)); \
+ } while (0)
+
+/* The datasheet tells us that multi-byte reads are non-atomic and
+ * that "it is recommended that you read all 16-bits twice or more
+ * until getting the same value". */
+#define w5500ll_read_reg(spidev, blockid, blocktyp, field) ({ \
+ typeof((blocktyp){}.field) val; \
+ w5500ll_read(spidev, \
+ offsetof(blocktyp, field), \
+ blockid, \
+ &val, \
+ sizeof(val)); \
+ if (sizeof(val) > 1) \
+ for (;;) { \
+ typeof(val) val2; \
+ w5500ll_read(spidev, \
+ offsetof(blocktyp, field), \
+ blockid, \
+ &val2, \
+ sizeof(val)); \
+ if (memcmp(&val2, &val, sizeof(val)) == 0) \
+ break; \
+ val = val2; \
+ } \
+ val; \
+ })
+
+#endif /* _LIBHW_W5500_LL_H_ */