summaryrefslogtreecommitdiff
path: root/libhw/w5500.c
diff options
context:
space:
mode:
authorLuke T. Shumaker <lukeshu@lukeshu.com>2024-10-27 23:22:01 -0600
committerLuke T. Shumaker <lukeshu@lukeshu.com>2024-10-27 23:49:37 -0600
commit88adb90f5e805bea27e619fd5209ef58dbff6fd1 (patch)
treec3e24877b40ce183f1d72f6e064b0478ecf92207 /libhw/w5500.c
parent89761191a98f7dce4d1049b9a84c3d645378222a (diff)
Factor out a libhw
Diffstat (limited to 'libhw/w5500.c')
-rw-r--r--libhw/w5500.c527
1 files changed, 527 insertions, 0 deletions
diff --git a/libhw/w5500.c b/libhw/w5500.c
new file mode 100644
index 0000000..70881ee
--- /dev/null
+++ b/libhw/w5500.c
@@ -0,0 +1,527 @@
+/* libhw/w5500.c - <libhw/generic/net.h> implementation for the WIZnet W5500 chip
+ *
+ * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-Licence-Identifier: AGPL-3.0-or-later
+ *
+ * -----------------------------------------------------------------------------
+ * https://github.com/Wiznet/ioLibrary_Driver/blob/b981401e7f3d07015619adf44c13998e13e777f9/Ethernet/wizchip_conf.c
+ * https://github.com/Wiznet/ioLibrary_Driver/blob/b981401e7f3d07015619adf44c13998e13e777f9/Ethernet/W5500/w5500.h
+ * https://github.com/Wiznet/ioLibrary_Driver/blob/b981401e7f3d07015619adf44c13998e13e777f9/Ethernet/W5500/w5500.c
+ * https://github.com/Wiznet/ioLibrary_Driver/blob/b981401e7f3d07015619adf44c13998e13e777f9/Ethernet/socket.c
+ *
+ * Copyright (c) 2013, WIZnet Co., LTD.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of the <ORGANIZATION> nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * SPDX-Licence-Identifier: BSD-3-Clause
+ *
+ * -----------------------------------------------------------------------------
+ * https://github.com/Wiznet/ioLibrary_Driver/blob/b981401e7f3d07015619adf44c13998e13e777f9/license.txt
+ *
+ * Copyright (c) 2014 WIZnet Co.,Ltd.
+ * Copyright (c) WIZnet ioLibrary Project.
+ * All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * SPDX-Licence-Identifier: MIT
+ */
+
+#include <pico/time.h> /* for sleep_ms() */
+#include <hardware/gpio.h> /* pico-sdk:hardware_gpio */
+
+#include <libcr/coroutine.h> /* for cr_yield() */
+#include <libmisc/vcall.h> /* for VCALL_SELF() */
+
+#include <libhw/w5500.h>
+
+#include "w5500_ll.h"
+
+/* Config *********************************************************************/
+
+#include "config.h"
+
+/* These are the default values of the Linux kernel's
+ * net.ipv4.ip_local_port_range, so I figure they're probably good
+ * values to use. */
+#ifndef CONFIG_W5500_LOCAL_PORT_MIN
+ #define CONFIG_W5500_LOCAL_PORT_MIN 32768
+#endif
+
+#ifndef CONFIG_W5500_LOCAL_PORT_MAX
+ #define CONFIG_W5500_LOCAL_PORT_MAX 60999
+#endif
+
+#ifndef CONFIG_W5500_NUM
+ #error config.h must define CONFIG_W5500_NUM
+#endif
+
+/* C language *****************************************************************/
+
+#define UNUSED(name)
+#define ARRAY_LEN(ary) (sizeof(ary)/sizeof((ary)[0]))
+
+/* mid-level utilities ********************************************************/
+
+#if 0
+static uint16_t w5500_get_local_port(struct w5500 *self) {
+ uint16_t ret = self->next_local_port++;
+ if (self->next_local_port > CONFIG_W5500_LOCAL_PORT_MAX)
+ self->next_local_port = CONFIG_W5500_LOCAL_PORT_MIN;
+ return ret;
+}
+#endif
+
+static COROUTINE w5500_irq_cr(void *_chip) {
+ struct w5500 *chip = _chip;
+ cr_begin();
+
+ for (;;) {
+ cr_sema_wait(&chip->intr);
+ if (w5500ll_read_common_reg(chip->spidev, chip_interrupt))
+ w5500ll_write_common_reg(chip->spidev, chip_interrupt, 0xFF);
+
+ uint8_t sockmask = w5500ll_read_common_reg(chip->spidev, sock_interrupt);
+ for (uint8_t socknum = 0; socknum < 8; socknum++) {
+ if (!(sockmask & (1<<socknum)))
+ continue;
+ struct _w5500_tcp_listener *listener = &chip->listeners[socknum];
+
+ uint8_t sockintr = w5500ll_read_sock_reg(chip->spidev, socknum, interrupt);
+
+ /* SOCKINTR_SEND_OK is useless. */
+ uint8_t listen_bits = sockintr & (SOCKINTR_TIMEOUT|SOCKINTR_CONN),
+ read_bits = sockintr & (SOCKINTR_TIMEOUT|SOCKINTR_RECV|SOCKINTR_FIN);
+
+ if (listen_bits)
+ cr_sema_signal(&listener->listen_sema);
+ if (read_bits)
+ cr_sema_signal(&listener->read_sema);
+
+ w5500ll_write_sock_reg(chip->spidev, socknum, interrupt, sockintr);
+ }
+ }
+
+ cr_end();
+}
+
+/* init() *********************************************************************/
+
+static implements_net_stream_conn *w5500_tcp_accept(implements_net_stream_listener *listener);
+static ssize_t w5500_tcp_read(implements_net_stream_conn *conn, void *buf, size_t count);
+static ssize_t w5500_tcp_write(implements_net_stream_conn *conn, void *buf, size_t count);
+static int w5500_tcp_close(implements_net_stream_conn *conn, bool rd, bool wr);
+
+static struct net_stream_listener_vtable w5500_tcp_listener_vtable = {
+ .accept = w5500_tcp_accept,
+};
+
+static struct net_stream_conn_vtable w5500_tcp_conn_vtable = {
+ .read = w5500_tcp_read,
+ .write = w5500_tcp_write,
+ .close = w5500_tcp_close,
+};
+
+static struct w5500 *w5500_chips[CONFIG_W5500_NUM] = {0};
+
+static void w5500_intrhandler(uint gpio, uint32_t UNUSED(event_mask)) {
+ for (size_t i = 0; i < ARRAY_LEN(w5500_chips); i++)
+ if (w5500_chips[i] && w5500_chips[i]->pin_intr == gpio)
+ cr_sema_signal_from_intrhandler(&w5500_chips[i]->intr);
+}
+
+void _w5500_init(struct w5500 *chip,
+ implements_spi *spi, uint pin_intr, uint pin_reset,
+ struct net_eth_addr addr) {
+ assert(chip);
+ assert(spi);
+
+ /* Initialize the data structures. */
+ *chip = (struct w5500){
+ /* const-after-init */
+ .spidev = spi,
+ .pin_intr = pin_intr,
+ .pin_reset = pin_reset,
+ .hwaddr = addr,
+ /* mutable */
+ .next_local_port = CONFIG_W5500_LOCAL_PORT_MIN,
+ };
+ for (uint8_t i = 0; i < 8; i++) {
+ chip->listeners[i] = (struct _w5500_tcp_listener){
+ /* const-after-init */
+ .vtable = &w5500_tcp_listener_vtable,
+ .socknum = i,
+ .active_conn = {
+ /* const-after-init */
+ .vtable = &w5500_tcp_conn_vtable,
+ /* mutable */
+ .read_open = false,
+ .write_open = false,
+ },
+ /* mutable */
+ .port = 0,
+ };
+ }
+
+ /* Initialize the hardware. */
+ gpio_set_irq_enabled_with_callback(pin_intr, GPIO_IRQ_EDGE_FALL, true, w5500_intrhandler);
+ gpio_set_dir(chip->pin_reset, GPIO_OUT);
+ w5500_hard_reset(chip);
+
+ /* Finally, wire in the interrupt handler. */
+ cr_disable_interrupts();
+ for (size_t i = 0; i < ARRAY_LEN(w5500_chips); i++) {
+ if (w5500_chips[i] == NULL) {
+ w5500_chips[i] = chip;
+ break;
+ }
+ }
+ cr_enable_interrupts();
+ coroutine_add(w5500_irq_cr, chip);
+}
+
+/* chip methods ***************************************************************/
+
+static inline void w5500_post_reset(struct w5500 *chip) {
+ /* The W5500 does not have a built-in MAC address, we must
+ * provide one. */
+ w5500ll_write_common_reg(chip->spidev, eth_addr, chip->hwaddr);
+
+ /* The RP2040 needs a 1/sys_clk hysteresis between interrupts
+ * for us to notice them. At the maximum-rated clock-rate of
+ * 133MHz, that means 7.5ns (but the sbc-harness overclocks
+ * the RP2040, so we could get away with even shorter).
+ *
+ * If intlevel is non-zero, then the hysteresis is
+ * (intlevel+1)*4/(150MHz), or (intlevel+1)*26.7ns; so even
+ * the shortest-possible hysteresis much larger than necessary
+ * for us. */
+ w5500ll_write_common_reg(chip->spidev, intlevel, uint16be_marshal(1));
+
+ /* This implementation does not care about any of the
+ * chip-level interrupts. */
+ w5500ll_write_common_reg(chip->spidev, chip_interrupt_mask, 0);
+
+ /* This implementation cares about interrupts for each
+ * socket. */
+ w5500ll_write_common_reg(chip->spidev, sock_interrupt_mask, 0xFF);
+
+ /* Configure retry/timeout.
+ *
+ * timeout_arp = 0.1ms * retry_time * (retry_count+1)
+ *
+ * retry_count
+ * timeout_tcp = 0.1ms * retry_time * Σ 2^min(n, floor(1+log_2(65535/retry_time)))
+ * n=0
+ *
+ * For retry_time=2000, retry_count=3, this means
+ *
+ * timeout_arp = 0.8s
+ * timeout_tcp = 3.0s
+ */
+ w5500ll_write_common_reg(chip->spidev, retry_time, uint16be_marshal(2000));
+ w5500ll_write_common_reg(chip->spidev, retry_count, 3);
+}
+
+void w5500_hard_reset(struct w5500 *chip) {
+ /* TODO: Replace blocking sleep_ms() with something libcr-friendly. */
+ gpio_put(chip->pin_reset, 0);
+ sleep_ms(1); /* minimum of 500us */
+ gpio_put(chip->pin_reset, 1);
+ sleep_ms(2); /* minimum of 1ms */
+
+ w5500_post_reset(chip);
+}
+
+void w5500_soft_reset(struct w5500 *chip) {
+ w5500ll_write_common_reg(chip->spidev, mode, CHIPMODE_RST);
+ while (w5500ll_read_common_reg(chip->spidev, mode) & CHIPMODE_RST)
+ cr_yield();
+
+ w5500_post_reset(chip);
+}
+
+void w5500_netcfg(struct w5500 *chip, struct w5500_netcfg cfg) {
+ w5500ll_write_common_reg(chip->spidev, ip_gateway_addr, cfg.gateway_addr);
+ w5500ll_write_common_reg(chip->spidev, ip_subnet_mask, cfg.subnet_mask);
+ w5500ll_write_common_reg(chip->spidev, ip_addr, cfg.addr);
+}
+
+implements_net_stream_listener *w5500_tcp_listen(struct w5500 *chip, uint8_t socknum,
+ uint16_t port) {
+ assert(chip);
+ assert(socknum < 8);
+ assert(port);
+
+ assert(chip->listeners[socknum].port == 0);
+ chip->listeners[socknum].port = port;
+
+ return &chip->listeners[socknum];
+}
+
+/*
+implements_net_packet_conn *w5500_udp_conn(struct w5500 *chip, uint8_t socknum,
+ uint16_t port) {
+ assert(chip);
+ assert(socknum < 8);
+ assert(port);
+
+ assert(chip->listeners[socknum].port == 0);
+ chip->listeners[socknum].port = port;
+
+ return &chip->listeners[socknum];
+}
+*/
+
+/* tcp_listener methods *******************************************************/
+
+static struct w5500 *w5500_tcp_listener_chip(struct _w5500_tcp_listener *listener) {
+ assert(listener);
+ assert(listener->socknum < 8);
+
+ struct _w5500_tcp_listener *sock0 = &listener[-listener->socknum];
+ assert(sock0);
+ struct w5500 *chip =
+ ((void *)sock0) - offsetof(struct w5500, listeners);
+ assert(chip);
+ return chip;
+}
+
+static inline void w5500_tcp_listener_cmd(struct _w5500_tcp_listener *listener, uint8_t cmd) {
+ assert(listener);
+ struct w5500 *chip = w5500_tcp_listener_chip(listener);
+ uint8_t socknum = listener->socknum;
+
+ cr_mutex_lock(&listener->cmd_mu);
+ w5500ll_write_sock_reg(chip->spidev, socknum, command, cmd);
+ while (w5500ll_read_sock_reg(chip->spidev, socknum, command) != 0x00)
+ cr_yield();
+ cr_mutex_unlock(&listener->cmd_mu);
+}
+
+static inline void w5500_tcp_listener_cmd_close(struct _w5500_tcp_listener *listener) {
+ assert(listener);
+ struct w5500 *chip = w5500_tcp_listener_chip(listener);
+ uint8_t socknum = listener->socknum;
+
+ w5500_tcp_listener_cmd(listener, CMD_CLOSE);
+ w5500ll_write_sock_reg(chip->spidev, socknum, interrupt, 0xFF);
+ while (w5500ll_read_sock_reg(chip->spidev, socknum, state) != STATE_CLOSED)
+ cr_yield();
+
+ listener->active_conn.read_open = listener->active_conn.write_open = false;
+}
+
+#define ASSERT_LISTENER() \
+ struct _w5500_tcp_listener *self = \
+ VCALL_SELF(struct _w5500_tcp_listener, \
+ implements_net_stream_listener, _self); \
+ struct w5500 *chip = w5500_tcp_listener_chip(self); \
+ uint8_t socknum = self->socknum;
+
+static implements_net_stream_conn *w5500_tcp_accept(implements_net_stream_listener *_self) {
+ ASSERT_LISTENER();
+
+ restart:
+ /* Mimics socket.c:socket(). */
+ w5500_tcp_listener_cmd_close(self);
+ w5500ll_write_sock_reg(chip->spidev, socknum, mode, SOCKMODE_TCP);
+ w5500ll_write_sock_reg(chip->spidev, socknum, local_port, uint16be_marshal(self->port));
+ w5500_tcp_listener_cmd(self, CMD_OPEN);
+ while (w5500ll_read_sock_reg(chip->spidev, socknum, state) != STATE_TCP_INIT)
+ cr_yield();
+
+ /* Mimics socket.c:listen(). */
+ w5500_tcp_listener_cmd(self, CMD_LISTEN);
+ for (;;) {
+ uint8_t state = w5500ll_read_sock_reg(chip->spidev, socknum, state);
+ switch (state) {
+ case STATE_TCP_LISTEN:
+ case STATE_TCP_SYNRECV:
+ cr_sema_wait(&self->listen_sema);
+ break;
+ case STATE_TCP_ESTABLISHED:
+ self->active_conn.read_open = true;
+ /* fall-through */
+ case STATE_TCP_CLOSE_WAIT:
+ self->active_conn.write_open = true;
+ return &self->active_conn;
+ default:
+ goto restart;
+ }
+ }
+}
+
+/* tcp_conn methods ***********************************************************/
+
+static struct _w5500_tcp_listener *w5500_tcp_conn_listener(struct _w5500_tcp_conn *conn) {
+ assert(conn);
+
+ struct _w5500_tcp_listener *list =
+ ((void *)conn) - offsetof(struct _w5500_tcp_listener, active_conn);
+ return list;
+}
+
+#define ASSERT_CONN() \
+ struct _w5500_tcp_conn *self = \
+ VCALL_SELF(struct _w5500_tcp_conn, implements_net_stream_conn, _self); \
+ struct _w5500_tcp_listener *listener = w5500_tcp_conn_listener(self); \
+ struct w5500 *chip = w5500_tcp_listener_chip(listener); \
+ uint8_t socknum = listener->socknum;
+
+static ssize_t w5500_tcp_write(implements_net_stream_conn *_self, void *buf, size_t count) {
+ ASSERT_CONN();
+ assert(buf);
+ assert(count);
+
+ /* What we really want is to pause until we receive an ACK for
+ * some data we just queued, so that we can line up some new
+ * data to keep the buffer full. But that's not what
+ * SEND_FINIAIUI, the SEND_FINISHED interrupt doesn't fire
+ * until we receive the *last* ACK for the data, when the
+ * buffer is entirely empty.
+ *
+ * Which means we basically have to busy-poll for space in the
+ * buffer becoming available.
+ *
+ * We'll add more data to the buffer whenever there is
+ * `min_free_space` in the buffer (or the rest of the data
+ * fits in the buffer).
+ *
+ * This `min_free_space` can probably stand to be tuned; must
+ * be >0, <=bufsize. `1500-58` is the 100BaseT MTU minus the
+ * Ethernet+IP+TCP overhead. */
+ uint16_t bufsize = ((uint16_t)w5500ll_read_sock_reg(chip->spidev, socknum, tx_buf_size))*1024;
+ uint16_t min_free_space = MIN(1500-58, bufsize/4);
+
+ size_t done = 0;
+ while (done < count) {
+ if (!self->write_open)
+ return -1;
+ uint8_t state = w5500ll_read_sock_reg(chip->spidev, socknum, state);
+ if (state != STATE_TCP_ESTABLISHED && state != STATE_TCP_CLOSE_WAIT)
+ return -1;
+
+ uint16_t freesize = uint16be_unmarshal(w5500ll_read_sock_reg(chip->spidev, socknum, tx_free_size));
+ if (freesize < count-done && freesize < min_free_space) {
+ /* Wait for more buffer space. */
+ cr_yield();
+ continue;
+ }
+
+ /* Queue data to be sent. */
+ uint16_t ptr = uint16be_unmarshal(w5500ll_read_sock_reg(chip->spidev, socknum, tx_write_pointer));
+ w5500ll_write(chip->spidev, ptr, CTL_BLOCK_SOCK(socknum, TX), &((char *)buf)[done], freesize);
+ w5500ll_write_sock_reg(chip->spidev, socknum, tx_write_pointer, uint16be_marshal(ptr+freesize));
+
+ /* Submit the queue. */
+ w5500_tcp_listener_cmd(listener, CMD_SEND);
+ done += freesize;
+ }
+ return done;
+}
+
+static ssize_t w5500_tcp_read(implements_net_stream_conn *_self, void *buf, size_t count) {
+ ASSERT_CONN();
+ assert(buf);
+ assert(count);
+
+ size_t done = 0;
+ while (!done) {
+ if (!self->read_open)
+ return -1;
+ uint8_t state = w5500ll_read_sock_reg(chip->spidev, socknum, state);
+ switch (state) {
+ case STATE_TCP_CLOSE_WAIT:
+ return 0; /* EOF */
+ case STATE_TCP_ESTABLISHED: case STATE_TCP_FIN_WAIT:
+ break; /* OK */
+ default:
+ return -1;
+ }
+
+ uint16_t avail = uint16be_unmarshal(w5500ll_read_sock_reg(chip->spidev, socknum, rx_size));
+ if (!avail) {
+ cr_sema_wait(&listener->read_sema);
+ continue;
+ }
+ if ((size_t)avail > count)
+ avail = count;
+ uint16_t ptr = uint16be_unmarshal(w5500ll_read_sock_reg(chip->spidev, socknum, rx_read_pointer));
+ w5500ll_read(chip->spidev, ptr, CTL_BLOCK_SOCK(socknum, RX), &((char *)buf)[done], avail);
+ w5500ll_write_sock_reg(chip->spidev, socknum, rx_read_pointer, uint16be_marshal(ptr+avail));
+
+ w5500_tcp_listener_cmd(listener, CMD_RECV);
+ done += avail;
+ }
+ return done;
+}
+
+static int w5500_tcp_close(implements_net_stream_conn *_self, bool rd, bool wr) {
+ ASSERT_CONN();
+
+ if (rd)
+ self->read_open = false;
+
+ if (wr && self->write_open) {
+ w5500_tcp_listener_cmd(listener, CMD_DISCON);
+ while (self->write_open) {
+ uint8_t state = w5500ll_read_sock_reg(chip->spidev, socknum, state);
+ switch (state) {
+ case STATE_TCP_FIN_WAIT:
+ self->write_open = false;
+ /* Can still read */
+ if (!self->read_open)
+ w5500_tcp_listener_cmd_close(listener);
+ break;
+ case STATE_CLOSED:
+ self->write_open = false;
+ break;
+ }
+ }
+ }
+
+ return 0;
+}
+
+/* udp_conn methods ***********************************************************/