diff options
Diffstat (limited to 'libhw/w5500.c')
-rw-r--r-- | libhw/w5500.c | 527 |
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 ***********************************************************/ |