diff options
Diffstat (limited to 'libhw_cr')
-rw-r--r-- | libhw_cr/CMakeLists.txt | 44 | ||||
-rw-r--r-- | libhw_cr/alarmclock.c | 23 | ||||
-rw-r--r-- | libhw_cr/host_alarmclock.c | 136 | ||||
-rw-r--r-- | libhw_cr/host_include/libhw/host_alarmclock.h | 26 | ||||
-rw-r--r-- | libhw_cr/host_include/libhw/host_net.h | 44 | ||||
-rw-r--r-- | libhw_cr/host_net.c | 602 | ||||
-rw-r--r-- | libhw_cr/host_util.c | 145 | ||||
-rw-r--r-- | libhw_cr/host_util.h | 30 | ||||
-rw-r--r-- | libhw_cr/rp2040_dma.c | 72 | ||||
-rw-r--r-- | libhw_cr/rp2040_dma.h | 128 | ||||
-rw-r--r-- | libhw_cr/rp2040_gpioirq.c | 75 | ||||
-rw-r--r-- | libhw_cr/rp2040_gpioirq.h | 33 | ||||
-rw-r--r-- | libhw_cr/rp2040_hwspi.c | 272 | ||||
-rw-r--r-- | libhw_cr/rp2040_hwtimer.c | 153 | ||||
-rw-r--r-- | libhw_cr/rp2040_include/libhw/rp2040_hwspi.h | 118 | ||||
-rw-r--r-- | libhw_cr/rp2040_include/libhw/rp2040_hwtimer.h | 25 | ||||
-rw-r--r-- | libhw_cr/rp2040_include/libhw/w5500.h | 114 | ||||
-rw-r--r-- | libhw_cr/w5500.c | 977 | ||||
-rw-r--r-- | libhw_cr/w5500_ll.c | 115 | ||||
-rw-r--r-- | libhw_cr/w5500_ll.h | 367 |
20 files changed, 3499 insertions, 0 deletions
diff --git a/libhw_cr/CMakeLists.txt b/libhw_cr/CMakeLists.txt new file mode 100644 index 0000000..9dd6a27 --- /dev/null +++ b/libhw_cr/CMakeLists.txt @@ -0,0 +1,44 @@ +# libhw_cr/CMakeLists.txt - Device drivers for libcr +# +# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> +# SPDX-License-Identifier: AGPL-3.0-or-later + +add_library(libhw_cr INTERFACE) +target_link_libraries(libhw_cr INTERFACE + libhw_generic + libcr +) + +target_sources(libhw_cr INTERFACE + alarmclock.c +) + +if (PICO_PLATFORM STREQUAL "rp2040") + target_include_directories(libhw_cr PUBLIC INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/rp2040_include) + target_link_libraries(libhw_cr INTERFACE + libcr_ipc + ) + target_sources(libhw_cr INTERFACE + rp2040_dma.c + rp2040_gpioirq.c + rp2040_hwspi.c + rp2040_hwtimer.c + w5500.c + w5500_ll.c + ) + target_link_libraries(libhw_cr INTERFACE + hardware_gpio + hardware_irq + hardware_spi + hardware_timer + ) +endif() + +if (PICO_PLATFORM STREQUAL "host") + target_include_directories(libhw_cr PUBLIC INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/host_include) + target_sources(libhw_cr INTERFACE + host_util.c + host_alarmclock.c + host_net.c + ) +endif() diff --git a/libhw_cr/alarmclock.c b/libhw_cr/alarmclock.c new file mode 100644 index 0000000..6eec52b --- /dev/null +++ b/libhw_cr/alarmclock.c @@ -0,0 +1,23 @@ +/* libhw_cr/alarmclock.c - sleep() implementation for libcr + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <libcr/coroutine.h> + +#include <libhw/generic/alarmclock.h> + +static void alarmclock_sleep_intrhandler(void *_arg) { + cid_t cid = *(cid_t *)_arg; + cr_unpause_from_intrhandler(cid); +} + +void alarmclock_sleep_until_ns(lo_interface alarmclock clock, uint64_t abstime_ns) { + bool saved = cr_save_and_disable_interrupts(); + cid_t cid = cr_getcid(); + struct alarmclock_trigger trigger; + LO_CALL(clock, add_trigger, &trigger, abstime_ns, alarmclock_sleep_intrhandler, &cid); + cr_pause_and_yield(); + cr_restore_interrupts(saved); +} diff --git a/libhw_cr/host_alarmclock.c b/libhw_cr/host_alarmclock.c new file mode 100644 index 0000000..325f7e0 --- /dev/null +++ b/libhw_cr/host_alarmclock.c @@ -0,0 +1,136 @@ +/* libhw_cr/host_alarmclock.c - <libhw/generic/alarmclock.h> implementation for POSIX hosts + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <errno.h> +#include <signal.h> + +#define error __error +#include <error.h> +#undef error + +#include <libcr/coroutine.h> +#include <libmisc/assert.h> + +#define IMPLEMENTATION_FOR_LIBHW_GENERIC_ALARMCLOCK_H YES +#include <libhw/generic/alarmclock.h> + +#define IMPLEMENTATION_FOR_LIBHW_HOST_ALARMCLOCK_H YES +#include <libhw/host_alarmclock.h> + +#include "host_util.h" /* for host_sigrt_alloc(), ns_to_host_ns_time() */ + +LO_IMPLEMENTATION_C(alarmclock, struct hostclock, hostclock, static); + +static uint64_t hostclock_get_time_ns(struct hostclock *alarmclock) { + assert(alarmclock); + + struct timespec ts; + + if (clock_gettime(alarmclock->clock_id, &ts) != 0) + __error(1, errno, "clock_gettime(%d)", (int)alarmclock->clock_id); + + return ns_from_host_ns_time(ts); +} + +static void hostclock_handle_sig_alarm(int LM_UNUSED(sig), siginfo_t *info, void *LM_UNUSED(ucontext)) { + struct hostclock *alarmclock = info->si_value.sival_ptr; + assert(alarmclock); + + while (alarmclock->queue && + alarmclock->queue->fire_at_ns <= hostclock_get_time_ns(alarmclock)) { + struct alarmclock_trigger *trigger = alarmclock->queue; + trigger->cb(trigger->cb_arg); + alarmclock->queue = trigger->next; + trigger->alarmclock = NULL; + trigger->next = NULL; + trigger->prev = NULL; + } + + if (alarmclock->queue) { + struct itimerspec alarmspec = { + .it_value = ns_to_host_ns_time(alarmclock->queue->fire_at_ns), + .it_interval = {}, + }; + if (timer_settime(alarmclock->timer_id, TIMER_ABSTIME, &alarmspec, NULL) != 0) + __error(1, errno, "timer_settime"); + } +} + +static bool hostclock_add_trigger(struct hostclock *alarmclock, + struct alarmclock_trigger *trigger, + uint64_t fire_at_ns, + void (*cb)(void *), + void *cb_arg) { + assert(alarmclock); + assert(trigger); + assert(fire_at_ns); + assert(cb); + + trigger->alarmclock = alarmclock; + trigger->fire_at_ns = fire_at_ns; + trigger->cb = cb; + trigger->cb_arg = cb_arg; + + bool saved = cr_save_and_disable_interrupts(); + struct alarmclock_trigger **dst = &alarmclock->queue; + while (*dst && fire_at_ns >= (*dst)->fire_at_ns) + dst = &(*dst)->next; + trigger->next = *dst; + trigger->prev = *dst ? (*dst)->prev : NULL; + if (*dst) + (*dst)->prev = trigger; + *dst = trigger; + if (!alarmclock->initialized) { + struct sigevent how_to_notify = { + .sigev_notify = SIGEV_SIGNAL, + .sigev_signo = host_sigrt_alloc(), + .sigev_value = { + .sival_ptr = alarmclock, + }, + }; + struct sigaction action = { + .sa_flags = SA_SIGINFO, + .sa_sigaction = hostclock_handle_sig_alarm, + }; + if (sigaction(how_to_notify.sigev_signo, &action, NULL) != 0) + __error(1, errno, "sigaction"); + if (timer_create(alarmclock->clock_id, &how_to_notify, &alarmclock->timer_id) != 0) + __error(1, errno, "timer_create(%d)", (int)alarmclock->clock_id); + alarmclock->initialized = true; + } + if (alarmclock->queue == trigger) { + struct itimerspec alarmspec = { + .it_value = ns_to_host_ns_time(trigger->fire_at_ns), + .it_interval = {}, + }; + if (timer_settime(alarmclock->timer_id, TIMER_ABSTIME, &alarmspec, NULL) != 0) + __error(1, errno, "timer_settime"); + } + cr_restore_interrupts(saved); + + return false; +} + +static void hostclock_del_trigger(struct hostclock *alarmclock, + struct alarmclock_trigger *trigger) { + assert(alarmclock); + assert(trigger); + + bool saved = cr_save_and_disable_interrupts(); + if (trigger->alarmclock == alarmclock) { + if (!trigger->prev) + alarmclock->queue = trigger->next; + else + trigger->prev->next = trigger->next; + if (trigger->next) + trigger->next->prev = trigger->prev; + trigger->alarmclock = NULL; + trigger->prev = NULL; + trigger->next = NULL; + } else + assert(!trigger->alarmclock); + cr_restore_interrupts(saved); +} diff --git a/libhw_cr/host_include/libhw/host_alarmclock.h b/libhw_cr/host_include/libhw/host_alarmclock.h new file mode 100644 index 0000000..2ddb054 --- /dev/null +++ b/libhw_cr/host_include/libhw/host_alarmclock.h @@ -0,0 +1,26 @@ +/* libhw/host_alarmclock.h - <libhw/generic/alarmclock.h> implementation for hosted glibc + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#ifndef _LIBHW_HOST_ALARMCLOCK_H_ +#define _LIBHW_HOST_ALARMCLOCK_H_ + +#include <time.h> /* for clockid_t, timer_t */ + +#include <libhw/generic/alarmclock.h> +#include <libmisc/private.h> + +struct hostclock { + clockid_t clock_id; + + BEGIN_PRIVATE(LIBHW_HOST_ALARMCLOCK_H); + bool initialized; + timer_t timer_id; + struct alarmclock_trigger *queue; + END_PRIVATE(LIBHW_HOST_ALARMCLOCK_H); +}; +LO_IMPLEMENTATION_H(alarmclock, struct hostclock, hostclock); + +#endif /* _LIBHW_HOST_ALARMCLOCK_H_ */ diff --git a/libhw_cr/host_include/libhw/host_net.h b/libhw_cr/host_include/libhw/host_net.h new file mode 100644 index 0000000..a16ed01 --- /dev/null +++ b/libhw_cr/host_include/libhw/host_net.h @@ -0,0 +1,44 @@ +/* libhw/host_net.h - <libhw/generic/net.h> implementation for hosted glibc + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#ifndef _LIBHW_HOST_NET_H_ +#define _LIBHW_HOST_NET_H_ + +#include <stdint.h> /* for uint16_6 */ + +#include <libmisc/private.h> + +#include <libhw/generic/net.h> + +struct _hostnet_tcp_conn { + BEGIN_PRIVATE(LIBHW_HOST_NET_H); + int fd; + uint64_t read_deadline_ns; + END_PRIVATE(LIBHW_HOST_NET_H); +}; +LO_IMPLEMENTATION_H(net_stream_conn, struct _hostnet_tcp_conn, hostnet_tcp); + +struct hostnet_tcp_listener { + BEGIN_PRIVATE(LIBHW_HOST_NET_H); + int fd; + struct _hostnet_tcp_conn active_conn; + END_PRIVATE(LIBHW_HOST_NET_H); +}; +LO_IMPLEMENTATION_H(net_stream_listener, struct hostnet_tcp_listener, hostnet_tcplist); + +void hostnet_tcp_listener_init(struct hostnet_tcp_listener *self, uint16_t port); + +struct hostnet_udp_conn { + BEGIN_PRIVATE(LIBHW_HOST_NET_H); + int fd; + uint64_t read_deadline_ns; + END_PRIVATE(LIBHW_HOST_NET_H); +}; +LO_IMPLEMENTATION_H(net_packet_conn, struct hostnet_udp_conn, hostnet_udp); + +void hostnet_udp_conn_init(struct hostnet_udp_conn *self, uint16_t port); + +#endif /* _LIBHW_HOST_NET_H_ */ diff --git a/libhw_cr/host_net.c b/libhw_cr/host_net.c new file mode 100644 index 0000000..39bfd46 --- /dev/null +++ b/libhw_cr/host_net.c @@ -0,0 +1,602 @@ +/* libhw_cr/host_net.c - <libhw/generic/net.h> implementation for hosted glibc + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#define _GNU_SOURCE /* for pthread_sigqueue(3gnu) */ +/* misc */ +#include <errno.h> /* for errno, EAGAIN, EINVAL */ +#define error __error +#include <error.h> /* for error(3gnu) */ +#undef error +#include <stdlib.h> /* for abs(), shutdown(), SHUT_RD, SHUT_WR, SHUT_RDWR */ +#include <unistd.h> /* for read(), write() */ +/* net */ +#include <arpa/inet.h> /* for htons(3p) */ +#include <netinet/in.h> /* for struct sockaddr_in */ +#include <sys/socket.h> /* for struct sockaddr{,_storage}, SOCK_*, SOL_*, SO_*, socket(), setsockopt(), bind(), listen(), accept() */ +/* async */ +#include <pthread.h> /* for pthread_* */ +#include <signal.h> /* for siginfo_t, struct sigaction, enum sigval, sigaction(), SA_SIGINFO */ + +#include <libcr/coroutine.h> +#include <libmisc/alloc.h> +#include <libmisc/assert.h> +#include <libmisc/macro.h> +#include <libmisc/obj.h> + +#include <libhw/generic/alarmclock.h> + +#define IMPLEMENTATION_FOR_LIBHW_HOST_NET_H YES +#include <libhw/host_net.h> + +#include "host_util.h" /* for host_sigrt_alloc(), ns_to_host_us_time() */ + +LO_IMPLEMENTATION_C(io_closer, struct hostnet_tcp_listener, hostnet_tcplist, static); +LO_IMPLEMENTATION_C(net_stream_listener, struct hostnet_tcp_listener, hostnet_tcplist, static); + +LO_IMPLEMENTATION_C(io_reader, struct _hostnet_tcp_conn, hostnet_tcp, static); +LO_IMPLEMENTATION_C(io_writer, struct _hostnet_tcp_conn, hostnet_tcp, static); +LO_IMPLEMENTATION_C(io_readwriter, struct _hostnet_tcp_conn, hostnet_tcp, static); +LO_IMPLEMENTATION_C(io_closer, struct _hostnet_tcp_conn, hostnet_tcp, static); +LO_IMPLEMENTATION_C(io_bidi_closer, struct _hostnet_tcp_conn, hostnet_tcp, static); +LO_IMPLEMENTATION_C(net_stream_conn, struct _hostnet_tcp_conn, hostnet_tcp, static); + +LO_IMPLEMENTATION_C(io_closer, struct hostnet_udp_conn, hostnet_udp, static); +LO_IMPLEMENTATION_C(net_packet_conn, struct hostnet_udp_conn, hostnet_udp, static); + +/* common *********************************************************************/ + +static int hostnet_sig_io = 0; + +static void hostnet_handle_sig_io(int LM_UNUSED(sig), siginfo_t *info, void *LM_UNUSED(ucontext)) { + cr_unpause_from_intrhandler((cid_t)info->si_value.sival_int); +} + +static void hostnet_init(void) { + struct sigaction action = {}; + + if (hostnet_sig_io) + return; + + hostnet_sig_io = host_sigrt_alloc(); + + action.sa_flags = SA_SIGINFO; + action.sa_sigaction = hostnet_handle_sig_io; + if (sigaction(hostnet_sig_io, &action, NULL) < 0) + __error(1, errno, "sigaction"); +} + +#define WAKE_COROUTINE(args) do { \ + int r; \ + union sigval val = {}; \ + val.sival_int = (int)((args)->cr_coroutine); \ + do { \ + r = pthread_sigqueue((args)->cr_thread, \ + hostnet_sig_io, \ + val); \ + assert(r == 0 || r == EAGAIN); \ + } while (r == EAGAIN); \ + } while (0) + +static inline host_errno_t RUN_PTHREAD(void *(*fn)(void *), void *args) { + pthread_t thread; + host_errno_t r; + bool saved = cr_save_and_disable_interrupts(); + r = pthread_create(&thread, NULL, fn, args); + if (r) { + cr_restore_interrupts(saved); + return r; + } + cr_pause_and_yield(); + cr_restore_interrupts(saved); + return pthread_join(thread, NULL); +} + +enum hostnet_timeout_op { + OP_CLOSE, + OP_SEND, + OP_RECV, +}; + +static inline error hostnet_error(host_errno_t errnum, enum hostnet_timeout_op op) { + assert(errnum > 0); + switch (errnum) { + case EHOSTUNREACH: + return error_new(E_NET_EARP_TIMEOUT); + case ETIMEDOUT: + switch (op) { + case OP_CLOSE: + assert_notreached("impossible ETIMEDOUT"); + case OP_SEND: + return error_new(E_NET_EACK_TIMEOUT); + case OP_RECV: + return error_new(E_NET_ERECV_TIMEOUT); + } + assert_notreached("invalid timeout op"); + case EBADF: + return error_new(E_NET_ECLOSED); + case EMSGSIZE: + return error_new(E_POSIX_EMSGSIZE); + default: + return errno2lm(errnum); + } +} + +/* TCP init() ( AKA socket(3) + listen(3) )************************************/ + +void hostnet_tcp_listener_init(struct hostnet_tcp_listener *self, uint16_t port) { + int listenerfd; + union { + struct sockaddr_in in; + struct sockaddr gen; + } addr = {}; + + hostnet_init(); + + addr.in.sin_family = AF_INET; + addr.in.sin_port = htons(port); + listenerfd = socket(AF_INET, SOCK_STREAM, 0); + if (listenerfd < 0) + __error(1, errno, "socket"); + if (setsockopt(listenerfd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)) < 0) + __error(1, errno, "setsockopt(fd=%d, SO_REUSEADDR=1)", listenerfd); + if (setsockopt(listenerfd, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int)) < 0) + __error(1, errno, "setsockopt(fd=%d, SO_REUSEPORT=1)", listenerfd); + if (bind(listenerfd, &addr.gen, sizeof addr) < 0) + __error(1, errno, "bind(fd=%d)", listenerfd); + if (listen(listenerfd, 0) < 0) + __error(1, errno, "listen(fd=%d)", listenerfd); + + self->fd = listenerfd; +} + +/* TCP listener accept() ******************************************************/ + +struct hostnet_pthread_accept_args { + pthread_t cr_thread; + cid_t cr_coroutine; + + int listenerfd; + + int *ret_connfd; + host_errno_t *ret_errno; +}; + +static void *hostnet_pthread_accept(void *_args) { + struct hostnet_pthread_accept_args *args = _args; + *(args->ret_connfd) = accept(args->listenerfd, NULL, NULL); + *(args->ret_errno) = errno; + WAKE_COROUTINE(args); + return NULL; +} + +static net_stream_conn_or_error hostnet_tcplist_accept(struct hostnet_tcp_listener *listener) { + assert(listener); + + int ret_connfd; + host_errno_t ret_errno; + struct hostnet_pthread_accept_args args = { + .cr_thread = pthread_self(), + .cr_coroutine = cr_getcid(), + .listenerfd = listener->fd, + .ret_connfd = &ret_connfd, + .ret_errno = &ret_errno, + }; + host_errno_t thread_errno = RUN_PTHREAD(hostnet_pthread_accept, &args); + if (thread_errno) + return ERROR_NEW_ERR(net_stream_conn, errno2lm(thread_errno)); + if (ret_connfd < 0) + return ERROR_NEW_ERR(net_stream_conn, hostnet_error(ret_errno, OP_RECV)); + + listener->active_conn.fd = ret_connfd; + listener->active_conn.read_deadline_ns = 0; + return ERROR_NEW_VAL(net_stream_conn, LO_BOX(net_stream_conn, &listener->active_conn)); +} + +/* TCP listener close() *******************************************************/ + +static error hostnet_tcplist_close(struct hostnet_tcp_listener *listener) { + assert(listener); + + if (shutdown(listener->fd, SHUT_RDWR)) + return hostnet_error(errno, OP_CLOSE); + return ERROR_NULL; +} + +/* TCP read() *****************************************************************/ + +static void hostnet_tcp_set_read_deadline(struct _hostnet_tcp_conn *conn, uint64_t ts_ns) { + assert(conn); + + conn->read_deadline_ns = ts_ns; +} + +struct hostnet_pthread_readv_args { + pthread_t cr_thread; + cid_t cr_coroutine; + + int connfd; + struct timeval timeout; + const struct iovec *iov; + int iovcnt; + + size_t *ret_size; + host_errno_t *ret_errno; +}; + +static void *hostnet_pthread_readv(void *_args) { + struct hostnet_pthread_readv_args *args = _args; + + int r_i = setsockopt(args->connfd, SOL_SOCKET, SO_RCVTIMEO, + &args->timeout, sizeof(args->timeout)); + if (r_i) { + *args->ret_size = 0; + *args->ret_errno = errno; + goto end; + } + + ssize_t r_ss = readv(args->connfd, args->iov, args->iovcnt); + if (r_ss < 0) { + *args->ret_size = 0; + *args->ret_errno = errno; + goto end; + } + *args->ret_size = r_ss; + *args->ret_errno = 0; + + end: + WAKE_COROUTINE(args); + return NULL; +} + +static size_t_or_error hostnet_tcp_readv(struct _hostnet_tcp_conn *conn, const struct iovec *iov, int iovcnt) { + assert(conn); + assert(iov); + assert(iovcnt > 0); + size_t count = 0; + for (int i = 0; i < iovcnt; i++) + count += iov[i].iov_len; + assert(count); + + size_t ret_size; + host_errno_t ret_errno; + struct hostnet_pthread_readv_args args = { + .cr_thread = pthread_self(), + .cr_coroutine = cr_getcid(), + + .connfd = conn->fd, + .iov = iov, + .iovcnt = iovcnt, + + .ret_size = &ret_size, + .ret_errno = &ret_errno, + }; + if (conn->read_deadline_ns) { + uint64_t now_ns = LO_CALL(bootclock, get_time_ns); + if (conn->read_deadline_ns < now_ns) + return ERROR_NEW_ERR(size_t, error_new(E_NET_ERECV_TIMEOUT)); + args.timeout = ns_to_host_us_time(conn->read_deadline_ns-now_ns); + } else { + args.timeout = (host_us_time_t){}; + } + + host_errno_t thread_errno = RUN_PTHREAD(hostnet_pthread_readv, &args); + if (thread_errno) + return ERROR_NEW_ERR(size_t, errno2lm(thread_errno)); + if (ret_errno) + return ERROR_NEW_ERR(size_t, hostnet_error(ret_errno, OP_RECV)); + if (!ret_size) + return ERROR_NEW_ERR(size_t, error_new(E_EOF)); + return ERROR_NEW_VAL(size_t, ret_size); +} + +/* TCP write() ****************************************************************/ + +struct hostnet_pthread_writev_args { + pthread_t cr_thread; + cid_t cr_coroutine; + + int connfd; + const struct iovec *iov; + int iovcnt; + + size_t *ret_size; + host_errno_t *ret_errno; +}; + +static void *hostnet_pthread_writev(void *_args) { + struct hostnet_pthread_writev_args *args = _args; + + size_t count = 0; + struct iovec *iov = stack_alloc(args->iovcnt, struct iovec); + for (int i = 0; i < args->iovcnt; i++) { + iov[i] = args->iov[i]; + count += args->iov[i].iov_len; + } + int iovcnt = args->iovcnt; + + size_t done = 0; + while (done < count) { + ssize_t r = writev(args->connfd, iov, iovcnt); + if (r < 0) { + *args->ret_errno = errno; + break; + } + done += r; + while (iovcnt && (size_t)r >= iov[0].iov_len) { + r -= iov[0].iov_len; + iov++; + iovcnt--; + } + if (r > 0) { + iov[0].iov_base += r; + iov[0].iov_len -= r; + } + } + if (done == count) + *args->ret_errno = 0; + *args->ret_size = done; + WAKE_COROUTINE(args); + return NULL; +} + +static size_t_and_error hostnet_tcp_writev(struct _hostnet_tcp_conn *conn, const struct iovec *iov, int iovcnt) { + assert(conn); + assert(iov); + assert(iovcnt > 0); + size_t count = 0; + for (int i = 0; i < iovcnt; i++) + count += iov[i].iov_len; + assert(count); + + size_t ret_size; + host_errno_t ret_errno; + struct hostnet_pthread_writev_args args = { + .cr_thread = pthread_self(), + .cr_coroutine = cr_getcid(), + + .connfd = conn->fd, + .iov = iov, + .iovcnt = iovcnt, + + .ret_size = &ret_size, + .ret_errno = &ret_errno, + }; + int thread_errno = RUN_PTHREAD(hostnet_pthread_writev, &args); + if (thread_errno) + return ERROR_AND(size_t, 0, errno2lm(thread_errno)); + return ERROR_AND(size_t, ret_size, ret_errno ? hostnet_error(ret_errno, OP_SEND) : ERROR_NULL); +} + +/* TCP close() ****************************************************************/ + +static error hostnet_tcp_close(struct _hostnet_tcp_conn *conn) { + assert(conn); + if (shutdown(conn->fd, SHUT_RDWR)) + return hostnet_error(errno, OP_CLOSE); + return ERROR_NULL; +} +static error hostnet_tcp_close_read(struct _hostnet_tcp_conn *conn) { + assert(conn); + if (shutdown(conn->fd, SHUT_RD)) + return hostnet_error(errno, OP_CLOSE); + return ERROR_NULL; +} +static error hostnet_tcp_close_write(struct _hostnet_tcp_conn *conn) { + assert(conn); + if (shutdown(conn->fd, SHUT_WR)) + return hostnet_error(errno, OP_CLOSE); + return ERROR_NULL; +} + +/* UDP init() *****************************************************************/ + +void hostnet_udp_conn_init(struct hostnet_udp_conn *self, uint16_t port) { + int fd; + union { + struct sockaddr_in in; + struct sockaddr gen; + struct sockaddr_storage stor; + } addr = {}; + + hostnet_init(); + + addr.in.sin_family = AF_INET; + addr.in.sin_port = htons(port); + fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) + __error(1, errno, "socket"); + if (setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &(int){1}, sizeof(int)) < 0) + __error(1, errno, "setsockopt(fd=%d, SO_BROADCAST=1)", fd); + if (bind(fd, &addr.gen, sizeof addr) < 0) + __error(1, errno, "bind"); + + self->fd = fd; + self->read_deadline_ns = 0; +} + +/* UDP sendto() ***************************************************************/ + +struct hostnet_pthread_sendto_args { + pthread_t cr_thread; + cid_t cr_coroutine; + + int connfd; + void *buf; + size_t count; + struct net_ip4_addr node; + uint16_t port; + + size_t *ret_size; + host_errno_t *ret_errno; +}; + +static void *hostnet_pthread_sendto(void *_args) { + struct hostnet_pthread_sendto_args *args = _args; + union { + struct sockaddr_in in; + struct sockaddr gen; + struct sockaddr_storage stor; + } addr = {}; + + addr.in.sin_family = AF_INET; + addr.in.sin_addr.s_addr = + (((uint32_t)args->node.octets[3])<<24) | + (((uint32_t)args->node.octets[2])<<16) | + (((uint32_t)args->node.octets[1])<< 8) | + (((uint32_t)args->node.octets[0])<< 0) ; + addr.in.sin_port = htons(args->port); + ssize_t r = sendto(args->connfd, args->buf, args->count, 0, &addr.gen, sizeof(addr)); + if (r < 0) { + *args->ret_size = 0; + *args->ret_errno = errno; + } else { + *args->ret_size = r; + *args->ret_errno = 0; + } + WAKE_COROUTINE(args); + return NULL; +} + +static error hostnet_udp_sendto(struct hostnet_udp_conn *conn, void *buf, size_t count, + struct net_ip4_addr node, uint16_t port) { + assert(conn); + + size_t ret_size; + host_errno_t ret_errno; + struct hostnet_pthread_sendto_args args = { + .cr_thread = pthread_self(), + .cr_coroutine = cr_getcid(), + + .connfd = conn->fd, + .buf = buf, + .count = count, + .node = node, + .port = port, + + .ret_size = &ret_size, + .ret_errno = &ret_errno, + }; + int thread_errno = RUN_PTHREAD(hostnet_pthread_sendto, &args); + if (thread_errno) + return errno2lm(thread_errno); + if (ret_errno) + return hostnet_error(ret_errno, OP_SEND); + return ERROR_NULL; +} + +/* UDP recvfrom() *************************************************************/ + +static void hostnet_udp_set_recv_deadline(struct hostnet_udp_conn *conn, + uint64_t ts_ns) { + assert(conn); + + conn->read_deadline_ns = ts_ns; +} + +struct hostnet_pthread_recvfrom_args { + pthread_t cr_thread; + cid_t cr_coroutine; + + int connfd; + struct timeval timeout; + void *buf; + size_t count; + + size_t *ret_size; + host_errno_t *ret_errno; + struct net_ip4_addr *ret_node; + uint16_t *ret_port; +}; + +static void *hostnet_pthread_recvfrom(void *_args) { + struct hostnet_pthread_recvfrom_args *args = _args; + + union { + struct sockaddr_in in; + struct sockaddr gen; + struct sockaddr_storage stor; + } addr = {}; + socklen_t addr_size = sizeof(addr); + + int r_i = setsockopt(args->connfd, SOL_SOCKET, SO_RCVTIMEO, + &args->timeout, sizeof(args->timeout)); + if (r_i) { + *args->ret_size = 0; + *args->ret_errno = errno; + goto end; + } + + ssize_t r_ss = recvfrom(args->connfd, args->buf, args->count, + MSG_TRUNC, &addr.gen, &addr_size); + if (r_ss < 0) { + *args->ret_size = 0; + *args->ret_errno = errno; + goto end; + } + *args->ret_size = r_ss; + *args->ret_errno = 0; + + assert(addr.in.sin_family == AF_INET); + if (args->ret_node) { + args->ret_node->octets[3] = (addr.in.sin_addr.s_addr >> 24) & 0xFF; + args->ret_node->octets[2] = (addr.in.sin_addr.s_addr >> 16) & 0xFF; + args->ret_node->octets[1] = (addr.in.sin_addr.s_addr >> 8) & 0xFF; + args->ret_node->octets[0] = (addr.in.sin_addr.s_addr >> 0) & 0xFF; + } + if (args->ret_port) { + (*args->ret_port) = ntohs(addr.in.sin_port); + } + + end: + WAKE_COROUTINE(args); + return NULL; +} + +static size_t_or_error hostnet_udp_recvfrom(struct hostnet_udp_conn *conn, void *buf, size_t count, + struct net_ip4_addr *ret_node, uint16_t *ret_port) { + assert(conn); + + size_t ret_size; + host_errno_t ret_errno; + struct hostnet_pthread_recvfrom_args args = { + .cr_thread = pthread_self(), + .cr_coroutine = cr_getcid(), + + .connfd = conn->fd, + .buf = buf, + .count = count, + + .ret_size = &ret_size, + .ret_errno = &ret_errno, + .ret_node = ret_node, + .ret_port = ret_port, + }; + if (conn->read_deadline_ns) { + uint64_t now_ns = LO_CALL(bootclock, get_time_ns); + if (conn->read_deadline_ns < now_ns) + return ERROR_NEW_ERR(size_t, error_new(E_NET_ERECV_TIMEOUT)); + args.timeout = ns_to_host_us_time(conn->read_deadline_ns-now_ns); + } else { + args.timeout = (host_us_time_t){}; + } + + host_errno_t thread_errno = RUN_PTHREAD(hostnet_pthread_recvfrom, &args); + if (thread_errno) + return ERROR_NEW_ERR(size_t, errno2lm(thread_errno)); + if (ret_errno) + return ERROR_NEW_ERR(size_t, hostnet_error(ret_errno, OP_RECV)); + return ERROR_NEW_VAL(size_t, ret_size); +} + +/* UDP close() ****************************************************************/ + +static error hostnet_udp_close(struct hostnet_udp_conn *conn) { + assert(conn); + + if (close(conn->fd)) + return hostnet_error(errno, OP_CLOSE); + return ERROR_NULL; +} diff --git a/libhw_cr/host_util.c b/libhw_cr/host_util.c new file mode 100644 index 0000000..2d45490 --- /dev/null +++ b/libhw_cr/host_util.c @@ -0,0 +1,145 @@ +/* libhw_cr/host_util.c - Utilities for GNU/Linux hosts + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <errno.h> /* for E* */ +#include <signal.h> /* for SIGRTMIN, SIGRTMAX */ + +#define error __error +#include <error.h> +#undef error + +#include <libhw/generic/alarmclock.h> /* for {X}S_PER_S */ + +#include "host_util.h" + +int host_sigrt_alloc(void) { + static int next = 0; + + if (!next) + next = SIGRTMIN; + int ret = next++; + if (ret > SIGRTMAX) + __error(1, 0, "SIGRTMAX exceeded"); + return ret; +} + +host_us_time_t ns_to_host_us_time(uint64_t time_ns) { + host_us_time_t ret; + ret.tv_sec = time_ns + /NS_PER_S; + ret.tv_usec = (time_ns - ((uint64_t)ret.tv_sec)*NS_PER_S) + /(NS_PER_S/US_PER_S); + return ret; +} + +host_ns_time_t ns_to_host_ns_time(uint64_t time_ns) { + host_ns_time_t ret; + ret.tv_sec = time_ns + /NS_PER_S; + ret.tv_nsec = time_ns - ((uint64_t)ret.tv_sec)*NS_PER_S; + return ret; +} + +uint64_t ns_from_host_us_time(host_us_time_t host_time) { + return (((uint64_t)host_time.tv_sec) * NS_PER_S) + + ((uint64_t)host_time.tv_usec * (NS_PER_S/US_PER_S)); +} + +uint64_t ns_from_host_ns_time(host_ns_time_t host_time) { + return (((uint64_t)host_time.tv_sec) * NS_PER_S) + + ((uint64_t)host_time.tv_nsec); +} + +_errnum errno_host2lm(host_errno_t host) { + switch (host) { + case E2BIG: return E_POSIX_E2BIG; + case EACCES: return E_POSIX_EACCES; + case EADDRINUSE: return E_POSIX_EADDRINUSE; + case EADDRNOTAVAIL: return E_POSIX_EADDRNOTAVAIL; + case EAFNOSUPPORT: return E_POSIX_EAFNOSUPPORT; + case EAGAIN: return E_POSIX_EAGAIN; + case EALREADY: return E_POSIX_EALREADY; + case EBADF: return E_POSIX_EBADF; + case EBADMSG: return E_POSIX_EBADMSG; + case EBUSY: return E_POSIX_EBUSY; + case ECANCELED: return E_POSIX_ECANCELED; + case ECHILD: return E_POSIX_ECHILD; + case ECONNABORTED: return E_POSIX_ECONNABORTED; + case ECONNREFUSED: return E_POSIX_ECONNREFUSED; + case ECONNRESET: return E_POSIX_ECONNRESET; + case EDEADLK: return E_POSIX_EDEADLK; + case EDESTADDRREQ: return E_POSIX_EDESTADDRREQ; + case EDOM: return E_POSIX_EDOM; + case EDQUOT: return E_POSIX_EDQUOT; + case EEXIST: return E_POSIX_EEXIST; + case EFAULT: return E_POSIX_EFAULT; + case EFBIG: return E_POSIX_EFBIG; + case EHOSTUNREACH: return E_POSIX_EHOSTUNREACH; + case EIDRM: return E_POSIX_EIDRM; + case EILSEQ: return E_POSIX_EILSEQ; + case EINPROGRESS: return E_POSIX_EINPROGRESS; + case EINTR: return E_POSIX_EINTR; + case EINVAL: return E_POSIX_EINVAL; + case EIO: return E_POSIX_EIO; + case EISCONN: return E_POSIX_EISCONN; + case EISDIR: return E_POSIX_EISDIR; + case ELOOP: return E_POSIX_ELOOP; + case EMFILE: return E_POSIX_EMFILE; + case EMLINK: return E_POSIX_EMLINK; + case EMSGSIZE: return E_POSIX_EMSGSIZE; + case EMULTIHOP: return E_POSIX_EMULTIHOP; + case ENAMETOOLONG: return E_POSIX_ENAMETOOLONG; + case ENETDOWN: return E_POSIX_ENETDOWN; + case ENETRESET: return E_POSIX_ENETRESET; + case ENETUNREACH: return E_POSIX_ENETUNREACH; + case ENFILE: return E_POSIX_ENFILE; + case ENOBUFS: return E_POSIX_ENOBUFS; + case ENODEV: return E_POSIX_ENODEV; + case ENOENT: return E_POSIX_ENOENT; + case ENOEXEC: return E_POSIX_ENOEXEC; + case ENOLCK: return E_POSIX_ENOLCK; + case ENOLINK: return E_POSIX_ENOLINK; + case ENOMEM: return E_POSIX_ENOMEM; + case ENOMSG: return E_POSIX_ENOMSG; + case ENOPROTOOPT: return E_POSIX_ENOPROTOOPT; + case ENOSPC: return E_POSIX_ENOSPC; + case ENOSYS: return E_POSIX_ENOSYS; + case ENOTCONN: return E_POSIX_ENOTCONN; + case ENOTDIR: return E_POSIX_ENOTDIR; + case ENOTEMPTY: return E_POSIX_ENOTEMPTY; + case ENOTRECOVERABLE: return E_POSIX_ENOTRECOVERABLE; + case ENOTSOCK: return E_POSIX_ENOTSOCK; + case ENOTSUP: return E_POSIX_ENOTSUP; + case ENOTTY: return E_POSIX_ENOTTY; + case ENXIO: return E_POSIX_ENXIO; + case EOVERFLOW: return E_POSIX_EOVERFLOW; + case EOWNERDEAD: return E_POSIX_EOWNERDEAD; + case EPERM: return E_POSIX_EPERM; + case EPIPE: return E_POSIX_EPIPE; + case EPROTO: return E_POSIX_EPROTO; + case EPROTONOSUPPORT: return E_POSIX_EPROTONOSUPPORT; + case EPROTOTYPE: return E_POSIX_EPROTOTYPE; + case ERANGE: return E_POSIX_ERANGE; + case EROFS: return E_POSIX_EROFS; + case ESOCKTNOSUPPORT: return E_POSIX_ESOCKTNOSUPPORT; + case ESPIPE: return E_POSIX_ESPIPE; + case ESRCH: return E_POSIX_ESRCH; + case ESTALE: return E_POSIX_ESTALE; + case ETIMEDOUT: return E_POSIX_ETIMEDOUT; + case ETXTBSY: return E_POSIX_ETXTBSY; + case EXDEV: return E_POSIX_EXDEV; + default: + switch (host) { + case EOPNOTSUPP: return E_POSIX_EOPNOTSUPP; + case EWOULDBLOCK: return E_POSIX_EWOULDBLOCK; + default: return E_EUNKNOWN; + } + } +} + +error errno2lm(host_errno_t host) { + return error_new(errno_host2lm(host)); +} diff --git a/libhw_cr/host_util.h b/libhw_cr/host_util.h new file mode 100644 index 0000000..7e559ef --- /dev/null +++ b/libhw_cr/host_util.h @@ -0,0 +1,30 @@ +/* libhw_cr/host_util.h - Utilities for GNU/Linux hosts + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#ifndef _LIBHW_CR_HOST_UTIL_H_ +#define _LIBHW_CR_HOST_UTIL_H_ + +#include <stdint.h> /* for uint{n}_t */ +#include <sys/time.h> /* for struct timeval */ +#include <time.h> /* for struct timespec */ + +#include <libmisc/error.h> /* for _errnum, error */ + +int host_sigrt_alloc(void); + +typedef struct timeval host_us_time_t; +typedef struct timespec host_ns_time_t; + +host_us_time_t ns_to_host_us_time(uint64_t time_ns); +host_ns_time_t ns_to_host_ns_time(uint64_t time_ns); +uint64_t ns_from_host_us_time(host_us_time_t host_time); +uint64_t ns_from_host_ns_time(host_ns_time_t host_time); + +#define host_errno_t int +_errnum errno_host2lm(host_errno_t host); +error errno2lm(host_errno_t host); + +#endif /* _LIBHW_CR_HOST_UTIL_H_ */ diff --git a/libhw_cr/rp2040_dma.c b/libhw_cr/rp2040_dma.c new file mode 100644 index 0000000..8901f06 --- /dev/null +++ b/libhw_cr/rp2040_dma.c @@ -0,0 +1,72 @@ +/* libhw_cr/rp2040_dma.c - Utilities for sharing the DMA IRQs + * + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <hardware/irq.h> /* for irq_set_exclusive_handler() */ + +#include "rp2040_dma.h" + +/* Borrowed from <hardware/dma.h> *********************************************/ + +dma_channel_hw_t *dma_channel_hw_addr(uint channel) { + assert(channel < NUM_DMA_CHANNELS); + return &dma_hw->ch[channel]; +} + +enum dma_channel_transfer_size { + DMA_SIZE_8 = 0, ///< Byte transfer (8 bits) + DMA_SIZE_16 = 1, ///< Half word transfer (16 bits) + DMA_SIZE_32 = 2 ///< Word transfer (32 bits) +}; + +/* Our own code ***************************************************************/ + +struct dmairq_handler_entry { + dmairq_handler_t fn; + void *arg; +}; +struct dmairq_handler_entry dmairq_handlers[NUM_DMA_CHANNELS] = {}; + +bool dmairq_initialized[NUM_DMA_IRQS] = {}; + +static void dmairq_handler(void) { + enum dmairq irq = __get_current_exception() - VTABLE_FIRST_IRQ; + size_t irq_idx = irq - DMAIRQ_0; + assert(irq_idx < NUM_DMA_IRQS); + + uint32_t regval = dma_hw->irq_ctrl[irq_idx].ints; + for (uint channel = 0; channel < NUM_DMA_CHANNELS; channel++) { + if (regval & 1u<<channel) { + struct dmairq_handler_entry *handler = &dmairq_handlers[channel]; + if (handler->fn) + handler->fn(handler->arg, irq, channel); + } + } + /* acknowledge irq */ + dma_hw->intr = regval; +} + +void dmairq_set_and_enable_exclusive_handler(enum dmairq irq, uint channel, dmairq_handler_t fn, void *arg) { + assert(irq == DMAIRQ_0 || irq == DMAIRQ_1); + assert(channel < NUM_DMA_CHANNELS); + assert(fn); + + assert(dmairq_handlers[channel].fn == NULL); + + dmairq_handlers[channel].fn = fn; + dmairq_handlers[channel].arg = arg; + + size_t irq_idx = irq - DMAIRQ_0; + hw_set_bits(&dma_hw->irq_ctrl[irq_idx].inte, 1u<<channel); + + if (!dmairq_initialized[irq_idx]) { + irq_set_exclusive_handler(irq, dmairq_handler); + irq_set_enabled(irq, true); + dmairq_initialized[irq_idx] = true; + } +} diff --git a/libhw_cr/rp2040_dma.h b/libhw_cr/rp2040_dma.h new file mode 100644 index 0000000..5c1c7bb --- /dev/null +++ b/libhw_cr/rp2040_dma.h @@ -0,0 +1,128 @@ +/* libhw_cr/rp2040_dma.h - Utilities for using DMA on the RP2040 (replaces <hardware/dma.h>) + * + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#ifndef _LIBHW_CR_RP2040_DMA_H_ +#define _LIBHW_CR_RP2040_DMA_H_ + +#include <assert.h> +#include <stddef.h> /* for offsetof() */ +#include <stdint.h> /* for uint32_t */ + +#include <hardware/regs/dreq.h> /* for DREQ_* for use with DMA_CTRL_TREQ_SEL() */ +#include <hardware/structs/dma.h> /* for dma_hw, dma_channel_hw_t, DMA_NUM_CHANNELS */ + +#include <libmisc/macro.h> /* for LM_FLOORLOG2() */ + +/* Borrowed from <hardware/dma.h> *********************************************/ + +dma_channel_hw_t *dma_channel_hw_addr(uint channel); + +enum dma_channel_transfer_size { + DMA_SIZE_8 = 0, ///< Byte transfer (8 bits) + DMA_SIZE_16 = 1, ///< Half word transfer (16 bits) + DMA_SIZE_32 = 2 ///< Word transfer (32 bits) +}; + +/* Our own code ***************************************************************/ + +enum dmairq { + DMAIRQ_0 = DMA_IRQ_0, + DMAIRQ_1 = DMA_IRQ_1, +}; + +typedef void (*dmairq_handler_t)(void *arg, enum dmairq irq, uint channel); + +/** + * Register `fn(arg, ...)` to be called when `channel` completes or + * has a NULL trigger (depending on the channel's configuration). + * + * Your handler does not need to acknowledge the IRQ; that will be + * done for you after your handler is called. + * + * It is illegal to enable the same channel on more than one IRQ. + */ +void dmairq_set_and_enable_exclusive_handler(enum dmairq irq, uint channel, dmairq_handler_t fn, void *arg); + +#define DMA_CTRL_ENABLE (1<<0) +#define DMA_CTRL_HI_PRIO (1<<1) +#define DMA_CTRL_DATA_SIZE(sz) ((sz)<<2) +#define DMA_CTRL_INCR_READ (1<<4) +#define DMA_CTRL_INCR_WRITE (1<<5) +#define _DMA_CTRL_RING_BITS(b) ((b)<<6) +#define _DMA_CTRL_RING_RD (0) +#define _DMA_CTRL_RING_WR (1<<10) +#define DMA_CTRL_RING(rdwr, bits) (_DMA_CTRL_RING_##rdwr | _DMA_CTRL_RING_BITS(bits)) +#define DMA_CTRL_CHAIN_TO(ch) ((ch)<<11) +#define DMA_CTRL_TREQ_SEL(dreq) ((dreq)<<15) +#define DMA_CTRL_IRQ_QUIET (1<<21) +#define DMA_CTRL_BSWAP (1<<22) +#define DMA_CTRL_SNIFF_EN (1<<23) + +/* | elem | val | name */ +#define READ_ADDR /*|*/volatile const void/*|*/ * /*|*/read_addr +#define WRITE_ADDR /*|*/volatile void/*|*/ * /*|*/write_addr +#define TRANS_COUNT /*|*/ /*|*/uint32_t/*|*/trans_count +#define CTRL /*|*/ /*|*/uint32_t/*|*/ctrl + +/* { +0x0 ; +0x4 ; +0x8 ; +0xC (Trigger) */ +struct dma_alias0 { READ_ADDR ; WRITE_ADDR ; TRANS_COUNT ; CTRL ; }; +struct dma_alias1 { CTRL ; READ_ADDR ; WRITE_ADDR ; TRANS_COUNT ; }; +struct dma_alias2 { CTRL ; TRANS_COUNT ; READ_ADDR ; WRITE_ADDR ; }; +struct dma_alias3 { CTRL ; WRITE_ADDR ; TRANS_COUNT ; READ_ADDR ; }; +struct dma_alias0_short2 { TRANS_COUNT ; CTRL ; }; +struct dma_alias1_short2 { WRITE_ADDR ; TRANS_COUNT ; }; +struct dma_alias2_short2 { READ_ADDR ; WRITE_ADDR ; }; +struct dma_alias3_short2 { TRANS_COUNT ; READ_ADDR ; }; +struct dma_alias0_short3 { CTRL ; }; +struct dma_alias1_short3 { TRANS_COUNT ; }; +struct dma_alias2_short3 { WRITE_ADDR ; }; +struct dma_alias3_short3 { READ_ADDR ; }; + +#undef CTRL +#undef TRANS_COUNT +#undef WRITE_ADDR +#undef READ_ADDR + +#define DMA_CHAN_ADDR(CH, TYP) ((TYP *volatile)_Generic((TYP){}, \ + struct dma_alias0: &dma_channel_hw_addr(CH)->read_addr, \ + struct dma_alias1: &dma_channel_hw_addr(CH)->al1_ctrl, \ + struct dma_alias2: &dma_channel_hw_addr(CH)->al2_ctrl, \ + struct dma_alias3: &dma_channel_hw_addr(CH)->al3_ctrl, \ + struct dma_alias0_short2: &dma_channel_hw_addr(CH)->transfer_count, \ + struct dma_alias1_short2: &dma_channel_hw_addr(CH)->al1_write_addr, \ + struct dma_alias2_short2: &dma_channel_hw_addr(CH)->al2_read_addr, \ + struct dma_alias3_short2: &dma_channel_hw_addr(CH)->al3_transfer_count, \ + struct dma_alias0_short3: &dma_channel_hw_addr(CH)->ctrl_trig, \ + struct dma_alias1_short3: &dma_channel_hw_addr(CH)->al1_transfer_count_trig, \ + struct dma_alias2_short3: &dma_channel_hw_addr(CH)->al2_write_addr_trig, \ + struct dma_alias3_short3: &dma_channel_hw_addr(CH)->al3_read_addr_trig)) + +#define DMA_IS_TRIGGER(TYP, FIELD) (offsetof(TYP, FIELD) == 0xC) + +#define DMA_CHAN_WR_TRANS_COUNT(TYP) \ + (sizeof(TYP)/4) + +#define DMA_CHAN_WR_CTRL(TYP) ( DMA_CTRL_DATA_SIZE(DMA_SIZE_32) \ + | DMA_CTRL_INCR_WRITE \ + | DMA_CTRL_RING(WR, LM_FLOORLOG2(sizeof(TYP))) \ + ) + +#define DMA_NONTRIGGER(CH, FIELD) (DMA_CHAN_ADDR(CH, _DMA_NONTRIGGER_##FIELD)->FIELD) +#define _DMA_NONTRIGGER_read_addr struct dma_alias0 +#define _DMA_NONTRIGGER_write_addr struct dma_alias0 +#define _DMA_NONTRIGGER_trans_count struct dma_alias0 +#define _DMA_NONTRIGGER_ctrl struct dma_alias1 + +#define DMA_TRIGGER(CH, FIELD) (DMA_CHAN_ADDR(CH, _DMA_TRIGGER_##FIELD)->FIELD) +#define _DMA_TRIGGER_read_addr struct dma_alias3 +#define _DMA_TRIGGER_write_addr struct dma_alias2 +#define _DMA_TRIGGER_trans_count struct dma_alias1 +#define _DMA_TRIGGER_ctrl struct dma_alias0 + +#endif /* _LIBHW_CR_RP2040_DMA_H_ */ diff --git a/libhw_cr/rp2040_gpioirq.c b/libhw_cr/rp2040_gpioirq.c new file mode 100644 index 0000000..ecbdb04 --- /dev/null +++ b/libhw_cr/rp2040_gpioirq.c @@ -0,0 +1,75 @@ +/* libhw_cr/rp2040_gpioirq.c - Utilities for sharing the GPIO IRQ (IO_IRQ_BANK0) + * + * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <hardware/irq.h> /* for irq_set_exclusive_handler() */ +#include <hardware/structs/io_bank0.h> /* for io_bank0_hw */ + +#include <libmisc/macro.h> + +#include "rp2040_gpioirq.h" + +struct gpioirq_handler_entry { + gpioirq_handler_t fn; + void *arg; +}; +struct gpioirq_handler_entry gpioirq_handlers[NUM_BANK0_GPIOS][4] = {}; + +int gpioirq_core = -1; + +static void gpioirq_handler(void) { + uint core = get_core_num(); + io_bank0_irq_ctrl_hw_t *irq_ctrl_base; + switch (core) { + case 0: irq_ctrl_base = &io_bank0_hw->proc0_irq_ctrl; break; + case 1: irq_ctrl_base = &io_bank0_hw->proc1_irq_ctrl; break; + default: assert_notreached("invalid core number"); + } + for (uint regnum = 0; regnum < LM_ARRAY_LEN(irq_ctrl_base->ints); regnum++) { + uint32_t regval = irq_ctrl_base->ints[regnum]; + for (uint bit = 0; bit < 32 && (regnum*8)+(bit/4) < NUM_BANK0_GPIOS; bit++) { + if (regval & 1u<<bit) { + uint gpio = (regnum*8)+(bit/4); + uint event_idx = bit%4; + struct gpioirq_handler_entry *handler = &gpioirq_handlers[gpio][event_idx]; + if (handler->fn) + handler->fn(handler->arg, gpio, 1u<<event_idx); + } + } + /* acknowledge irq */ + io_bank0_hw->intr[regnum] = regval; + } +} + +void gpioirq_set_and_enable_exclusive_handler(uint gpio, enum gpio_irq_level event, gpioirq_handler_t fn, void *arg) { + assert(gpio < NUM_BANK0_GPIOS); + assert(event == GPIO_IRQ_LEVEL_LOW || + event == GPIO_IRQ_LEVEL_HIGH || + event == GPIO_IRQ_EDGE_FALL || + event == GPIO_IRQ_EDGE_RISE); + assert(fn); + + uint event_idx = LM_FLOORLOG2(event); + assert(gpioirq_handlers[gpio][event_idx].fn == NULL); + + uint core = get_core_num(); + assert(gpioirq_core == -1 || gpioirq_core == (int)core); + + io_bank0_irq_ctrl_hw_t *irq_ctrl_base; + switch (core) { + case 0: irq_ctrl_base = &io_bank0_hw->proc0_irq_ctrl; break; + case 1: irq_ctrl_base = &io_bank0_hw->proc1_irq_ctrl; break; + default: assert_notreached("invalid core number"); + } + + gpioirq_handlers[gpio][event_idx].fn = fn; + gpioirq_handlers[gpio][event_idx].arg = arg; + hw_set_bits(&irq_ctrl_base->inte[gpio/8], 1u<<((4*(gpio%8))+event_idx)); + if (gpioirq_core == -1) { + irq_set_exclusive_handler(IO_IRQ_BANK0, gpioirq_handler); + irq_set_enabled(IO_IRQ_BANK0, true); + gpioirq_core = core; + } +} diff --git a/libhw_cr/rp2040_gpioirq.h b/libhw_cr/rp2040_gpioirq.h new file mode 100644 index 0000000..75a264f --- /dev/null +++ b/libhw_cr/rp2040_gpioirq.h @@ -0,0 +1,33 @@ +/* libhw_cr/rp2040_gpioirq.h - Utilities for sharing the GPIO IRQ (IO_IRQ_BANK0) + * + * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#ifndef _LIBHW_CR_RP2040_GPIOIRQ_H_ +#define _LIBHW_CR_RP2040_GPIOIRQ_H_ + +#include <hardware/gpio.h> /* for enum gpio_irq_level */ + +typedef void (*gpioirq_handler_t)(void *arg, uint gpio, enum gpio_irq_level event); + +/** + * Register `fn(arg, ...)` to be called when `event` fires on GPIO pin + * `gpio`. + * + * If multiple events happen close together, the handlers will not + * necessarily be called in-order. + * + * Your handler does not need to acknowledge the IRQ; that will be + * done for you after your handler is called. + * + * It is illegal to call gpioirq_set_and_enable_exclusive_handler() + * on multiple cores. + * + * This is better than the Pico-SDK <hardware/gpio.h> IRQ functions + * because their functions call the handlers for every event, and it + * is up to you to de-mux them in your handler. + */ +void gpioirq_set_and_enable_exclusive_handler(uint gpio, enum gpio_irq_level event, gpioirq_handler_t fn, void *arg); + +#endif /* _LIBHW_CR_RP2040_GPIOIRQ_H_ */ diff --git a/libhw_cr/rp2040_hwspi.c b/libhw_cr/rp2040_hwspi.c new file mode 100644 index 0000000..d717a79 --- /dev/null +++ b/libhw_cr/rp2040_hwspi.c @@ -0,0 +1,272 @@ +/* libhw_cr/rp2040_hwspi.c - <libhw/generic/spi.h> implementation for the RP2040's ARM Primecell SSP (PL022) + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <hardware/clocks.h> /* for clock_get_hz() and clk_peri */ +#include <hardware/gpio.h> +#include <hardware/spi.h> + +#include <libcr/coroutine.h> +#include <libmisc/alloc.h> +#include <libmisc/assert.h> + +#define LOG_NAME RP2040_SPI +#include <libmisc/log.h> + +#define IMPLEMENTATION_FOR_LIBHW_RP2040_HWSPI_H YES +#include <libhw/rp2040_hwspi.h> + +#include <libhw/generic/alarmclock.h> + +#include "rp2040_dma.h" + +#include "config.h" + +#ifndef CONFIG_RP2040_SPI_DEBUG + #error config.h must define CONFIG_RP2040_SPI_DEBUG (bool) +#endif + +LO_IMPLEMENTATION_C(io_duplex_readwriter, struct rp2040_hwspi, rp2040_hwspi, static); +LO_IMPLEMENTATION_C(spi, struct rp2040_hwspi, rp2040_hwspi, static); + +static void rp2040_hwspi_intrhandler(void *_self, enum dmairq LM_UNUSED(irq), uint LM_UNUSED(channel)) { + struct rp2040_hwspi *self = _self; + gpio_put(self->pin_cs, 1); + cr_sema_signal_from_intrhandler(&self->sema); +} + +#define assert_4distinct(a, b, c, d) \ + assert(a != b); \ + assert(a != c); \ + assert(a != d); \ + assert(b != c); \ + assert(b != d); \ + assert(c != d); + +void _rp2040_hwspi_init(struct rp2040_hwspi *self, + enum rp2040_hwspi_instance inst_num, + enum spi_mode mode, + uint baudrate_hz, + uint64_t min_delay_ns, + uint8_t bogus_data, + uint pin_miso, + uint pin_mosi, + uint pin_clk, + uint pin_cs, + uint dma1, + uint dma2, + uint dma3, + uint dma4) +{ + /* Be not weary: This is but 12 lines of actual code; and many + * lines of comments and assert()s. */ + spi_inst_t *inst; + uint actual_baudrate_hz; + + assert(self); + assert(baudrate_hz); + uint32_t clk_peri_hz = clock_get_hz(clk_peri); + log_debugln("clk_peri = ", clk_peri_hz, "Hz"); + assert(baudrate_hz*2 <= clk_peri_hz); + assert_4distinct(pin_miso, pin_mosi, pin_clk, pin_cs); + assert_4distinct(dma1, dma2, dma3, dma4); + + /* 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_notreached("invalid hwspi instance number"); + } + + actual_baudrate_hz = spi_init(inst, baudrate_hz); + log_debugln("baudrate = ", actual_baudrate_hz, "Hz"); + assert(actual_baudrate_hz == 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); + + /* Initialize self. */ + self->inst = inst; + self->min_delay_ns = min_delay_ns; + self->bogus_data = bogus_data; + self->pin_cs = pin_cs; + self->dma_tx_ctrl = dma1; + self->dma_rx_ctrl = dma2; + self->dma_tx_data = dma3; + self->dma_rx_data = dma4; + self->dead_until_ns = 0; + self->sema = (cr_sema_t){}; + + /* Initialize the interrupt handler. */ + /* We do this on (just) the rx channel, because the way the + * SSP works reads necessarily complete *after* writes. */ + dmairq_set_and_enable_exclusive_handler(DMAIRQ_0, self->dma_rx_data, rp2040_hwspi_intrhandler, self); +} + +static size_t_and_error rp2040_hwspi_readwritev(struct rp2040_hwspi *self, const struct duplex_iovec *iov, int iovcnt) { + assert(self); + assert(self->inst); + assert(iov); + assert(iovcnt > 0); + + /* At this time, I have no intention to run SPI faster than + * 80MHz (= 80Mb/s = 10MB/s). If we ran the CPU at just + * 100MHz (we'll be running it faster than that, maybe even + * 200MHz), that means we'd have 10 clock cycles to send each + * byte. + * + * This affords us substantial simplifications, like being + * able to afford 4-cycle changeovers between DMA blocks, and + * not having to worry about alignment because we can just use + * DMA_SIZE_8. + */ + + uint8_t bogus_rx_dst; + + size_t count = 0; + int pruned_iovcnt = 0; + for (int i = 0; i < iovcnt; i++) { + count += iov[i].iov_len; + if (iov[i].iov_len) + pruned_iovcnt++; + } + assert(count); + + /* It doesn't *really* matter which aliases we choose: + * + * - None of our fields can be NULL (so no + * false-termination). + * + * - Moving const fields first so they don't have to be + * re-programmed each time isn't possible for us; there + * need to be at least 2 const fields, and we only have 1 + * (read_addr for rx_data_blocks, and write_addr for + * tx_data_blocks). + * + * The code following this initial declaration is generic to + * the alias, so changing which alias is used is easy. + * + * Since we have no hard requirements, here are some mild + * preferences: + * + * - I like the aliases being different for each channel, + * because it helps prevent alias-specific code from + * sneaking in. + * + * - I like the rx channel (the channel the interrupt handler + * is wired to) having ctrl be the trigger, so that we + * don't have to worry about DMA_CTRL_IRQ_QUIET being + * cleared before the trigger, and at the end the control + * block is clean and zeroed-out. + * + * - Conversely, I like the tx channel (the non-interrupt + * channel) having ctrl *not* be the trigger, so that + * DMA_CTRL_IRQ_QUIET is cleared by the time the trigger + * happens, so the IRQ machinery doesn't need to be engaged + * at all. + */ + struct dma_alias1 *tx_data_blocks = stack_alloc(pruned_iovcnt+1, struct dma_alias1); + struct dma_alias0 *rx_data_blocks = stack_alloc(pruned_iovcnt+1, struct dma_alias0); + static_assert(!DMA_IS_TRIGGER(typeof(tx_data_blocks[0]), ctrl)); + static_assert(DMA_IS_TRIGGER(typeof(rx_data_blocks[0]), ctrl)); + + for (int i = 0, j = 0; i < iovcnt; i++) { + if (!iov[i].iov_len) + continue; + tx_data_blocks[j] = (typeof(tx_data_blocks[0])){ + .read_addr = (iov[i].iov_write_from != IOVEC_DISCARD) ? iov[i].iov_write_from : &self->bogus_data, + .write_addr = &spi_get_hw(self->inst)->dr, + .trans_count = iov[i].iov_len, + .ctrl = (DMA_CTRL_ENABLE + | DMA_CTRL_DATA_SIZE(DMA_SIZE_8) + | ((iov[i].iov_write_from != IOVEC_DISCARD) ? DMA_CTRL_INCR_READ : 0) + | DMA_CTRL_CHAIN_TO(self->dma_tx_ctrl) + | DMA_CTRL_TREQ_SEL(SPI_DREQ_NUM(self->inst, true)) + | DMA_CTRL_IRQ_QUIET), + }; + rx_data_blocks[j] = (typeof(rx_data_blocks[0])){ + .read_addr = &spi_get_hw(self->inst)->dr, + .write_addr = (iov[i].iov_read_to != IOVEC_DISCARD) ? iov[i].iov_read_to : &bogus_rx_dst, + .trans_count = iov[i].iov_len, + .ctrl = (DMA_CTRL_ENABLE + | DMA_CTRL_DATA_SIZE(DMA_SIZE_8) + | ((iov[i].iov_read_to != IOVEC_DISCARD) ? DMA_CTRL_INCR_WRITE : 0) + | DMA_CTRL_CHAIN_TO(self->dma_rx_ctrl) + | DMA_CTRL_TREQ_SEL(SPI_DREQ_NUM(self->inst, false)) + | DMA_CTRL_IRQ_QUIET), + }; + j++; + } + tx_data_blocks[pruned_iovcnt] = (typeof(tx_data_blocks[0])){}; + rx_data_blocks[pruned_iovcnt] = (typeof(rx_data_blocks[0])){}; + /* If ctrl isn't the trigger then we need to make sure that + * DMA_CTRL_IRQ_QUIET isn't cleared before the trigger + * happens. */ + if (!DMA_IS_TRIGGER(typeof(rx_data_blocks[0]), ctrl)) + rx_data_blocks[pruned_iovcnt].ctrl = DMA_CTRL_IRQ_QUIET; + + /* Set up ctrl. */ + DMA_NONTRIGGER(self->dma_tx_ctrl, read_addr) = tx_data_blocks; + DMA_NONTRIGGER(self->dma_tx_ctrl, write_addr) = DMA_CHAN_ADDR(self->dma_tx_data, typeof(tx_data_blocks[0])); + DMA_NONTRIGGER(self->dma_tx_ctrl, trans_count) = DMA_CHAN_WR_TRANS_COUNT(typeof(tx_data_blocks[0])); + DMA_NONTRIGGER(self->dma_tx_ctrl, ctrl) = (DMA_CTRL_ENABLE + | DMA_CHAN_WR_CTRL(typeof(tx_data_blocks[0])) + | DMA_CTRL_INCR_READ + | DMA_CTRL_CHAIN_TO(self->dma_tx_data) + | DMA_CTRL_TREQ_SEL(DREQ_FORCE) + | DMA_CTRL_IRQ_QUIET); + DMA_NONTRIGGER(self->dma_rx_ctrl, read_addr) = rx_data_blocks; + DMA_NONTRIGGER(self->dma_rx_ctrl, write_addr) = DMA_CHAN_ADDR(self->dma_rx_data, typeof(rx_data_blocks[0])); + DMA_NONTRIGGER(self->dma_rx_ctrl, trans_count) = DMA_CHAN_WR_TRANS_COUNT(typeof(rx_data_blocks[0])); + DMA_NONTRIGGER(self->dma_rx_ctrl, ctrl) = (DMA_CTRL_ENABLE + | DMA_CHAN_WR_CTRL(typeof(rx_data_blocks[0])) + | DMA_CTRL_INCR_READ + | DMA_CTRL_CHAIN_TO(self->dma_rx_data) + | DMA_CTRL_TREQ_SEL(DREQ_FORCE) + | DMA_CTRL_IRQ_QUIET); + + /* Run. */ + uint64_t now = LO_CALL(bootclock, get_time_ns); + if (now < self->dead_until_ns) + sleep_until_ns(self->dead_until_ns); + bool saved = cr_save_and_disable_interrupts(); + gpio_put(self->pin_cs, 0); + dma_hw->multi_channel_trigger = (1u<<self->dma_tx_ctrl) | (1u<<self->dma_rx_ctrl); + cr_restore_interrupts(saved); + cr_sema_wait(&self->sema); + self->dead_until_ns = LO_CALL(bootclock, get_time_ns) + self->min_delay_ns; + return ERROR_AND(size_t, count, ERROR_NULL); +} diff --git a/libhw_cr/rp2040_hwtimer.c b/libhw_cr/rp2040_hwtimer.c new file mode 100644 index 0000000..d9f0a24 --- /dev/null +++ b/libhw_cr/rp2040_hwtimer.c @@ -0,0 +1,153 @@ +/* libhw_cr/rp2040_hwtimer.c - <libhw/generic/alarmclock.h> implementation for the RP2040's hardware timer + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <hardware/irq.h> /* pico-sdk:hardware_irq */ +#include <hardware/timer.h> /* pico-sdk:hardware_timer */ + +#include <libcr/coroutine.h> +#include <libmisc/assert.h> + +#define IMPLEMENTATION_FOR_LIBHW_GENERIC_ALARMCLOCK_H YES +#include <libhw/generic/alarmclock.h> + +#include <libhw/rp2040_hwtimer.h> + +/******************************************************************************/ + +/** Conflict with pico-sdk:pico_time:!PICO_TIME_DEFAULT_ALARM_POOL_DISABLED. */ +void add_alarm_at(void) {}; + +/* Types **********************************************************************/ + +struct rp2040_hwtimer { + enum rp2040_hwalarm_instance alarm_num; + bool initialized; + struct alarmclock_trigger *queue; +}; +LO_IMPLEMENTATION_H(alarmclock, struct rp2040_hwtimer, rp2040_hwtimer); +LO_IMPLEMENTATION_C(alarmclock, struct rp2040_hwtimer, rp2040_hwtimer, static); + +/* Globals ********************************************************************/ + +static struct rp2040_hwtimer hwtimers[] = { + { .alarm_num = 0 }, + { .alarm_num = 1 }, + { .alarm_num = 2 }, + { .alarm_num = 3 }, +}; +static_assert(sizeof(hwtimers)/sizeof(hwtimers[0]) == _RP2040_HWALARM_NUM); + +/* Main implementation ********************************************************/ + +lo_interface alarmclock rp2040_hwtimer(enum rp2040_hwalarm_instance alarm_num) { + assert(alarm_num < _RP2040_HWALARM_NUM); + return LO_BOX(alarmclock, &hwtimers[alarm_num]); +} + + +static uint64_t rp2040_hwtimer_get_time_ns(struct rp2040_hwtimer *) { + return timer_time_us_64(timer_hw) * (NS_PER_S/US_PER_S); +} + +#define NS_TO_US_ROUNDUP(x) LM_CEILDIV(x, NS_PER_S/US_PER_S) + +static void rp2040_hwtimer_intrhandler(void) { + uint irq_num = __get_current_exception() - VTABLE_FIRST_IRQ; + enum rp2040_hwalarm_instance alarm_num = TIMER_ALARM_NUM_FROM_IRQ(irq_num); + assert(alarm_num < _RP2040_HWALARM_NUM); + + struct rp2040_hwtimer *alarmclock = &hwtimers[alarm_num]; + + while (alarmclock->queue && + NS_TO_US_ROUNDUP(alarmclock->queue->fire_at_ns) <= timer_time_us_64(timer_hw)) { + struct alarmclock_trigger *trigger = alarmclock->queue; + trigger->cb(trigger->cb_arg); + alarmclock->queue = trigger->next; + trigger->alarmclock = NULL; + trigger->next = NULL; + trigger->prev = NULL; + } + + hw_clear_bits(&timer_hw->intf, 1 << alarm_num); /* Clear "force"ing the interrupt. */ + hw_clear_bits(&timer_hw->intr, 1 << alarm_num); /* Clear natural firing of the alarm. */ + if (alarmclock->queue) + timer_hw->alarm[alarm_num] = (uint32_t)NS_TO_US_ROUNDUP(alarmclock->queue->fire_at_ns); +} + +static bool rp2040_hwtimer_add_trigger(struct rp2040_hwtimer *alarmclock, + struct alarmclock_trigger *trigger, + uint64_t fire_at_ns, + void (*cb)(void *), + void *cb_arg) { + assert(alarmclock); + assert(trigger); + assert(fire_at_ns); + assert(cb); + + uint64_t now_us = timer_time_us_64(timer_hw); + if (NS_TO_US_ROUNDUP(fire_at_ns) > now_us && + (NS_TO_US_ROUNDUP(fire_at_ns) - now_us) > UINT32_MAX) + /* Too far in the future. */ + return true; + + trigger->alarmclock = alarmclock; + trigger->fire_at_ns = fire_at_ns; + trigger->cb = cb; + trigger->cb_arg = cb_arg; + + bool saved = cr_save_and_disable_interrupts(); + struct alarmclock_trigger **dst = &alarmclock->queue; + while (*dst && fire_at_ns >= (*dst)->fire_at_ns) + dst = &(*dst)->next; + trigger->next = *dst; + trigger->prev = *dst ? (*dst)->prev : NULL; + if (*dst) + (*dst)->prev = trigger; + *dst = trigger; + if (!alarmclock->initialized) { + hw_set_bits(&timer_hw->inte, 1 << alarmclock->alarm_num); + irq_set_exclusive_handler(TIMER_ALARM_IRQ_NUM(timer_hw, alarmclock->alarm_num), + rp2040_hwtimer_intrhandler); + irq_set_enabled(TIMER_ALARM_IRQ_NUM(timer_hw, alarmclock->alarm_num), true); + alarmclock->initialized = true; + } + if (alarmclock->queue == trigger) { + /* "Force" the interrupt handler to trigger as soon as + * we enable interrupts. This handles the case of + * when fire_at_ns is before when we called + * cr_save_and_disable_interrupts(). We could check + * timer_time_us_64() again after calling + * cr_save_and_disable_interrupts() and do this + * conditionally, but I don't think that would be any + * more efficient than just letting the interrupt + * fire. */ + hw_set_bits(&timer_hw->intf, 1 << alarmclock->alarm_num); + } + cr_restore_interrupts(saved); + + return false; +} + +static void rp2040_hwtimer_del_trigger(struct rp2040_hwtimer *alarmclock, + struct alarmclock_trigger *trigger) { + assert(alarmclock); + assert(trigger); + + bool saved = cr_save_and_disable_interrupts(); + if (trigger->alarmclock == alarmclock) { + if (!trigger->prev) + alarmclock->queue = trigger->next; + else + trigger->prev->next = trigger->next; + if (trigger->next) + trigger->next->prev = trigger->prev; + trigger->alarmclock = NULL; + trigger->prev = NULL; + trigger->next = NULL; + } else + assert(!trigger->alarmclock); + cr_restore_interrupts(saved); +} diff --git a/libhw_cr/rp2040_include/libhw/rp2040_hwspi.h b/libhw_cr/rp2040_include/libhw/rp2040_hwspi.h new file mode 100644 index 0000000..8d4effa --- /dev/null +++ b/libhw_cr/rp2040_include/libhw/rp2040_hwspi.h @@ -0,0 +1,118 @@ +/* libhw/rp2040_hwspi.h - <libhw/generic/spi.h> implementation for the RP2040's ARM Primecell SSP (PL022) + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#ifndef _LIBHW_RP2040_HWSPI_H_ +#define _LIBHW_RP2040_HWSPI_H_ + +#include <pico/binary_info.h> /* for bi_* */ + +#include <libcr_ipc/sema.h> +#include <libmisc/private.h> + +#include <libhw/generic/spi.h> + +enum rp2040_hwspi_instance { + RP2040_HWSPI_0 = 0, + RP2040_HWSPI_1 = 1, +}; + +struct rp2040_hwspi { + BEGIN_PRIVATE(LIBHW_RP2040_HWSPI_H); + /* const */ + LM_IF(IS_IMPLEMENTATION_FOR(LIBHW_RP2040_HWSPI_H))(spi_inst_t)(void) *inst; + uint64_t min_delay_ns; + uint8_t bogus_data; + uint pin_cs; + uint dma_tx_data; + uint dma_tx_ctrl; + uint dma_rx_data; + uint dma_rx_ctrl; + + /* mutable */ + uint64_t dead_until_ns; + cr_sema_t sema; + END_PRIVATE(LIBHW_RP2040_HWSPI_H); +}; +LO_IMPLEMENTATION_H(io_duplex_readwriter, struct rp2040_hwspi, rp2040_hwspi); +LO_IMPLEMENTATION_H(spi, struct rp2040_hwspi, rp2040_hwspi); + +/** + * 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 baudrate_hz : uint : baudrate in Hz + * @param min_delay_ns: uint64_t : minimum time for pin_cs to be high between messages + * @param bogus_data : uint8_t : bogus data to write when .iov_write_from is IOVEC_DISCARD + * @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 + * @param dma{1-4} : uint : DMA channel; any unused channel + * + * There is no bit-order argument; the RP2040's hardware SPI always + * uses MSB-first bit order. + * + * I know we called this "hwspi", but we're actually going to + * disconnect the CS pin from the PL022 SSP and manually GPIO it from + * the CPU. This is because the PL022 has a maximum of 16-bit frames, + * but 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. + * + * Restrictions on baudrate: + * + * - The PL022 requires that the baudrate is an even-number fraction + * of clk_peri. + * + This implies that the maximum baudrate is clk_peri/2. + * + Pico-SDK' default clk_peri is 125MHz, max is 200MHz. + * - The CS-from-GPIO hack above means that that we can't go so fast + * that the CPU can't do things in time. + * + Experimentally: + * | clk_sys=125MHz | baud=31.25MHz | works OK | + * | clk_sys=160MHz | baud=40 MHz | works OK | + * | clk_sys=170MHz | baud=42.5 MHz | works OK | + * | clk_sys=180MHz | baud=45 MHz | mangled in funny ways? | + * | clk_sys=200MHz | baud=50 MHz | messages get shifted right a bit | + * | clk_sys=125MHz | baud=62.5 MHz | messages get shifted right a bit | + * + * Both of these restrictions aught to be avoidable by using a + * PIO-based SPI driver instead of this PLL02-based driver. + */ +#define rp2040_hwspi_init(self, name, \ + inst_num, mode, baudrate_hz, \ + min_delay_ns, bogus_data, \ + pin_miso, pin_mosi, pin_clk, pin_cs, \ + dma1, dma2, dma3, dma4) \ + do { \ + bi_decl(bi_4pins_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, \ + min_delay_ns, bogus_data, \ + pin_miso, pin_mosi, pin_clk, pin_cs, \ + dma1, dma2, dma3, dma4); \ + } while (0) +void _rp2040_hwspi_init(struct rp2040_hwspi *self, + enum rp2040_hwspi_instance inst_num, + enum spi_mode mode, + uint baudrate_hz, + uint64_t min_delay_ns, + uint8_t bogus_data, + uint pin_miso, + uint pin_mosi, + uint pin_clk, + uint pin_cs, + uint dma1, + uint dma2, + uint dma3, + uint dma4); + +#endif /* _LIBHW_RP2040_HWSPI_H_ */ diff --git a/libhw_cr/rp2040_include/libhw/rp2040_hwtimer.h b/libhw_cr/rp2040_include/libhw/rp2040_hwtimer.h new file mode 100644 index 0000000..40e4172 --- /dev/null +++ b/libhw_cr/rp2040_include/libhw/rp2040_hwtimer.h @@ -0,0 +1,25 @@ +/* libhw/rp2040_hwtimer.h - <libhw/generic/alarmclock.h> implementation for the RP2040's hardware timer + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#ifndef _LIBHW_RP2040_HWTIMER_H_ +#define _LIBHW_RP2040_HWTIMER_H_ + +#include <libhw/generic/alarmclock.h> + +/** + * The RP2040 has one system "timer" with 4 alarm interrupts. + */ +enum rp2040_hwalarm_instance { + RP2040_HWALARM_0 = 0, + RP2040_HWALARM_1 = 1, + RP2040_HWALARM_2 = 2, + RP2040_HWALARM_3 = 3, + _RP2040_HWALARM_NUM, +}; + +lo_interface alarmclock rp2040_hwtimer(enum rp2040_hwalarm_instance alarm_num); + +#endif /* _LIBHW_RP2040_HWTIMER_H_ */ diff --git a/libhw_cr/rp2040_include/libhw/w5500.h b/libhw_cr/rp2040_include/libhw/w5500.h new file mode 100644 index 0000000..8dda1a1 --- /dev/null +++ b/libhw_cr/rp2040_include/libhw/w5500.h @@ -0,0 +1,114 @@ +/* libhw/w5500.h - <libhw/generic/net.h> implementation for the WIZnet W5500 chip + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#ifndef _LIBHW_W5500_H_ +#define _LIBHW_W5500_H_ + +#include <pico/binary_info.h> /* for bi_* */ + +#include <libcr_ipc/chan.h> +#include <libcr_ipc/mutex.h> +#include <libcr_ipc/sema.h> +#include <libmisc/private.h> + +#include <libhw/generic/net.h> +#include <libhw/generic/spi.h> + +CR_CHAN_DECLARE(_w5500_sockintr_ch, uint8_t); + +struct _w5500_socket { + BEGIN_PRIVATE(LIBHW_W5500_H); + /* const-after-init */ + uint8_t socknum; + + /* mutable */ + struct _w5500_socket *next_free; + enum { + W5500_MODE_NONE = 0, + W5500_MODE_TCP, + W5500_MODE_UDP, + } mode; + uint16_t port; /* MODE_{TCP,UDP} */ + uint64_t read_deadline_ns; /* MODE_{TCP,UDP} */ + cr_sema_t listen_sema; /* MODE_TCP */ + cr_sema_t read_sema; /* MODE_{TCP,UDP} */ + _w5500_sockintr_ch_t write_ch; /* MODE_{TCP,UDP} */ + bool list_open, read_open, write_open; /* MODE_TCP */ + + END_PRIVATE(LIBHW_W5500_H); +}; + +LO_IMPLEMENTATION_H(io_closer, struct _w5500_socket, w5500_tcplist); +LO_IMPLEMENTATION_H(net_stream_listener, struct _w5500_socket, w5500_tcplist); + +LO_IMPLEMENTATION_H(io_reader, struct _w5500_socket, w5500_tcp); +LO_IMPLEMENTATION_H(io_writer, struct _w5500_socket, w5500_tcp); +LO_IMPLEMENTATION_H(io_readwriter, struct _w5500_socket, w5500_tcp); +LO_IMPLEMENTATION_H(io_closer, struct _w5500_socket, w5500_tcp); +LO_IMPLEMENTATION_H(io_bidi_closer, struct _w5500_socket, w5500_tcp); +LO_IMPLEMENTATION_H(net_stream_conn, struct _w5500_socket, w5500_tcp); + +LO_IMPLEMENTATION_H(io_closer, struct _w5500_socket, w5500_udp); +LO_IMPLEMENTATION_H(net_packet_conn, struct _w5500_socket, w5500_udp); + +struct w5500 { + BEGIN_PRIVATE(LIBHW_W5500_H); + /* const-after-init */ + lo_interface spi spidev; + uint pin_intr; + uint pin_reset; + struct net_eth_addr hwaddr; + + /* mutable */ + uint16_t next_local_port; + struct _w5500_socket sockets[8]; + struct _w5500_socket *free; + cr_sema_t intr; + cr_mutex_t mu; + END_PRIVATE(LIBHW_W5500_H); +}; +LO_IMPLEMENTATION_H(net_iface, struct w5500, w5500_if); + +/** + * Initialize a WIZnet W5500 Ethernet-and-TCP/IP-offload chip. + * + * The W5500 has 3 lines 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.) + * - A reset pin that the MCU can pull low to reset the W5500. + */ +#define w5500_init(self, name, spi, pin_intr, pin_reset, eth_addr) do { \ + bi_decl(bi_2pins_with_names(pin_intr, name" interrupt", \ + pin_reset, name" reset")); \ + _w5500_init(self, spi, pin_intr, pin_reset, eth_addr); \ + } while (0) +void _w5500_init(struct w5500 *self, + lo_interface spi spi, uint pin_intr, uint pin_reset, + struct net_eth_addr addr); + +/** + * Perform a hard reset on the chip (pull the reset line low). + * + * If you have any in-use sockets when you call this, you're going to + * have a bad time. + */ +void w5500_hard_reset(struct w5500 *self); + +/** + * Perform a soft reset on the chip (send the RST command). + * + * If you have any in-use sockets when you call this, you're going to + * have a bad time. + */ +void w5500_soft_reset(struct w5500 *self); + +#endif /* _LIBHW_W5500_H_ */ diff --git a/libhw_cr/w5500.c b/libhw_cr/w5500.c new file mode 100644 index 0000000..594b391 --- /dev/null +++ b/libhw_cr/w5500.c @@ -0,0 +1,977 @@ +/* libhw_cr/w5500.c - <libhw/generic/net.h> implementation for the WIZnet W5500 chip + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-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 + * https://github.com/Wiznet/ioLibrary_Driver/blob/b981401e7f3d07015619adf44c13998e13e777f9/Internet/DHCP/dhcp.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-License-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-License-Identifier: MIT + */ + +#include <string.h> /* for memcmp() */ + +/* TODO: Write a <libhw/generic/gpio.h> to avoid w5500.c being + * pico-sdk-specific. */ +#include "rp2040_gpioirq.h" +#include <hardware/gpio.h> /* pico-sdk:hardware_gpio */ + +#include <libcr/coroutine.h> /* for cr_yield() */ + +#include <libhw/generic/alarmclock.h> /* for sleep_*() */ + +#define LOG_NAME W5500 +#include <libmisc/log.h> + +#define IMPLEMENTATION_FOR_LIBHW_W5500_H YES +#include <libhw/w5500.h> + +#include "w5500_ll.h" + +/* Config *********************************************************************/ + +#include "config.h" + +#ifndef CONFIG_W5500_LOCAL_PORT_MIN + #error config.h must define CONFIG_W5500_LOCAL_PORT_MIN +#endif +#ifndef CONFIG_W5500_LOCAL_PORT_MAX + #error config.h must define CONFIG_W5500_LOCAL_PORT_MAX +#endif +#ifndef CONFIG_W5500_VALIDATE_SPI + #error config.h must define CONFIG_W5500_VALIDATE_SPI +#endif +#ifndef CONFIG_W5500_DEBUG + #error config.h must define CONFIG_W5500_DEBUG +#endif + +/* C language *****************************************************************/ + +static const char *w5500_state_str(uint8_t state) { + switch (state) { + case STATE_CLOSED: return "STATE_CLOSED"; + case STATE_TCP_INIT: return "STATE_TCP_INIT"; + case STATE_TCP_LISTEN: return "STATE_TCP_LISTEN"; + case STATE_TCP_SYNSENT: return "STATE_TCP_SYNSENT"; + case STATE_TCP_SYNRECV: return "STATE_TCP_SYNRECV"; + case STATE_TCP_ESTABLISHED: return "STATE_TCP_ESTABLISHED"; + case STATE_TCP_FIN_WAIT: return "STATE_TCP_FIN_WAIT"; + case STATE_TCP_CLOSING: return "STATE_TCP_CLOSING"; + case STATE_TCP_TIME_WAIT: return "STATE_TCP_TIME_WAIT"; + case STATE_TCP_CLOSE_WAIT: return "STATE_TCP_CLOSE_WAIT"; + case STATE_TCP_LAST_ACK: return "STATE_TCP_LAST_ACK"; + case STATE_UDP: return "STATE_UDP"; + case STATE_MACRAW: return "STATE_MACRAW"; + default: return const_byte_str(state); + } +} + +/* libmisc/obj.h **************************************************************/ + +LO_IMPLEMENTATION_C(io_closer, struct _w5500_socket, w5500_tcplist, static); +LO_IMPLEMENTATION_C(net_stream_listener, struct _w5500_socket, w5500_tcplist, static); + +LO_IMPLEMENTATION_C(io_reader, struct _w5500_socket, w5500_tcp, static); +LO_IMPLEMENTATION_C(io_writer, struct _w5500_socket, w5500_tcp, static); +LO_IMPLEMENTATION_C(io_readwriter, struct _w5500_socket, w5500_tcp, static); +LO_IMPLEMENTATION_C(io_closer, struct _w5500_socket, w5500_tcp, static); +LO_IMPLEMENTATION_C(io_bidi_closer, struct _w5500_socket, w5500_tcp, static); +LO_IMPLEMENTATION_C(net_stream_conn, struct _w5500_socket, w5500_tcp, static); + +LO_IMPLEMENTATION_C(io_closer, struct _w5500_socket, w5500_udp, static); +LO_IMPLEMENTATION_C(net_packet_conn, struct _w5500_socket, w5500_udp, static); + +LO_IMPLEMENTATION_C(net_iface, struct w5500, w5500_if, static); + +/* mid-level utilities ********************************************************/ + +static uint16_t w5500_alloc_local_port(struct w5500 *self) { + assert(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; +} + +static struct _w5500_socket *w5500_alloc_socket(struct w5500 *self) { + assert(self); + struct _w5500_socket *sock = self->free; + if (!sock) + return NULL; + self->free = sock->next_free; + sock->next_free = NULL; + assert(sock->mode == W5500_MODE_NONE); + return sock; +} + +static void w5500_free_socket(struct w5500 *self, struct _w5500_socket *sock) { + assert(self); + assert(sock); + sock->mode = W5500_MODE_NONE; + sock->next_free = self->free; + self->free = sock; +} + +static void w5500_tcp_maybe_free(struct w5500 *chip, struct _w5500_socket *sock) { + assert(chip); + assert(sock); + assert(sock->mode == W5500_MODE_TCP); + if (!sock->list_open && !sock->read_open && !sock->write_open) + w5500_free_socket(chip, sock); +} + +static COROUTINE w5500_irq_cr(void *_chip) { + struct w5500 *chip = _chip; + cr_begin(); + + for (;;) { + cr_mutex_lock(&chip->mu); + bool had_intr = false; + + uint8_t chipintr = w5500ll_read_common_reg(chip->spidev, chip_interrupt); + log_n_debugln(W5500_LL, "w5500_irq_cr(): chipintr=", chipintr); + had_intr = had_intr || (chipintr != 0); + if (chipintr) + w5500ll_write_common_reg(chip->spidev, chip_interrupt, 0xFF); + + for (uint8_t socknum = 0; socknum < 8; socknum++) { + struct _w5500_socket *socket = &chip->sockets[socknum]; + + uint8_t sockintr = w5500ll_read_sock_reg(chip->spidev, socknum, interrupt); + log_n_debugln(W5500_LL, "w5500_irq_cr(): sockintr[", socknum, "]=", sockintr); + had_intr = had_intr || (sockintr != 0); + + switch (socket->mode) { + case W5500_MODE_NONE: + break; + case W5500_MODE_TCP: case W5500_MODE_UDP: + uint8_t listen_bits = sockintr & SOCKINTR_CONN, + send_bits = sockintr & (SOCKINTR_SEND_OK|SOCKINTR_SEND_TIMEOUT), + recv_bits = sockintr & (SOCKINTR_RECV_DAT|SOCKINTR_RECV_FIN); + + if (listen_bits) { + log_debugln("w5500_irq_cr(): signal sock[", socknum, "]->listen_sema"); + cr_sema_signal(&socket->listen_sema); + } + if (recv_bits) { + log_debugln("w5500_irq_cr(): signal sock[", socknum, "]->read_sema"); + cr_sema_signal(&socket->read_sema); + } + if (send_bits) { + log_debugln("w5500_irq_cr(): signal sock[", socknum, "]->write_ch"); + cr_chan_send(&socket->write_ch, send_bits); + } + break; + } + + w5500ll_write_sock_reg(chip->spidev, socknum, interrupt, sockintr); + } + + cr_mutex_unlock(&chip->mu); + + if (!had_intr && gpio_get(chip->pin_intr)) { + log_debugln("w5500_irq_cr(): looks like all interrupts have been processed, sleeping..."); + cr_sema_wait(&chip->intr); + } else + cr_yield(); + } + + cr_end(); +} + +static struct w5500 *w5500_socket_chip(struct _w5500_socket *socket) { + assert(socket); + assert(socket->socknum < 8); + + struct _w5500_socket *sock0 = &socket[-(socket->socknum)]; + assert(sock0); + struct w5500 *chip = + ((void *)sock0) - offsetof(struct w5500, sockets); + assert(chip); + return chip; +} + +static inline void w5500_socket_cmd(struct _w5500_socket *socket, uint8_t cmd) { + assert(socket); + struct w5500 *chip = w5500_socket_chip(socket); + uint8_t socknum = socket->socknum; + + w5500ll_write_sock_reg(chip->spidev, socknum, command, cmd); + while (w5500ll_read_sock_reg(chip->spidev, socknum, command) != 0x00) + cr_yield(); +} + +static inline void w5500_socket_close(struct _w5500_socket *socket) { + assert(socket); + struct w5500 *chip = w5500_socket_chip(socket); + uint8_t socknum = socket->socknum; + + /* Send CMD_CLOSE. */ + w5500_socket_cmd(socket, CMD_CLOSE); + /* Wait for it to transition to STATE_CLOSED. */ + while (w5500ll_read_sock_reg(chip->spidev, socknum, state) != STATE_CLOSED) + cr_yield(); +} + +#define ASSERT_SELF(_iface, _mode) \ + assert(socket); \ + uint8_t socknum = socket->socknum; \ + assert(socknum < 8); \ + assert(socket->mode == W5500_MODE_##_mode); \ + struct w5500 *chip = w5500_socket_chip(socket); \ + assert(chip); + +/* init() *********************************************************************/ + +static void w5500_intrhandler(void *_chip, uint LM_UNUSED(gpio), enum gpio_irq_level LM_UNUSED(event)) { + struct w5500 *chip = _chip; + log_debugln("w5500_intrhandler()"); + cr_sema_signal_from_intrhandler(&chip->intr); +} + +void _w5500_init(struct w5500 *chip, + lo_interface spi spi, uint pin_intr, uint pin_reset, + struct net_eth_addr addr) { + assert(chip); + assert(!LO_IS_NULL(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, + }; + chip->free = &chip->sockets[0]; + for (uint8_t i = 0; i < 8; i++) { + chip->sockets[i] = (struct _w5500_socket){ + /* const-after-init */ + .socknum = i, + /* mutable */ + .next_free = (i + 1 < 8) ? &chip->sockets[i+1] : NULL, + /* The rest of the mutable members get + * initialized to the zero values. */ + }; + } + +#if CONFIG_W5500_VALIDATE_SPI + /* Validate that SPI works correctly. */ + bool spi_ok = true; + for (uint16_t a = 0; a < 0x100; a++) { + w5500ll_write_sock_reg(chip->spidev, 0, mode, a); + uint8_t b = w5500ll_read_sock_reg(chip->spidev, 0, mode); + if (b != a) { + log_errorln("SPI to W5500 does not appear to be functional: wrote:", (base16_u8_, a), " != read:", (base16_u8_, b)); + spi_ok = false; + } + } + if (!spi_ok) + __lm_abort(); + w5500ll_write_sock_reg(chip->spidev, 0, mode, 0); +#endif + + /* Initialize the hardware. */ + gpioirq_set_and_enable_exclusive_handler(pin_intr, GPIO_IRQ_EDGE_FALL, w5500_intrhandler, chip); + gpio_set_dir(chip->pin_reset, GPIO_OUT); + w5500_hard_reset(chip); + + /* Finally, wire in the interrupt handler. */ + coroutine_add("w5500_irq", w5500_irq_cr, chip); +} + +/* chip methods ***************************************************************/ + +static 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 default clock-rate of + * 125MHz, that means 8ns; and at the maximum-rated clock-rate + * of 200MHz, that means 5ns. + * + * 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) { + cr_mutex_lock(&chip->mu); + + gpio_put(chip->pin_reset, 0); + sleep_for_ms(1); /* minimum of 500us */ + gpio_put(chip->pin_reset, 1); + sleep_for_ms(2); /* minimum of 1ms */ + + w5500_post_reset(chip); + + cr_mutex_unlock(&chip->mu); +} + +void w5500_soft_reset(struct w5500 *chip) { + cr_mutex_lock(&chip->mu); + + 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); + + cr_mutex_unlock(&chip->mu); +} + +static struct net_eth_addr w5500_if_hwaddr(struct w5500 *chip) { + assert(chip); + + return chip->hwaddr; +} + +static void _w5500_if_up(struct w5500 *chip, struct net_iface_config cfg) { + assert(chip); + + cr_mutex_lock(&chip->mu); + + 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); + + cr_mutex_unlock(&chip->mu); +} + +static void w5500_if_ifup(struct w5500 *chip, struct net_iface_config cfg) { + log_debugln("if_up()"); + log_debugln(":: addr = ", (net_ip4_addr, cfg.addr)); + log_debugln(":: gateway_addr = ", (net_ip4_addr, cfg.gateway_addr)); + log_debugln(":: subnet_mask = ", (net_ip4_addr, cfg.subnet_mask)); + _w5500_if_up(chip, cfg); +} + +static void w5500_if_ifdown(struct w5500 *chip) { + log_debugln("if_down()"); + _w5500_if_up(chip, (struct net_iface_config){}); +} + +static lo_interface net_stream_listener w5500_if_tcp_listen(struct w5500 *chip, uint16_t local_port) { + assert(chip); + + struct _w5500_socket *sock = w5500_alloc_socket(chip); + if (!sock) { + log_debugln("tcp_listen() => no sock"); + return LO_NULL(net_stream_listener); + } + log_debugln("tcp_listen() => sock[", sock->socknum, "]"); + + if (!local_port) + local_port = w5500_alloc_local_port(chip); + + assert(sock->mode == W5500_MODE_NONE); + sock->mode = W5500_MODE_TCP; + sock->port = local_port; + sock->read_deadline_ns = 0; + sock->list_open = true; + + return LO_BOX(net_stream_listener, sock); +} + +static net_stream_conn_or_error w5500_if_tcp_dial(struct w5500 *chip, + struct net_ip4_addr node, uint16_t port) { + assert(chip); + assert(memcmp(node.octets, net_ip4_addr_zero.octets, 4)); + assert(memcmp(node.octets, net_ip4_addr_broadcast.octets, 4)); + assert(port); + + struct _w5500_socket *socket = w5500_alloc_socket(chip); + if (!socket) { + log_debugln("tcp_dial() => no sock"); + return ERROR_NEW_ERR(net_stream_conn, error_new(E_POSIX_ENOTSOCK)); + } + uint8_t socknum = socket->socknum; + log_debugln("tcp_dial() => sock[", socknum, "]"); + + uint16_t local_port = w5500_alloc_local_port(chip); + + assert(socket->mode == W5500_MODE_NONE); + socket->mode = W5500_MODE_TCP; + socket->port = local_port; + socket->read_deadline_ns = 0; + socket->read_open = socket->write_open = true; + + restart: + cr_mutex_lock(&chip->mu); + + /* Mimics socket.c:socket(). */ + w5500_socket_close(socket); + w5500ll_write_sock_reg(chip->spidev, socknum, mode, SOCKMODE_TCP); + w5500ll_write_sock_reg(chip->spidev, socknum, local_port, uint16be_marshal(socket->port)); + w5500_socket_cmd(socket, CMD_OPEN); + while (w5500ll_read_sock_reg(chip->spidev, socknum, state) != STATE_TCP_INIT) + cr_yield(); + + /* Mimics socket.c:connect(). */ + w5500ll_write_sock_reg(chip->spidev, socknum, remote_ip_addr, node); + w5500ll_write_sock_reg(chip->spidev, socknum, remote_port, uint16be_marshal(port)); + w5500_socket_cmd(socket, CMD_CONNECT); + cr_mutex_unlock(&chip->mu); + for (;;) { + uint8_t state = w5500ll_read_sock_reg(chip->spidev, socknum, state); + log_debugln("tcp_dial(): state=", w5500_state_str(state)); + switch (state) { + case STATE_TCP_SYNSENT: + cr_yield(); + break; + case STATE_TCP_ESTABLISHED: + return ERROR_NEW_VAL(net_stream_conn, LO_BOX(net_stream_conn, socket)); + default: + goto restart; + } + } +} + +static lo_interface net_packet_conn w5500_if_udp_conn(struct w5500 *chip, uint16_t local_port) { + assert(chip); + + struct _w5500_socket *socket = w5500_alloc_socket(chip); + if (!socket) { + log_debugln("udp_conn() => no sock"); + return LO_NULL(net_packet_conn); + } + uint8_t socknum = socket->socknum; + log_debugln("udp_conn() => sock[", socknum, "]"); + + if (!local_port) + local_port = w5500_alloc_local_port(chip); + + assert(socket->mode == W5500_MODE_NONE); + socket->mode = W5500_MODE_UDP; + socket->port = local_port; + socket->read_deadline_ns = 0; + + /* Mimics socket.c:socket(). */ + cr_mutex_lock(&chip->mu); + w5500_socket_close(socket); + w5500ll_write_sock_reg(chip->spidev, socknum, mode, SOCKMODE_UDP); + w5500ll_write_sock_reg(chip->spidev, socknum, local_port, uint16be_marshal(socket->port)); + w5500_socket_cmd(socket, CMD_OPEN); + while (w5500ll_read_sock_reg(chip->spidev, socknum, state) != STATE_UDP) + cr_yield(); + cr_mutex_unlock(&chip->mu); + + return LO_BOX(net_packet_conn, socket); +} + +static bool w5500_if_arp_ping(struct w5500 *chip, struct net_ip4_addr addr) { + /* FIXME: This arp_ping implementation is really bad (and + * assumes that a UDP socket is open, which is "safe" because + * I only use it from inside of a DHCP client). */ + assert(chip); + struct _w5500_socket *sock = NULL; + for (size_t i = 0; i < LM_ARRAY_LEN(chip->sockets) && !sock; i++) { + if (chip->sockets[i].mode == W5500_MODE_UDP) + sock = &chip->sockets[i]; + } + assert(sock); + error err = w5500_udp_sendto(sock, "BOGUS_PACKET_TO_TRIGGER_AN_ARP", 17, addr, 5000); + log_debugln("arp_ping => ", (error, err)); + return err.num != E_NET_EARP_TIMEOUT; +} + +/* tcp_listener methods *******************************************************/ + +static net_stream_conn_or_error w5500_tcplist_accept(struct _w5500_socket *socket) { + ASSERT_SELF(stream_listener, TCP); + + restart: + if (!socket->list_open) { + log_debugln("tcp_listener.accept() => already closed"); + return ERROR_NEW_ERR(net_stream_conn, error_new(E_NET_ECLOSED)); + } + + cr_mutex_lock(&chip->mu); + + /* Mimics socket.c:socket(). */ + w5500_socket_close(socket); + w5500ll_write_sock_reg(chip->spidev, socknum, mode, SOCKMODE_TCP); + w5500ll_write_sock_reg(chip->spidev, socknum, local_port, uint16be_marshal(socket->port)); + w5500_socket_cmd(socket, CMD_OPEN); + while (w5500ll_read_sock_reg(chip->spidev, socknum, state) != STATE_TCP_INIT) + cr_yield(); + + /* Mimics socket.c:listen(). */ + w5500_socket_cmd(socket, CMD_LISTEN); + cr_mutex_unlock(&chip->mu); + for (;;) { + uint8_t state = w5500ll_read_sock_reg(chip->spidev, socknum, state); + log_debugln("tcp_listener.accept() => state=", w5500_state_str(state)); + switch (state) { + case STATE_TCP_LISTEN: + case STATE_TCP_SYNRECV: + cr_sema_wait(&socket->listen_sema); + break; + case STATE_TCP_ESTABLISHED: + socket->read_open = true; + [[fallthrough]]; + case STATE_TCP_CLOSE_WAIT: + socket->write_open = true; + return ERROR_NEW_VAL(net_stream_conn, LO_BOX(net_stream_conn, socket)); + default: + goto restart; + } + } +} + +static error w5500_tcplist_close(struct _w5500_socket *socket) { + log_debugln("tcp_listener.close()"); + ASSERT_SELF(stream_listener, TCP); + + socket->list_open = false; + w5500_tcp_maybe_free(chip, socket); + return ERROR_NULL; +} + +/* tcp_conn methods ***********************************************************/ + +static size_t_and_error w5500_tcp_writev(struct _w5500_socket *socket, const struct iovec *iov, int iovcnt) { + assert(iov); + assert(iovcnt > 0); + size_t count = 0; + for (int i = 0; i < iovcnt; i++) + count += iov[i].iov_len; + assert(count); + log_debugln("tcp_conn.write(", count, ")"); + ASSERT_SELF(stream_conn, TCP); + + /* 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_FINISHED does AIUI, 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 (!socket->write_open) { + log_debugln(" => soft closed"); + return ERROR_AND(size_t, done, error_new(E_NET_ECLOSED)); + } + cr_mutex_lock(&chip->mu); + uint8_t state = w5500ll_read_sock_reg(chip->spidev, socknum, state); + if (state != STATE_TCP_ESTABLISHED && state != STATE_TCP_CLOSE_WAIT) { + cr_mutex_unlock(&chip->mu); + log_debugln(" => hard closed"); + return ERROR_AND(size_t, done, error_new(E_NET_ECLOSED)); + } + + 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_mutex_unlock(&chip->mu); + cr_yield(); + continue; + } + + /* Queue data to be sent. */ + if ((size_t)freesize > count-done) + freesize = count-done; + uint16_t ptr = uint16be_unmarshal(w5500ll_read_sock_reg(chip->spidev, socknum, tx_write_pointer)); + w5500ll_writev(chip->spidev, ptr, CTL_BLOCK_SOCK(socknum, TX), iov, iovcnt, done, freesize); + w5500ll_write_sock_reg(chip->spidev, socknum, tx_write_pointer, uint16be_marshal(ptr+freesize)); + + /* Submit the queue. */ + w5500_socket_cmd(socket, CMD_SEND); + + cr_mutex_unlock(&chip->mu); + switch (cr_chan_recv(&socket->write_ch)) { + case SOCKINTR_SEND_OK: + log_debugln(" => sent ", freesize); + done += freesize; + break; + case SOCKINTR_SEND_TIMEOUT: + log_debugln(" => ACK timeout"); + return ERROR_AND(size_t, done, error_new(E_NET_EACK_TIMEOUT)); + case SOCKINTR_SEND_OK|SOCKINTR_SEND_TIMEOUT: + assert_notreached("send both OK and timed out?"); + default: + assert_notreached("invalid write_ch bits"); + } + } + log_debugln(" => send finished"); + return ERROR_AND(size_t, done, ERROR_NULL); +} + +static void w5500_tcp_set_read_deadline(struct _w5500_socket *socket, uint64_t ns) { + log_debugln("tcp_conn.set_read_deadline(", ns, ")"); + ASSERT_SELF(stream_conn, TCP); + socket->read_deadline_ns = ns; +} + +static void w5500_tcp_alarm_handler(void *_arg) { + struct _w5500_socket *socket = _arg; + cr_sema_signal_from_intrhandler(&socket->read_sema); +} + +static size_t_or_error w5500_tcp_readv(struct _w5500_socket *socket, const struct iovec *iov, int iovcnt) { + assert(iov); + assert(iovcnt > 0); + size_t count = 0; + for (int i = 0; i < iovcnt; i++) + count += iov[i].iov_len; + assert(count); + log_debugln("tcp_conn.read(", count, ")"); + ASSERT_SELF(stream_conn, TCP); + + struct alarmclock_trigger trigger = {}; + if (socket->read_deadline_ns) + LO_CALL(bootclock, add_trigger, &trigger, + socket->read_deadline_ns, + w5500_tcp_alarm_handler, + socket); + + /* Wait until there is data to read. */ + uint16_t avail = 0; + for (;;) { + if (!socket->read_open) { + LO_CALL(bootclock, del_trigger, &trigger); + log_debugln(" => soft closed"); + return ERROR_NEW_ERR(size_t, error_new(E_NET_ECLOSED)); + } + if (socket->read_deadline_ns && socket->read_deadline_ns <= LO_CALL(bootclock, get_time_ns)) { + LO_CALL(bootclock, del_trigger, &trigger); + log_debugln(" => recv timeout"); + return ERROR_NEW_ERR(size_t, error_new(E_NET_ERECV_TIMEOUT)); + } + cr_mutex_lock(&chip->mu); + uint8_t state = w5500ll_read_sock_reg(chip->spidev, socknum, state); + switch (state) { + case STATE_TCP_CLOSE_WAIT: + case STATE_TCP_ESTABLISHED: + case STATE_TCP_FIN_WAIT: + break; /* OK */ + default: + LO_CALL(bootclock, del_trigger, &trigger); + cr_mutex_unlock(&chip->mu); + log_debugln(" => hard closed"); + return ERROR_NEW_ERR(size_t, error_new(E_NET_ECLOSED)); + } + + avail = uint16be_unmarshal(w5500ll_read_sock_reg(chip->spidev, socknum, rx_size)); + if (avail) + /* We have data to read. */ + break; + if (state == STATE_TCP_CLOSE_WAIT) { + LO_CALL(bootclock, del_trigger, &trigger); + cr_mutex_unlock(&chip->mu); + log_debugln(" => EOF"); + return ERROR_NEW_ERR(size_t, error_new(E_EOF)); + } + + cr_mutex_unlock(&chip->mu); + cr_sema_wait(&socket->read_sema); + } + assert(avail); + log_debugln(" => received ", avail, " bytes"); + uint16_t ptr = uint16be_unmarshal(w5500ll_read_sock_reg(chip->spidev, socknum, rx_read_pointer)); + /* Read the data. */ + if ((size_t)avail > count) + avail = count; + w5500ll_readv(chip->spidev, ptr, CTL_BLOCK_SOCK(socknum, RX), iov, iovcnt, avail); + /* Tell the chip that we read the data. */ + w5500ll_write_sock_reg(chip->spidev, socknum, rx_read_pointer, uint16be_marshal(ptr+avail)); + w5500_socket_cmd(socket, CMD_RECV); + /* Return. */ + LO_CALL(bootclock, del_trigger, &trigger); + cr_mutex_unlock(&chip->mu); + return ERROR_NEW_VAL(size_t, avail); +} + +static error w5500_tcp_close_inner(struct _w5500_socket *socket, bool rd, bool wr) { + log_debugln("tcp_conn.close(rd=", rd, ", wr=", wr, ")"); + ASSERT_SELF(stream_conn, TCP); + + if (rd) + socket->read_open = false; + + if (wr && socket->write_open) { + cr_mutex_lock(&chip->mu); + w5500_socket_cmd(socket, CMD_DISCON); + while (socket->write_open) { + uint8_t state = w5500ll_read_sock_reg(chip->spidev, socknum, state); + switch (state) { + case STATE_TCP_FIN_WAIT: + socket->write_open = false; + /* Can still read */ + if (!socket->read_open) + w5500_socket_close(socket); + break; + case STATE_CLOSED: + socket->write_open = false; + break; + } + } + cr_mutex_unlock(&chip->mu); + } + + w5500_tcp_maybe_free(chip, socket); + return ERROR_NULL; +} + +static error w5500_tcp_close(struct _w5500_socket *socket) { return w5500_tcp_close_inner(socket, true, true); } +static error w5500_tcp_close_read(struct _w5500_socket *socket) { return w5500_tcp_close_inner(socket, true, false); } +static error w5500_tcp_close_write(struct _w5500_socket *socket) { return w5500_tcp_close_inner(socket, false, true); } + +/* udp_conn methods ***********************************************************/ + +static error w5500_udp_sendto(struct _w5500_socket *socket, void *buf, size_t count, + struct net_ip4_addr node, uint16_t port) { + log_debugln("udp_conn.sendto()"); + ASSERT_SELF(packet_conn, UDP); + assert(buf); + assert(count); + + uint16_t bufsize = ((uint16_t)w5500ll_read_sock_reg(chip->spidev, socknum, tx_buf_size))*1024; + if (count > bufsize) { + log_debugln(" => msg too large"); + return error_new(E_POSIX_EMSGSIZE); + } + + for (;;) { + cr_mutex_lock(&chip->mu); + uint8_t state = w5500ll_read_sock_reg(chip->spidev, socknum, state); + if (state != STATE_UDP) { + cr_mutex_unlock(&chip->mu); + log_debugln(" => closed"); + return error_new(E_NET_ECLOSED); + } + + uint16_t freesize = uint16be_unmarshal(w5500ll_read_sock_reg(chip->spidev, socknum, tx_free_size)); + if (freesize >= count) + /* We can send. */ + break; + + /* Wait for more buffer space. */ + cr_mutex_unlock(&chip->mu); + cr_yield(); + } + + /* Where we're sending it. */ + w5500ll_write_sock_reg(chip->spidev, socknum, remote_ip_addr, node); + w5500ll_write_sock_reg(chip->spidev, socknum, remote_port, uint16be_marshal(port)); + /* Queue data to be sent. */ + uint16_t ptr = uint16be_unmarshal(w5500ll_read_sock_reg(chip->spidev, socknum, tx_write_pointer)); + w5500ll_writev(chip->spidev, ptr, CTL_BLOCK_SOCK(socknum, TX), &((struct iovec){ + .iov_base = buf, + .iov_len = count, + }), 1, 0, 0); + w5500ll_write_sock_reg(chip->spidev, socknum, tx_write_pointer, uint16be_marshal(ptr+count)); + /* Submit the queue. */ + w5500_socket_cmd(socket, CMD_SEND); + + cr_mutex_unlock(&chip->mu); + switch (cr_chan_recv(&socket->write_ch)) { + case SOCKINTR_SEND_OK: + log_debugln(" => sent"); + return ERROR_NULL; + case SOCKINTR_SEND_TIMEOUT: + log_debugln(" => ARP timeout"); + return error_new(E_NET_EARP_TIMEOUT); + case SOCKINTR_SEND_OK|SOCKINTR_SEND_TIMEOUT: + assert_notreached("send both OK and timed out?"); + default: + assert_notreached("invalid write_ch bits"); + } +} + +static void w5500_udp_set_recv_deadline(struct _w5500_socket *socket, uint64_t ns) { + log_debugln("udp_conn.set_recv_deadline(", ns, ")"); + ASSERT_SELF(packet_conn, UDP); + socket->read_deadline_ns = ns; +} + +static void w5500_udp_alarm_handler(void *_arg) { + struct _w5500_socket *socket = _arg; + cr_sema_signal_from_intrhandler(&socket->read_sema); +} + +static size_t_or_error w5500_udp_recvfrom(struct _w5500_socket *socket, void *buf, size_t count, + struct net_ip4_addr *ret_node, uint16_t *ret_port) { + log_debugln("udp_conn.recvfrom()"); + ASSERT_SELF(packet_conn, UDP); + assert(buf); + assert(count); + + struct alarmclock_trigger trigger = {}; + if (socket->read_deadline_ns) + LO_CALL(bootclock, add_trigger, &trigger, + socket->read_deadline_ns, + w5500_udp_alarm_handler, + socket); + + /* Wait until there is data to read. */ + uint16_t avail = 0; + for (;;) { + if (socket->read_deadline_ns && socket->read_deadline_ns <= LO_CALL(bootclock, get_time_ns)) { + LO_CALL(bootclock, del_trigger, &trigger); + log_debugln(" => recv timeout"); + return ERROR_NEW_ERR(size_t, error_new(E_NET_ERECV_TIMEOUT)); + } + cr_mutex_lock(&chip->mu); + uint8_t state = w5500ll_read_sock_reg(chip->spidev, socknum, state); + if (state != STATE_UDP) { + LO_CALL(bootclock, del_trigger, &trigger); + log_debugln(" => hard closed"); + return ERROR_NEW_ERR(size_t, error_new(E_NET_ECLOSED)); + } + + avail = uint16be_unmarshal(w5500ll_read_sock_reg(chip->spidev, socknum, rx_size)); + if (avail) + /* We have data to read. */ + break; + + cr_mutex_unlock(&chip->mu); + cr_sema_wait(&socket->read_sema); + } + assert(avail >= 8); + uint16_t ptr = uint16be_unmarshal(w5500ll_read_sock_reg(chip->spidev, socknum, rx_read_pointer)); + /* Read a munged form of the UDP packet header. I + * can't find in the datasheet where it describes + * this; this is based off of socket.c:recvfrom(). */ + uint8_t hdr[8]; + w5500ll_readv(chip->spidev, ptr, CTL_BLOCK_SOCK(socknum, RX), &((struct iovec){ + .iov_base = hdr, + .iov_len = sizeof(hdr), + }), 1, 0); + if (ret_node) { + ret_node->octets[0] = hdr[0]; + ret_node->octets[1] = hdr[1]; + ret_node->octets[2] = hdr[2]; + ret_node->octets[3] = hdr[3]; + } + if (ret_port) + *ret_port = uint16be_decode(&hdr[4]); + uint16_t len = uint16be_decode(&hdr[6]); + log_debugln(" => received ", len, " bytes", len < avail-8 ? " (plus more messages)" : ""); + /* Now read the actual data. */ + if (count > len) + count = len; + w5500ll_readv(chip->spidev, ptr+8, CTL_BLOCK_SOCK(socknum, RX), &((struct iovec){ + .iov_base = buf, + .iov_len = len, + }), 1, 0); + /* Tell the chip that we read the data. */ + w5500ll_write_sock_reg(chip->spidev, socknum, rx_read_pointer, uint16be_marshal(ptr+8+len)); + w5500_socket_cmd(socket, CMD_RECV); + /* Return. */ + LO_CALL(bootclock, del_trigger, &trigger); + cr_mutex_unlock(&chip->mu); + return ERROR_NEW_VAL(size_t, len); +} + +static error w5500_udp_close(struct _w5500_socket *socket) { + log_debugln("udp_conn.close()"); + ASSERT_SELF(packet_conn, UDP); + + w5500_socket_close(socket); + w5500_free_socket(chip, socket); + return ERROR_NULL; +} diff --git a/libhw_cr/w5500_ll.c b/libhw_cr/w5500_ll.c new file mode 100644 index 0000000..c60e045 --- /dev/null +++ b/libhw_cr/w5500_ll.c @@ -0,0 +1,115 @@ +/* libhw_cr/w5500_ll.c - Low-level library for the WIZnet W5500 chip + * + * Based entirely on the W5500 datasheet (v1.1.0), not on reference code. + * https://docs.wiznet.io/img/products/w5500/W5500_ds_v110e.pdf + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <string.h> /* for memcmp() and mempy() */ + +#include <libmisc/alloc.h> /* for stack_alloc() */ +#include <libmisc/fmt.h> + +#include "w5500_ll.h" + +#if CONFIG_W5500_LL_DEBUG +static void fmt_print_ctl_block(lo_interface fmt_dest, uint8_t b) { + static char *strs[] = { + "RES", + "REG", + "TX", + "RX", + }; + fmt_print("CTL_BLOCK_SOCK(", (base10, (((b)>>5) & 0b111)), ", ", strs[((b)>>3)&0b11], ")"); +} +#endif + +void _w5500ll_n_writev(const char *func, + lo_interface spi spidev, uint16_t addr, uint8_t block, + const struct iovec *iov, int iovcnt, + size_t skip, size_t max) +{ + assert(!LO_IS_NULL(spidev)); + assert((block & ~CTL_MASK_BLOCK) == 0); + assert(iov); + assert(iovcnt > 0); +#if CONFIG_W5500_LL_DEBUG + log_n_debugln(W5500_LL, + func, "(): w5500ll_write(spidev", + ", addr=", (base16_u16_, addr), + ", block=", (ctl_block, block), + ", iov", + ", iovcnt=", iovcnt, + ")"); +#endif + + uint8_t header[] = { + (uint8_t)((addr >> 8) & 0xFF), + (uint8_t)(addr & 0xFF), + (block & CTL_MASK_BLOCK) | CTL_W | CTL_OM_VDM, + }; + int inner_cnt = 1+io_slice_cnt(iov, iovcnt, skip, max); + struct duplex_iovec *inner = stack_alloc(inner_cnt, struct duplex_iovec); + inner[0] = (struct duplex_iovec){ + .iov_read_to = IOVEC_DISCARD, + .iov_write_from = header, + .iov_len = sizeof(header), + }; + io_slice_wr_to_duplex(&inner[1], iov, iovcnt, skip, max); + LO_CALL(spidev, readwritev, inner, inner_cnt); +} + + +void _w5500ll_n_readv(const char *func, + lo_interface spi spidev, uint16_t addr, uint8_t block, + const struct iovec *iov, int iovcnt, + size_t max) +{ + assert(!LO_IS_NULL(spidev)); + assert((block & ~CTL_MASK_BLOCK) == 0); + assert(iov); + assert(iovcnt > 0); +#if CONFIG_W5500_LL_DEBUG + log_n_debugln(W5500_LL, + func, "(): w5500ll_read(spidev", + ", addr=", (base16_u16_, addr), + ", block=", (ctl_block, block), + ", iov", + ", iovcnt=", iovcnt, + ")"); +#endif + + uint8_t header[] = { + (uint8_t)((addr >> 8) & 0xFF), + (uint8_t)(addr & 0xFF), + (block & CTL_MASK_BLOCK) | CTL_R | CTL_OM_VDM, + }; + int inner_cnt = 1+io_slice_cnt(iov, iovcnt, 0, max); + struct duplex_iovec *inner = stack_alloc(inner_cnt, struct duplex_iovec); + inner[0] = (struct duplex_iovec){ + .iov_read_to = IOVEC_DISCARD, + .iov_write_from = header, + .iov_len = sizeof(header), + }; + io_slice_rd_to_duplex(&inner[1], iov, iovcnt, 0, max); + LO_CALL(spidev, readwritev, inner, inner_cnt); +} + +void _w5500ll_n_read_repeated(const char *func, + lo_interface spi spidev, uint16_t addr, uint8_t block, + void *buf, void *buf_scratch, size_t len) +{ + _w5500ll_n_readv(func, spidev, addr, block, + &((struct iovec){.iov_base=buf, .iov_len=len}), 1, + 0); + for (;;) { + _w5500ll_n_readv(func, spidev, addr, block, + &((struct iovec){.iov_base=buf_scratch, .iov_len=len}), 1, + 0); + if (memcmp(buf, buf_scratch, len) == 0) + break; + memcpy(buf, buf_scratch, len); + } +} diff --git a/libhw_cr/w5500_ll.h b/libhw_cr/w5500_ll.h new file mode 100644 index 0000000..28116a4 --- /dev/null +++ b/libhw_cr/w5500_ll.h @@ -0,0 +1,367 @@ +/* libhw_cr/w5500_ll.h - Low-level library for the WIZnet W5500 chip + * + * Based entirely on the W5500 datasheet (v1.1.0), not on reference code. + * https://docs.wiznet.io/img/products/w5500/W5500_ds_v110e.pdf + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#ifndef _LIBHW_CR_W5500_LL_H_ +#define _LIBHW_CR_W5500_LL_H_ + +#include <stdint.h> /* for uint{n}_t */ + +#include <libmisc/assert.h> /* for static_assert() */ +#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 lo_interface spi */ + +/* Config. ***************************************************************************************/ + +#include "config.h" + +#ifndef CONFIG_W5500_LL_DEBUG + #error config.h must define CONFIG_W5500_LL_DEBUG +#endif + +/* 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 /* chip-wide registers on socknum=0, REServed on socknum>=1 */ +#define _CTL_BLOCK_REG 0b01000 /* socknum-specific registers */ +#define _CTL_BLOCK_TX 0b10000 /* socknum-specific transmit buffer */ +#define _CTL_BLOCK_RX 0b11000 /* socknum-specific receive buffer */ +#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 /* variable-length data mode */ +#define CTL_OM_FDM1 0b01 /* fixed-length data mode: 1 byte data length */ +#define CTL_OM_FDM2 0b10 /* fixed-length data mode: 2 byte data length */ +#define CTL_OM_FDM4 0b11 /* fixed-length data mode: 4 byte data length */ + +/* Even though SPI is a full-duplex protocol, the W5500's spiframe on + * top of it is only half-duplex. Lame. */ + +#if CONFIG_W5500_LL_DEBUG + #define w5500ll_writev(...) _w5500ll_n_writev (__func__, __VA_ARGS__) + #define w5500ll_readv(...) _w5500ll_n_readv (__func__, __VA_ARGS__) +#else + #define _w5500ll_n_writev(func, ...) w5500ll_writev (__VA_ARGS__) + #define _w5500ll_n_readv(func, ...) w5500ll_readv (__VA_ARGS__) +#endif + +/* `skip` and `max` correspond to the <libhw/generic/io.h> `slice` + * functions' `byte_start` and `max_byte_cnt` arguments for working on + * just a subset of the iovec list. */ + +void _w5500ll_n_writev(const char *func, + lo_interface spi spidev, uint16_t addr, uint8_t block, + const struct iovec *iov, int iovcnt, + size_t skip, size_t max); +void _w5500ll_n_readv(const char *func, + lo_interface spi spidev, uint16_t addr, uint8_t block, + const struct iovec *iov, int iovcnt, + size_t max); + +/* Operate on registers. *************************************************************************/ + +/* + * Given a `blocktyp` that is a struct describing the layout of + * registers in a block (e.g. `blocktyp = struct + * w5500_ll_block_common_reg`), read or write the `field` register + * (where `field` must be a field in that struct). + */ + +#define w5500ll_write_reg(spidev, blockid, blocktyp, field, val) do { \ + typeof((blocktyp){}.field) lval = val; \ + w5500ll_writev(spidev, \ + offsetof(blocktyp, field), blockid, \ + &((struct iovec){ \ + .iov_base = &lval, \ + .iov_len = sizeof(lval), \ + }), 1, \ + 0, 0); \ + } 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". */ +#if CONFIG_W5500_LL_DEBUG + #define _w5500ll_read_repeated(...) _w5500ll_n_read_repeated (__func__, __VA_ARGS__) +#else + #define _w5500ll_n_read_repeated(func, ...) _w5500ll_read_repeated (__VA_ARGS__) +#endif +void _w5500ll_n_read_repeated(const char *func, + lo_interface spi spidev, uint16_t addr, uint8_t block, + void *buf, void *buf_scratch, size_t len); +#define w5500ll_read_reg(spidev, blockid, blocktyp, field) ({ \ + typeof((blocktyp){}.field) val; \ + if (sizeof(val) == 1) { \ + w5500ll_readv(spidev, \ + offsetof(blocktyp, field), blockid, \ + &((struct iovec){ \ + .iov_base = &val, \ + .iov_len = sizeof(val), \ + }), 1, \ + 0); \ + } else { \ + typeof(val) val2; \ + _w5500ll_read_repeated(spidev, \ + offsetof(blocktyp, field), blockid, \ + &val, &val2, sizeof(val)); \ + } \ + val; \ + }) + +/* Register blocks. ******************************************************************************/ + +/* 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)) /* Use IGMPv1 instead of v2 */ +#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_IPV6 ((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) /* TODO: determine precise meaning */ +#define SOCKINTR_SEND_TIMEOUT ((uint8_t)1<<3) /* ARP or TCP */ +#define SOCKINTR_RECV_DAT ((uint8_t)1<<2) /* received data */ +#define SOCKINTR_RECV_FIN ((uint8_t)1<<1) /* received FIN */ +#define SOCKINTR_CONN ((uint8_t)1<<0) /* 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) + +#endif /* _LIBHW_CR_W5500_LL_H_ */ |